From 9d549af616e5a58bc829cd233dd54ede6fe6fa56 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 22 Jun 2019 23:18:20 +0900 Subject: [PATCH 001/736] Supports textarea --- src/lib/hooks/useInput.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/hooks/useInput.tsx b/src/lib/hooks/useInput.tsx index 7f1056ef..a27c0190 100644 --- a/src/lib/hooks/useInput.tsx +++ b/src/lib/hooks/useInput.tsx @@ -2,8 +2,11 @@ import { useState, useCallback } from 'react'; export default function useInput(defaultValue: string) { const [input, setInput] = useState(defaultValue); - const onChange = useCallback((e: React.ChangeEvent) => { - setInput(e.target.value); - }, []); + const onChange = useCallback( + (e: React.ChangeEvent) => { + setInput(e.target.value); + }, + [], + ); return [input, onChange] as [string, typeof onChange]; } From 1c6ceb7ac3f2efd4444ce4f6776464eb8492453c Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 22 Jun 2019 23:18:51 +0900 Subject: [PATCH 002/736] Remove useless generic --- src/components/register/RegisterForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/register/RegisterForm.tsx b/src/components/register/RegisterForm.tsx index 5a0d3b80..9781a3a8 100644 --- a/src/components/register/RegisterForm.tsx +++ b/src/components/register/RegisterForm.tsx @@ -38,7 +38,7 @@ const RegisterForm: React.SFC = ({ defaultEmail, error, }) => { - const [form, onChange] = useInputs({ + const [form, onChange] = useInputs({ displayName: '', email: '', username: '', From bfd7509b30ba2590b4f6306e22c4a8ef8283aefc Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 22 Jun 2019 23:36:22 +0900 Subject: [PATCH 003/736] Add onReset to useInput hook --- src/lib/hooks/useInput.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/hooks/useInput.tsx b/src/lib/hooks/useInput.tsx index a27c0190..278fe504 100644 --- a/src/lib/hooks/useInput.tsx +++ b/src/lib/hooks/useInput.tsx @@ -8,5 +8,10 @@ export default function useInput(defaultValue: string) { }, [], ); - return [input, onChange] as [string, typeof onChange]; + const onReset = useCallback(() => setInput(''), []); + return [input, onChange, onReset] as [ + string, + typeof onChange, + typeof onReset + ]; } From 91f5658729f2bc811adc66b56b61fd690d4de9ff Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 22 Jun 2019 23:54:12 +0900 Subject: [PATCH 004/736] Implements most of the features in PostRepliesContainer --- src/components/post/PostCommentItem.tsx | 2 +- src/components/post/PostReplies.tsx | 34 ++++++++-- .../post/__tests__/PostReplies.test.tsx | 65 +++++++++++++++++-- src/containers/post/PostRepliesContainer.tsx | 44 +++++++++++-- src/lib/graphql/post.ts | 5 ++ 5 files changed, 134 insertions(+), 16 deletions(-) diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index 89ae80f7..95c63ace 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -126,7 +126,7 @@ const PostCommentItem: React.FC = ({ comment }) => { - {open && } + {open && } ); diff --git a/src/components/post/PostReplies.tsx b/src/components/post/PostReplies.tsx index 2adfb8af..0717f1d1 100644 --- a/src/components/post/PostReplies.tsx +++ b/src/components/post/PostReplies.tsx @@ -5,6 +5,7 @@ import PostCommentsList from './PostCommentsList'; import useBoolean from '../../lib/hooks/useBoolean'; import palette from '../../lib/styles/palette'; import PostCommentsWrite from './PostCommentsWrite'; +import useInput from '../../lib/hooks/useInput'; const PostRepliesBlock = styled.div` border: solid 1px rgba(0, 0, 0, 0.02); @@ -47,22 +48,43 @@ const StartWritingButton = styled.button` export interface PostRepliesProps { comments: Comment[]; + onReply: (text: string) => any; + onHide: () => void; } -const PostReplies: React.FC = ({ comments }) => { +const PostReplies: React.FC = ({ + comments, + onReply, + onHide, +}) => { const [writing, onToggle] = useBoolean(false); + const [comment, onChangeComment, onResetComment] = useInput(''); const hasComments = comments.length > 0; + const onWrite = () => { + onReply(comment); + if (writing) { + onToggle(); + } + onResetComment(); + }; + const onCancel = () => { + if (comments.length > 0) { + onToggle(); + } else { + onHide(); + } + }; return ( - + {comments.length > 0 && } {hasComments && } {writing || !hasComments ? ( {}} - onChange={() => {}} - onCancel={onToggle} + comment={comment} + onWrite={onWrite} + onChange={onChangeComment} + onCancel={onCancel} forReplies /> ) : ( diff --git a/src/components/post/__tests__/PostReplies.test.tsx b/src/components/post/__tests__/PostReplies.test.tsx index fe4de5b2..2058368a 100644 --- a/src/components/post/__tests__/PostReplies.test.tsx +++ b/src/components/post/__tests__/PostReplies.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render, fireEvent } from 'react-testing-library'; import PostReplies, { PostRepliesProps } from '../PostReplies'; const sampleComments = [ @@ -12,7 +12,7 @@ const sampleComments = [ thumbnail: null, }, }, - text: 'ㅋㅋㅋ', + text: '첫번째 댓글', replies_count: 2, created_at: '2019-06-19T14:56:26.022Z', }, @@ -25,7 +25,7 @@ const sampleComments = [ thumbnail: null, }, }, - text: 'sed', + text: '두번째 댓글', replies_count: 0, created_at: '2019-06-19T15:08:08.085Z', }, @@ -34,6 +34,8 @@ describe('PostReplies', () => { const setup = (props: Partial = {}) => { const initialProps: PostRepliesProps = { comments: sampleComments, + onReply: () => {}, + onHide: () => {}, }; const utils = render(); return { @@ -41,6 +43,61 @@ describe('PostReplies', () => { }; }; it('renders properly', () => { - setup(); + const { getByText } = setup(); + getByText('첫번째 댓글'); + getByText('두번째 댓글'); + getByText('답글 작성하기'); + }); + it('shows PostCommentsWrite when StartWritingButton is clicked', () => { + const { getByText, getByPlaceholderText } = setup(); + const button = getByText('답글 작성하기'); + fireEvent.click(button); + getByPlaceholderText('댓글을 작성하세요'); + }); + it('shows PostCommentsWrite when comments array is empty', () => { + const { getByPlaceholderText } = setup({ comments: [] }); + getByPlaceholderText('댓글을 작성하세요'); + }); + it('calls onHide when comments array is empty and cancel button is clicked', () => { + const onHide = jest.fn(); + const { getByText } = setup({ comments: [], onHide }); + const cancelButton = getByText('취소'); + fireEvent.click(cancelButton); + expect(onHide).toBeCalled(); + }); + it('changes textarea', () => { + const { getByPlaceholderText } = setup({ comments: [] }); + const textarea = getByPlaceholderText( + '댓글을 작성하세요', + ) as HTMLTextAreaElement; + fireEvent.change(textarea, { + target: { + value: '안녕하세요', + }, + }); + expect(textarea).toHaveProperty('value', '안녕하세요'); + }); + it('calls onReply when submit button is clicked', () => { + const onReply = jest.fn(); + const { getByPlaceholderText, getByText } = setup({ onReply }); + + const button = getByText('답글 작성하기'); + fireEvent.click(button); + + const textarea = getByPlaceholderText( + '댓글을 작성하세요', + ) as HTMLTextAreaElement; + fireEvent.change(textarea, { + target: { + value: '안녕하세요', + }, + }); + + const submitButton = getByText('댓글 작성'); + fireEvent.click(submitButton); + + expect(onReply).toBeCalledWith('안녕하세요'); + + getByText('답글 작성하기'); }); }); diff --git a/src/containers/post/PostRepliesContainer.tsx b/src/containers/post/PostRepliesContainer.tsx index 82bba365..f80d3c8f 100644 --- a/src/containers/post/PostRepliesContainer.tsx +++ b/src/containers/post/PostRepliesContainer.tsx @@ -1,15 +1,23 @@ import React from 'react'; -import { Query, QueryResult } from 'react-apollo'; -import { GET_REPLIES, CommentWithReplies } from '../../lib/graphql/post'; +import { Query, QueryResult, Mutation } from 'react-apollo'; +import { + GET_REPLIES, + CommentWithReplies, + WRITE_COMMENT, +} from '../../lib/graphql/post'; import PostReplies from '../../components/post/PostReplies'; import { useSelector } from 'react-redux'; import { RootState } from '../../modules'; export interface PostRepliesProps { commentId: string; + onHide: () => void; } -const PostRepliesContainer: React.FC = ({ commentId }) => { +const PostRepliesContainer: React.FC = ({ + commentId, + onHide, +}) => { const postId = useSelector((state: RootState) => state.post.id); return ( @@ -17,10 +25,36 @@ const PostRepliesContainer: React.FC = ({ commentId }) => { loading, error, data, + refetch, }: QueryResult<{ comment: CommentWithReplies }>) => { - console.log(loading, error, data); if (loading || !data) return null; - return ; + return ( + + {writeComment => { + const onReply = async (text: string) => { + try { + await writeComment({ + variables: { + post_id: postId, + comment_id: commentId, + text, + }, + }); + refetch(); + } catch (e) { + console.log(e); + } + }; + return ( + + ); + }} + + ); }} ); diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index cb3b3dcc..ec4feae5 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -144,7 +144,9 @@ export const READ_POST = gql` } text replies_count + level created_at + level } } } @@ -165,6 +167,7 @@ export const RELOAD_COMMENTS = gql` } text replies_count + level created_at } } @@ -184,6 +187,7 @@ export const GET_COMMENT = gql` } text replies_count + level created_at } } @@ -205,6 +209,7 @@ export const GET_REPLIES = gql` text replies_count created_at + level } } } From 160964db5367c2d2070afbc0ef148a1dda0f74f7 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 22 Jun 2019 23:57:05 +0900 Subject: [PATCH 005/736] Add field level to the fake post data --- src/containers/post/__tests__/PostViewer.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/containers/post/__tests__/PostViewer.test.tsx b/src/containers/post/__tests__/PostViewer.test.tsx index ae934f00..9e19ecd9 100644 --- a/src/containers/post/__tests__/PostViewer.test.tsx +++ b/src/containers/post/__tests__/PostViewer.test.tsx @@ -63,6 +63,7 @@ describe('PostViewer', () => { text: 'Hey there', replies_count: 0, created_at: '2019-06-13T14:27:25.463Z', + level: 0, }, { id: '0f700ebd-0dec-469a-adb8-c47bae2b8f18', @@ -76,6 +77,7 @@ describe('PostViewer', () => { text: 'Hey there', replies_count: 0, created_at: '2019-06-13T14:27:31.265Z', + level: 0, }, { id: '90f3b558-4af3-41bb-95dd-ed9571609f25', @@ -89,6 +91,7 @@ describe('PostViewer', () => { text: 'Hey there', replies_count: 0, created_at: '2019-06-13T14:27:53.898Z', + level: 0, }, { id: 'd4365762-08e8-4928-a387-4255e4392291', @@ -102,6 +105,7 @@ describe('PostViewer', () => { text: 'Hey there', replies_count: 3, created_at: '2019-06-13T14:42:28.873Z', + level: 0, }, { id: '383d590d-d3ed-4f07-994e-cbea3127195d', @@ -115,6 +119,7 @@ describe('PostViewer', () => { text: 'Hey there', replies_count: 0, created_at: '2019-06-13T14:42:58.100Z', + level: 0, }, { id: 'a0bc6974-582a-4eb1-8273-627e1df7c527', @@ -128,6 +133,7 @@ describe('PostViewer', () => { text: 'aw3b5aw35w35bw3a', replies_count: 0, created_at: '2019-06-14T14:43:22.312Z', + level: 0, }, { id: '86fa0b3f-1493-424c-aff4-49e1b059caf6', @@ -141,6 +147,7 @@ describe('PostViewer', () => { text: '잘된거니?', replies_count: 0, created_at: '2019-06-14T14:43:27.840Z', + level: 0, }, { id: '4e76da59-048a-4d2d-b11f-4f6194dff4f5', @@ -154,6 +161,7 @@ describe('PostViewer', () => { text: 'afasdfsdf', replies_count: 0, created_at: '2019-06-14T14:49:08.871Z', + level: 0, }, ], }, From 94ea704f624d4cec6a839ff83b8160b978b11cb7 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 23 Jun 2019 23:32:14 +0900 Subject: [PATCH 006/736] Scroll to bottom after writing a comment --- src/containers/post/PostComments.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/containers/post/PostComments.tsx b/src/containers/post/PostComments.tsx index e41afa17..60f32fc4 100644 --- a/src/containers/post/PostComments.tsx +++ b/src/containers/post/PostComments.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import PostCommentsTemplate from '../../components/post/PostCommentsTemplate'; import PostCommentsWriteContainer from './PostCommentsWriteContainer'; -import { Comment, WRITE_COMMENT } from '../../lib/graphql/post'; +import { Comment } from '../../lib/graphql/post'; import PostCommentsList from '../../components/post/PostCommentsList'; -import { MutationResult, Mutation } from 'react-apollo'; import styled from 'styled-components'; +import { useUserId } from '../../lib/hooks/useUser'; export interface PostCommentsProps { comments: Comment[]; @@ -16,11 +16,13 @@ const MarginTop = styled.div` `; const PostComments: React.FC = ({ comments, postId }) => { + const currentUserId = useUserId(); + return ( - + ); From 372f33082cf164893719e2d257d0c83290f0b05b Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 23 Jun 2019 23:32:34 +0900 Subject: [PATCH 007/736] Show action buttons only when it is ownComment --- src/components/post/PostCommentItem.tsx | 22 ++++++++++++------- src/components/post/PostCommentsList.tsx | 12 ++++++++-- src/components/post/PostReplies.tsx | 4 +++- .../post/__tests__/PostCommentItem.test.tsx | 16 +++++++++++--- .../post/__tests__/PostCommentList.test.tsx | 3 +-- .../post/__tests__/PostReplies.test.tsx | 3 ++- .../post/PostCommentsWriteContainer.tsx | 8 +++++-- src/lib/hooks/useUser.tsx | 14 ++++++++++++ 8 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 src/lib/hooks/useUser.tsx diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index 95c63ace..6c9936f5 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Comment } from '../../lib/graphql/post'; import palette from '../../lib/styles/palette'; @@ -80,6 +80,7 @@ const TogglerBlock = styled.div` export interface PostCommentItemProps { comment: Comment; + ownComment: boolean; } interface TogglerProps { @@ -99,12 +100,15 @@ const Toggler: React.FC = ({ open, onToggle, count }) => { ); }; -const PostCommentItem: React.FC = ({ comment }) => { +const PostCommentItem: React.FC = ({ + comment, + ownComment, +}) => { const { id, user, created_at, text, replies_count } = comment; const [open, onToggle] = useBoolean(false); return ( - +
= ({ comment }) => {
{formatDate(created_at)}
-
- 수정 - 삭제 -
+ {ownComment && ( +
+ 수정 + 삭제 +
+ )}
{text} @@ -132,4 +138,4 @@ const PostCommentItem: React.FC = ({ comment }) => { ); }; -export default PostCommentItem; +export default React.memo(PostCommentItem); diff --git a/src/components/post/PostCommentsList.tsx b/src/components/post/PostCommentsList.tsx index 23ed2be3..7d18d25f 100644 --- a/src/components/post/PostCommentsList.tsx +++ b/src/components/post/PostCommentsList.tsx @@ -7,13 +7,21 @@ const PostCommentsListBlock = styled.div``; export interface PostCommentsListProps { comments: Comment[]; + currentUserId: null | string; } -const PostCommentsList: React.FC = ({ comments }) => { +const PostCommentsList: React.FC = ({ + comments, + currentUserId, +}) => { return ( {comments.map(comment => ( - + ))} ); diff --git a/src/components/post/PostReplies.tsx b/src/components/post/PostReplies.tsx index 0717f1d1..e6df6390 100644 --- a/src/components/post/PostReplies.tsx +++ b/src/components/post/PostReplies.tsx @@ -6,6 +6,7 @@ import useBoolean from '../../lib/hooks/useBoolean'; import palette from '../../lib/styles/palette'; import PostCommentsWrite from './PostCommentsWrite'; import useInput from '../../lib/hooks/useInput'; +import { useUserId } from '../../lib/hooks/useUser'; const PostRepliesBlock = styled.div` border: solid 1px rgba(0, 0, 0, 0.02); @@ -58,6 +59,7 @@ const PostReplies: React.FC = ({ onHide, }) => { const [writing, onToggle] = useBoolean(false); + const currentUserId = useUserId(); const [comment, onChangeComment, onResetComment] = useInput(''); const hasComments = comments.length > 0; const onWrite = () => { @@ -77,7 +79,7 @@ const PostReplies: React.FC = ({ return ( {comments.length > 0 && } - + {hasComments && } {writing || !hasComments ? ( { const setup = (props: Partial = {}) => { const initialProps: PostCommentItemProps = { comment: sampleComment, - onLoadReplies: () => Promise.resolve(), - onReply: () => {}, + ownComment: false, }; const utils = render(); return { @@ -31,7 +30,7 @@ describe('PostCommentItem', () => { }; }; it('renders properly', () => { - const { getByText, getByAltText } = setup(); + const { getByText, getByAltText, queryByText } = setup(); getByText(sampleComment.user.username); getByText(formatDate(sampleComment.created_at)); expect(getByAltText('comment-user-thumbnail')).toHaveAttribute( @@ -39,5 +38,16 @@ describe('PostCommentItem', () => { sampleComment.user.profile.thumbnail!, ); getByText(sampleComment.text); + const edit = queryByText('수정'); + const remove = queryByText('삭제'); + expect(edit).toBeFalsy(); + expect(remove).toBeFalsy(); + }); + it('render action buttons when ownComment is true', () => { + const { getByText } = setup({ + ownComment: true, + }); + getByText('수정'); + getByText('삭제'); }); }); diff --git a/src/components/post/__tests__/PostCommentList.test.tsx b/src/components/post/__tests__/PostCommentList.test.tsx index 6fe8ee63..b5b0fb4c 100644 --- a/src/components/post/__tests__/PostCommentList.test.tsx +++ b/src/components/post/__tests__/PostCommentList.test.tsx @@ -47,8 +47,7 @@ describe('PostCommentsList', () => { const setup = (props: Partial = {}) => { const initialProps: PostCommentsListProps = { comments: sampleComments, - onReply: () => {}, - onLoadReplies: () => {}, + currentUserId: null, }; const utils = render(); return { diff --git a/src/components/post/__tests__/PostReplies.test.tsx b/src/components/post/__tests__/PostReplies.test.tsx index 2058368a..b7583a20 100644 --- a/src/components/post/__tests__/PostReplies.test.tsx +++ b/src/components/post/__tests__/PostReplies.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render, fireEvent } from 'react-testing-library'; import PostReplies, { PostRepliesProps } from '../PostReplies'; +import renderWithRedux from '../../../lib/renderWithRedux'; const sampleComments = [ { @@ -37,7 +38,7 @@ describe('PostReplies', () => { onReply: () => {}, onHide: () => {}, }; - const utils = render(); + const utils = renderWithRedux(); return { ...utils, }; diff --git a/src/containers/post/PostCommentsWriteContainer.tsx b/src/containers/post/PostCommentsWriteContainer.tsx index 95d71ae7..4e9b3ca4 100644 --- a/src/containers/post/PostCommentsWriteContainer.tsx +++ b/src/containers/post/PostCommentsWriteContainer.tsx @@ -1,8 +1,7 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import PostCommentsWrite from '../../components/post/PostCommentsWrite'; import { Mutation, MutationResult } from 'react-apollo'; import { WRITE_COMMENT, RELOAD_COMMENTS } from '../../lib/graphql/post'; -import gql from 'graphql-tag'; export interface PostCommentsWriteContainerProps { postId: string; @@ -38,6 +37,11 @@ const PostCommentsWriteContainer: React.FC = ({ }, fetchPolicy: 'network-only', }); + // window.scrollTo({ top: document.body.scrollHeight }); + const comments = document.querySelectorAll('.comment'); + if (comments.length === 0) return; + const lastComment = comments.item(comments.length - 1); + lastComment.scrollIntoView(); } catch (e) { console.log(e); } diff --git a/src/lib/hooks/useUser.tsx b/src/lib/hooks/useUser.tsx new file mode 100644 index 00000000..4dc7e407 --- /dev/null +++ b/src/lib/hooks/useUser.tsx @@ -0,0 +1,14 @@ +import { useSelector } from 'react-redux'; +import { RootState } from '../../modules'; + +const useUser = () => { + const user = useSelector((state: RootState) => state.core.user); + return user; +}; + +export const useUserId = () => { + const user = useUser(); + return user && user.id; +}; + +export default useUser; From 4a56c18ac0f0c1714b5f4d93cfbbfeace03b3880 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 24 Jun 2019 23:36:11 +0900 Subject: [PATCH 008/736] Renders removed comment --- src/components/post/PostCommentItem.tsx | 23 ++++++++++++++----- src/components/post/PostCommentsList.tsx | 2 +- .../post/__tests__/PostCommentItem.test.tsx | 21 ++++++++++++++--- .../post/__tests__/PostCommentList.test.tsx | 3 +++ .../post/__tests__/PostViewer.test.tsx | 8 +++++++ src/lib/graphql/post.ts | 8 +++++-- 6 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index 6c9936f5..fec88ad9 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { Comment } from '../../lib/graphql/post'; import palette from '../../lib/styles/palette'; import { formatDate } from '../../lib/utils'; @@ -60,7 +60,14 @@ const CommentHead = styled.div` } `; -const CommentText = styled.p``; +const CommentText = styled.p<{ deleted: boolean }>` + ${props => + props.deleted && + css` + color: ${palette.gray6}; + font-style: italic; + `} +`; const CommentFoot = styled.div` margin-top: 2rem; `; @@ -104,7 +111,7 @@ const PostCommentItem: React.FC = ({ comment, ownComment, }) => { - const { id, user, created_at, text, replies_count } = comment; + const { id, user, created_at, text, replies_count, deleted } = comment; const [open, onToggle] = useBoolean(false); return ( @@ -112,11 +119,13 @@ const PostCommentItem: React.FC = ({
comment-user-thumbnail
-
{user.username}
+
+ {user ? user.username : '알 수 없음'} +
{formatDate(created_at)}
@@ -128,7 +137,9 @@ const PostCommentItem: React.FC = ({ )}
- {text} + + {text || '삭제된 댓글입니다.'} + diff --git a/src/components/post/PostCommentsList.tsx b/src/components/post/PostCommentsList.tsx index 7d18d25f..97f93686 100644 --- a/src/components/post/PostCommentsList.tsx +++ b/src/components/post/PostCommentsList.tsx @@ -20,7 +20,7 @@ const PostCommentsList: React.FC = ({ ))} diff --git a/src/components/post/__tests__/PostCommentItem.test.tsx b/src/components/post/__tests__/PostCommentItem.test.tsx index e1701ed2..0bfc54d1 100644 --- a/src/components/post/__tests__/PostCommentItem.test.tsx +++ b/src/components/post/__tests__/PostCommentItem.test.tsx @@ -17,6 +17,15 @@ describe('PostCommentItem', () => { text: 'Hey there', replies_count: 0, created_at: '2019-06-14T14:53:11.979Z', + deleted: false, + }; + const deletedComment: Comment = { + id: '70d24d3b-a6ce-46a3-86c5-1cba95843841', + user: null, + text: null, + replies_count: 0, + created_at: '2019-06-14T14:53:11.979Z', + deleted: true, }; const setup = (props: Partial = {}) => { @@ -31,13 +40,13 @@ describe('PostCommentItem', () => { }; it('renders properly', () => { const { getByText, getByAltText, queryByText } = setup(); - getByText(sampleComment.user.username); + getByText(sampleComment.user!.username); getByText(formatDate(sampleComment.created_at)); expect(getByAltText('comment-user-thumbnail')).toHaveAttribute( 'src', - sampleComment.user.profile.thumbnail!, + sampleComment.user!.profile.thumbnail!, ); - getByText(sampleComment.text); + getByText(sampleComment.text!); const edit = queryByText('수정'); const remove = queryByText('삭제'); expect(edit).toBeFalsy(); @@ -50,4 +59,10 @@ describe('PostCommentItem', () => { getByText('수정'); getByText('삭제'); }); + it('renders deleted comment', () => { + const { getByText } = setup({ + comment: deletedComment, + }); + getByText('삭제된 댓글입니다.'); + }); }); diff --git a/src/components/post/__tests__/PostCommentList.test.tsx b/src/components/post/__tests__/PostCommentList.test.tsx index b5b0fb4c..91f69a2e 100644 --- a/src/components/post/__tests__/PostCommentList.test.tsx +++ b/src/components/post/__tests__/PostCommentList.test.tsx @@ -16,6 +16,7 @@ describe('PostCommentsList', () => { text: 'Comment #1', replies_count: 0, created_at: '2019-06-15T14:22:28.198Z', + deleted: false, }, { id: '069f8843-44bb-483b-94f7-fca4cf9fcf4a', @@ -29,6 +30,7 @@ describe('PostCommentsList', () => { text: 'Comment #2', replies_count: 0, created_at: '2019-06-17T14:19:03.263Z', + deleted: false, }, { id: 'd93d1975-f99e-415f-9d41-594a7ff6921a', @@ -42,6 +44,7 @@ describe('PostCommentsList', () => { text: 'Comment #3', replies_count: 0, created_at: '2019-06-17T14:24:14.457Z', + deleted: false, }, ]; const setup = (props: Partial = {}) => { diff --git a/src/containers/post/__tests__/PostViewer.test.tsx b/src/containers/post/__tests__/PostViewer.test.tsx index 9e19ecd9..e04cc3b1 100644 --- a/src/containers/post/__tests__/PostViewer.test.tsx +++ b/src/containers/post/__tests__/PostViewer.test.tsx @@ -64,6 +64,7 @@ describe('PostViewer', () => { replies_count: 0, created_at: '2019-06-13T14:27:25.463Z', level: 0, + deleted: false, }, { id: '0f700ebd-0dec-469a-adb8-c47bae2b8f18', @@ -78,6 +79,7 @@ describe('PostViewer', () => { replies_count: 0, created_at: '2019-06-13T14:27:31.265Z', level: 0, + deleted: false, }, { id: '90f3b558-4af3-41bb-95dd-ed9571609f25', @@ -92,6 +94,7 @@ describe('PostViewer', () => { replies_count: 0, created_at: '2019-06-13T14:27:53.898Z', level: 0, + deleted: false, }, { id: 'd4365762-08e8-4928-a387-4255e4392291', @@ -106,6 +109,7 @@ describe('PostViewer', () => { replies_count: 3, created_at: '2019-06-13T14:42:28.873Z', level: 0, + deleted: false, }, { id: '383d590d-d3ed-4f07-994e-cbea3127195d', @@ -120,6 +124,7 @@ describe('PostViewer', () => { replies_count: 0, created_at: '2019-06-13T14:42:58.100Z', level: 0, + deleted: false, }, { id: 'a0bc6974-582a-4eb1-8273-627e1df7c527', @@ -134,6 +139,7 @@ describe('PostViewer', () => { replies_count: 0, created_at: '2019-06-14T14:43:22.312Z', level: 0, + deleted: false, }, { id: '86fa0b3f-1493-424c-aff4-49e1b059caf6', @@ -148,6 +154,7 @@ describe('PostViewer', () => { replies_count: 0, created_at: '2019-06-14T14:43:27.840Z', level: 0, + deleted: false, }, { id: '4e76da59-048a-4d2d-b11f-4f6194dff4f5', @@ -162,6 +169,7 @@ describe('PostViewer', () => { replies_count: 0, created_at: '2019-06-14T14:49:08.871Z', level: 0, + deleted: false, }, ], }, diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index ec4feae5..4d598931 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -31,11 +31,12 @@ export interface Comment { profile: { thumbnail: string | null; }; - }; - text: string; + } | null; + text: string | null; replies_count: number; replies?: Comment[]; created_at: string; + deleted: boolean; } // Post Type for PostList @@ -147,6 +148,7 @@ export const READ_POST = gql` level created_at level + deleted } } } @@ -189,6 +191,7 @@ export const GET_COMMENT = gql` replies_count level created_at + deleted } } `; @@ -210,6 +213,7 @@ export const GET_REPLIES = gql` replies_count created_at level + deleted } } } From de3b3cb22c8630cfc8d5207e94bbb7420f654386 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 24 Jun 2019 23:42:53 +0900 Subject: [PATCH 009/736] Add mutation and apply react-apollo-hooks Cannot wait for react-apollo v3 !! --- package.json | 1 + src/index.tsx | 9 ++++++--- src/lib/graphql/post.ts | 6 ++++++ yarn.lock | 7 +++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 571afb05..76846b98 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "ramda": "^0.26.1", "react": "^16.8.6", "react-apollo": "^2.4.1", + "react-apollo-hooks": "^0.4.5", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.2", "react-dom": "^16.8.6", diff --git a/src/index.tsx b/src/index.tsx index a2f7c847..7850c595 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,6 +14,7 @@ import client from './lib/graphql/client'; import rootReducer from './modules'; import storage from './lib/storage'; import { setUser } from './modules/core'; +import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks'; const store = createStore(rootReducer, composeWithDevTools()); @@ -42,9 +43,11 @@ if (process.env.NODE_ENV === 'production') { ReactDOM.render( - - - + + + + + , document.getElementById('root'), diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 4d598931..42c99e8e 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -275,3 +275,9 @@ export const WRITE_COMMENT = gql` } } `; + +export const REMOVE_COMMENT = gql` + mutation RemoveComment($id: ID!) { + removeComment(id: $id) + } +`; diff --git a/yarn.lock b/yarn.lock index 5bf6e4f8..cd954ec2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9534,6 +9534,13 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-apollo-hooks@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/react-apollo-hooks/-/react-apollo-hooks-0.4.5.tgz#7fe6a8ddfdc92df2da664d399ea77a0da4a10bad" + integrity sha512-fq04h88hg4ONtlzxYInmv7Okqqstzk3UCp55jHWOlIO2lFKoZPhQrtCz7A+TrcRu8GGwtG1SsioRKN8kIQaAOQ== + dependencies: + lodash "^4.17.11" + react-apollo@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.4.1.tgz#89db63ebacf01c1603553bb476f089492aaeab2c" From f922c4413e80e86bbd886b14e1ca17eb58bdc3fd Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 25 Jun 2019 00:05:17 +0900 Subject: [PATCH 010/736] Resolve test error for ApolloHooksProvider and implement onRemove comment --- package.json | 1 + src/components/post/PostCommentItem.tsx | 4 +- .../post/__tests__/PostCommentItem.test.tsx | 13 +++- src/containers/post/PostComments.tsx | 8 ++- src/containers/post/PostRepliesContainer.tsx | 64 ++++++++----------- .../post/__tests__/PostViewer.test.tsx | 22 ++++++- yarn.lock | 51 +++++++++++++++ 7 files changed, 120 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 76846b98..90a4fff0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@typescript-eslint/eslint-plugin": "^1.9.0", "@typescript-eslint/parser": "^1.9.0", "apollo-boost": "^0.1.28", + "apollo-link-mock": "^1.0.1", "axios": "^0.18.0", "babel-core": "7.0.0-bridge.0", "babel-eslint": "9.0.0", diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index fec88ad9..31cd160d 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -88,6 +88,7 @@ const TogglerBlock = styled.div` export interface PostCommentItemProps { comment: Comment; ownComment: boolean; + onRemove: (id: string) => any; } interface TogglerProps { @@ -110,6 +111,7 @@ const Toggler: React.FC = ({ open, onToggle, count }) => { const PostCommentItem: React.FC = ({ comment, ownComment, + onRemove, }) => { const { id, user, created_at, text, replies_count, deleted } = comment; const [open, onToggle] = useBoolean(false); @@ -132,7 +134,7 @@ const PostCommentItem: React.FC = ({ {ownComment && (
수정 - 삭제 + onRemove(id)}>삭제
)} diff --git a/src/components/post/__tests__/PostCommentItem.test.tsx b/src/components/post/__tests__/PostCommentItem.test.tsx index 0bfc54d1..6f95589e 100644 --- a/src/components/post/__tests__/PostCommentItem.test.tsx +++ b/src/components/post/__tests__/PostCommentItem.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render, fireEvent } from 'react-testing-library'; import PostCommentItem, { PostCommentItemProps } from '../PostCommentItem'; import { Comment } from '../../../lib/graphql/post'; import { formatDate } from '../../../lib/utils'; @@ -32,6 +32,7 @@ describe('PostCommentItem', () => { const initialProps: PostCommentItemProps = { comment: sampleComment, ownComment: false, + onRemove: () => {}, }; const utils = render(); return { @@ -65,4 +66,14 @@ describe('PostCommentItem', () => { }); getByText('삭제된 댓글입니다.'); }); + it('calls onRemove with id when remove is clicked', () => { + const onRemove = jest.fn(); + const { getByText } = setup({ + ownComment: true, + onRemove, + }); + const remove = getByText('삭제'); + fireEvent.click(remove); + expect(onRemove).toBeCalledWith(sampleComment.id); + }); }); diff --git a/src/containers/post/PostComments.tsx b/src/containers/post/PostComments.tsx index 60f32fc4..fa4efd76 100644 --- a/src/containers/post/PostComments.tsx +++ b/src/containers/post/PostComments.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import PostCommentsTemplate from '../../components/post/PostCommentsTemplate'; import PostCommentsWriteContainer from './PostCommentsWriteContainer'; -import { Comment } from '../../lib/graphql/post'; +import { Comment, REMOVE_COMMENT } from '../../lib/graphql/post'; import PostCommentsList from '../../components/post/PostCommentsList'; import styled from 'styled-components'; import { useUserId } from '../../lib/hooks/useUser'; +import { useMutation } from 'react-apollo-hooks'; export interface PostCommentsProps { comments: Comment[]; @@ -17,6 +18,11 @@ const MarginTop = styled.div` const PostComments: React.FC = ({ comments, postId }) => { const currentUserId = useUserId(); + const removeComment = useMutation(REMOVE_COMMENT); + + const onRemove = async (id: string) => { + await removeComment({ variables: { id } }); + }; return ( diff --git a/src/containers/post/PostRepliesContainer.tsx b/src/containers/post/PostRepliesContainer.tsx index f80d3c8f..264b67a0 100644 --- a/src/containers/post/PostRepliesContainer.tsx +++ b/src/containers/post/PostRepliesContainer.tsx @@ -8,6 +8,7 @@ import { import PostReplies from '../../components/post/PostReplies'; import { useSelector } from 'react-redux'; import { RootState } from '../../modules'; +import { useQuery, useMutation } from 'react-apollo-hooks'; export interface PostRepliesProps { commentId: string; @@ -19,44 +20,33 @@ const PostRepliesContainer: React.FC = ({ onHide, }) => { const postId = useSelector((state: RootState) => state.post.id); + const replies = useQuery<{ comment: CommentWithReplies }>(GET_REPLIES, { + variables: { + id: commentId, + }, + }); + const writeComment = useMutation(WRITE_COMMENT); + const onReply = async (text: string) => { + await writeComment({ + variables: { + post_id: postId, + comment_id: commentId, + text, + }, + }); + replies.refetch(); + }; + + if (replies.loading || !replies.data) { + return null; + } + return ( - - {({ - loading, - error, - data, - refetch, - }: QueryResult<{ comment: CommentWithReplies }>) => { - if (loading || !data) return null; - return ( - - {writeComment => { - const onReply = async (text: string) => { - try { - await writeComment({ - variables: { - post_id: postId, - comment_id: commentId, - text, - }, - }); - refetch(); - } catch (e) { - console.log(e); - } - }; - return ( - - ); - }} - - ); - }} - + ); }; diff --git a/src/containers/post/__tests__/PostViewer.test.tsx b/src/containers/post/__tests__/PostViewer.test.tsx index e04cc3b1..28813587 100644 --- a/src/containers/post/__tests__/PostViewer.test.tsx +++ b/src/containers/post/__tests__/PostViewer.test.tsx @@ -5,6 +5,17 @@ import { MemoryRouter } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import { READ_POST } from '../../../lib/graphql/post'; import renderWithRedux from '../../../lib/renderWithRedux'; +import { InMemoryCache } from 'apollo-cache-inmemory'; +import { ApolloClient } from 'apollo-client'; +import { MockLink, MockedResponse } from 'apollo-link-mock'; +import { ApolloProvider } from 'react-apollo-hooks'; + +function createClient(mocks: MockedResponse[]) { + return new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mocks), + }); +} describe('PostViewer', () => { const setup = (props: Partial = {}, overrideMocks?: any) => { @@ -177,11 +188,16 @@ describe('PostViewer', () => { }, }, ]; + + const client = createClient([]); + const utils = renderWithRedux( - - - + + + + + , ); return { diff --git a/yarn.lock b/yarn.lock index cd954ec2..9f074095 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1572,6 +1572,13 @@ "@webassemblyjs/wast-parser" "1.7.11" "@xtuc/long" "4.2.1" +"@wry/equality@^0.1.2": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" + integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ== + dependencies: + tslib "^1.9.3" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -1823,6 +1830,15 @@ apollo-link-http@^1.3.1: apollo-link "^1.2.8" apollo-link-http-common "^0.2.10" +apollo-link-mock@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/apollo-link-mock/-/apollo-link-mock-1.0.1.tgz#266ebcf0b972382fd701ff0e94b292138648786f" + integrity sha512-2sUCU2KVu0dOYpviKlvH4IzHiWqf6p5D4rP6VoaQovkHS/OJm1sFnL1tExiPe3SnK6v6EoyrDqhxuDAmCfEB0w== + dependencies: + apollo-link "^1.2.3" + apollo-utilities "^1.0.25" + lodash.isequal "^4.5.0" + apollo-link-state@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.2.tgz#ac00e9be9b0ca89eae0be6ba31fe904b80bbe2e8" @@ -1838,6 +1854,16 @@ apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.8: dependencies: zen-observable-ts "^0.8.15" +apollo-link@^1.2.3: + version "1.2.12" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429" + integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.19" + apollo-utilities@1.1.3, apollo-utilities@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.1.3.tgz#a8883c0392f6b46eac0d366204ebf34be9307c87" @@ -1846,6 +1872,16 @@ apollo-utilities@1.1.3, apollo-utilities@^1.1.3: fast-json-stable-stringify "^2.0.0" tslib "^1.9.3" +apollo-utilities@^1.0.25, apollo-utilities@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" + integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== + dependencies: + "@wry/equality" "^0.1.2" + fast-json-stable-stringify "^2.0.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + apollo-utilities@^1.0.8, apollo-utilities@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.1.2.tgz#aa5eca9d1f1eb721c381a22e0dde03559d856db3" @@ -11324,6 +11360,13 @@ tryer@^1.0.0: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== +ts-invariant@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" + integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== + dependencies: + tslib "^1.9.3" + ts-pnp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.0.0.tgz#44a3a9e8c13fcb711bcda75d7b576c21af120c9d" @@ -12402,6 +12445,14 @@ zen-observable-ts@^0.8.15: dependencies: zen-observable "^0.8.0" +zen-observable-ts@^0.8.19: + version "0.8.19" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694" + integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + zen-observable@^0.8.0: version "0.8.13" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.13.tgz#a9f1b9dbdfd2d60a08761ceac6a861427d44ae2e" From 418f560c379383de2426c9de555dd598873aa0e9 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 26 Jun 2019 00:07:01 +0900 Subject: [PATCH 011/736] Implements comment removal --- src/components/common/PopupOKCancel.tsx | 8 ++- .../__snapshots__/PopupOKCancel.test.tsx.snap | 6 -- src/components/post/PostCommentItem.tsx | 3 + src/components/post/PostCommentsList.tsx | 3 + src/components/post/PostReplies.tsx | 8 ++- .../post/__tests__/PostCommentItem.test.tsx | 12 +++- src/containers/post/PostComments.tsx | 70 +++++++++++++++---- src/containers/post/PostRepliesContainer.tsx | 45 ++++++++++-- src/containers/post/PostViewer.tsx | 6 +- .../post/__tests__/PostViewer.test.tsx | 2 + src/lib/graphql/post.ts | 3 + src/lib/hooks/usePopup.tsx | 56 +++++++++++++++ 12 files changed, 191 insertions(+), 31 deletions(-) create mode 100644 src/lib/hooks/usePopup.tsx diff --git a/src/components/common/PopupOKCancel.tsx b/src/components/common/PopupOKCancel.tsx index 28f91e5b..42fe5b9e 100644 --- a/src/components/common/PopupOKCancel.tsx +++ b/src/components/common/PopupOKCancel.tsx @@ -49,9 +49,11 @@ const PopupOKCancel: React.FC = ({ {title &&

{title}

}
{children}
- + {onCancel && ( + + )}
diff --git a/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap b/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap index 13d5b27e..0735f6f1 100644 --- a/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap +++ b/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap @@ -25,12 +25,6 @@ exports[`PopupOKCancel matches snapshot 1`] = `
-
- {ownComment && ( + {ownComment && !editing && (
- 수정 + 수정 onRemove(id)}>삭제
)} - - - {text || '삭제된 댓글입니다.'} - - + {editing ? ( + + ) : ( + + + {text || '삭제된 댓글입니다.'} + + + )} - - {open && } + + {open && }
); diff --git a/src/components/post/PostCommentsWrite.tsx b/src/components/post/PostCommentsWrite.tsx index 2697365d..a5e88208 100644 --- a/src/components/post/PostCommentsWrite.tsx +++ b/src/components/post/PostCommentsWrite.tsx @@ -33,7 +33,6 @@ export interface PostCommentsWriteProps { comment: string; onChange: (e: React.ChangeEvent) => void; onWrite: () => void; - forReplies?: boolean; onCancel?: () => void; } @@ -42,7 +41,6 @@ const PostCommentsWrite: React.FC = ({ onChange, onWrite, onCancel, - forReplies, }) => { return ( @@ -52,14 +50,14 @@ const PostCommentsWrite: React.FC = ({ onChange={onChange} />
- - {forReplies && ( + {onCancel && ( )} +
); diff --git a/src/components/post/PostReplies.tsx b/src/components/post/PostReplies.tsx index 115f3f51..81bfc5ce 100644 --- a/src/components/post/PostReplies.tsx +++ b/src/components/post/PostReplies.tsx @@ -93,7 +93,6 @@ const PostReplies: React.FC = ({ onWrite={onWrite} onChange={onChangeComment} onCancel={onCancel} - forReplies /> ) : ( diff --git a/src/components/post/__tests__/PostCommentItem.test.tsx b/src/components/post/__tests__/PostCommentItem.test.tsx index 0be7d9e5..1b3fe7d0 100644 --- a/src/components/post/__tests__/PostCommentItem.test.tsx +++ b/src/components/post/__tests__/PostCommentItem.test.tsx @@ -18,6 +18,7 @@ describe('PostCommentItem', () => { replies_count: 0, created_at: '2019-06-14T14:53:11.979Z', deleted: false, + level: 0, }; const deletedComment: Comment = { id: '70d24d3b-a6ce-46a3-86c5-1cba95843841', @@ -26,6 +27,7 @@ describe('PostCommentItem', () => { replies_count: 0, created_at: '2019-06-14T14:53:11.979Z', deleted: true, + level: 0, }; const setup = (props: Partial = {}) => { diff --git a/src/components/post/__tests__/PostCommentList.test.tsx b/src/components/post/__tests__/PostCommentList.test.tsx index 91f69a2e..1ac4b749 100644 --- a/src/components/post/__tests__/PostCommentList.test.tsx +++ b/src/components/post/__tests__/PostCommentList.test.tsx @@ -17,6 +17,7 @@ describe('PostCommentsList', () => { replies_count: 0, created_at: '2019-06-15T14:22:28.198Z', deleted: false, + level: 0, }, { id: '069f8843-44bb-483b-94f7-fca4cf9fcf4a', @@ -31,6 +32,7 @@ describe('PostCommentsList', () => { replies_count: 0, created_at: '2019-06-17T14:19:03.263Z', deleted: false, + level: 0, }, { id: 'd93d1975-f99e-415f-9d41-594a7ff6921a', @@ -45,12 +47,14 @@ describe('PostCommentsList', () => { replies_count: 0, created_at: '2019-06-17T14:24:14.457Z', deleted: false, + level: 0, }, ]; const setup = (props: Partial = {}) => { const initialProps: PostCommentsListProps = { comments: sampleComments, currentUserId: null, + onRemove: () => {}, }; const utils = render(); return { diff --git a/src/components/post/__tests__/PostCommentsWrite.test.tsx b/src/components/post/__tests__/PostCommentsWrite.test.tsx index 048f9c62..4232bc96 100644 --- a/src/components/post/__tests__/PostCommentsWrite.test.tsx +++ b/src/components/post/__tests__/PostCommentsWrite.test.tsx @@ -53,13 +53,13 @@ describe('PostCommentsWrite', () => { expect(onWrite).toBeCalled(); }); - it('shows cancel button when forReplies is true', () => { - const { getByText } = setup({ forReplies: true }); + it('shows cancel button when onCancel exists', () => { + const { getByText } = setup({ onCancel: () => {} }); getByText('취소'); }); it('calls onCancel', () => { const onCancel = jest.fn(); - const { getByText } = setup({ forReplies: true, onCancel }); + const { getByText } = setup({ onCancel }); fireEvent.click(getByText('취소')); expect(onCancel).toBeCalled(); }); diff --git a/src/containers/post/PostEditComment.tsx b/src/containers/post/PostEditComment.tsx new file mode 100644 index 00000000..7ea7817c --- /dev/null +++ b/src/containers/post/PostEditComment.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import useInput from '../../lib/hooks/useInput'; +import PostCommentsWrite from '../../components/post/PostCommentsWrite'; +import { useSelector } from 'react-redux'; +import { EDIT_COMMENT } from '../../lib/graphql/post'; +import { useMutation } from 'react-apollo-hooks'; + +export interface PostEditCommentProps { + id: string; + defaultText: string; + onCancel: () => any; +} + +const PostEditComment: React.FC = ({ + id, + defaultText, + onCancel, +}) => { + const [comment, onChange] = useInput(defaultText); + const editComment = useMutation(EDIT_COMMENT); + + const onWrite = async () => { + await editComment({ + variables: { + id, + text: comment, + }, + }); + onCancel(); + }; + + return ( + + ); +}; + +export default PostEditComment; diff --git a/src/containers/post/PostRepliesContainer.tsx b/src/containers/post/PostRepliesContainer.tsx index 0bc0bceb..151a5cf6 100644 --- a/src/containers/post/PostRepliesContainer.tsx +++ b/src/containers/post/PostRepliesContainer.tsx @@ -4,6 +4,7 @@ import { CommentWithReplies, WRITE_COMMENT, REMOVE_COMMENT, + GET_COMMENTS_COUNT, } from '../../lib/graphql/post'; import PostReplies from '../../components/post/PostReplies'; import { useSelector } from 'react-redux'; @@ -30,6 +31,12 @@ const PostRepliesContainer: React.FC = ({ id: commentId, }, }); + const getCommentsCount = useQuery(GET_COMMENTS_COUNT, { + variables: { + id: postId, + }, + skip: true, + }); const writeComment = useMutation(WRITE_COMMENT); const removeComment = useMutation(REMOVE_COMMENT); @@ -42,13 +49,15 @@ const PostRepliesContainer: React.FC = ({ }, }); replies.refetch(); + getCommentsCount.refetch(); }; const onConfirmRemove = useCallback(async () => { onToggleAskRemove(); await removeComment({ variables: { id: removeId } }); replies.refetch(); - }, [onToggleAskRemove, removeComment, removeId, replies]); + getCommentsCount.refetch(); + }, [getCommentsCount, onToggleAskRemove, removeComment, removeId, replies]); const onRemove = useCallback( (id: string) => { diff --git a/src/containers/post/__tests__/PostComments.test.tsx b/src/containers/post/__tests__/PostComments.test.tsx index 4ef1cc86..3d713b68 100644 --- a/src/containers/post/__tests__/PostComments.test.tsx +++ b/src/containers/post/__tests__/PostComments.test.tsx @@ -96,6 +96,7 @@ describe('PostComments', () => { data: { post: { id: '6533da20-b351-11e8-9696-f1fffe8a36f1', + comments_count: 2, comments: sampleComments.slice(1, sampleComments.length - 1), }, }, diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index fa41fed9..0835a79e 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -157,10 +157,20 @@ export const READ_POST = gql` } `; +export const GET_COMMENTS_COUNT = gql` + query GetCommentsCount($id: ID!) { + post(id: $id) { + id + comments_count + } + } +`; + export const RELOAD_COMMENTS = gql` query ReloadComments($id: ID!) { post(id: $id) { id + comments_count comments { id user { @@ -280,6 +290,15 @@ export const WRITE_COMMENT = gql` } `; +export const EDIT_COMMENT = gql` + mutation EditComment($id: ID!, $text: String!) { + editComment(id: $id, text: $text) { + id + text + } + } +`; + export const REMOVE_COMMENT = gql` mutation RemoveComment($id: ID!) { removeComment(id: $id) diff --git a/src/lib/renderWithLib.tsx b/src/lib/renderWithLib.tsx new file mode 100644 index 00000000..e450e68a --- /dev/null +++ b/src/lib/renderWithLib.tsx @@ -0,0 +1 @@ +export default function renderWithLib() {} diff --git a/src/modules/post.ts b/src/modules/post.ts index a87214cf..7560ee18 100644 --- a/src/modules/post.ts +++ b/src/modules/post.ts @@ -4,6 +4,7 @@ import { ActionType, } from 'typesafe-actions'; import { updateKey } from '../lib/utils'; +import { RootState } from '.'; const SET_POST_ID = 'post/SET_POST_ID'; @@ -28,4 +29,6 @@ const post = createReducer(initialState).handleAction( (state, action) => updateKey(state, 'id', action.payload), ); +export const selectPostId = (state: RootState) => state.post.id; + export default post; From e34e3b884a6334b474524263cd8c0f0669d9506e Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 28 Jun 2019 23:32:29 +0900 Subject: [PATCH 016/736] Write test code for edit process --- src/components/post/PostCommentItem.tsx | 2 - src/components/post/PostCommentsWrite.tsx | 4 +- .../post/__tests__/PostCommentItem.test.tsx | 31 ++++++++++- .../post/__tests__/PostCommentsWrite.test.tsx | 6 ++- .../post/PostCommentsWriteContainer.tsx | 1 - src/containers/post/PostEditComment.tsx | 2 +- src/lib/renderWithLib.tsx | 1 - src/lib/renderWithProviders.tsx | 53 +++++++++++++++++++ 8 files changed, 92 insertions(+), 8 deletions(-) delete mode 100644 src/lib/renderWithLib.tsx create mode 100644 src/lib/renderWithProviders.tsx diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index c268a8ca..da0c1cdc 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -8,8 +8,6 @@ import { PlusBoxIcon, MinusBoxIcon } from '../../static/svg'; import { userThumbnail } from '../../static/images'; import useBoolean from '../../lib/hooks/useBoolean'; import PostRepliesContainer from '../../containers/post/PostRepliesContainer'; -import PostCommentsWrite from './PostCommentsWrite'; -import useInput from '../../lib/hooks/useInput'; import PostEditComment from '../../containers/post/PostEditComment'; const PostCommentItemBlock = styled.div` diff --git a/src/components/post/PostCommentsWrite.tsx b/src/components/post/PostCommentsWrite.tsx index a5e88208..97d877c3 100644 --- a/src/components/post/PostCommentsWrite.tsx +++ b/src/components/post/PostCommentsWrite.tsx @@ -34,6 +34,7 @@ export interface PostCommentsWriteProps { onChange: (e: React.ChangeEvent) => void; onWrite: () => void; onCancel?: () => void; + edit?: boolean; } const PostCommentsWrite: React.FC = ({ @@ -41,6 +42,7 @@ const PostCommentsWrite: React.FC = ({ onChange, onWrite, onCancel, + edit, }) => { return ( @@ -56,7 +58,7 @@ const PostCommentsWrite: React.FC = ({ )} diff --git a/src/components/post/__tests__/PostCommentItem.test.tsx b/src/components/post/__tests__/PostCommentItem.test.tsx index 1b3fe7d0..4098658a 100644 --- a/src/components/post/__tests__/PostCommentItem.test.tsx +++ b/src/components/post/__tests__/PostCommentItem.test.tsx @@ -3,6 +3,7 @@ import { render, fireEvent } from 'react-testing-library'; import PostCommentItem, { PostCommentItemProps } from '../PostCommentItem'; import { Comment } from '../../../lib/graphql/post'; import { formatDate } from '../../../lib/utils'; +import renderWithProviders from '../../../lib/renderWithProviders'; describe('PostCommentItem', () => { const sampleComment: Comment = { @@ -36,7 +37,9 @@ describe('PostCommentItem', () => { ownComment: false, onRemove: () => {}, }; - const utils = render(); + const utils = renderWithProviders( + , + ); return { ...utils, }; @@ -88,4 +91,30 @@ describe('PostCommentItem', () => { fireEvent.click(remove); expect(onRemove).toBeCalledWith(sampleComment.id); }); + it('shows PostEditComment when edit is clicked', () => { + const { getByText } = setup({ + ownComment: true, + }); + const edit = getByText('수정'); + fireEvent.click(edit); + getByText('댓글 수정'); + }); + it('simultates comment edit', () => { + const { getByText, getByDisplayValue } = setup({ + ownComment: true, + }); + const edit = getByText('수정'); + fireEvent.click(edit); + getByText('댓글 수정'); + const textarea = getByDisplayValue('Hey there') as HTMLTextAreaElement; + fireEvent.change(textarea, { + target: { + value: 'Hello world', + }, + }); + expect(textarea).toHaveProperty('value', 'Hello world'); + const cancel = getByText('취소'); + fireEvent.click(cancel); + expect(textarea).not.toBeInTheDocument(); + }); }); diff --git a/src/components/post/__tests__/PostCommentsWrite.test.tsx b/src/components/post/__tests__/PostCommentsWrite.test.tsx index 4232bc96..6b12375b 100644 --- a/src/components/post/__tests__/PostCommentsWrite.test.tsx +++ b/src/components/post/__tests__/PostCommentsWrite.test.tsx @@ -16,7 +16,7 @@ describe('PostCommentsWrite', () => { const textarea = utils.getByPlaceholderText( '댓글을 작성하세요', ) as HTMLTextAreaElement; - const writeButton = utils.getByText('댓글 작성'); + const writeButton = utils.getByText(/댓글 (작성|수정)/); return { textarea, writeButton, @@ -63,4 +63,8 @@ describe('PostCommentsWrite', () => { fireEvent.click(getByText('취소')); expect(onCancel).toBeCalled(); }); + it('shows edit text when edit is true', () => { + const { writeButton } = setup({ edit: true }); + expect(writeButton.textContent).toBe('댓글 수정'); + }); }); diff --git a/src/containers/post/PostCommentsWriteContainer.tsx b/src/containers/post/PostCommentsWriteContainer.tsx index e1d53a73..992ead96 100644 --- a/src/containers/post/PostCommentsWriteContainer.tsx +++ b/src/containers/post/PostCommentsWriteContainer.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import PostCommentsWrite from '../../components/post/PostCommentsWrite'; -import { Mutation, MutationResult } from 'react-apollo'; import { WRITE_COMMENT, RELOAD_COMMENTS } from '../../lib/graphql/post'; import { useMutation, useQuery } from 'react-apollo-hooks'; diff --git a/src/containers/post/PostEditComment.tsx b/src/containers/post/PostEditComment.tsx index 7ea7817c..17a9721d 100644 --- a/src/containers/post/PostEditComment.tsx +++ b/src/containers/post/PostEditComment.tsx @@ -1,7 +1,6 @@ import React from 'react'; import useInput from '../../lib/hooks/useInput'; import PostCommentsWrite from '../../components/post/PostCommentsWrite'; -import { useSelector } from 'react-redux'; import { EDIT_COMMENT } from '../../lib/graphql/post'; import { useMutation } from 'react-apollo-hooks'; @@ -35,6 +34,7 @@ const PostEditComment: React.FC = ({ onChange={onChange} onWrite={onWrite} onCancel={onCancel} + edit /> ); }; diff --git a/src/lib/renderWithLib.tsx b/src/lib/renderWithLib.tsx deleted file mode 100644 index e450e68a..00000000 --- a/src/lib/renderWithLib.tsx +++ /dev/null @@ -1 +0,0 @@ -export default function renderWithLib() {} diff --git a/src/lib/renderWithProviders.tsx b/src/lib/renderWithProviders.tsx new file mode 100644 index 00000000..d7f96df1 --- /dev/null +++ b/src/lib/renderWithProviders.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { MockedResponse, MockLink } from 'apollo-link-mock'; +import { ApolloClient } from 'apollo-client'; +import { ApolloProvider } from 'react-apollo-hooks'; +import { InMemoryCache } from 'apollo-boost'; +import { render } from 'react-testing-library'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; +import rootReducer, { RootState } from '../modules'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import { safe } from './utils'; + +type Options = { + initialState?: RootState; + mocks?: MockedResponse[]; + routeOptions?: { + route: string; + history?: MemoryHistory; + }; +}; + +export function createClient(mocks: MockedResponse[]) { + return new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mocks), + }); +} + +export default function renderWithProviders( + ui: React.ReactNode, + { initialState, mocks, routeOptions }: Options = {}, +) { + const store = createStore(rootReducer, initialState); + const client = createClient(mocks || []); + const history = + safe(() => routeOptions!.history) || + createMemoryHistory({ + initialEntries: [safe(() => routeOptions!.route) || '/'], + }); + + return { + ...render( + + + {ui} + + , + ), + store, + client, + }; +} From f88150a9367699932e9940f4217ff8c6de468047 Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 28 Jun 2019 23:38:06 +0900 Subject: [PATCH 017/736] Finalize comment feature --- src/components/post/PostCommentItem.tsx | 6 ++++-- src/components/post/__tests__/PostCommentItem.test.tsx | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index da0c1cdc..06a644f4 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -114,7 +114,7 @@ const PostCommentItem: React.FC = ({ ownComment, onRemove, }) => { - const { id, user, created_at, text, replies_count, deleted } = comment; + const { id, user, created_at, text, replies_count, deleted, level } = comment; const [open, onToggleOpen] = useBoolean(false); const [editing, onToggleEditing] = useBoolean(false); @@ -157,7 +157,9 @@ const PostCommentItem: React.FC = ({ )} - + {level < 3 && ( + + )} {open && }
diff --git a/src/components/post/__tests__/PostCommentItem.test.tsx b/src/components/post/__tests__/PostCommentItem.test.tsx index 4098658a..6887ed99 100644 --- a/src/components/post/__tests__/PostCommentItem.test.tsx +++ b/src/components/post/__tests__/PostCommentItem.test.tsx @@ -117,4 +117,13 @@ describe('PostCommentItem', () => { fireEvent.click(cancel); expect(textarea).not.toBeInTheDocument(); }); + it('hides reply when level is 4', () => { + const { queryByText } = setup({ + comment: { + ...sampleComment, + level: 4, + }, + }); + expect(queryByText('답글 달기')).toBeFalsy(); + }); }); From a12d14c1d6104baa33a7e25bca4c9e0167849c1c Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 28 Jun 2019 23:44:07 +0900 Subject: [PATCH 018/736] Change post preview text --- src/components/write/PublishPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/write/PublishPreview.tsx b/src/components/write/PublishPreview.tsx index 7bc383c6..3e853bc0 100644 --- a/src/components/write/PublishPreview.tsx +++ b/src/components/write/PublishPreview.tsx @@ -209,7 +209,7 @@ const PublishPreview: React.FC = ({ [], ); return ( - + Date: Fri, 28 Jun 2019 23:46:03 +0900 Subject: [PATCH 019/736] Update test code --- .../__tests__/__snapshots__/PublishPreview.test.tsx.snap | 2 +- src/containers/write/__tests__/PublishScreen.test.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/write/__tests__/__snapshots__/PublishPreview.test.tsx.snap b/src/components/write/__tests__/__snapshots__/PublishPreview.test.tsx.snap index 8e8af251..7a66b611 100644 --- a/src/components/write/__tests__/__snapshots__/PublishPreview.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/PublishPreview.test.tsx.snap @@ -6,7 +6,7 @@ exports[`PublishPreview matches snapshot 1`] = ` class="sc-bwzfXH uTxCW sc-bdVaJa ezdyV" >

- 포스트 카드 미리보기 + 포스트 미리보기

{ }); it('handles visibility properly', () => { const utils = setup(); - expect(utils.queryByText('포스트 카드 미리보기')).toBeFalsy(); + expect(utils.queryByText('포스트 미리보기')).toBeFalsy(); utils.store.dispatch(openPublish()); - utils.getByText('포스트 카드 미리보기'); + utils.getByText('포스트 미리보기'); }); it('closes PublishScreen when cancel button is clicked', async () => { const utils = setup(); utils.store.dispatch(openPublish()); - utils.getByText('포스트 카드 미리보기'); + utils.getByText('포스트 미리보기'); const cancelButton = utils.getByText('취소'); fireEvent.click(cancelButton); await wait(() => { From f16090ba8e8af82b1e8fc8d972b04f30cda0f750 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 1 Jul 2019 23:17:47 +0900 Subject: [PATCH 020/736] Minor changes in comment --- src/components/post/PostCommentItem.tsx | 2 +- src/components/post/PostCommentsWrite.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index 06a644f4..916cbe99 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -73,7 +73,7 @@ const CommentFoot = styled.div` margin-top: 2rem; `; const TogglerBlock = styled.div` - display: flex; + display: inline-flex; align-items: center; color: ${palette.teal6}; font-weight: bold; diff --git a/src/components/post/PostCommentsWrite.tsx b/src/components/post/PostCommentsWrite.tsx index 97d877c3..3e33f2bc 100644 --- a/src/components/post/PostCommentsWrite.tsx +++ b/src/components/post/PostCommentsWrite.tsx @@ -53,7 +53,7 @@ const PostCommentsWrite: React.FC = ({ />
{onCancel && ( - )} From be7f0087c7ab1e394490cd72cd2ffdb7fd368117 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 1 Jul 2019 23:30:48 +0900 Subject: [PATCH 021/736] Add editSeries field to write module --- src/modules/__tests__/write.test.ts | 8 ++++++++ src/modules/write.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/modules/__tests__/write.test.ts b/src/modules/__tests__/write.test.ts index 6d46b233..f9eedb0c 100644 --- a/src/modules/__tests__/write.test.ts +++ b/src/modules/__tests__/write.test.ts @@ -19,6 +19,7 @@ describe('write redux module', () => { isPrivate: false, urlSlug: '', thumbnail: null, + editSeries: false, }); }); describe('action handlers', () => { @@ -100,5 +101,12 @@ describe('write redux module', () => { ); expect(state.thumbnail).toBe('https://images.velog.io/sample.jpg'); }); + it('TOGGLE_EDIT_SERIES', () => { + let state = getInitialState(); + state = reducer(state, write.toggleEditSeries()); + expect(state.editSeries).toBe(true); + state = reducer(state, write.toggleEditSeries()); + expect(state.editSeries).toBe(false); + }); }); }); diff --git a/src/modules/write.ts b/src/modules/write.ts index d01291c4..44a6cc8d 100644 --- a/src/modules/write.ts +++ b/src/modules/write.ts @@ -14,6 +14,7 @@ const CHANGE_DESCRIPTION = 'write/CHANGE_DESCRIPTION'; const SET_PRIVACY = 'write/SET_PRIVACY'; const CHANGE_URL_SLUG = 'write/CHANGE_URL'; const SET_THUMBNAIL = 'write/SET_THUMBNAIL'; +const TOGGLE_EDIT_SERIES = 'write/TOGGLE_EDIT_SERIES'; export const changeMarkdown = createStandardAction(CHANGE_MARKDOWN)(); export const changeTitle = createStandardAction(CHANGE_TITLE)(); @@ -34,6 +35,9 @@ export const changeUrlSlug = createStandardAction(CHANGE_URL_SLUG)(); export const setThumbnail = createStandardAction(SET_THUMBNAIL)< string | null >(); +export const toggleEditSeries = createStandardAction(TOGGLE_EDIT_SERIES)< + undefined +>(); type ChangeMarkdown = ReturnType; type ChangeTitle = ReturnType; @@ -66,6 +70,7 @@ export type WriteState = { isPrivate: boolean; urlSlug: string; thumbnail: string | null; + editSeries: boolean; }; const initialState: WriteState = { @@ -81,6 +86,7 @@ const initialState: WriteState = { isPrivate: false, urlSlug: '', thumbnail: null, + editSeries: false, }; const write = createReducer( @@ -119,6 +125,8 @@ const write = createReducer( updateKey(state, 'urlSlug', urlSlug), [SET_THUMBNAIL]: (state, { payload: thumbnail }: SetThumbnail) => updateKey(state, 'thumbnail', thumbnail), + [TOGGLE_EDIT_SERIES]: state => + updateKey(state, 'editSeries', !state.editSeries), }, initialState, ); From 991542f47b87eb4d642db558c201768cbd9e551a Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 1 Jul 2019 23:38:40 +0900 Subject: [PATCH 022/736] Call onEdit when editSeries is clicked --- src/components/write/PublishSeriesSection.tsx | 10 +++++--- .../__tests__/PublishSeriesSection.test.tsx | 24 +++++++++++++++++++ src/containers/write/PublishScreen.tsx | 14 ++++------- .../write/PublishSeriesSectionContainer.tsx | 24 ++++++++----------- src/containers/write/PublishSettings.tsx | 22 +++++++++++++++++ .../PublishSeriesSectionContainer.test.tsx | 24 +++++++++++++++++++ 6 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 src/components/write/__tests__/PublishSeriesSection.test.tsx create mode 100644 src/containers/write/PublishSettings.tsx create mode 100644 src/containers/write/__tests__/PublishSeriesSectionContainer.test.tsx diff --git a/src/components/write/PublishSeriesSection.tsx b/src/components/write/PublishSeriesSection.tsx index acc904e7..c998f098 100644 --- a/src/components/write/PublishSeriesSection.tsx +++ b/src/components/write/PublishSeriesSection.tsx @@ -26,12 +26,16 @@ const SeriesButton = styled.button` background: #fdfdfd; } `; -export interface PublishSeriesSectionProps {} +export interface PublishSeriesSectionProps { + onEdit: () => void; +} -const PublishSeriesSection: React.FC = props => { +const PublishSeriesSection: React.FC = ({ + onEdit, +}) => { return ( - + 시리즈에 추가하기 diff --git a/src/components/write/__tests__/PublishSeriesSection.test.tsx b/src/components/write/__tests__/PublishSeriesSection.test.tsx new file mode 100644 index 00000000..d1d46794 --- /dev/null +++ b/src/components/write/__tests__/PublishSeriesSection.test.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { render, fireEvent } from 'react-testing-library'; +import PublishSeriesSection, { + PublishSeriesSectionProps, +} from '../PublishSeriesSection'; + +describe('PublishSeriesSection', () => { + const setup = (props: Partial = {}) => { + const initialProps: PublishSeriesSectionProps = { + onEdit: () => {}, + }; + const utils = render(); + return { + ...utils, + }; + }; + it('calls onEditSeries', () => { + const onEdit = jest.fn(); + const { getByText } = setup({ onEdit }); + const button = getByText('시리즈에 추가하기'); + fireEvent.click(button); + expect(onEdit).toBeCalled(); + }); +}); diff --git a/src/containers/write/PublishScreen.tsx b/src/containers/write/PublishScreen.tsx index 17a47c7a..8c0817b2 100644 --- a/src/containers/write/PublishScreen.tsx +++ b/src/containers/write/PublishScreen.tsx @@ -8,6 +8,8 @@ import PublishURLSettingContainer from './PublishURLSettingContainer'; import PublishSeriesSectionContainer from './PublishSeriesSectionContainer'; import PublishActionButtonsContainer from './PublishActionButtonsContainer'; +import PublishSettings from './PublishSettings'; +import useBoolean from '../../lib/hooks/useBoolean'; interface OwnProps {} @@ -21,20 +23,12 @@ type DispatchProps = typeof mapDispatchToProps; export type PublishScreenProps = OwnProps & StateProps & DispatchProps; const PublishScreen: React.FC = ({ visible }) => { + const [editSeries, onToggleEditSeries] = useBoolean(false); return ( } - right={ - <> -
- - - -
- - - } + right={} /> ); }; diff --git a/src/containers/write/PublishSeriesSectionContainer.tsx b/src/containers/write/PublishSeriesSectionContainer.tsx index 69c317cd..98331e25 100644 --- a/src/containers/write/PublishSeriesSectionContainer.tsx +++ b/src/containers/write/PublishSeriesSectionContainer.tsx @@ -1,23 +1,19 @@ import * as React from 'react'; -import { connect } from 'react-redux'; -import { RootState } from '../../modules'; -import PublishSeriesSection from '../../components/write/PublishSeriesSection'; -interface OwnProps {} -type StateProps = ReturnType; -type DispatchProps = typeof mapDispatchToProps; -type PublishSeriesSectionContainerProps = OwnProps & StateProps & DispatchProps; +import PublishSeriesSection from '../../components/write/PublishSeriesSection'; +import { useDispatch } from 'react-redux'; +import { toggleEditSeries } from '../../modules/write'; -const mapStateToProps = (state: RootState) => ({}); -const mapDispatchToProps = {}; +export interface PublishSeriesSectionContainerProps {} const PublishSeriesSectionContainer: React.FC< PublishSeriesSectionContainerProps > = props => { - return ; + const dispatch = useDispatch(); + const onEdit = () => { + dispatch(toggleEditSeries()); + }; + return ; }; -export default connect( - mapStateToProps, - mapDispatchToProps, -)(PublishSeriesSectionContainer); +export default PublishSeriesSectionContainer; diff --git a/src/containers/write/PublishSettings.tsx b/src/containers/write/PublishSettings.tsx new file mode 100644 index 00000000..a6748d8d --- /dev/null +++ b/src/containers/write/PublishSettings.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PublishPrivacySettingContainer from './PublishPrivacySettingContainer'; +import PublishURLSettingContainer from './PublishURLSettingContainer'; +import PublishSeriesSectionContainer from './PublishSeriesSectionContainer'; +import PublishActionButtonsContainer from './PublishActionButtonsContainer'; + +export interface PublishSettingsProps {} + +const PublishSettings: React.FC = props => { + return ( + <> +
+ + + +
+ + + ); +}; + +export default PublishSettings; diff --git a/src/containers/write/__tests__/PublishSeriesSectionContainer.test.tsx b/src/containers/write/__tests__/PublishSeriesSectionContainer.test.tsx new file mode 100644 index 00000000..346a9add --- /dev/null +++ b/src/containers/write/__tests__/PublishSeriesSectionContainer.test.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import PublishSeriesSectionContainer, { + PublishSeriesSectionContainerProps, +} from '../PublishSeriesSectionContainer'; +import renderWithProviders from '../../../lib/renderWithProviders'; +import { fireEvent } from 'react-testing-library'; + +describe('PublishSeriesSectionContainer', () => { + const setup = (props: Partial = {}) => { + const initialProps: PublishSeriesSectionContainerProps = {}; + const utils = renderWithProviders( + , + ); + return { + ...utils, + }; + }; + it('renders properly', () => { + const { getByText, store } = setup(); + const button = getByText('시리즈에 추가하기'); + fireEvent.click(button); + expect(store.getState().write.editSeries).toBe(true); + }); +}); From 4e5a519b993bd1190a9b48a9ce5594c79af9af50 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 1 Jul 2019 23:55:53 +0900 Subject: [PATCH 023/736] Initialize PublishSeriesConfig --- src/components/write/PublishSeriesCreate.tsx | 34 +++++++++++++++++++ src/containers/write/PublishScreen.tsx | 33 ++++++------------ src/containers/write/PublishSeriesConfig.tsx | 15 ++++++++ .../write/__tests__/PublishScreen.test.tsx | 8 +++++ 4 files changed, 67 insertions(+), 23 deletions(-) create mode 100644 src/components/write/PublishSeriesCreate.tsx create mode 100644 src/containers/write/PublishSeriesConfig.tsx diff --git a/src/components/write/PublishSeriesCreate.tsx b/src/components/write/PublishSeriesCreate.tsx new file mode 100644 index 00000000..433cb495 --- /dev/null +++ b/src/components/write/PublishSeriesCreate.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import styled from 'styled-components'; +import palette from '../../lib/styles/palette'; + +const PublishSeriesCreateBlock = styled.div` + background: ${palette.gray2}; + padding: 1rem; +`; + +const Input = styled.input` + height: 2rem; + width: 100%; + padding: 0.5rem; + font-size: 0.875rem; + border-radius: 2px; + border: none; + outline: none; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.03); + &::placeholder { + color: ${palette.gray5}; + } +`; + +export interface PublishSeriesCreateProps {} + +const PublishSeriesCreate: React.FC = props => { + return ( + + + + ); +}; + +export default PublishSeriesCreate; diff --git a/src/containers/write/PublishScreen.tsx b/src/containers/write/PublishScreen.tsx index 8c0817b2..7924380e 100644 --- a/src/containers/write/PublishScreen.tsx +++ b/src/containers/write/PublishScreen.tsx @@ -1,39 +1,26 @@ import * as React from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { RootState } from '../../modules'; import PublishScreenTemplate from '../../components/write/PublishScreenTemplate'; import PublishPreviewContainer from './PublishPreviewContainer'; -import PublishPrivacySettingContainer from './PublishPrivacySettingContainer'; -import PublishURLSettingContainer from './PublishURLSettingContainer'; -import PublishSeriesSectionContainer from './PublishSeriesSectionContainer'; - -import PublishActionButtonsContainer from './PublishActionButtonsContainer'; import PublishSettings from './PublishSettings'; -import useBoolean from '../../lib/hooks/useBoolean'; - -interface OwnProps {} +import PublishSeriesConfig from './PublishSeriesConfig'; -const mapStateToProps = ({ write }: RootState) => ({ - visible: write.publish, -}); -const mapDispatchToProps = {}; +export interface PublishScreenProps {} -type StateProps = ReturnType; -type DispatchProps = typeof mapDispatchToProps; -export type PublishScreenProps = OwnProps & StateProps & DispatchProps; +const PublishScreen: React.FC = () => { + const { visible, editSeries } = useSelector((state: RootState) => ({ + visible: state.write.publish, + editSeries: state.write.editSeries, + })); -const PublishScreen: React.FC = ({ visible }) => { - const [editSeries, onToggleEditSeries] = useBoolean(false); return ( } - right={} + right={editSeries ? : } /> ); }; -export default connect( - mapStateToProps, - mapDispatchToProps, -)(PublishScreen); +export default PublishScreen; diff --git a/src/containers/write/PublishSeriesConfig.tsx b/src/containers/write/PublishSeriesConfig.tsx new file mode 100644 index 00000000..80e8440a --- /dev/null +++ b/src/containers/write/PublishSeriesConfig.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PublishSection from '../../components/write/PublishSection'; +import PublishSeriesCreate from '../../components/write/PublishSeriesCreate'; + +export interface PublishSeriesConfigProps {} + +const PublishSeriesConfig: React.FC = props => { + return ( + + + + ); +}; + +export default PublishSeriesConfig; diff --git a/src/containers/write/__tests__/PublishScreen.test.tsx b/src/containers/write/__tests__/PublishScreen.test.tsx index ff6da7df..3ba34cbc 100644 --- a/src/containers/write/__tests__/PublishScreen.test.tsx +++ b/src/containers/write/__tests__/PublishScreen.test.tsx @@ -8,6 +8,7 @@ import { closePublish, openPublish, setDefaultDescription, + toggleEditSeries, } from '../../../modules/write'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -64,4 +65,11 @@ describe('PublishScreen', () => { expect(utils.store.getState().write.description).toBe('helloworld'); expect(textarea.value).toBe('helloworld'); }); + it('hides PublishSettins when editSeries is true', () => { + const utils = setup(); + utils.store.dispatch(openPublish()); + utils.store.dispatch(toggleEditSeries()); + const text = utils.queryByText('공개 설정'); + expect(text).toBeFalsy(); + }); }); From 3af6edd91893a694e5e6ddf530422985f8a41b33 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 3 Jul 2019 00:01:26 +0900 Subject: [PATCH 024/736] Implement PublishSeriesCreate basic feature --- src/components/write/PublishSeriesCreate.tsx | 147 +++++++++++++++++- .../__tests__/PublishSeriesCreate.test.tsx | 24 +++ 2 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 src/components/write/__tests__/PublishSeriesCreate.test.tsx diff --git a/src/components/write/PublishSeriesCreate.tsx b/src/components/write/PublishSeriesCreate.tsx index 433cb495..f138dc38 100644 --- a/src/components/write/PublishSeriesCreate.tsx +++ b/src/components/write/PublishSeriesCreate.tsx @@ -1,10 +1,40 @@ -import React from 'react'; -import styled from 'styled-components'; +import React, { useState, useEffect } from 'react'; +import styled, { css, keyframes } from 'styled-components'; import palette from '../../lib/styles/palette'; +import useBoolean from '../../lib/hooks/useBoolean'; +import OutsideClickHandler from 'react-outside-click-handler'; +import Button from '../common/Button'; +import useInputs from '../../lib/hooks/useInputs'; +import { escapeForUrl } from '../../lib/utils'; -const PublishSeriesCreateBlock = styled.div` +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const fadeOut = keyframes` + from { + opacity: 1; + } + to { + opacity: 0; + } +`; + +const PublishSeriesCreateBlock = styled.div<{ open: boolean }>` background: ${palette.gray2}; padding: 1rem; + height: 4rem; + transition: 0.125s all ease-in; + ${props => + props.open && + css` + height: 9rem; + `} `; const Input = styled.input` @@ -16,18 +46,123 @@ const Input = styled.input` border: none; outline: none; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.03); + color: ${palette.gray8}; &::placeholder { color: ${palette.gray5}; } `; +const URLInput = styled.div` + height: 2rem; + width: 100%; + display: flex; + padding: 0.5rem; + font-size: 0.75rem; + border-radius: 2px; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.03); + background: white; + color: ${palette.gray6}; + input { + color: ${palette.gray8}; + flex: 1; + outline: none; + border: none; + font-size: inherit; + } +`; + +const OpenBlock = styled.div<{ disappear: boolean }>` + margin-top: 0.5rem; + animation-duration: 0.125s; + animation-timing-function: ease-out; + animation-name: ${fadeIn}; + animation-fill-mode: forwards; + + ${props => + props.disappear && + css` + animation-name: ${fadeOut}; + `} +`; + +const Buttons = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 0.5rem; +`; + export interface PublishSeriesCreateProps {} const PublishSeriesCreate: React.FC = props => { + const [open, toggleOpen, setOpen] = useBoolean(false); + const [showOpenBlock, setShowOpenBlock] = useState(false); + const [disappear, setDisappear] = useState(false); + const [form, onChange] = useInputs({ + name: '', + urlSlug: '', + }); + const [defaultUrlSlug, setDefaultUrlSlug] = useState(''); + + useEffect(() => { + let timeoutId: number | null = null; + if (open) { + timeoutId = setTimeout(() => setShowOpenBlock(true), 125); + } else { + setShowOpenBlock(false); + } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [open]); + + useEffect(() => { + setDefaultUrlSlug(escapeForUrl(form.name)); + }, [form.name]); + + const onHide = () => { + setDisappear(true); + setTimeout(() => { + setOpen(false); + setDisappear(false); + setShowOpenBlock(false); + }, 125); + }; + return ( - - - + + + setOpen(true)} + name="name" + onChange={onChange} + value={form.name} + /> + {showOpenBlock && ( + + + /@velopert/series/ + + + + + + + + )} + + ); }; diff --git a/src/components/write/__tests__/PublishSeriesCreate.test.tsx b/src/components/write/__tests__/PublishSeriesCreate.test.tsx new file mode 100644 index 00000000..88f4fe10 --- /dev/null +++ b/src/components/write/__tests__/PublishSeriesCreate.test.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { render, fireEvent, waitForElement } from 'react-testing-library'; +import PublishSeriesCreate, { + PublishSeriesCreateProps, +} from '../PublishSeriesCreate'; + +describe('PublishSeriesCreate', () => { + const setup = (props: Partial = {}) => { + const initialProps: PublishSeriesCreateProps = {}; + const utils = render(); + return { + ...utils, + }; + }; + + it('shows openblock when focused to series input', async () => { + const { getByText, getByPlaceholderText } = setup(); + const seriesInput = getByPlaceholderText('새로운 시리즈 이름을 입력하세요'); + fireEvent.focus(seriesInput); + await waitForElement(() => getByText('시리즈 추가')); + }); + + it('handles change event from inputs', () => {}); +}); From be1623632d45136fd599cd3f5eacc2aea4462e26 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 3 Jul 2019 23:26:45 +0900 Subject: [PATCH 025/736] Implement PublishSeriesCreate --- src/components/write/PublishSeriesCreate.tsx | 33 ++++++--- .../__tests__/PublishSeriesCreate.test.tsx | 73 ++++++++++++++++++- src/containers/write/PublishSeriesConfig.tsx | 2 +- 3 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/components/write/PublishSeriesCreate.tsx b/src/components/write/PublishSeriesCreate.tsx index f138dc38..c0509a26 100644 --- a/src/components/write/PublishSeriesCreate.tsx +++ b/src/components/write/PublishSeriesCreate.tsx @@ -1,7 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, FormEvent } from 'react'; import styled, { css, keyframes } from 'styled-components'; import palette from '../../lib/styles/palette'; -import useBoolean from '../../lib/hooks/useBoolean'; import OutsideClickHandler from 'react-outside-click-handler'; import Button from '../common/Button'; import useInputs from '../../lib/hooks/useInputs'; @@ -25,7 +24,7 @@ const fadeOut = keyframes` } `; -const PublishSeriesCreateBlock = styled.div<{ open: boolean }>` +const PublishSeriesCreateBlock = styled.form<{ open: boolean }>` background: ${palette.gray2}; padding: 1rem; height: 4rem; @@ -91,10 +90,16 @@ const Buttons = styled.div` margin-top: 0.5rem; `; -export interface PublishSeriesCreateProps {} +export interface PublishSeriesCreateProps { + onSubmit: (form: { name: string; urlSlug: string }) => void; + username: string; +} -const PublishSeriesCreate: React.FC = props => { - const [open, toggleOpen, setOpen] = useBoolean(false); +const PublishSeriesCreate: React.FC = ({ + onSubmit, + username, +}) => { + const [open, setOpen] = useState(false); const [showOpenBlock, setShowOpenBlock] = useState(false); const [disappear, setDisappear] = useState(false); const [form, onChange] = useInputs({ @@ -130,9 +135,17 @@ const PublishSeriesCreate: React.FC = props => { }, 125); }; + const submit = (e: FormEvent) => { + e.preventDefault(); + onSubmit({ + name: form.name, + urlSlug: form.urlSlug || defaultUrlSlug, + }); + }; + return ( - + setOpen(true)} @@ -143,7 +156,7 @@ const PublishSeriesCreate: React.FC = props => { {showOpenBlock && ( - /@velopert/series/ + /@{username}/series/ = props => { /> - - diff --git a/src/components/write/__tests__/PublishSeriesCreate.test.tsx b/src/components/write/__tests__/PublishSeriesCreate.test.tsx index 88f4fe10..aec07032 100644 --- a/src/components/write/__tests__/PublishSeriesCreate.test.tsx +++ b/src/components/write/__tests__/PublishSeriesCreate.test.tsx @@ -1,12 +1,20 @@ import * as React from 'react'; -import { render, fireEvent, waitForElement } from 'react-testing-library'; +import { + render, + fireEvent, + waitForElement, + waitForElementToBeRemoved, +} from 'react-testing-library'; import PublishSeriesCreate, { PublishSeriesCreateProps, } from '../PublishSeriesCreate'; describe('PublishSeriesCreate', () => { const setup = (props: Partial = {}) => { - const initialProps: PublishSeriesCreateProps = {}; + const initialProps: PublishSeriesCreateProps = { + onSubmit: () => {}, + username: 'testuser', + }; const utils = render(); return { ...utils, @@ -20,5 +28,64 @@ describe('PublishSeriesCreate', () => { await waitForElement(() => getByText('시리즈 추가')); }); - it('handles change event from inputs', () => {}); + it('handles change event from inputs', async () => { + const { getByPlaceholderText, getByTestId } = setup(); + const seriesInput = getByPlaceholderText('새로운 시리즈 이름을 입력하세요'); + fireEvent.focus(seriesInput); + fireEvent.change(seriesInput, { + target: { + value: '새로운 시리즈', + }, + }); + expect(seriesInput).toHaveProperty('value', '새로운 시리즈'); + const urlSlugInput = await waitForElement(() => + getByTestId('urlslug-input'), + ); + expect(urlSlugInput).toHaveProperty('value', '새로운-시리즈'); + fireEvent.change(urlSlugInput, { + target: { + value: 'new-series', + }, + }); + expect(urlSlugInput).toHaveProperty('value', 'new-series'); + }); + + it('hides open block when cancel button is clicked', async () => { + const { getByPlaceholderText, getByText, queryByText } = setup(); + const seriesInput = getByPlaceholderText('새로운 시리즈 이름을 입력하세요'); + fireEvent.focus(seriesInput); + const cancelButton = await waitForElement(() => getByText('취소')); + fireEvent.click(cancelButton); + await waitForElementToBeRemoved(() => queryByText('취소')); + }); + + it('calls onSubmit', async () => { + const onSubmit = jest.fn(); + const { getByPlaceholderText, getByText } = setup({ onSubmit }); + const seriesInput = getByPlaceholderText('새로운 시리즈 이름을 입력하세요'); + fireEvent.focus(seriesInput); + fireEvent.change(seriesInput, { + target: { + value: '새로운 시리즈', + }, + }); + expect(seriesInput).toHaveProperty('value', '새로운 시리즈'); + const createSeriesButton = await waitForElement(() => + getByText('시리즈 추가'), + ); + fireEvent.click(createSeriesButton); + expect(onSubmit).toHaveBeenCalledWith({ + name: '새로운 시리즈', + urlSlug: '새로운-시리즈', + }); + }); + + it('shows username recevied via props', async () => { + const { getByPlaceholderText, getByText } = setup({ + username: 'helloworld', + }); + const seriesInput = getByPlaceholderText('새로운 시리즈 이름을 입력하세요'); + fireEvent.focus(seriesInput); + await waitForElement(() => getByText(`/@helloworld/series/`)); + }); }); diff --git a/src/containers/write/PublishSeriesConfig.tsx b/src/containers/write/PublishSeriesConfig.tsx index 80e8440a..4400da5d 100644 --- a/src/containers/write/PublishSeriesConfig.tsx +++ b/src/containers/write/PublishSeriesConfig.tsx @@ -7,7 +7,7 @@ export interface PublishSeriesConfigProps {} const PublishSeriesConfig: React.FC = props => { return ( - + {}} username="fakeusername" /> ); }; From a4d0d70c5c30b81a4e13e3a0aea3a025b73ecccb Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 4 Jul 2019 00:02:54 +0900 Subject: [PATCH 026/736] Initialize SelectableList component --- src/components/common/SelectableList.tsx | 58 +++++++++++++++++++ .../write/PublishSeriesConfigTemplate.tsx | 27 +++++++++ src/containers/write/PublishSeriesConfig.tsx | 35 +++++++++-- src/containers/write/PublishSeriesList.tsx | 8 +++ 4 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 src/components/common/SelectableList.tsx create mode 100644 src/components/write/PublishSeriesConfigTemplate.tsx create mode 100644 src/containers/write/PublishSeriesList.tsx diff --git a/src/components/common/SelectableList.tsx b/src/components/common/SelectableList.tsx new file mode 100644 index 00000000..4b42ca87 --- /dev/null +++ b/src/components/common/SelectableList.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import palette from '../../lib/styles/palette'; + +const SelectableListBlock = styled.ul` + padding-left: 0; + list-style: none; + margin: 0; + background: white; +`; + +const ListItem = styled.li<{ active: boolean }>` + padding: 0.875rem 1rem; + font-size: 1rem; + line-height: 1; + color: ${palette.gray7}; + border-bottom: 1px solid ${palette.gray2}; + + ${props => + props.active && + css` + background: ${palette.teal6}; + color: white; + `}; +`; + +export interface SelectableListProps { + list: { + id: string | number; + text: string; + }[]; + selectedId: string | number | null; + className?: string; + onChangeId: (id: any) => void; +} + +const SelectableList: React.FC = ({ + className, + list, + selectedId, + onChangeId, +}) => { + return ( + + {list.map(item => ( + onChangeId(item.id)} + > + {item.text} + + ))} + + ); +}; + +export default SelectableList; diff --git a/src/components/write/PublishSeriesConfigTemplate.tsx b/src/components/write/PublishSeriesConfigTemplate.tsx new file mode 100644 index 00000000..065f51df --- /dev/null +++ b/src/components/write/PublishSeriesConfigTemplate.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from 'styled-components'; +import PublishSection from './PublishSection'; + +export interface PublishSeriesConfigTemplateProps {} + +const SeriesBlock = styled.div` + display: flex; + width: 100%; + height: 329px; + flex-direction: column; + border-radius: 2px; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.03); + overflow: hidden; +`; + +const PublishSeriesConfigTemplate: React.FC< + PublishSeriesConfigTemplateProps +> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default PublishSeriesConfigTemplate; diff --git a/src/containers/write/PublishSeriesConfig.tsx b/src/containers/write/PublishSeriesConfig.tsx index 4400da5d..9db9698e 100644 --- a/src/containers/write/PublishSeriesConfig.tsx +++ b/src/containers/write/PublishSeriesConfig.tsx @@ -1,14 +1,39 @@ -import React from 'react'; -import PublishSection from '../../components/write/PublishSection'; +import React, { useState } from 'react'; import PublishSeriesCreate from '../../components/write/PublishSeriesCreate'; +import useUser from '../../lib/hooks/useUser'; +import { safe } from '../../lib/utils'; +import PublishSeriesConfigTemplate from '../../components/write/PublishSeriesConfigTemplate'; +import PublishSeriesList from './PublishSeriesList'; export interface PublishSeriesConfigProps {} const PublishSeriesConfig: React.FC = props => { + const user = useUser(); + const [selectedId, setSelectedId] = useState(null); + + const onChangeId = (id: number) => { + if (selectedId === id) { + setSelectedId(null); + } + setSelectedId(id); + }; return ( - - {}} username="fakeusername" /> - + + {}} + username={safe(() => user!.username) || ''} + /> + + ); }; diff --git a/src/containers/write/PublishSeriesList.tsx b/src/containers/write/PublishSeriesList.tsx new file mode 100644 index 00000000..8ab10801 --- /dev/null +++ b/src/containers/write/PublishSeriesList.tsx @@ -0,0 +1,8 @@ +import SelectableList from '../../components/common/SelectableList'; +import styled from 'styled-components'; + +const PublishSeriesList = styled(SelectableList)` + flex: 1; +`; + +export default PublishSeriesList; From 0dda4998e459ade87de34a9057c0ce5a5c0a3f97 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 6 Jul 2019 00:02:39 +0900 Subject: [PATCH 027/736] Complete CREATE_SERIES_MUTATION --- src/components/common/SelectableList.tsx | 44 ++++++------ .../common/__tests__/SelectableList.test.tsx | 42 +++++++++++ src/components/write/PublishSeriesCreate.tsx | 6 +- src/containers/post/PostViewer.tsx | 1 + src/containers/write/PublishSeriesConfig.tsx | 69 ++++++++++++++++--- src/lib/graphql/series.ts | 50 ++++++++++++++ 6 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 src/components/common/__tests__/SelectableList.test.tsx create mode 100644 src/lib/graphql/series.ts diff --git a/src/components/common/SelectableList.tsx b/src/components/common/SelectableList.tsx index 4b42ca87..d1a638f1 100644 --- a/src/components/common/SelectableList.tsx +++ b/src/components/common/SelectableList.tsx @@ -7,6 +7,7 @@ const SelectableListBlock = styled.ul` list-style: none; margin: 0; background: white; + overflow-y: auto; `; const ListItem = styled.li<{ active: boolean }>` @@ -34,25 +35,28 @@ export interface SelectableListProps { onChangeId: (id: any) => void; } -const SelectableList: React.FC = ({ - className, - list, - selectedId, - onChangeId, -}) => { - return ( - - {list.map(item => ( - onChangeId(item.id)} - > - {item.text} - - ))} - - ); -}; +const SelectableList: React.ComponentType< + SelectableListProps +> = React.forwardRef( + ( + { list, selectedId, className, onChangeId }: SelectableListProps, + ref: React.Ref, + ) => { + return ( + + {list.map(item => ( + onChangeId(item.id)} + > + {item.text} + + ))} + + ); + }, +); export default SelectableList; diff --git a/src/components/common/__tests__/SelectableList.test.tsx b/src/components/common/__tests__/SelectableList.test.tsx new file mode 100644 index 00000000..dac3bbba --- /dev/null +++ b/src/components/common/__tests__/SelectableList.test.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { render, fireEvent } from 'react-testing-library'; +import SelectableList, { SelectableListProps } from '../SelectableList'; + +describe('SelectableList', () => { + const setup = (props: Partial = {}) => { + const initialProps: SelectableListProps = { + list: [ + { + id: 1, + text: '아이템 #1', + }, + { + id: 2, + text: '아이템 #2', + }, + ], + selectedId: 1, + onChangeId: () => {}, + }; + const utils = render(); + return { + ...utils, + }; + }; + it('renders properly', () => { + const { getByText } = setup(); + getByText('아이템 #1'); + getByText('아이템 #2'); + }); + it('calls onChangeId', () => { + const onChangeId = jest.fn(); + const { getByText } = setup({ onChangeId }); + fireEvent.click(getByText('아이템 #2')); + expect(onChangeId).toBeCalledWith(2); + }); + it('shows active style', () => { + const { getByText } = setup(); + const activeItem = getByText('아이템 #1'); + expect(activeItem).toHaveStyle('background: rgb(18, 184, 134);'); + }); +}); diff --git a/src/components/write/PublishSeriesCreate.tsx b/src/components/write/PublishSeriesCreate.tsx index c0509a26..ea680e4f 100644 --- a/src/components/write/PublishSeriesCreate.tsx +++ b/src/components/write/PublishSeriesCreate.tsx @@ -91,7 +91,7 @@ const Buttons = styled.div` `; export interface PublishSeriesCreateProps { - onSubmit: (form: { name: string; urlSlug: string }) => void; + onSubmit: (form: { name: string; urlSlug: string }) => any; username: string; } @@ -102,7 +102,7 @@ const PublishSeriesCreate: React.FC = ({ const [open, setOpen] = useState(false); const [showOpenBlock, setShowOpenBlock] = useState(false); const [disappear, setDisappear] = useState(false); - const [form, onChange] = useInputs({ + const [form, onChange, onReset] = useInputs({ name: '', urlSlug: '', }); @@ -141,6 +141,8 @@ const PublishSeriesCreate: React.FC = ({ name: form.name, urlSlug: form.urlSlug || defaultUrlSlug, }); + onReset(); + onHide(); }; return ( diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 89189083..b2135f03 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -25,6 +25,7 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { }} > {({ loading, error, data }: QueryResult<{ post: SinglePost }>) => { + console.log(data); if (error) { console.log(error); return null; // SHOW ERROR diff --git a/src/containers/write/PublishSeriesConfig.tsx b/src/containers/write/PublishSeriesConfig.tsx index 9db9698e..2626b5bf 100644 --- a/src/containers/write/PublishSeriesConfig.tsx +++ b/src/containers/write/PublishSeriesConfig.tsx @@ -1,35 +1,84 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import PublishSeriesCreate from '../../components/write/PublishSeriesCreate'; import useUser from '../../lib/hooks/useUser'; import { safe } from '../../lib/utils'; import PublishSeriesConfigTemplate from '../../components/write/PublishSeriesConfigTemplate'; import PublishSeriesList from './PublishSeriesList'; +import { useQuery, useMutation } from 'react-apollo-hooks'; +import { + GetSeriesListResponse, + GET_SERIES_LIST, + CREATE_SERIES, + CreateSeriesResponse, +} from '../../lib/graphql/series'; export interface PublishSeriesConfigProps {} const PublishSeriesConfig: React.FC = props => { const user = useUser(); - const [selectedId, setSelectedId] = useState(null); + const [selectedId, setSelectedId] = useState(null); + const seriesList = useQuery(GET_SERIES_LIST, { + variables: { + username: safe(() => user!.username), + }, + }); + const createSeries = useMutation(CREATE_SERIES); - const onChangeId = (id: number) => { + const serialized = useMemo(() => { + if (!seriesList.data || !seriesList.data.seriesList) return []; + return seriesList.data.seriesList + .sort((a, b) => { + // sort by date + const dateA = new Date(a.created_at); + const dateB = new Date(b.created_at); + if (dateA < dateB) return -1; + if (dateA > dateB) return 1; + return 0; + }) + .map(series => ({ + id: series.id, + text: series.name, + })); + }, [seriesList.data]); + + const onChangeId = (id: string) => { if (selectedId === id) { setSelectedId(null); } setSelectedId(id); }; + + const onCreateSeries = async ({ + name, + urlSlug, + }: { + name: string; + urlSlug: string; + }) => { + const result = await createSeries({ + variables: { + name, + url_slug: urlSlug, + }, + }); + if (!result.data) return; + await new Promise(resolve => setTimeout(resolve, 150)); + await seriesList.refetch(); + setSelectedId(result.data.createSeries.id); + const items = document.querySelectorAll('.list-item'); + if (items.length === 0) return; + const lastItem = items.item(items.length - 1); + lastItem.scrollIntoView(); + }; + return ( {}} + onSubmit={onCreateSeries} username={safe(() => user!.username) || ''} /> diff --git a/src/lib/graphql/series.ts b/src/lib/graphql/series.ts new file mode 100644 index 00000000..a9d5626b --- /dev/null +++ b/src/lib/graphql/series.ts @@ -0,0 +1,50 @@ +import gql from 'graphql-tag'; + +export type Series = { + id: string; + url_slug: string; + name: string; + created_at: string; +}; +export const GET_SERIES_LIST = gql` + query GetSeriesList($username: String) { + seriesList(username: $username) { + id + url_slug + name + created_at + } + } +`; + +export type GetSeriesListResponse = { + seriesList: Series[]; +}; + +export const CREATE_SERIES = gql` + mutation CreateSeries($name: String!, $url_slug: String!) { + createSeries(name: $name, url_slug: $url_slug) { + id + name + url_slug + created_at + } + } +`; + +export type CreateSeriesResponse = { + createSeries: Series; +}; + +/* +query { + auth { + id + series_list { + id + name + url_slug + } + } +} +*/ From bad8700badbe115940a077f699a77b2ae2a1efa8 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 7 Jul 2019 00:00:21 +0900 Subject: [PATCH 028/736] Create PublishSeriesConfigButtons --- src/components/common/Button.tsx | 37 +++++++++++++++--- .../__snapshots__/PopupOKCancel.test.tsx.snap | 2 +- src/components/write/PublishActionButtons.tsx | 21 +++++----- .../write/PublishSeriesConfigButtons.tsx | 33 ++++++++++++++++ .../write/PublishSeriesConfigTemplate.tsx | 7 +++- .../PublishSeriesConfigButtons.test.tsx | 38 +++++++++++++++++++ .../AskChangeEditor.test.tsx.snap | 4 +- .../PublishActionButtons.test.tsx.snap | 4 +- .../__snapshots__/WriteFooter.test.tsx.snap | 4 +- src/containers/post/PostViewer.tsx | 1 - src/containers/write/PublishSeriesConfig.tsx | 19 ++++++++-- .../write/__tests__/PublishScreen.test.tsx | 3 +- .../__snapshots__/ActiveEditor.test.tsx.snap | 4 +- src/lib/utils.ts | 4 ++ 14 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 src/components/write/PublishSeriesConfigButtons.tsx create mode 100644 src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index d21b7da7..f520d119 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -3,14 +3,15 @@ import styled, { css } from 'styled-components'; import { buttonColorMap } from '../../lib/styles/palette'; type ColorType = 'teal' | 'gray' | 'darkGray' | 'lightGray'; +type ButtonSize = 'medium' | 'large'; -const ButtonBlock = styled.button<{ color: ColorType; inline: boolean }>` +const ButtonBlock = styled.button<{ + color: ColorType; + inline: boolean; + size: ButtonSize; +}>` display: inline-flex; align-items: center; - height: 2rem; - padding-left: 1.25rem; - padding-right: 1.25rem; - font-size: 1rem; font-weight: bold; cursor: pointer; outline: none; @@ -32,11 +33,33 @@ const ButtonBlock = styled.button<{ color: ColorType; inline: boolean }>` margin-left: 0.5rem; } `} + + ${props => + props.size === 'medium' && + css` + height: 2rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + font-size: 1rem; + `} + + ${props => + props.size === 'large' && + css` + height: 2.5rem; + padding-left: 1.125rem; + padding-right: 1.125rem; + & + & { + margin-left: 0.875rem; + } + font-size: 1.125rem; + `} `; -interface ButtonProps extends React.HTMLProps { +interface ButtonProps extends Omit, 'size'> { color?: ColorType; inline?: boolean; + size?: ButtonSize; } const Button: React.SFC = ({ @@ -44,6 +67,7 @@ const Button: React.SFC = ({ ref, color = 'teal', inline, + size = 'medium', ...rest }) => { const htmlProps = rest as any; @@ -51,6 +75,7 @@ const Button: React.SFC = ({ { if (htmlProps.onClick) { diff --git a/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap b/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap index 0735f6f1..cece09dd 100644 --- a/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap +++ b/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap @@ -26,7 +26,7 @@ exports[`PopupOKCancel matches snapshot 1`] = ` class="button-area" > + ); }; diff --git a/src/components/write/PublishSeriesConfigButtons.tsx b/src/components/write/PublishSeriesConfigButtons.tsx new file mode 100644 index 00000000..c268baf0 --- /dev/null +++ b/src/components/write/PublishSeriesConfigButtons.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import styled from 'styled-components'; +import Button from '../common/Button'; + +const PublishSeriesConfigButtonsBlock = styled.div` + margin-top: 1rem; + padding-top: 1px; + display: flex; + justify-content: flex-end; +`; + +export interface PublishSeriesConfigButtonsProps { + onCancel: () => any; + onConfirm: () => any; +} + +const PublishSeriesConfigButtons: React.FC = ({ + onCancel, + onConfirm, +}) => { + return ( + + + + + ); +}; + +export default PublishSeriesConfigButtons; diff --git a/src/components/write/PublishSeriesConfigTemplate.tsx b/src/components/write/PublishSeriesConfigTemplate.tsx index 065f51df..1153512e 100644 --- a/src/components/write/PublishSeriesConfigTemplate.tsx +++ b/src/components/write/PublishSeriesConfigTemplate.tsx @@ -2,7 +2,9 @@ import React from 'react'; import styled from 'styled-components'; import PublishSection from './PublishSection'; -export interface PublishSeriesConfigTemplateProps {} +export interface PublishSeriesConfigTemplateProps { + buttons: React.ReactNode; +} const SeriesBlock = styled.div` display: flex; @@ -16,10 +18,11 @@ const SeriesBlock = styled.div` const PublishSeriesConfigTemplate: React.FC< PublishSeriesConfigTemplateProps -> = ({ children }) => { +> = ({ children, buttons }) => { return ( {children} + {buttons} ); }; diff --git a/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx b/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx new file mode 100644 index 00000000..66ef685f --- /dev/null +++ b/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { render, fireEvent } from 'react-testing-library'; +import PublishSeriesConfigButtons, { + PublishSeriesConfigButtonsProps, +} from '../PublishSeriesConfigButtons'; + +describe('PublishSeriesConfigButtons', () => { + const setup = (props: Partial = {}) => { + const initialProps: PublishSeriesConfigButtonsProps = { + onCancel: () => {}, + onConfirm: () => {}, + }; + const utils = render( + , + ); + return { + ...utils, + }; + }; + it('calls onCancel and onConfirm', () => { + const onCancel = jest.fn(); + const onConfirm = jest.fn(); + + const { getByText } = setup({ + onCancel, + onConfirm, + }); + + const cancelButton = getByText('취소'); + const confirmButton = getByText('선택하기'); + + fireEvent.click(cancelButton); + expect(onCancel).toBeCalled(); + + fireEvent.click(confirmButton); + expect(onConfirm).toBeCalled(); + }); +}); diff --git a/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap b/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap index 9e2b4b7b..e2bafffa 100644 --- a/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap @@ -26,13 +26,13 @@ exports[`AskChangeEditor matches snapshot 1`] = ` class="button-area" > - diff --git a/src/components/write/PublishSeriesSection.tsx b/src/components/write/PublishSeriesSection.tsx index c998f098..c71551b0 100644 --- a/src/components/write/PublishSeriesSection.tsx +++ b/src/components/write/PublishSeriesSection.tsx @@ -3,6 +3,8 @@ import styled from 'styled-components'; import PublishSection from './PublishSection'; import { AddListIcon } from '../../static/svg'; import palette from '../../lib/styles/palette'; +import { MdSettings } from 'react-icons/md'; +import { ellipsis } from '../../lib/styles/utils'; const PublishSeriesSectionBlock = styled(PublishSection)``; const SeriesButton = styled.button` @@ -26,19 +28,78 @@ const SeriesButton = styled.button` background: #fdfdfd; } `; + +const EditSeries = styled.div` + height: 3rem; + width: 100%; + display: flex; + border-radius: 4px; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.05); + overflow: hidden; + .name-wrapper { + min-width: 0; + display: flex; + align-items: center; + padding-left: 1rem; + padding-right: 1rem; + background: white; + font-size: 1.125rem; + line-height: 1; + color: ${palette.gray7}; + flex: 1; + } + .name { + ${ellipsis} + } + button { + outline: none; + color: white; + background: ${palette.teal6}; + font-size: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + border: none; + cursor: pointer; + &:hover { + background: ${palette.teal5}; + } + &:active { + background: ${palette.teal7}; + } + } +`; + export interface PublishSeriesSectionProps { onEdit: () => void; + selected: { + id: string; + name: string; + } | null; } const PublishSeriesSection: React.FC = ({ onEdit, + selected, }) => { return ( - - - 시리즈에 추가하기 - + {selected ? ( + +
+
{selected.name}
+
+ +
+ ) : ( + + + 시리즈에 추가하기 + + )}
); }; diff --git a/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx b/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx index 66ef685f..69fd67b3 100644 --- a/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx +++ b/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx @@ -9,6 +9,7 @@ describe('PublishSeriesConfigButtons', () => { const initialProps: PublishSeriesConfigButtonsProps = { onCancel: () => {}, onConfirm: () => {}, + disableConfirm: false, }; const utils = render( , @@ -35,4 +36,12 @@ describe('PublishSeriesConfigButtons', () => { fireEvent.click(confirmButton); expect(onConfirm).toBeCalled(); }); + it('disables confirm button', () => { + const { getByText } = setup({ + disableConfirm: true, + }); + + const confirmButton = getByText('선택하기') as HTMLButtonElement; + expect(confirmButton.disabled).toBe(true); + }); }); diff --git a/src/components/write/__tests__/PublishSeriesSection.test.tsx b/src/components/write/__tests__/PublishSeriesSection.test.tsx index d1d46794..95b94b6f 100644 --- a/src/components/write/__tests__/PublishSeriesSection.test.tsx +++ b/src/components/write/__tests__/PublishSeriesSection.test.tsx @@ -8,6 +8,7 @@ describe('PublishSeriesSection', () => { const setup = (props: Partial = {}) => { const initialProps: PublishSeriesSectionProps = { onEdit: () => {}, + selected: null, }; const utils = render(); return { @@ -21,4 +22,19 @@ describe('PublishSeriesSection', () => { fireEvent.click(button); expect(onEdit).toBeCalled(); }); + it('shows edit series button when selected', () => { + const sample = { + id: '03567f36-d01a-43b2-9006-006f7a9a8d8a', + name: 'sample series', + }; + const onEdit = jest.fn(); + const { getByText, getByTestId } = setup({ + selected: sample, + }); + getByText(sample.name); + + const settingButton = getByTestId('setting-button'); + fireEvent.click(settingButton); + expect(onEdit).toBeCalled(); + }); }); diff --git a/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap b/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap index e2bafffa..87cc6218 100644 --- a/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap @@ -26,13 +26,13 @@ exports[`AskChangeEditor matches snapshot 1`] = ` class="button-area" > + + + )} {series && ( diff --git a/src/components/post/__tests__/PostHead.test.tsx b/src/components/post/__tests__/PostHead.test.tsx index 1498f082..d06a397a 100644 --- a/src/components/post/__tests__/PostHead.test.tsx +++ b/src/components/post/__tests__/PostHead.test.tsx @@ -13,6 +13,9 @@ describe('PostHead', () => { thumbnail: 'https://images.velog.io/post-images/velopert/ac519a50-7732-11e9-bded-7fa91ac5b455/image.png', hideThumbnail: false, + ownPost: false, + series: null, + postId: '7ae82a11-f56a-4332-aaef-2202c80d9fdd', }; const utils = render( @@ -54,4 +57,19 @@ describe('PostHead', () => { const thumbnail = queryByAltText('post-thumbnail'); expect(thumbnail).toBeFalsy(); }); + + it('hides edit and remove when ownPost is false', () => { + const { queryByText } = setup({ + ownPost: false, + }); + const remove = queryByText('삭제'); + expect(remove).toBeFalsy(); + }); + + it('hides edit and remove when ownPost is false', () => { + const { getByText } = setup({ + ownPost: true, + }); + getByText('삭제'); + }); }); diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index d11740a7..648a733e 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -8,6 +8,7 @@ import PostComments from './PostComments'; import { RootState } from '../../modules'; import { postActions } from '../../modules/post'; import PostViewerProvider from '../../components/post/PostViewerProvider'; +import { useUserId } from '../../lib/hooks/useUser'; export interface PostViewerProps { username: string; @@ -16,6 +17,7 @@ export interface PostViewerProps { const PostViewer: React.FC = ({ username, urlSlug }) => { const postId = useSelector((state: RootState) => state.post.id); + const userId = useUserId(); const dispatch = useDispatch(); return ( @@ -51,6 +53,7 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { !!post.thumbnail && post.body.includes(post.thumbnail) } postId={post.id} + ownPost={post.user.id === userId} /> Date: Mon, 15 Jul 2019 23:39:52 +0900 Subject: [PATCH 040/736] Create removePost mutation --- src/lib/graphql/post.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 9ba4ef8a..2b6026c3 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -341,3 +341,9 @@ export const REMOVE_COMMENT = gql` removeComment(id: $id) } `; + +export const REMOVE_POST = gql` + mutation RemovePost($id: ID!) { + removePost(id: $id) + } +`; From 844ba01a446585ff4c420275991ef084c223c549 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 15 Jul 2019 23:53:04 +0900 Subject: [PATCH 041/736] Convert PostViewer to use apollo hooks --- src/containers/post/PostViewer.tsx | 95 +++--- .../post/__tests__/PostViewer.test.tsx | 286 +++++------------- 2 files changed, 133 insertions(+), 248 deletions(-) diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 648a733e..dfe4e638 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useReducer, Dispatch, useContext } from 'react'; +import React, { + createContext, + useReducer, + Dispatch, + useContext, + useEffect, +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Query, QueryResult } from 'react-apollo'; import { READ_POST, SinglePost } from '../../lib/graphql/post'; @@ -9,6 +15,7 @@ import { RootState } from '../../modules'; import { postActions } from '../../modules/post'; import PostViewerProvider from '../../components/post/PostViewerProvider'; import { useUserId } from '../../lib/hooks/useUser'; +import { useQuery } from 'react-apollo-hooks'; export interface PostViewerProps { username: string; @@ -19,52 +26,50 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { const postId = useSelector((state: RootState) => state.post.id); const userId = useUserId(); const dispatch = useDispatch(); + const readPost = useQuery<{ post: SinglePost }>(READ_POST, { + variables: { + username, + url_slug: urlSlug, + }, + }); + + const { loading, error, data } = readPost; + + useEffect(() => { + if (!data) return; + if (!data.post) return; + dispatch(postActions.setPostId(data.post.id)); + }, [data, dispatch]); + + if (error) { + console.log(error); + return null; + } + + if (loading) return null; + if (!data) return null; + + const { post } = data; + return ( - - {({ loading, error, data }: QueryResult<{ post: SinglePost }>) => { - if (error) { - console.log(error); - return null; // SHOW ERROR - } - if (loading) return null; // TODO: show placeholder - if (!data || !data.post) return null; - const { post } = data; - - if (postId !== post.id) { - dispatch(postActions.setPostId(post.id)); - } - return ( - <> - - - - - ); - }} - + + + ); }; diff --git a/src/containers/post/__tests__/PostViewer.test.tsx b/src/containers/post/__tests__/PostViewer.test.tsx index 253ff186..99b32ecf 100644 --- a/src/containers/post/__tests__/PostViewer.test.tsx +++ b/src/containers/post/__tests__/PostViewer.test.tsx @@ -17,13 +17,90 @@ function createClient(mocks: MockedResponse[]) { }); } +const samplePost = { + id: '6533da20-b351-11e8-9696-f1fffe8a36f1', + title: '제목입니다', + released_at: '2018-09-08T10:24:40.642Z', + updated_at: '2019-01-28T13:51:10.999Z', + tags: ['react', 'redux'], + body: '내용입니다', + is_markdown: true, + is_private: false, + is_temp: false, + thumbnail: + 'https://images.velog.io/post-images/velopert/654650b0-b351-11e8-9696-f1fffe8a36f1/redux.png', + user: { + id: 'c76ccc50-b34d-11e8-b01f-598f1220d1c8', + username: 'velopert', + profile: { + display_name: 'Minjun Kim', + thumbnail: + 'https://images.velog.io/profiles/velopert/thumbnails/1536400727.98.png', + short_bio: 'velopert@Laftel Inc. 재미있는것만 골라서 하는 개발자입니다.', + }, + velog_config: { + id: '5ad9f60b-4b95-42e9-a32c-eae222c79bf1', + title: 'VELOPERT.LOG', + }, + }, + comments_count: 2, + comments: [ + { + id: '70d24d3b-a6ce-46a3-86c5-1cba95843841', + user: { + id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', + username: 'blablabla', + profile: { + thumbnail: null, + }, + }, + text: 'Hey there', + replies_count: 0, + created_at: '2019-06-13T14:27:25.463Z', + level: 0, + deleted: false, + }, + { + id: '0f700ebd-0dec-469a-adb8-c47bae2b8f18', + user: { + id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', + username: 'blablabla', + profile: { + thumbnail: null, + }, + }, + text: 'Hey there', + replies_count: 0, + created_at: '2019-06-13T14:27:31.265Z', + level: 0, + deleted: false, + }, + ], + series: null, +}; + describe('PostViewer', () => { const setup = (props: Partial = {}, overrideMocks?: any) => { const initialProps: PostViewerProps = { username: 'velopert', urlSlug: 'sample', }; - const mocks = [ + + const client = createClient(overrideMocks || []); + + const utils = renderWithRedux( + + + + + , + ); + return { + ...utils, + }; + }; + it('renders post correctly', async () => { + const { getByText, getByAltText } = setup({}, [ { request: { query: READ_POST, @@ -34,185 +111,16 @@ describe('PostViewer', () => { }, result: { data: { - post: { - id: '6533da20-b351-11e8-9696-f1fffe8a36f1', - title: '제목입니다', - released_at: '2018-09-08T10:24:40.642Z', - updated_at: '2019-01-28T13:51:10.999Z', - tags: ['react', 'redux'], - body: '내용입니다', - is_markdown: true, - is_private: false, - is_temp: false, - thumbnail: - 'https://images.velog.io/post-images/velopert/654650b0-b351-11e8-9696-f1fffe8a36f1/redux.png', - user: { - id: 'c76ccc50-b34d-11e8-b01f-598f1220d1c8', - username: 'velopert', - profile: { - display_name: 'Minjun Kim', - thumbnail: - 'https://images.velog.io/profiles/velopert/thumbnails/1536400727.98.png', - short_bio: - 'velopert@Laftel Inc. 재미있는것만 골라서 하는 개발자입니다.', - }, - velog_config: { - id: '5ad9f60b-4b95-42e9-a32c-eae222c79bf1', - title: 'VELOPERT.LOG', - }, - }, - comments_count: 10, - comments: [ - { - id: '70d24d3b-a6ce-46a3-86c5-1cba95843841', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: 'Hey there', - replies_count: 0, - created_at: '2019-06-13T14:27:25.463Z', - level: 0, - deleted: false, - }, - { - id: '0f700ebd-0dec-469a-adb8-c47bae2b8f18', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: 'Hey there', - replies_count: 0, - created_at: '2019-06-13T14:27:31.265Z', - level: 0, - deleted: false, - }, - { - id: '90f3b558-4af3-41bb-95dd-ed9571609f25', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: 'Hey there', - replies_count: 0, - created_at: '2019-06-13T14:27:53.898Z', - level: 0, - deleted: false, - }, - { - id: 'd4365762-08e8-4928-a387-4255e4392291', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: 'Hey there', - replies_count: 3, - created_at: '2019-06-13T14:42:28.873Z', - level: 0, - deleted: false, - }, - { - id: '383d590d-d3ed-4f07-994e-cbea3127195d', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: 'Hey there', - replies_count: 0, - created_at: '2019-06-13T14:42:58.100Z', - level: 0, - deleted: false, - }, - { - id: 'a0bc6974-582a-4eb1-8273-627e1df7c527', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: 'aw3b5aw35w35bw3a', - replies_count: 0, - created_at: '2019-06-14T14:43:22.312Z', - level: 0, - deleted: false, - }, - { - id: '86fa0b3f-1493-424c-aff4-49e1b059caf6', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: '잘된거니?', - replies_count: 0, - created_at: '2019-06-14T14:43:27.840Z', - level: 0, - deleted: false, - }, - { - id: '4e76da59-048a-4d2d-b11f-4f6194dff4f5', - user: { - id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', - username: 'blablabla', - profile: { - thumbnail: null, - }, - }, - text: 'afasdfsdf', - replies_count: 0, - created_at: '2019-06-14T14:49:08.871Z', - level: 0, - deleted: false, - }, - ], - series: null, - }, + post: samplePost, }, }, }, - ]; - - const client = createClient([]); - - const utils = renderWithRedux( - - - - - - - , - ); - return { - ...utils, - }; - }; - it('renders post correctly', async () => { - const { getByText, getByAltText } = setup(); + ]); await waitForElement(() => getByText('제목입니다')); waitForElement(() => getByAltText('post-thumbnail')); }); it('hides thumbnail if exists in body', async () => { - const mocks = [ + const { getByText, queryByAltText } = setup({}, [ { request: { query: READ_POST, @@ -224,42 +132,14 @@ describe('PostViewer', () => { result: { data: { post: { - id: '6533da20-b351-11e8-9696-f1fffe8a36f1', - title: '제목입니다', - released_at: '2018-09-08T10:24:40.642Z', - updated_at: '2019-01-28T13:51:10.999Z', - tags: ['react', 'redux'], + ...samplePost, body: '내용입니다 ![](https://images.velog.io/post-images/velopert/654650b0-b351-11e8-9696-f1fffe8a36f1/redux.png)', - is_markdown: true, - is_private: false, - is_temp: false, - thumbnail: - 'https://images.velog.io/post-images/velopert/654650b0-b351-11e8-9696-f1fffe8a36f1/redux.png', - user: { - id: 'c76ccc50-b34d-11e8-b01f-598f1220d1c8', - username: 'velopert', - profile: { - display_name: 'Minjun Kim', - thumbnail: - 'https://images.velog.io/profiles/velopert/thumbnails/1536400727.98.png', - short_bio: - 'velopert@Laftel Inc. 재미있는것만 골라서 하는 개발자입니다.', - }, - velog_config: { - id: '5ad9f60b-4b95-42e9-a32c-eae222c79bf1', - title: 'VELOPERT.LOG', - }, - }, - comments: [], - comments_count: 0, - series: null, }, }, }, }, - ]; - const { getByText, queryByAltText } = setup(undefined, mocks); + ]); await waitForElement(() => getByText('제목입니다')); const thumbnail = queryByAltText('post-thumbnail'); expect(thumbnail).toBeFalsy(); From 06a32d1bfc30ba4687a837dd393fd63bd2ee9e48 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 15 Jul 2019 23:56:00 +0900 Subject: [PATCH 042/736] Create removePost mutation in PostViewer --- src/components/post/PostHead.tsx | 2 ++ src/containers/post/PostViewer.tsx | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index 6a64047a..6574b21b 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -82,6 +82,7 @@ export interface PostHeadProps { } | null; postId: string; ownPost: boolean; + onRemove: () => any; } const PostHead: React.FC = ({ @@ -94,6 +95,7 @@ const PostHead: React.FC = ({ series, postId, ownPost, + onRemove, }) => { return ( diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index dfe4e638..6deda24d 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -7,7 +7,7 @@ import React, { } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Query, QueryResult } from 'react-apollo'; -import { READ_POST, SinglePost } from '../../lib/graphql/post'; +import { READ_POST, SinglePost, REMOVE_POST } from '../../lib/graphql/post'; import PostHead from '../../components/post/PostHead'; import PostContent from '../../components/post/PostContent'; import PostComments from './PostComments'; @@ -15,7 +15,7 @@ import { RootState } from '../../modules'; import { postActions } from '../../modules/post'; import PostViewerProvider from '../../components/post/PostViewerProvider'; import { useUserId } from '../../lib/hooks/useUser'; -import { useQuery } from 'react-apollo-hooks'; +import { useQuery, useMutation } from 'react-apollo-hooks'; export interface PostViewerProps { username: string; @@ -23,7 +23,6 @@ export interface PostViewerProps { } const PostViewer: React.FC = ({ username, urlSlug }) => { - const postId = useSelector((state: RootState) => state.post.id); const userId = useUserId(); const dispatch = useDispatch(); const readPost = useQuery<{ post: SinglePost }>(READ_POST, { @@ -32,6 +31,7 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { url_slug: urlSlug, }, }); + const removePost = useMutation(REMOVE_POST); const { loading, error, data } = readPost; @@ -41,6 +41,15 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { dispatch(postActions.setPostId(data.post.id)); }, [data, dispatch]); + const onRemove = () => { + if (!data || !data.post) return; + removePost({ + variables: { + id: data.post.id, + }, + }); + }; + if (error) { console.log(error); return null; @@ -63,6 +72,7 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { hideThumbnail={!!post.thumbnail && post.body.includes(post.thumbnail)} postId={post.id} ownPost={post.user.id === userId} + onRemove={onRemove} /> Date: Tue, 16 Jul 2019 23:26:33 +0900 Subject: [PATCH 043/736] Implement onRemove --- src/components/common/PopupOKCancel.tsx | 1 + .../__snapshots__/PopupOKCancel.test.tsx.snap | 2 +- src/components/post/PostHead.tsx | 18 ++++++++- .../post/__tests__/PostHead.test.tsx | 25 +++++++++++- .../AskChangeEditor.test.tsx.snap | 2 +- src/containers/post/PostViewer.tsx | 38 +++++++++---------- 6 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/components/common/PopupOKCancel.tsx b/src/components/common/PopupOKCancel.tsx index 42fe5b9e..2876bdf0 100644 --- a/src/components/common/PopupOKCancel.tsx +++ b/src/components/common/PopupOKCancel.tsx @@ -20,6 +20,7 @@ const PopupOKCancelBlock = styled.div` margin-bottom: 1rem; } .button-area { + margin-top: 2rem; display: flex; justify-content: flex-end; button + button { diff --git a/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap b/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap index 2fc55695..4ddd0cde 100644 --- a/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap +++ b/src/components/common/__tests__/__snapshots__/PopupOKCancel.test.tsx.snap @@ -12,7 +12,7 @@ exports[`PopupOKCancel matches snapshot 1`] = ` class="sc-htpNat rpSHm" >

제목 diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index 6574b21b..3b031b12 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -6,6 +6,8 @@ import { formatDate } from '../../lib/utils'; import PostTags from './PostTags'; import { SeriesPost } from '../../lib/graphql/post'; import PostSeriesInfo from './PostSeriesInfo'; +import useToggle from '../../lib/hooks/useToggle'; +import PopupOKCancel from '../common/PopupOKCancel'; const PostHeadBlock = styled(VelogResponsive)` margin-top: 5.5rem; @@ -97,6 +99,12 @@ const PostHead: React.FC = ({ ownPost, onRemove, }) => { + const [askRemove, toggleAskRemove] = useToggle(false); + + const onConfirmRemove = () => { + toggleAskRemove(); + onRemove(); + }; return (

{title}

@@ -109,7 +117,7 @@ const PostHead: React.FC = ({ {ownPost && ( - + )} @@ -125,6 +133,14 @@ const PostHead: React.FC = ({ {!hideThumbnail && thumbnail && ( )} + + 정말로 삭제하시겠습니까? +
); }; diff --git a/src/components/post/__tests__/PostHead.test.tsx b/src/components/post/__tests__/PostHead.test.tsx index d06a397a..ab095a46 100644 --- a/src/components/post/__tests__/PostHead.test.tsx +++ b/src/components/post/__tests__/PostHead.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render, fireEvent } from 'react-testing-library'; import PostHead, { PostHeadProps } from '../PostHead'; import { MemoryRouter } from 'react-router'; @@ -16,6 +16,7 @@ describe('PostHead', () => { ownPost: false, series: null, postId: '7ae82a11-f56a-4332-aaef-2202c80d9fdd', + onRemove: () => {}, }; const utils = render( @@ -72,4 +73,26 @@ describe('PostHead', () => { }); getByText('삭제'); }); + + it('opens askRemove modal', () => { + const { getByText } = setup({ + ownPost: true, + }); + const removeButton = getByText('삭제'); + fireEvent.click(removeButton); + getByText('정말로 삭제하시겠습니까?'); + }); + + it('calls onRemove', () => { + const onRemove = jest.fn(); + const { getByText } = setup({ + ownPost: true, + onRemove, + }); + const removeButton = getByText('삭제'); + fireEvent.click(removeButton); + const confirm = getByText('확인'); + fireEvent.click(confirm); + expect(onRemove).toBeCalled(); + }); }); diff --git a/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap b/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap index 87cc6218..7b2af44f 100644 --- a/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/AskChangeEditor.test.tsx.snap @@ -12,7 +12,7 @@ exports[`AskChangeEditor matches snapshot 1`] = ` class="sc-htpNat rpSHm" >

마크다운 에디터로 전환 diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 6deda24d..456e3975 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -1,28 +1,25 @@ -import React, { - createContext, - useReducer, - Dispatch, - useContext, - useEffect, -} from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Query, QueryResult } from 'react-apollo'; +import React, { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; import { READ_POST, SinglePost, REMOVE_POST } from '../../lib/graphql/post'; import PostHead from '../../components/post/PostHead'; import PostContent from '../../components/post/PostContent'; import PostComments from './PostComments'; -import { RootState } from '../../modules'; import { postActions } from '../../modules/post'; import PostViewerProvider from '../../components/post/PostViewerProvider'; import { useUserId } from '../../lib/hooks/useUser'; import { useQuery, useMutation } from 'react-apollo-hooks'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; -export interface PostViewerProps { +export interface PostViewerProps extends RouteComponentProps { username: string; urlSlug: string; } -const PostViewer: React.FC = ({ username, urlSlug }) => { +const PostViewer: React.FC = ({ + username, + urlSlug, + history, +}) => { const userId = useUserId(); const dispatch = useDispatch(); const readPost = useQuery<{ post: SinglePost }>(READ_POST, { @@ -41,13 +38,16 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { dispatch(postActions.setPostId(data.post.id)); }, [data, dispatch]); - const onRemove = () => { + const onRemove = async () => { if (!data || !data.post) return; - removePost({ - variables: { - id: data.post.id, - }, - }); + try { + const result = await removePost({ + variables: { + id: data.post.id, + }, + }); + history.push('/'); + } catch (e) {} }; if (error) { @@ -84,4 +84,4 @@ const PostViewer: React.FC = ({ username, urlSlug }) => { ); }; -export default PostViewer; +export default withRouter(PostViewer); From ec08d033be6bdc18313268631f16c51e64cf5a27 Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 16 Jul 2019 23:56:01 +0900 Subject: [PATCH 044/736] Tempfix - remove Post:id from cache --- src/containers/post/PostViewer.tsx | 8 +++++++- src/lib/graphql/client.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 456e3975..2afcf19a 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -7,8 +7,9 @@ import PostComments from './PostComments'; import { postActions } from '../../modules/post'; import PostViewerProvider from '../../components/post/PostViewerProvider'; import { useUserId } from '../../lib/hooks/useUser'; -import { useQuery, useMutation } from 'react-apollo-hooks'; +import { useQuery, useMutation, useApolloClient } from 'react-apollo-hooks'; import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { NormalizedCache } from 'apollo-boost'; export interface PostViewerProps extends RouteComponentProps { username: string; @@ -28,6 +29,8 @@ const PostViewer: React.FC = ({ url_slug: urlSlug, }, }); + const client = useApolloClient(); + const removePost = useMutation(REMOVE_POST); const { loading, error, data } = readPost; @@ -46,6 +49,9 @@ const PostViewer: React.FC = ({ id: data.post.id, }, }); + // TEMP FIX + const cache = (client.cache as any).data as NormalizedCache; + cache.delete(`Post:${data.post.id}`); history.push('/'); } catch (e) {} }; diff --git a/src/lib/graphql/client.ts b/src/lib/graphql/client.ts index cc645510..8976774d 100644 --- a/src/lib/graphql/client.ts +++ b/src/lib/graphql/client.ts @@ -6,4 +6,6 @@ const client = new ApolloClient({ cache: new InMemoryCache().restore((window as any).__APOLLO_STATE__), }); +(window as any).client = client; + export default client; From 534db2a1ab1d6918d74a3613c05fa93eb8f28099 Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 18 Jul 2019 00:32:13 +0900 Subject: [PATCH 045/736] Rewrite pagination --- src/components/common/PaginateWithScroll.tsx | 40 ++++++ src/containers/main/RecentPosts.tsx | 123 ++++++++++++------- 2 files changed, 118 insertions(+), 45 deletions(-) create mode 100644 src/components/common/PaginateWithScroll.tsx diff --git a/src/components/common/PaginateWithScroll.tsx b/src/components/common/PaginateWithScroll.tsx new file mode 100644 index 00000000..932ddb02 --- /dev/null +++ b/src/components/common/PaginateWithScroll.tsx @@ -0,0 +1,40 @@ +import React, { useEffect, useCallback, useRef } from 'react'; +import { getScrollBottom } from '../../lib/utils'; + +export interface PaginateWithScrollProps { + cursor: string | null; + onLoadMore: (cursor: string) => any; +} + +const PaginateWithScroll: React.FC = ({ + cursor, + onLoadMore, +}) => { + const lastCursor = useRef(null); + + const loadMore = useCallback(() => { + if (!cursor) return; + if (cursor === lastCursor.current) return; + onLoadMore(cursor); + lastCursor.current = cursor; + }, [cursor, onLoadMore]); + + const onScroll = useCallback(() => { + const scrollBottom = getScrollBottom(); + if (scrollBottom < 768) { + loadMore(); + } + }, [loadMore]); + + useEffect(() => { + console.log('register scroll event'); + window.addEventListener('scroll', onScroll); + return () => { + console.log('unregister scroll event'); + window.removeEventListener('scroll', onScroll); + }; + }, [onScroll]); + return null; +}; + +export default PaginateWithScroll; diff --git a/src/containers/main/RecentPosts.tsx b/src/containers/main/RecentPosts.tsx index 9a274423..8c1d90e2 100644 --- a/src/containers/main/RecentPosts.tsx +++ b/src/containers/main/RecentPosts.tsx @@ -1,59 +1,92 @@ -import * as React from 'react'; +import React, { useCallback } from 'react'; import PostCardList from '../../components/common/PostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; import { QueryResult, Query } from 'react-apollo'; import ScrollingPagination from '../../components/common/ScrollingPagination'; +import { useQuery, useApolloClient } from 'react-apollo-hooks'; +import PaginateWithScroll from '../../components/common/PaginateWithScroll'; interface RecentPostsProps {} const { useState } = React; const RecentPosts: React.SFC = props => { - const [loadingMore, setLoadingMore] = useState(false); + const getPostList = useQuery<{ posts: PartialPost[] }>(GET_POST_LIST); + const client = useApolloClient(); + + const { loading, data } = getPostList; + const onLoadMore = useCallback( + (cursor: string) => { + getPostList.fetchMore({ + variables: { + cursor, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return { + posts: [...prev.posts, ...fetchMoreResult.posts], + }; + }, + }); + }, + [getPostList], + ); + + if (!data || !data.posts) return null; + + const cursor = data.posts[data.posts.length - 1].id; + return ( - - {({ - loading, - error, - data, - fetchMore, - client, - }: QueryResult<{ posts: PartialPost[] }>) => { - if (error || !data || !data.posts) return null; - return ( - <> - - { - try { - const result = await client.query<{ posts: PartialPost[] }>({ - query: GET_POST_LIST, - variables: { cursor }, - }); - client.writeQuery({ - query: GET_POST_LIST, - data: { - posts: [...data.posts, ...result.data.posts], - }, - }); - } catch (e) { - console.log(e); - } - setLoadingMore(false); - }} - onPrefetch={cursor => { - client.query({ - query: GET_POST_LIST, - variables: { cursor }, - }); - }} - /> - - ); - }} - + <> + + + ); + // const [loadingMore, setLoadingMore] = useState(false); + // return ( + // + // {({ + // loading, + // error, + // data, + // fetchMore, + // client, + // }: QueryResult<{ posts: PartialPost[] }>) => { + // if (error || !data || !data.posts) return null; + // return ( + // <> + // + // { + // try { + // const result = await client.query<{ posts: PartialPost[] }>({ + // query: GET_POST_LIST, + // variables: { cursor }, + // }); + // client.writeQuery({ + // query: GET_POST_LIST, + // data: { + // posts: [...data.posts, ...result.data.posts], + // }, + // }); + // } catch (e) { + // console.log(e); + // } + // setLoadingMore(false); + // }} + // onPrefetch={cursor => { + // client.query({ + // query: GET_POST_LIST, + // variables: { cursor }, + // }); + // }} + // /> + // + // ); + // }} + // + // ); }; export default RecentPosts; From b93bf9a48c56341410a9882bdd4ec4043f782386 Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 18 Jul 2019 23:49:43 +0900 Subject: [PATCH 046/736] Clear cache after removing post --- src/containers/main/RecentPosts.tsx | 9 +++++---- src/containers/post/PostViewer.tsx | 24 +++++++++++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/containers/main/RecentPosts.tsx b/src/containers/main/RecentPosts.tsx index 8c1d90e2..fae30e9f 100644 --- a/src/containers/main/RecentPosts.tsx +++ b/src/containers/main/RecentPosts.tsx @@ -1,19 +1,20 @@ import React, { useCallback } from 'react'; import PostCardList from '../../components/common/PostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; -import { QueryResult, Query } from 'react-apollo'; -import ScrollingPagination from '../../components/common/ScrollingPagination'; import { useQuery, useApolloClient } from 'react-apollo-hooks'; import PaginateWithScroll from '../../components/common/PaginateWithScroll'; interface RecentPostsProps {} -const { useState } = React; const RecentPosts: React.SFC = props => { const getPostList = useQuery<{ posts: PartialPost[] }>(GET_POST_LIST); const client = useApolloClient(); - const { loading, data } = getPostList; + (window as any).utils = { + client, + GET_POST_LIST, + }; + const { data } = getPostList; const onLoadMore = useCallback( (cursor: string) => { getPostList.fetchMore({ diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 2afcf19a..68e52a8f 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -49,9 +49,27 @@ const PostViewer: React.FC = ({ id: data.post.id, }, }); - // TEMP FIX - const cache = (client.cache as any).data as NormalizedCache; - cache.delete(`Post:${data.post.id}`); + + client.resetStore(); + + // UPDATE posts + // const recentPosts = client.readQuery<{ posts: PartialPost[] }>({ + // query: GET_POST_LIST, + // }); + // if (recentPosts) { + // console.log('writing...'); + // const nextPosts = recentPosts.posts.filter( + // post => post.id !== data.post.id, + // ); + // console.log(nextPosts); + // client.writeQuery<{ posts: PartialPost[] }>({ + // query: GET_POST_LIST, + // data: { + // posts: nextPosts, + // }, + // }); + // } + history.push('/'); } catch (e) {} }; From e929c8b09198a757e72d20a6b2326e134f33b64b Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 19 Jul 2019 00:03:02 +0900 Subject: [PATCH 047/736] Create CLEAR_EDITOR action --- src/containers/write/ActiveEditor.tsx | 28 +++++++++++++-------------- src/modules/__tests__/write.test.ts | 10 ++++++++++ src/modules/write.ts | 4 ++++ 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/containers/write/ActiveEditor.tsx b/src/containers/write/ActiveEditor.tsx index 5abdf38e..22f8c3e0 100644 --- a/src/containers/write/ActiveEditor.tsx +++ b/src/containers/write/ActiveEditor.tsx @@ -1,21 +1,22 @@ import * as React from 'react'; -import { connect } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../modules'; -import { WriteMode } from '../../modules/write'; +import { WriteMode, clearEditor } from '../../modules/write'; import EditorPanesContainer from './EditorPanesContainer'; import QuillEditorContainer from './QuillEditorContainer'; -interface OwnProps {} -type StateProps = ReturnType; -type DispatchProps = typeof mapDispatchToProps; -export type ActiveEditorProps = OwnProps & StateProps & DispatchProps; +type ActiveEditorProps = {}; -const mapStateToProps = ({ write }: RootState) => ({ - mode: write.mode, -}); -const mapDispatchToProps = {}; +const ActiveEditor: React.FC = () => { + const mode = useSelector((state: RootState) => state.write.mode); + const dispatch = useDispatch(); + + React.useEffect(() => { + return () => { + dispatch(clearEditor()); + }; + }, [dispatch]); -const ActiveEditor: React.FC = ({ mode }) => { return mode === WriteMode.MARKDOWN ? ( <> @@ -25,7 +26,4 @@ const ActiveEditor: React.FC = ({ mode }) => { ); }; -export default connect( - mapStateToProps, - mapDispatchToProps, -)(ActiveEditor); +export default ActiveEditor; diff --git a/src/modules/__tests__/write.test.ts b/src/modules/__tests__/write.test.ts index 4027b60b..d14bd4fe 100644 --- a/src/modules/__tests__/write.test.ts +++ b/src/modules/__tests__/write.test.ts @@ -118,5 +118,15 @@ describe('write redux module', () => { state = reducer(state, write.selectSeries(sample)); expect(state.selectedSeries).toBe(sample); }); + it('CLEAR_EDITOR', () => { + let state = getInitialState(); + state = { + ...state, + title: 'blabla', + }; + expect(state.title).toBe('blabla'); + state = reducer(state, write.clearEditor()); + expect(state.title).toBe(''); + }); }); }); diff --git a/src/modules/write.ts b/src/modules/write.ts index 9d27868f..6e49aac7 100644 --- a/src/modules/write.ts +++ b/src/modules/write.ts @@ -16,6 +16,7 @@ const CHANGE_URL_SLUG = 'write/CHANGE_URL'; const SET_THUMBNAIL = 'write/SET_THUMBNAIL'; const TOGGLE_EDIT_SERIES = 'write/TOGGLE_EDIT_SERIES'; const SELECT_SERIES = 'write/SELECT_SERIES'; +const CLEAR_EDITOR = 'write/CLEAR_EDTIOR'; export const changeMarkdown = createStandardAction(CHANGE_MARKDOWN)(); export const changeTitle = createStandardAction(CHANGE_TITLE)(); @@ -43,6 +44,7 @@ export const selectSeries = createStandardAction(SELECT_SERIES)<{ id: string; name: string; }>(); +export const clearEditor = createStandardAction(CLEAR_EDITOR)(); type ChangeMarkdown = ReturnType; type ChangeTitle = ReturnType; @@ -57,6 +59,7 @@ type SetPrivacy = ReturnType; type ChangeUrlSlug = ReturnType; type SetThumbnail = ReturnType; type SelectSeries = ReturnType; +type ClearEditor = ReturnType; export enum WriteMode { MARKDOWN = 'MARKDOWN', @@ -140,6 +143,7 @@ const write = createReducer( updateKey(state, 'editSeries', !state.editSeries), [SELECT_SERIES]: (state, action: SelectSeries) => updateKey(state, 'selectedSeries', action.payload), + [CLEAR_EDITOR]: () => initialState, }, initialState, ); From fde9962176763cdc7ed2d3639dc6776ad73f82c6 Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 19 Jul 2019 00:05:13 +0900 Subject: [PATCH 048/736] Remove utils from windows --- src/containers/main/RecentPosts.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/containers/main/RecentPosts.tsx b/src/containers/main/RecentPosts.tsx index fae30e9f..a0312b57 100644 --- a/src/containers/main/RecentPosts.tsx +++ b/src/containers/main/RecentPosts.tsx @@ -1,19 +1,14 @@ import React, { useCallback } from 'react'; import PostCardList from '../../components/common/PostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; -import { useQuery, useApolloClient } from 'react-apollo-hooks'; +import { useQuery } from 'react-apollo-hooks'; import PaginateWithScroll from '../../components/common/PaginateWithScroll'; interface RecentPostsProps {} const RecentPosts: React.SFC = props => { const getPostList = useQuery<{ posts: PartialPost[] }>(GET_POST_LIST); - const client = useApolloClient(); - (window as any).utils = { - client, - GET_POST_LIST, - }; const { data } = getPostList; const onLoadMore = useCallback( (cursor: string) => { From 103a14f462a61f6fbbf0f8636982e09fa2448d9c Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 19 Jul 2019 23:26:28 +0900 Subject: [PATCH 049/736] Create PREPARE_EDIT action --- src/containers/post/PostViewer.tsx | 23 +------------- src/modules/__tests__/write.test.ts | 24 ++++++++++++++ src/modules/write.ts | 49 +++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 68e52a8f..d5d64ea2 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -9,7 +9,6 @@ import PostViewerProvider from '../../components/post/PostViewerProvider'; import { useUserId } from '../../lib/hooks/useUser'; import { useQuery, useMutation, useApolloClient } from 'react-apollo-hooks'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { NormalizedCache } from 'apollo-boost'; export interface PostViewerProps extends RouteComponentProps { username: string; @@ -44,32 +43,12 @@ const PostViewer: React.FC = ({ const onRemove = async () => { if (!data || !data.post) return; try { - const result = await removePost({ + await removePost({ variables: { id: data.post.id, }, }); - client.resetStore(); - - // UPDATE posts - // const recentPosts = client.readQuery<{ posts: PartialPost[] }>({ - // query: GET_POST_LIST, - // }); - // if (recentPosts) { - // console.log('writing...'); - // const nextPosts = recentPosts.posts.filter( - // post => post.id !== data.post.id, - // ); - // console.log(nextPosts); - // client.writeQuery<{ posts: PartialPost[] }>({ - // query: GET_POST_LIST, - // data: { - // posts: nextPosts, - // }, - // }); - // } - history.push('/'); } catch (e) {} }; diff --git a/src/modules/__tests__/write.test.ts b/src/modules/__tests__/write.test.ts index d14bd4fe..78b8d8f9 100644 --- a/src/modules/__tests__/write.test.ts +++ b/src/modules/__tests__/write.test.ts @@ -21,6 +21,7 @@ describe('write redux module', () => { thumbnail: null, editSeries: false, selectedSeries: null, + postId: null, }); }); describe('action handlers', () => { @@ -128,5 +129,28 @@ describe('write redux module', () => { state = reducer(state, write.clearEditor()); expect(state.title).toBe(''); }); + it('PREPARE_EDIT', () => { + let state = getInitialState(); + state = reducer( + state, + write.prepareEdit({ + id: '12345', + body: '# This is markdown\nHello World', + description: 'This is markdown', + isMarkdown: true, + isPrivate: false, + series: { + name: 'sample-series', + id: '12345', + }, + tags: ['tag1', ' ㅅㅁㅎ2'], + title: 'title', + urlSlug: 'sample-url-slug', + }), + ); + expect(state.mode).toBe(write.WriteMode.MARKDOWN); + expect(state.postId).toBe('12345'); + expect(state.title).toBe('title'); + }); }); }); diff --git a/src/modules/write.ts b/src/modules/write.ts index 6e49aac7..438590df 100644 --- a/src/modules/write.ts +++ b/src/modules/write.ts @@ -17,6 +17,7 @@ const SET_THUMBNAIL = 'write/SET_THUMBNAIL'; const TOGGLE_EDIT_SERIES = 'write/TOGGLE_EDIT_SERIES'; const SELECT_SERIES = 'write/SELECT_SERIES'; const CLEAR_EDITOR = 'write/CLEAR_EDTIOR'; +const PREPARE_EDIT = 'write/PREPARE_EDIT'; export const changeMarkdown = createStandardAction(CHANGE_MARKDOWN)(); export const changeTitle = createStandardAction(CHANGE_TITLE)(); @@ -46,6 +47,25 @@ export const selectSeries = createStandardAction(SELECT_SERIES)<{ }>(); export const clearEditor = createStandardAction(CLEAR_EDITOR)(); +export type PrepareEditPayload = { + id: string; + title: string; + body: string; + tags: string[]; + description: string; + urlSlug: string; + series: { + name: string; + id: string; + } | null; + isPrivate: boolean; + isMarkdown: boolean; +}; + +export const prepareEdit = createStandardAction(PREPARE_EDIT)< + PrepareEditPayload +>(); + type ChangeMarkdown = ReturnType; type ChangeTitle = ReturnType; type SetHtml = ReturnType; @@ -60,6 +80,7 @@ type ChangeUrlSlug = ReturnType; type SetThumbnail = ReturnType; type SelectSeries = ReturnType; type ClearEditor = ReturnType; +type PrepareEdit = ReturnType; export enum WriteMode { MARKDOWN = 'MARKDOWN', @@ -84,6 +105,7 @@ export type WriteState = { id: string; name: string; } | null; + postId: null | string; }; const initialState: WriteState = { @@ -101,6 +123,7 @@ const initialState: WriteState = { thumbnail: null, editSeries: false, selectedSeries: null, + postId: null, }; const write = createReducer( @@ -144,6 +167,32 @@ const write = createReducer( [SELECT_SERIES]: (state, action: SelectSeries) => updateKey(state, 'selectedSeries', action.payload), [CLEAR_EDITOR]: () => initialState, + [PREPARE_EDIT]: (state, { payload }: PrepareEdit) => { + const { + isMarkdown, + body, + title, + tags, + description, + urlSlug, + series, + id, + isPrivate, + } = payload; + const key = isMarkdown ? 'markdown' : 'html'; + return { + ...state, + title, + tags, + description, + urlSlug, + isPrivate, + mode: isMarkdown ? WriteMode.MARKDOWN : WriteMode.WYSIWYG, + [key]: body, + selectedSeries: series, + postId: id, + }; + }, }, initialState, ); From 8c00bf6221e6b8baa993349a7a824daa1b1ab538 Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 19 Jul 2019 23:40:43 +0900 Subject: [PATCH 050/736] Implement prepare edit --- src/components/post/PostHead.tsx | 4 ++- .../post/__tests__/PostHead.test.tsx | 12 +++++++++ src/containers/post/PostViewer.tsx | 26 +++++++++++++++++++ .../post/__tests__/PostViewer.test.tsx | 2 ++ src/lib/graphql/post.ts | 4 +++ 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index 3b031b12..91785ec0 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -85,6 +85,7 @@ export interface PostHeadProps { postId: string; ownPost: boolean; onRemove: () => any; + onEdit: () => any; } const PostHead: React.FC = ({ @@ -98,6 +99,7 @@ const PostHead: React.FC = ({ postId, ownPost, onRemove, + onEdit, }) => { const [askRemove, toggleAskRemove] = useToggle(false); @@ -116,7 +118,7 @@ const PostHead: React.FC = ({

{ownPost && ( - + )} diff --git a/src/components/post/__tests__/PostHead.test.tsx b/src/components/post/__tests__/PostHead.test.tsx index ab095a46..49574227 100644 --- a/src/components/post/__tests__/PostHead.test.tsx +++ b/src/components/post/__tests__/PostHead.test.tsx @@ -17,6 +17,7 @@ describe('PostHead', () => { series: null, postId: '7ae82a11-f56a-4332-aaef-2202c80d9fdd', onRemove: () => {}, + onEdit: () => {}, }; const utils = render( @@ -95,4 +96,15 @@ describe('PostHead', () => { fireEvent.click(confirm); expect(onRemove).toBeCalled(); }); + + it('calls onEdit', () => { + const onEdit = jest.fn(); + const { getByText } = setup({ + ownPost: true, + onEdit, + }); + const editButton = getByText('수정'); + fireEvent.click(editButton); + expect(onEdit).toBeCalled(); + }); }); diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index d5d64ea2..6180ae59 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -9,6 +9,7 @@ import PostViewerProvider from '../../components/post/PostViewerProvider'; import { useUserId } from '../../lib/hooks/useUser'; import { useQuery, useMutation, useApolloClient } from 'react-apollo-hooks'; import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { prepareEdit } from '../../modules/write'; export interface PostViewerProps extends RouteComponentProps { username: string; @@ -58,6 +59,30 @@ const PostViewer: React.FC = ({ return null; } + const onEdit = () => { + if (!data) return; + const { post } = data; + dispatch( + prepareEdit({ + id: post.id, + body: post.body, + description: post.short_description, + isMarkdown: post.is_markdown, + isPrivate: post.is_private, + series: post.series + ? { + id: post.series.id, + name: post.series.name, + } + : null, + tags: post.tags, + title: post.title, + urlSlug: post.url_slug, + }), + ); + history.push('/write'); + }; + if (loading) return null; if (!data) return null; @@ -76,6 +101,7 @@ const PostViewer: React.FC = ({ postId={post.id} ownPost={post.user.id === userId} onRemove={onRemove} + onEdit={onEdit} /> Date: Fri, 19 Jul 2019 23:44:45 +0900 Subject: [PATCH 051/736] Fix withRouter HOC props type issue --- src/containers/post/PostViewer.tsx | 5 ++++- src/containers/post/__tests__/PostViewer.test.tsx | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 6180ae59..354f6b72 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -11,10 +11,13 @@ import { useQuery, useMutation, useApolloClient } from 'react-apollo-hooks'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { prepareEdit } from '../../modules/write'; -export interface PostViewerProps extends RouteComponentProps { +export interface PostViewerOwnProps { username: string; urlSlug: string; } +export interface PostViewerProps + extends PostViewerOwnProps, + RouteComponentProps {} const PostViewer: React.FC = ({ username, diff --git a/src/containers/post/__tests__/PostViewer.test.tsx b/src/containers/post/__tests__/PostViewer.test.tsx index 0e99682b..ef1c3179 100644 --- a/src/containers/post/__tests__/PostViewer.test.tsx +++ b/src/containers/post/__tests__/PostViewer.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { render, waitForElement } from 'react-testing-library'; -import PostViewer, { PostViewerProps } from '../PostViewer'; +import PostViewer, { PostViewerProps, PostViewerOwnProps } from '../PostViewer'; import { MemoryRouter } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import { READ_POST } from '../../../lib/graphql/post'; @@ -83,7 +83,7 @@ const samplePost = { describe('PostViewer', () => { const setup = (props: Partial = {}, overrideMocks?: any) => { - const initialProps: PostViewerProps = { + const initialProps: PostViewerOwnProps = { username: 'velopert', urlSlug: 'sample', }; From 891efd8e95d20f795596fc0696b18d36d651b54b Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 20 Jul 2019 23:44:31 +0900 Subject: [PATCH 052/736] Prepare EDIT_POST mutation --- package.json | 1 + .../write/PublishActionButtonsContainer.tsx | 8 ++- src/lib/graphql/post.ts | 57 +++++++++++++++++++ yarn.lock | 12 ++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 90a4fff0..5b5176ea 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "typescript": "^3.5.1", "unist-util-visit": "^1.4.0", "url-loader": "1.1.2", + "use-react-router": "^1.0.7", "webpack": "4.28.3", "webpack-bundle-analyzer": "^3.2.0", "webpack-dev-server": "3.1.14", diff --git a/src/containers/write/PublishActionButtonsContainer.tsx b/src/containers/write/PublishActionButtonsContainer.tsx index 1e667863..c3d76c30 100644 --- a/src/containers/write/PublishActionButtonsContainer.tsx +++ b/src/containers/write/PublishActionButtonsContainer.tsx @@ -7,12 +7,15 @@ import { WRITE_POST, WritePostResponse } from '../../lib/graphql/post'; import { pick } from 'ramda'; import { escapeForUrl, safe } from '../../lib/utils'; import { useMutation } from 'react-apollo-hooks'; +import useRouter from 'use-react-router'; type PublishActionButtonsContainerProps = {}; const PublishActionButtonsContainer: React.FC< PublishActionButtonsContainerProps > = () => { + const { history } = useRouter(); + const options = useSelector((state: RootState) => pick( [ @@ -56,9 +59,8 @@ const PublishActionButtonsContainer: React.FC< }, }); if (!response.data) return; - // const { user, url_slug } = response.data.writePost; - - // const path = `/@${user.username}/${url_slug}`; + const { user, url_slug } = response.data.writePost; + history.push(`/@${user.username}/${url_slug}`); }; return ; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index a75b0825..dcb5f274 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -314,6 +314,63 @@ export type WritePostResponse = { }; }; +export const EDIT_POST = gql` + mutation EditPost( + $id: ID! + $title: String + $body: String + $tags: [String] + $is_markdown: Boolean + $is_temp: Boolean + $url_slug: String + $thumbnail: String + $meta: JSON + $series_id: ID + ) { + editPost( + id: $id + title: $title + body: $body + tags: $tags + is_markdown: $is_markdown + is_temp: $is_temp + url_slug: $url_slug + thumbnail: $thumbnail + meta: $meta + series_id: $series_id + ) { + id + title + released_at + updated_at + tags + body + short_description + is_markdown + is_private + is_temp + thumbnail + url_slug + series { + id + name + series_posts { + id + post { + id + title + url_slug + user { + id + username + } + } + } + } + } + } +`; + export const WRITE_COMMENT = gql` mutation WriteComment($post_id: ID!, $text: String!, $comment_id: ID) { writeComment(post_id: $post_id, text: $text, comment_id: $comment_id) { diff --git a/yarn.lock b/yarn.lock index 9f074095..092a7c88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11744,6 +11744,18 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-force-update@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/use-force-update/-/use-force-update-1.0.7.tgz#f5e672633f5a398b25c33b2287150918bcb3526b" + integrity sha512-k5dppYhO+I5X/cd7ildbrzeMZJkWwdAh5adaIk0qKN2euh7J0h2GBGBcB4QZ385eyHHnp7LIygvebdRx3XKdwA== + +use-react-router@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/use-react-router/-/use-react-router-1.0.7.tgz#04216066d87e45040309f24d2fd5e9f28308b3e2" + integrity sha512-gdqIHEO28E+qDJQ+tOMGQPthPib7mhLI/4MY7wtxJuaVUkosbP+FAYcHmmE7/FjYoMsuzL/bvY/25st7QHodpw== + dependencies: + use-force-update "^1.0.5" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From 20252d6bd621c8dc974ee4dde85c7934de520470 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 21 Jul 2019 00:00:42 +0900 Subject: [PATCH 053/736] Create edit props to WriteFooter --- src/components/write/PublishActionButtons.tsx | 4 +- src/components/write/WriteFooter.tsx | 9 +- .../__tests__/PublishActionButtons.test.tsx | 9 +- .../write/__tests__/WriteFooter.test.tsx | 8 +- .../write/MarkdownEditorContainer.tsx | 110 ++++++++---------- .../write/PublishActionButtonsContainer.tsx | 9 +- src/containers/write/QuillEditorContainer.tsx | 8 +- 7 files changed, 89 insertions(+), 68 deletions(-) diff --git a/src/components/write/PublishActionButtons.tsx b/src/components/write/PublishActionButtons.tsx index 74321943..ac0e1ee0 100644 --- a/src/components/write/PublishActionButtons.tsx +++ b/src/components/write/PublishActionButtons.tsx @@ -10,11 +10,13 @@ const PublishActionButtonsBlock = styled.div` export interface PublishActionButtonsProps { onCancel: () => void; onPublish: () => void; + edit: boolean; } const PublishActionButtons: React.FC = ({ onCancel, onPublish, + edit, }) => { return ( @@ -27,7 +29,7 @@ const PublishActionButtons: React.FC = ({ 취소 ); diff --git a/src/components/write/WriteFooter.tsx b/src/components/write/WriteFooter.tsx index d4a9a925..4ac9bca3 100644 --- a/src/components/write/WriteFooter.tsx +++ b/src/components/write/WriteFooter.tsx @@ -25,16 +25,21 @@ const StyledButton = styled(Button)` export interface WriteFooterProps { onTempSave: () => void; onPublish: () => void; + edit: boolean; } -const WriteFooter: React.FC = ({ onTempSave, onPublish }) => { +const WriteFooter: React.FC = ({ + onTempSave, + onPublish, + edit, +}) => { return ( 임시저장 - 출간하기 + {edit ? '수정하기' : '출간하기'} ); diff --git a/src/components/write/__tests__/PublishActionButtons.test.tsx b/src/components/write/__tests__/PublishActionButtons.test.tsx index 5025eaba..6e3bf7fa 100644 --- a/src/components/write/__tests__/PublishActionButtons.test.tsx +++ b/src/components/write/__tests__/PublishActionButtons.test.tsx @@ -9,11 +9,12 @@ describe('PublishActionButtons', () => { const initialProps: PublishActionButtonsProps = { onCancel: () => {}, onPublish: () => {}, + edit: false, }; const utils = render(); const buttons = { cancel: utils.getByText('취소'), - publish: utils.getByText('출간하기'), + publish: utils.getByText(/(출간|수정)하기/), }; return { ...utils, @@ -39,4 +40,10 @@ describe('PublishActionButtons', () => { fireEvent.click(utils.buttons.cancel); expect(onCancel).toBeCalled(); }); + it('changes button text when edit is true', () => { + const { getByText } = setup({ + edit: true, + }); + getByText('수정하기'); + }); }); diff --git a/src/components/write/__tests__/WriteFooter.test.tsx b/src/components/write/__tests__/WriteFooter.test.tsx index 638bf277..3f2bdcca 100644 --- a/src/components/write/__tests__/WriteFooter.test.tsx +++ b/src/components/write/__tests__/WriteFooter.test.tsx @@ -11,7 +11,7 @@ describe('WriteFooter', () => { const utils = render(); const buttons = { tempSave: utils.getByText('임시저장'), - publish: utils.getByText('출간하기'), + publish: utils.getByText(/(출간|수정)하기/), }; return { ...utils, @@ -35,4 +35,10 @@ describe('WriteFooter', () => { fireEvent.click(publish); expect(onPublish).toBeCalled(); }); + it('changes button text when edit is true', () => { + const { buttons } = setup({ + edit: true, + }); + expect(buttons.publish.textContent).toBe('수정하기'); + }); }); diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index 4631f752..d9951385 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; +import React, { useMemo } from 'react'; import MarkdownEditor from '../../components/write/MarkdownEditor'; -import { connect } from 'react-redux'; +import { connect, useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../modules'; import { changeMarkdown, @@ -22,50 +22,48 @@ import useUpload from '../../lib/hooks/useUpload'; import useS3Upload from '../../lib/hooks/useS3Upload'; import DragDropUpload from '../../components/common/DragDropUpload'; import PasteUpload from '../../components/common/PasteUpload'; +import { bindActionCreators } from 'redux'; -interface OwnProps {} -interface StateProps { - title: string; - markdown: string; - thumbnail: null | string; - publish: boolean; -} -interface DispatchProps { - changeMarkdown: typeof changeMarkdown; - changeTitle: typeof changeTitle; - setHtml: typeof setHtml; - convertEditorMode: typeof convertEditorMode; - openPublish: typeof openPublish; - setDefaultDescription: typeof setDefaultDescription; - setThumbnail: typeof setThumbnail; -} - -export type MarkdownEditorContainerProps = OwnProps & - StateProps & - DispatchProps; +export type MarkdownEditorContainerProps = {}; const { useCallback, useEffect } = React; -const MarkdownEditorContainer: React.SFC = ({ - changeMarkdown, - changeTitle, - setHtml, - convertEditorMode, - title, - markdown, - thumbnail, - openPublish, - setDefaultDescription, - publish, -}) => { +const MarkdownEditorContainer: React.SFC = () => { + const { title, markdown, thumbnail, publish, postId } = useSelector( + (state: RootState) => ({ + title: state.write.title, + markdown: state.write.markdown, + thumbnail: state.write.thumbnail, + publish: state.write.publish, + postId: state.write.postId, + }), + ); + const dispatch = useDispatch(); + const actionCreators = useMemo( + () => + bindActionCreators( + { + changeMarkdown, + changeTitle, + setHtml, + convertEditorMode, + openPublish, + setDefaultDescription, + setThumbnail, + }, + dispatch, + ), + [dispatch], + ); + const onConvert = (markdown: string) => { remark() .use(breaks) .use(htmlPlugin) .process(markdown, (err: any, file: any) => { const html = String(file); - setHtml(html); - convertEditorMode(); + actionCreators.setHtml(html); + actionCreators.convertEditorMode(); }); }; @@ -76,10 +74,10 @@ const MarkdownEditorContainer: React.SFC = ({ .process(markdown.replace(/#(.*?)\n/g, ''), (err: any, file: any) => { const text = String(file); const sliced = text.replace(/\n/g, '').slice(0, 150); - setDefaultDescription(sliced); - openPublish(); + actionCreators.setDefaultDescription(sliced); + actionCreators.openPublish(); }); - }, [markdown, openPublish, setDefaultDescription]); + }, [actionCreators, markdown]); const [upload, file] = useUpload(); const [s3Upload, image] = useS3Upload(); @@ -93,9 +91,9 @@ const MarkdownEditorContainer: React.SFC = ({ useEffect(() => { if (!thumbnail && image) { - setThumbnail(image); + actionCreators.setThumbnail(image); } - }, [image, thumbnail]); + }, [actionCreators, image, thumbnail]); const onDragDropUpload = useCallback( (file: File) => { @@ -112,11 +110,17 @@ const MarkdownEditorContainer: React.SFC = ({ lastUploadedImage={publish ? null : image} title={title} markdown={markdown} - onChangeMarkdown={changeMarkdown} - onChangeTitle={changeTitle} + onChangeMarkdown={actionCreators.changeMarkdown} + onChangeTitle={actionCreators.changeTitle} onConvert={onConvert} tagInput={} - footer={ {}} />} + footer={ + {}} + edit={!!postId} + /> + } /> @@ -124,20 +128,4 @@ const MarkdownEditorContainer: React.SFC = ({ ); }; -export default connect( - state => ({ - title: state.write.title, - markdown: state.write.markdown, - thumbnail: state.write.thumbnail, - publish: state.write.publish, - }), - { - changeMarkdown, - changeTitle, - setHtml, - convertEditorMode, - openPublish, - setDefaultDescription, - setThumbnail, - }, -)(MarkdownEditorContainer); +export default MarkdownEditorContainer; diff --git a/src/containers/write/PublishActionButtonsContainer.tsx b/src/containers/write/PublishActionButtonsContainer.tsx index c3d76c30..87b25810 100644 --- a/src/containers/write/PublishActionButtonsContainer.tsx +++ b/src/containers/write/PublishActionButtonsContainer.tsx @@ -30,6 +30,7 @@ const PublishActionButtonsContainer: React.FC< 'urlSlug', 'thumbnail', 'selectedSeries', + 'postId', ], state.write, ), @@ -63,7 +64,13 @@ const PublishActionButtonsContainer: React.FC< history.push(`/@${user.username}/${url_slug}`); }; - return ; + return ( + + ); }; export default PublishActionButtonsContainer; diff --git a/src/containers/write/QuillEditorContainer.tsx b/src/containers/write/QuillEditorContainer.tsx index 5dfeebe2..e60fa4f2 100644 --- a/src/containers/write/QuillEditorContainer.tsx +++ b/src/containers/write/QuillEditorContainer.tsx @@ -112,7 +112,13 @@ const QuillEditorContainer: React.FC = ({ tagInput={} onChangeHtml={onChangeHtml} onChangeTextBody={onChangeTextBody} - footer={ {}} />} + footer={ + {}} + edit={false} + /> + } onUpload={upload} lastUploadedImage={publish ? null : image} /> From 547abe4a0d98800296a211e68af4a17494157930 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 21 Jul 2019 23:28:12 +0900 Subject: [PATCH 054/736] Implement post edit --- .../write/MarkdownEditorContainer.tsx | 2 +- .../write/PublishActionButtonsContainer.tsx | 48 +++++--- src/containers/write/QuillEditorContainer.tsx | 111 +++++++++--------- src/lib/graphql/post.ts | 33 ++++++ 4 files changed, 124 insertions(+), 70 deletions(-) diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index d9951385..81111527 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import MarkdownEditor from '../../components/write/MarkdownEditor'; -import { connect, useSelector, useDispatch } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../modules'; import { changeMarkdown, diff --git a/src/containers/write/PublishActionButtonsContainer.tsx b/src/containers/write/PublishActionButtonsContainer.tsx index 87b25810..cea8136c 100644 --- a/src/containers/write/PublishActionButtonsContainer.tsx +++ b/src/containers/write/PublishActionButtonsContainer.tsx @@ -3,7 +3,12 @@ import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../modules'; import PublishActionButtons from '../../components/write/PublishActionButtons'; import { closePublish, WriteMode } from '../../modules/write'; -import { WRITE_POST, WritePostResponse } from '../../lib/graphql/post'; +import { + WRITE_POST, + WritePostResponse, + EditPostResult, + EDIT_POST, +} from '../../lib/graphql/post'; import { pick } from 'ramda'; import { escapeForUrl, safe } from '../../lib/utils'; import { useMutation } from 'react-apollo-hooks'; @@ -42,32 +47,47 @@ const PublishActionButtonsContainer: React.FC< }, [dispatch]); const writePost = useMutation(WRITE_POST); + const editPost = useMutation(EDIT_POST); + + const variables = { + title: options.title, + body: options.mode === WriteMode.MARKDOWN ? options.markdown : options.html, + tags: options.tags, + is_markdown: options.mode === WriteMode.MARKDOWN, + is_temp: false, + is_private: options.isPrivate, + url_slug: options.urlSlug || escapeForUrl(options.title), + thumbnail: options.thumbnail, + meta: {}, + series_id: safe(() => options.selectedSeries!.id), + }; const onPublish = async () => { const response = await writePost({ + variables: variables, + }); + if (!response.data) return; + const { user, url_slug } = response.data.writePost; + history.push(`/@${user.username}/${url_slug}`); + }; + + const onEdit = async () => { + const response = await editPost({ variables: { - title: options.title, - body: - options.mode === WriteMode.MARKDOWN ? options.markdown : options.html, - tags: options.tags, - is_markdown: options.mode === WriteMode.MARKDOWN, - is_temp: false, - is_private: options.isPrivate, - url_slug: options.urlSlug || escapeForUrl(options.title), - thumbnail: options.thumbnail, - meta: {}, - series_id: safe(() => options.selectedSeries!.id), + id: options.postId, + ...variables, }, }); if (!response.data) return; - const { user, url_slug } = response.data.writePost; + console.log(response.data); + const { user, url_slug } = response.data.editPost; history.push(`/@${user.username}/${url_slug}`); }; return ( ); diff --git a/src/containers/write/QuillEditorContainer.tsx b/src/containers/write/QuillEditorContainer.tsx index e60fa4f2..5a87a402 100644 --- a/src/containers/write/QuillEditorContainer.tsx +++ b/src/containers/write/QuillEditorContainer.tsx @@ -1,5 +1,5 @@ -import * as React from 'react'; -import { connect, batch } from 'react-redux'; +import React, { useMemo } from 'react'; +import { connect, batch, useSelector, useDispatch } from 'react-redux'; import QuillEditor from '../../components/write/QuillEditor'; import { RootState } from '../../modules'; import { @@ -18,64 +18,68 @@ import useUpload from '../../lib/hooks/useUpload'; import useS3Upload from '../../lib/hooks/useS3Upload'; import DragDropUpload from '../../components/common/DragDropUpload'; import PasteUpload from '../../components/common/PasteUpload'; +import { bindActionCreators } from 'redux'; -interface OwnProps {} -type StateProps = ReturnType; -type DispatchProps = typeof mapDispatchToProps; -export type QuillEditorContainerProps = OwnProps & StateProps & DispatchProps; - -const mapStateToProps = ({ write }: RootState) => ({ - mode: write.mode, - title: write.title, - html: write.html, - textBody: write.textBody, - thumbnail: write.thumbnail, - publish: write.publish, -}); -const mapDispatchToProps = { - convertEditorMode, - changeTitle, - changeMarkdown, - openPublish, - setHtml, - setTextBody, - setDefaultDescription, - setThumbnail, -}; +export type QuillEditorContainerProps = {}; const { useCallback, useEffect } = React; -const QuillEditorContainer: React.FC = ({ - title, - changeMarkdown, - convertEditorMode, - changeTitle, - openPublish, - html, - setHtml, - setTextBody, - setDefaultDescription, - textBody, - thumbnail, - setThumbnail, - publish, -}) => { +const QuillEditorContainer: React.FC = () => { + const { + mode, + title, + html, + textBody, + thumbnail, + publish, + postId, + } = useSelector(({ write }: RootState) => ({ + mode: write.mode, + title: write.title, + html: write.html, + textBody: write.textBody, + thumbnail: write.thumbnail, + publish: write.publish, + postId: write.postId, + })); + const dispatch = useDispatch(); + const actionCreators = useMemo( + () => + bindActionCreators( + { + convertEditorMode, + changeTitle, + changeMarkdown, + openPublish, + setHtml, + setTextBody, + setDefaultDescription, + setThumbnail, + }, + dispatch, + ), + [dispatch], + ); + const onConvertEditorMode = (markdown: string) => { batch(() => { - changeMarkdown(markdown); - convertEditorMode(); + actionCreators.changeMarkdown(markdown); + actionCreators.convertEditorMode(); }); }; // after transition - const onChangeTitle = (title: string) => changeTitle(title); - const onChangeHtml = (html: string) => setHtml(html); - const onChangeTextBody = (textBody: string) => setTextBody(textBody); + const onChangeTitle = (title: string) => actionCreators.changeTitle(title); + const onChangeHtml = (html: string) => actionCreators.setHtml(html); + const onChangeTextBody = (textBody: string) => + actionCreators.setTextBody(textBody); const onPublish = useCallback(() => { // window.document.body.style.overflowX = 'hidden'; // window.document.body.style.overflowY = 'hidden'; - openPublish(); - setDefaultDescription(textBody.replace(/\n/g, '').slice(0, 150)); - }, [openPublish, setDefaultDescription, textBody]); + actionCreators.openPublish(); + actionCreators.setDefaultDescription( + textBody.replace(/\n/g, '').slice(0, 150), + ); + }, [actionCreators, textBody]); const [upload, file] = useUpload(); const [s3Upload, image] = useS3Upload(); @@ -89,9 +93,9 @@ const QuillEditorContainer: React.FC = ({ useEffect(() => { if (!thumbnail && image) { - setThumbnail(image); + actionCreators.setThumbnail(image); } - }, [image, setThumbnail, thumbnail]); + }, [actionCreators, image, thumbnail]); const onDragDropUpload = useCallback( (file: File) => { @@ -116,7 +120,7 @@ const QuillEditorContainer: React.FC = ({ {}} - edit={false} + edit={!!postId} /> } onUpload={upload} @@ -128,7 +132,4 @@ const QuillEditorContainer: React.FC = ({ ); }; -export default connect( - mapStateToProps, - mapDispatchToProps, -)(QuillEditorContainer); +export default QuillEditorContainer; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index dcb5f274..f25b2a9a 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -350,7 +350,36 @@ export const EDIT_POST = gql` is_private is_temp thumbnail + comments_count url_slug + user { + id + username + profile { + display_name + thumbnail + short_bio + } + velog_config { + title + } + } + comments { + id + user { + id + username + profile { + thumbnail + } + } + text + replies_count + level + created_at + level + deleted + } series { id name @@ -371,6 +400,10 @@ export const EDIT_POST = gql` } `; +export type EditPostResult = { + editPost: SinglePost; +}; + export const WRITE_COMMENT = gql` mutation WriteComment($post_id: ID!, $text: String!, $comment_id: ID) { writeComment(post_id: $post_id, text: $text, comment_id: $comment_id) { From b77c4f9fdd6c8943d2b6ab995fdfc62b266e1868 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 21 Jul 2019 23:48:52 +0900 Subject: [PATCH 055/736] Implement remove from series --- .../post/__tests__/PostSeriesInfo.test.tsx | 2 +- src/components/write/PublishSeriesSection.tsx | 42 +++++++++++++++---- .../__tests__/PublishSeriesSection.test.tsx | 6 +++ .../write/PublishSeriesSectionContainer.tsx | 13 +++++- src/modules/write.ts | 2 +- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/components/post/__tests__/PostSeriesInfo.test.tsx b/src/components/post/__tests__/PostSeriesInfo.test.tsx index 2a86bd22..742d46fc 100644 --- a/src/components/post/__tests__/PostSeriesInfo.test.tsx +++ b/src/components/post/__tests__/PostSeriesInfo.test.tsx @@ -103,7 +103,7 @@ describe('PostSeriesInfo', () => { // title getByText('Sample Series Name'); // series-number - getByText('1 / 8'); + getByText('1/8'); }); it('opens series list', () => { const { getByText } = setup(); diff --git a/src/components/write/PublishSeriesSection.tsx b/src/components/write/PublishSeriesSection.tsx index c71551b0..770d73ad 100644 --- a/src/components/write/PublishSeriesSection.tsx +++ b/src/components/write/PublishSeriesSection.tsx @@ -71,8 +71,28 @@ const EditSeries = styled.div` } `; +const RemoveFromSeries = styled.div` + margin-top: 8px; + display: flex; + justify-content: flex-end; + button { + outline: none; + border: none; + background: none; + font-size: 1rem; + color: ${palette.red5}; + padding: 0; + &:hover { + color: ${palette.red6}; + text-decoration: underline; + } + cursor: pointer; + } +`; + export interface PublishSeriesSectionProps { onEdit: () => void; + onReset: () => void; selected: { id: string; name: string; @@ -81,19 +101,25 @@ export interface PublishSeriesSectionProps { const PublishSeriesSection: React.FC = ({ onEdit, + onReset, selected, }) => { return ( {selected ? ( - -
-
{selected.name}
-
- -
+ <> + +
+
{selected.name}
+
+ +
+ + + + ) : ( diff --git a/src/components/write/__tests__/PublishSeriesSection.test.tsx b/src/components/write/__tests__/PublishSeriesSection.test.tsx index 62def93d..0f1eadef 100644 --- a/src/components/write/__tests__/PublishSeriesSection.test.tsx +++ b/src/components/write/__tests__/PublishSeriesSection.test.tsx @@ -28,14 +28,20 @@ describe('PublishSeriesSection', () => { name: 'sample series', }; const onEdit = jest.fn(); + const onReset = jest.fn(); const { getByText, getByTestId } = setup({ selected: sample, onEdit, + onReset, }); getByText(sample.name); const settingButton = getByTestId('setting-button'); fireEvent.click(settingButton); expect(onEdit).toBeCalled(); + + const removeButton = getByText('시리즈에서 제거'); + fireEvent.click(removeButton); + expect(onReset).toBeCalled(); }); }); diff --git a/src/containers/write/PublishSeriesSectionContainer.tsx b/src/containers/write/PublishSeriesSectionContainer.tsx index 2d0bdf70..60b09ee8 100644 --- a/src/containers/write/PublishSeriesSectionContainer.tsx +++ b/src/containers/write/PublishSeriesSectionContainer.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PublishSeriesSection from '../../components/write/PublishSeriesSection'; import { useDispatch, useSelector } from 'react-redux'; -import { toggleEditSeries } from '../../modules/write'; +import { toggleEditSeries, selectSeries } from '../../modules/write'; import { RootState } from '../../modules'; export interface PublishSeriesSectionContainerProps {} @@ -17,7 +17,16 @@ const PublishSeriesSectionContainer: React.FC< const onEdit = () => { dispatch(toggleEditSeries()); }; - return ; + const onReset = () => { + dispatch(selectSeries(null)); + }; + return ( + + ); }; export default PublishSeriesSectionContainer; diff --git a/src/modules/write.ts b/src/modules/write.ts index 438590df..dbef5604 100644 --- a/src/modules/write.ts +++ b/src/modules/write.ts @@ -44,7 +44,7 @@ export const toggleEditSeries = createStandardAction(TOGGLE_EDIT_SERIES)< export const selectSeries = createStandardAction(SELECT_SERIES)<{ id: string; name: string; -}>(); +} | null>(); export const clearEditor = createStandardAction(CLEAR_EDITOR)(); export type PrepareEditPayload = { From ba854fc39334f9e877dcb1f916de4baa72065a1d Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 23 Jul 2019 23:14:21 +0900 Subject: [PATCH 056/736] Remove unused vars --- src/containers/write/QuillEditorContainer.tsx | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/containers/write/QuillEditorContainer.tsx b/src/containers/write/QuillEditorContainer.tsx index 5a87a402..f40bf5b1 100644 --- a/src/containers/write/QuillEditorContainer.tsx +++ b/src/containers/write/QuillEditorContainer.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { connect, batch, useSelector, useDispatch } from 'react-redux'; +import { batch, useSelector, useDispatch } from 'react-redux'; import QuillEditor from '../../components/write/QuillEditor'; import { RootState } from '../../modules'; import { @@ -25,23 +25,17 @@ export type QuillEditorContainerProps = {}; const { useCallback, useEffect } = React; const QuillEditorContainer: React.FC = () => { - const { - mode, - title, - html, - textBody, - thumbnail, - publish, - postId, - } = useSelector(({ write }: RootState) => ({ - mode: write.mode, - title: write.title, - html: write.html, - textBody: write.textBody, - thumbnail: write.thumbnail, - publish: write.publish, - postId: write.postId, - })); + const { title, html, textBody, thumbnail, publish, postId } = useSelector( + ({ write }: RootState) => ({ + mode: write.mode, + title: write.title, + html: write.html, + textBody: write.textBody, + thumbnail: write.thumbnail, + publish: write.publish, + postId: write.postId, + }), + ); const dispatch = useDispatch(); const actionCreators = useMemo( () => From 6e8cf09f2260c4ac403c6a250702f76dd1575bbe Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 23 Jul 2019 23:14:27 +0900 Subject: [PATCH 057/736] Add icon license --- asset-license.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asset-license.md b/asset-license.md index 89cef52a..2dfc4bfa 100644 --- a/asset-license.md +++ b/asset-license.md @@ -3,4 +3,7 @@ Icons made by [Smashicons](https://www.flaticon.com/authors/smashicons) from [ww
Icons made by SimpleIcon from www.flaticon.com is licensed by CC 3.0 BY
-
Icons made by SimpleIcon from www.flaticon.com is licensed by CC 3.0 BY
\ No newline at end of file +
Icons made by SimpleIcon from www.flaticon.com is licensed by CC 3.0 BY
+ + +Heart Icon: https://iconmonstr.com/favorite-8-svg/ \ No newline at end of file From 5fcdbd6ccaed33fa6954f2bfa41afcf00dbda52f Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 23 Jul 2019 23:14:42 +0900 Subject: [PATCH 058/736] Set min-height for postcard thumbnail --- src/components/common/PostCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index 97f6e558..e02fbdc8 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -38,6 +38,7 @@ const PostCardBlock = styled.div` } .post-thumbnail { width: 100%; + min-height: 369px; max-height: 369px; margin-bottom: 1rem; object-fit: cover; From 048e86a63957d5a3a425597e5fbfe819a98e47f0 Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 25 Jul 2019 23:17:41 +0900 Subject: [PATCH 059/736] Prepare PostLikeShareButtons UI --- src/components/post/PostHead.tsx | 3 + src/components/post/PostLikeShareButtons.tsx | 120 ++++++++++++++++++ src/components/post/PostTags.tsx | 1 + .../__tests__/PostLikeShareButtons.test.tsx | 18 +++ src/static/svg/icon-like.svg | 1 + src/static/svg/icon-share.svg | 1 + src/static/svg/index.ts | 2 + 7 files changed, 146 insertions(+) create mode 100644 src/components/post/PostLikeShareButtons.tsx create mode 100644 src/components/post/__tests__/PostLikeShareButtons.test.tsx create mode 100644 src/static/svg/icon-like.svg create mode 100644 src/static/svg/icon-share.svg diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index 91785ec0..35f4563f 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -8,6 +8,7 @@ import { SeriesPost } from '../../lib/graphql/post'; import PostSeriesInfo from './PostSeriesInfo'; import useToggle from '../../lib/hooks/useToggle'; import PopupOKCancel from '../common/PopupOKCancel'; +import PostLikeShareButtons from './PostLikeShareButtons'; const PostHeadBlock = styled(VelogResponsive)` margin-top: 5.5rem; @@ -124,6 +125,7 @@ const PostHead: React.FC = ({ )} + {series && ( = ({ username={username} /> )} + {!hideThumbnail && thumbnail && ( )} diff --git a/src/components/post/PostLikeShareButtons.tsx b/src/components/post/PostLikeShareButtons.tsx new file mode 100644 index 00000000..f08e7d5e --- /dev/null +++ b/src/components/post/PostLikeShareButtons.tsx @@ -0,0 +1,120 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import styled, { css } from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { getScrollTop } from '../../lib/utils'; +import { LikeIcon, ShareIcon } from '../../static/svg'; + +const Wrapper = styled.div` + position: relative; + margin-top: 2rem; +`; +const Positioner = styled.div` + position: absolute; + left: -7rem; +`; +const PostLikeShareButtonsBlock = styled.div<{ fixed: boolean }>` + width: 4rem; + background: ${palette.gray0}; + border: 1px solid ${palette.gray1}; + border-radius: 2rem; + padding: 0.5rem; + display: flex; + flex-direction: column; + align-items: center; + ${props => + props.fixed && + css` + position: fixed; + top: 7rem; + `} +`; + +const CircleButton = styled.div` + height: 3rem; + width: 3rem; + display: flex; + align-items: center; + justify-content: center; + background: white; + border: 1px solid ${palette.gray5}; + border-radius: 1.5rem; + color: ${palette.gray6}; + cursor: pointer; + svg { + width: 24px; + height: 24px; + &.share { + width: 20px; + height: 20px; + } + } + &:hover { + color: ${palette.gray9}; + border-color: ${palette.gray9}; + } +`; + +const LikeCount = styled.div` + margin-top: 0.5rem; + color: ${palette.gray7}; + line-height: 1; + font-size: 0.75rem; + margin-bottom: 1rem; + font-weight: bold; +`; + +export interface PostLikeShareButtonsProps {} + +const PostLikeShareButtons: React.FC = props => { + const [top, setTop] = useState(0); + const [fixed, setFixed] = useState(false); + const element = useRef(null); + + const setup = useCallback(() => { + if (!element.current) return; + const pos = element.current.getBoundingClientRect(); + setTop(pos.top + getScrollTop()); + }, []); + + const onScroll = useCallback(() => { + const scrollTop = getScrollTop(); + console.log({ scrollTop, top }); + const nextFixed = scrollTop + 112 > top; + if (fixed !== nextFixed) { + setFixed(nextFixed); + console.log('switfh~'); + } + }, [fixed, top]); + + // setup + useEffect(() => { + if (!element.current) return; + setup(); + }, [setup]); + + // register scroll event + useEffect(() => { + window.addEventListener('scroll', onScroll); + return () => { + window.removeEventListener('scroll', onScroll); + }; + }, [onScroll]); + + return ( + + + + + + + 0 + + + + + + + ); +}; + +export default PostLikeShareButtons; diff --git a/src/components/post/PostTags.tsx b/src/components/post/PostTags.tsx index 00c2f9e6..19563b7f 100644 --- a/src/components/post/PostTags.tsx +++ b/src/components/post/PostTags.tsx @@ -6,6 +6,7 @@ import palette from '../../lib/styles/palette'; const PostTagsBlock = styled.div` margin-top: 0.875rem; margin-bottom: -0.875rem; + min-height: 0.875rem; `; const Tag = styled(Link)` diff --git a/src/components/post/__tests__/PostLikeShareButtons.test.tsx b/src/components/post/__tests__/PostLikeShareButtons.test.tsx new file mode 100644 index 00000000..1f01e5bc --- /dev/null +++ b/src/components/post/__tests__/PostLikeShareButtons.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from 'react-testing-library'; +import PostLikeShareButtons, { + PostLikeShareButtonsProps, +} from '../PostLikeShareButtons'; + +describe('PostLikeShareButtons', () => { + const setup = (props: Partial = {}) => { + const initialProps: PostLikeShareButtonsProps = {}; + const utils = render(); + return { + ...utils, + }; + }; + it('renders properly', () => { + setup(); + }); +}); diff --git a/src/static/svg/icon-like.svg b/src/static/svg/icon-like.svg new file mode 100644 index 00000000..49f69d03 --- /dev/null +++ b/src/static/svg/icon-like.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/svg/icon-share.svg b/src/static/svg/icon-share.svg new file mode 100644 index 00000000..d12a74dd --- /dev/null +++ b/src/static/svg/icon-share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/svg/index.ts b/src/static/svg/index.ts index cc1455d6..8baf3506 100644 --- a/src/static/svg/index.ts +++ b/src/static/svg/index.ts @@ -9,3 +9,5 @@ export { ReactComponent as ImageVector } from './vector-image.svg'; export { ReactComponent as MinusBoxIcon } from './icon-minus-box.svg'; export { ReactComponent as PlusBoxIcon } from './icon-plus-box.svg'; export { ReactComponent as SeriesImage } from './image-series.svg'; +export { ReactComponent as LikeIcon } from './icon-like.svg'; +export { ReactComponent as ShareIcon } from './icon-share.svg'; From 48944221221af3d9690b01c26c51dfcd3eb7765e Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 26 Jul 2019 00:05:28 +0900 Subject: [PATCH 060/736] Upgrade react-apollo-hooks and fix breaking changes for useMutation --- package.json | 2 +- src/components/post/PostHead.tsx | 4 +- src/components/post/PostLikeShareButtons.tsx | 34 +++++++-- .../post/__tests__/PostHead.test.tsx | 1 + .../__tests__/PostLikeShareButtons.test.tsx | 23 +++++- src/containers/post/PostComments.tsx | 2 +- .../post/PostCommentsWriteContainer.tsx | 2 +- src/containers/post/PostEditComment.tsx | 2 +- src/containers/post/PostRepliesContainer.tsx | 4 +- src/containers/post/PostViewer.tsx | 75 ++++++++++++++++++- .../post/__tests__/PostViewer.test.tsx | 2 + .../write/PublishActionButtonsContainer.tsx | 4 +- src/containers/write/PublishSeriesConfig.tsx | 2 +- src/lib/graphql/post.ts | 24 ++++++ yarn.lock | 8 +- 15 files changed, 165 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 5b5176ea..0ad9c13e 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "ramda": "^0.26.1", "react": "^16.8.6", "react-apollo": "^2.4.1", - "react-apollo-hooks": "^0.4.5", + "react-apollo-hooks": "^0.5.0", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.2", "react-dom": "^16.8.6", diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index 35f4563f..d927f3de 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -87,6 +87,7 @@ export interface PostHeadProps { ownPost: boolean; onRemove: () => any; onEdit: () => any; + shareButtons: React.ReactNode; } const PostHead: React.FC = ({ @@ -101,6 +102,7 @@ const PostHead: React.FC = ({ ownPost, onRemove, onEdit, + shareButtons, }) => { const [askRemove, toggleAskRemove] = useToggle(false); @@ -125,7 +127,7 @@ const PostHead: React.FC = ({ )} - + {shareButtons} {series && ( ` `} `; -const CircleButton = styled.div` +const CircleButton = styled.div<{ active?: boolean }>` height: 3rem; width: 3rem; display: flex; @@ -52,6 +52,18 @@ const CircleButton = styled.div` color: ${palette.gray9}; border-color: ${palette.gray9}; } + ${props => + props.active && + css` + background: ${palette.teal5}; + border-color: ${palette.teal5}; + color: white; + &:hover { + background: ${palette.teal4}; + border-color: ${palette.teal4}; + color: white; + } + `} `; const LikeCount = styled.div` @@ -63,9 +75,17 @@ const LikeCount = styled.div` font-weight: bold; `; -export interface PostLikeShareButtonsProps {} +export interface PostLikeShareButtonsProps { + onLikeToggle: () => any; + likes: number; + liked: boolean; +} -const PostLikeShareButtons: React.FC = props => { +const PostLikeShareButtons: React.FC = ({ + onLikeToggle, + likes, + liked, +}) => { const [top, setTop] = useState(0); const [fixed, setFixed] = useState(false); const element = useRef(null); @@ -104,10 +124,14 @@ const PostLikeShareButtons: React.FC = props => { - + - 0 + {likes} diff --git a/src/components/post/__tests__/PostHead.test.tsx b/src/components/post/__tests__/PostHead.test.tsx index 49574227..b61cd481 100644 --- a/src/components/post/__tests__/PostHead.test.tsx +++ b/src/components/post/__tests__/PostHead.test.tsx @@ -18,6 +18,7 @@ describe('PostHead', () => { postId: '7ae82a11-f56a-4332-aaef-2202c80d9fdd', onRemove: () => {}, onEdit: () => {}, + shareButtons: null, }; const utils = render( diff --git a/src/components/post/__tests__/PostLikeShareButtons.test.tsx b/src/components/post/__tests__/PostLikeShareButtons.test.tsx index 1f01e5bc..6d6e3a46 100644 --- a/src/components/post/__tests__/PostLikeShareButtons.test.tsx +++ b/src/components/post/__tests__/PostLikeShareButtons.test.tsx @@ -1,12 +1,16 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render, fireEvent } from 'react-testing-library'; import PostLikeShareButtons, { PostLikeShareButtonsProps, } from '../PostLikeShareButtons'; describe('PostLikeShareButtons', () => { const setup = (props: Partial = {}) => { - const initialProps: PostLikeShareButtonsProps = {}; + const initialProps: PostLikeShareButtonsProps = { + onLikeToggle: () => {}, + likes: 0, + liked: false, + }; const utils = render(); return { ...utils, @@ -15,4 +19,19 @@ describe('PostLikeShareButtons', () => { it('renders properly', () => { setup(); }); + it('renders likes', () => { + const { getByText } = setup({ + likes: 123, + }); + getByText('123'); + }); + it('calls onLikeToggle', () => { + const onLikeToggle = jest.fn(); + const { getByTestId } = setup({ + onLikeToggle, + }); + const likeButton = getByTestId('like'); + fireEvent.click(likeButton); + expect(onLikeToggle).toBeCalled(); + }); }); diff --git a/src/containers/post/PostComments.tsx b/src/containers/post/PostComments.tsx index 50ee6a2b..18b7dfa2 100644 --- a/src/containers/post/PostComments.tsx +++ b/src/containers/post/PostComments.tsx @@ -32,7 +32,7 @@ const PostComments: React.FC = ({ const [removeId, setRemoveId] = useState(''); const currentUserId = useUserId(); - const removeComment = useMutation(REMOVE_COMMENT); + const [removeComment] = useMutation(REMOVE_COMMENT); const reloadComments = useQuery(RELOAD_COMMENTS, { variables: { id: postId, diff --git a/src/containers/post/PostCommentsWriteContainer.tsx b/src/containers/post/PostCommentsWriteContainer.tsx index 992ead96..38323cd8 100644 --- a/src/containers/post/PostCommentsWriteContainer.tsx +++ b/src/containers/post/PostCommentsWriteContainer.tsx @@ -15,7 +15,7 @@ const PostCommentsWriteContainer: React.FC = ({ const onChange = (e: React.ChangeEvent) => { setComment(e.target.value); }; - const writeComment = useMutation(WRITE_COMMENT); + const [writeComment] = useMutation(WRITE_COMMENT); const reloadComments = useQuery(RELOAD_COMMENTS, { skip: true, fetchPolicy: 'network-only', diff --git a/src/containers/post/PostEditComment.tsx b/src/containers/post/PostEditComment.tsx index 17a9721d..d66ad637 100644 --- a/src/containers/post/PostEditComment.tsx +++ b/src/containers/post/PostEditComment.tsx @@ -16,7 +16,7 @@ const PostEditComment: React.FC = ({ onCancel, }) => { const [comment, onChange] = useInput(defaultText); - const editComment = useMutation(EDIT_COMMENT); + const [editComment] = useMutation(EDIT_COMMENT); const onWrite = async () => { await editComment({ diff --git a/src/containers/post/PostRepliesContainer.tsx b/src/containers/post/PostRepliesContainer.tsx index 151a5cf6..e1be72f7 100644 --- a/src/containers/post/PostRepliesContainer.tsx +++ b/src/containers/post/PostRepliesContainer.tsx @@ -37,8 +37,8 @@ const PostRepliesContainer: React.FC = ({ }, skip: true, }); - const writeComment = useMutation(WRITE_COMMENT); - const removeComment = useMutation(REMOVE_COMMENT); + const [writeComment] = useMutation(WRITE_COMMENT); + const [removeComment] = useMutation(REMOVE_COMMENT); const onReply = async (text: string) => { await writeComment({ diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 354f6b72..51cd431c 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -1,6 +1,12 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { READ_POST, SinglePost, REMOVE_POST } from '../../lib/graphql/post'; +import { + READ_POST, + SinglePost, + REMOVE_POST, + LIKE_POST, + UNLIKE_POST, +} from '../../lib/graphql/post'; import PostHead from '../../components/post/PostHead'; import PostContent from '../../components/post/PostContent'; import PostComments from './PostComments'; @@ -10,6 +16,8 @@ import { useUserId } from '../../lib/hooks/useUser'; import { useQuery, useMutation, useApolloClient } from 'react-apollo-hooks'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { prepareEdit } from '../../modules/write'; +import PostLikeShareButtons from '../../components/post/PostLikeShareButtons'; +import gql from 'graphql-tag'; export interface PostViewerOwnProps { username: string; @@ -33,8 +41,11 @@ const PostViewer: React.FC = ({ }, }); const client = useApolloClient(); + const [likeInProcess, setLikeInProcess] = useState(false); - const removePost = useMutation(REMOVE_POST); + const [removePost] = useMutation(REMOVE_POST); + const [likePost] = useMutation(LIKE_POST); + const [unlikePost] = useMutation(UNLIKE_POST); const { loading, error, data } = readPost; @@ -86,6 +97,57 @@ const PostViewer: React.FC = ({ history.push('/write'); }; + const onLikeToggle = async () => { + if (likeInProcess) return; + setLikeInProcess(true); + const variables = { + id: post.id, + }; + const likeFragment = gql` + fragment post on Post { + liked + likes + } + `; + + // IF SPLITTED TO ANOTHER CONTAINER + // const data = client.readFragment<{ liked: boolean; likes: number }>({ + // id: `Post:${post.id}`, + // fragment: likeFragment + // }); + + try { + if (post.liked) { + client.writeFragment({ + id: `Post:${post.id}`, + fragment: likeFragment, + data: { + liked: false, + likes: post.likes - 1, + }, + }); + await unlikePost({ + variables, + }); + } else { + client.writeFragment({ + id: `Post:${post.id}`, + fragment: likeFragment, + data: { + liked: true, + likes: post.likes + 1, + }, + }); + await likePost({ + variables, + }); + } + } catch (e) { + console.log(e); + } + setLikeInProcess(false); + }; + if (loading) return null; if (!data) return null; @@ -105,6 +167,13 @@ const PostViewer: React.FC = ({ ownPost={post.user.id === userId} onRemove={onRemove} onEdit={onEdit} + shareButtons={ + + } /> (WRITE_POST); - const editPost = useMutation(EDIT_POST); + const [writePost] = useMutation(WRITE_POST); + const [editPost] = useMutation(EDIT_POST); const variables = { title: options.title, diff --git a/src/containers/write/PublishSeriesConfig.tsx b/src/containers/write/PublishSeriesConfig.tsx index e7c447b4..6322d669 100644 --- a/src/containers/write/PublishSeriesConfig.tsx +++ b/src/containers/write/PublishSeriesConfig.tsx @@ -32,7 +32,7 @@ const PublishSeriesConfig: React.FC = props => { username: safe(() => user!.username), }, }); - const createSeries = useMutation(CREATE_SERIES); + const [createSeries] = useMutation(CREATE_SERIES); const serialized = useMemo(() => { if (!seriesList.data || !seriesList.data.seriesList) return []; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index f25b2a9a..8f620c71 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -100,6 +100,8 @@ export interface SinglePost { name: string; series_posts: SeriesPost[]; } | null; + liked: boolean; + likes: number; } export interface CommentWithReplies { @@ -146,6 +148,8 @@ export const READ_POST = gql` thumbnail comments_count url_slug + likes + liked user { id username @@ -441,3 +445,23 @@ export const REMOVE_POST = gql` removePost(id: $id) } `; + +export const LIKE_POST = gql` + mutation LikePost($id: ID!) { + likePost(id: $id) { + id + likes + liked + } + } +`; + +export const UNLIKE_POST = gql` + mutation UnlikePost($id: ID!) { + unlikePost(id: $id) { + id + likes + liked + } + } +`; diff --git a/yarn.lock b/yarn.lock index 092a7c88..3f4db61d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9570,10 +9570,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-apollo-hooks@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/react-apollo-hooks/-/react-apollo-hooks-0.4.5.tgz#7fe6a8ddfdc92df2da664d399ea77a0da4a10bad" - integrity sha512-fq04h88hg4ONtlzxYInmv7Okqqstzk3UCp55jHWOlIO2lFKoZPhQrtCz7A+TrcRu8GGwtG1SsioRKN8kIQaAOQ== +react-apollo-hooks@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/react-apollo-hooks/-/react-apollo-hooks-0.5.0.tgz#49607fbe9cdfd0be08ea5defd39085bf5f42e10e" + integrity sha512-Us5KqFe7/c6vY1NaiyfhnD2Pz4lPLTojQXLppShaBVYU/vYvJrRjmj4MzIPXnExXaSfnQ+K2bWDr4lP4efbsRQ== dependencies: lodash "^4.17.11" From 6fd5bb1a58c70cb56d71c057122ab14283726bfb Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 26 Jul 2019 00:09:43 +0900 Subject: [PATCH 061/736] Complete like / unlike --- src/containers/post/PostViewer.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 51cd431c..76dfab60 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -41,11 +41,10 @@ const PostViewer: React.FC = ({ }, }); const client = useApolloClient(); - const [likeInProcess, setLikeInProcess] = useState(false); const [removePost] = useMutation(REMOVE_POST); - const [likePost] = useMutation(LIKE_POST); - const [unlikePost] = useMutation(UNLIKE_POST); + const [likePost, { loading: loadingLike }] = useMutation(LIKE_POST); + const [unlikePost, { loading: loadingUnlike }] = useMutation(UNLIKE_POST); const { loading, error, data } = readPost; @@ -98,8 +97,7 @@ const PostViewer: React.FC = ({ }; const onLikeToggle = async () => { - if (likeInProcess) return; - setLikeInProcess(true); + if (loadingLike || loadingUnlike) return; const variables = { id: post.id, }; @@ -124,6 +122,7 @@ const PostViewer: React.FC = ({ data: { liked: false, likes: post.likes - 1, + __typename: 'Post', }, }); await unlikePost({ @@ -136,6 +135,7 @@ const PostViewer: React.FC = ({ data: { liked: true, likes: post.likes + 1, + __typename: 'Post', }, }); await likePost({ @@ -145,7 +145,6 @@ const PostViewer: React.FC = ({ } catch (e) { console.log(e); } - setLikeInProcess(false); }; if (loading) return null; From a4485fc6714a9ce4aa95699bfcf88705c54ca745 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 27 Jul 2019 01:14:44 +0900 Subject: [PATCH 062/736] Implement animation using react-spring --- package.json | 1 + src/components/post/PostHead.tsx | 1 - src/components/post/PostLikeShareButtons.tsx | 109 ++++++++++++++++++- src/containers/post/PostViewer.tsx | 2 +- yarn.lock | 33 ++++-- 5 files changed, 133 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 0ad9c13e..ecbb2e96 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "react-outside-click-handler": "^1.2.3", "react-redux": "^7.1.0", "react-router-dom": "^5.0.0", + "react-spring": "^8.0.27", "react-testing-library": "^7.0.0", "react-textarea-autosize": "^7.1.0", "redux": "^4.0.1", diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index d927f3de..b788d11d 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -8,7 +8,6 @@ import { SeriesPost } from '../../lib/graphql/post'; import PostSeriesInfo from './PostSeriesInfo'; import useToggle from '../../lib/hooks/useToggle'; import PopupOKCancel from '../common/PopupOKCancel'; -import PostLikeShareButtons from './PostLikeShareButtons'; const PostHeadBlock = styled(VelogResponsive)` margin-top: 5.5rem; diff --git a/src/components/post/PostLikeShareButtons.tsx b/src/components/post/PostLikeShareButtons.tsx index 6a4ba1c1..986ae5a9 100644 --- a/src/components/post/PostLikeShareButtons.tsx +++ b/src/components/post/PostLikeShareButtons.tsx @@ -3,6 +3,8 @@ import styled, { css } from 'styled-components'; import palette from '../../lib/styles/palette'; import { getScrollTop } from '../../lib/utils'; import { LikeIcon, ShareIcon } from '../../static/svg'; +import { useSpring, animated, config } from 'react-spring'; +import useBoolean from '../../lib/hooks/useBoolean'; const Wrapper = styled.div` position: relative; @@ -29,7 +31,7 @@ const PostLikeShareButtonsBlock = styled.div<{ fixed: boolean }>` `} `; -const CircleButton = styled.div<{ active?: boolean }>` +const CircleButton = styled(animated.div)<{ active?: boolean }>` height: 3rem; width: 3rem; display: flex; @@ -40,6 +42,7 @@ const CircleButton = styled.div<{ active?: boolean }>` border-radius: 1.5rem; color: ${palette.gray6}; cursor: pointer; + z-index: 5; svg { width: 24px; height: 24px; @@ -75,6 +78,22 @@ const LikeCount = styled.div` font-weight: bold; `; +const ShareButtons = styled.div` + position: relative; + width: 100%; + .positioner { + position: absolute; + } +`; + +const ShareButton = styled(animated.div)` + top: 0; + left: 0; + position: absolute; + width: 48px; + height: 48px; +`; + export interface PostLikeShareButtonsProps { onLikeToggle: () => any; likes: number; @@ -120,6 +139,36 @@ const PostLikeShareButtons: React.FC = ({ }; }, [onScroll]); + const [animateLike, setAnimateLike] = useState(false); + const [prevLiked, setPrevLiked] = useState(liked); + const [open, toggle] = useBoolean(false); + + console.log(open); + + useEffect(() => { + setPrevLiked(liked); + if (!prevLiked && liked) { + setAnimateLike(true); + } + }, [liked, prevLiked]); + + const { x } = useSpring({ + from: { x: 0 }, + x: animateLike ? 1 : 0, + immediate: !animateLike, + config: { + duration: 600, + }, + onRest: () => setAnimateLike(false), + }); + const { shareX } = useSpring({ + from: { shareX: 0 }, + shareX: open ? 1 : 0, + config: config.gentle, + }); + + console.log(shareX); + return ( @@ -128,11 +177,67 @@ const PostLikeShareButtons: React.FC = ({ data-testid="like" onClick={onLikeToggle} active={liked} + style={{ + transform: x + .interpolate({ + range: [0, 0.25, 0.5, 0.6, 1], + output: [1, 1.25, 1, 1.25, 1], + }) + .interpolate(x => `scale(${x})`), + }} > {likes} - + +
+ + `translate(${shareX * 48}px, -${shareX * 52}px)`, + ), + }} + > + + + `translate(${shareX * 72}px)`), + }} + > + + + `translate(${shareX * 48}px, ${shareX * 52}px)`, + ), + }} + > + + +
+
+
diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 76dfab60..313872fd 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { READ_POST, diff --git a/yarn.lock b/yarn.lock index 3f4db61d..179d2da3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -734,6 +734,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.3.1": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" + integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.4.3": version "7.4.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc" @@ -9345,15 +9352,7 @@ prop-types@^15.5.4, prop-types@^15.6.0: object-assign "^4.1.1" react-is "^16.8.1" -prop-types@^15.6.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" - integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== - dependencies: - loose-envify "^1.3.1" - object-assign "^4.1.1" - -prop-types@^15.7.2: +prop-types@^15.5.8, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9362,6 +9361,14 @@ prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +prop-types@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" + integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + property-information@^5.0.0, property-information@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.0.1.tgz#c3b09f4f5750b1634c0b24205adbf78f18bdf94f" @@ -9717,6 +9724,14 @@ react-router@5.0.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-spring@^8.0.27: + version "8.0.27" + resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a" + integrity sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g== + dependencies: + "@babel/runtime" "^7.3.1" + prop-types "^15.5.8" + react-testing-library@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-7.0.0.tgz#d3b535e44de94d7b0a83c56cd2e3cfed752dcec1" From 53c05b0197c3d794b9652dad1d6491d18b785f70 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 27 Jul 2019 23:51:35 +0900 Subject: [PATCH 063/736] Implement post share --- asset-license.md | 3 +- src/components/post/PostLikeShareButtons.tsx | 34 +++++++++++++------- src/containers/post/PostViewer.tsx | 28 ++++++++++++++++ src/index.tsx | 20 ++++++++++++ src/lib/share.ts | 31 ++++++++++++++++++ src/static/svg/icon-clip.svg | 1 + src/static/svg/index.ts | 1 + 7 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 src/lib/share.ts create mode 100644 src/static/svg/icon-clip.svg diff --git a/asset-license.md b/asset-license.md index 2dfc4bfa..dd473534 100644 --- a/asset-license.md +++ b/asset-license.md @@ -6,4 +6,5 @@ Icons made by [Smashicons](https://www.flaticon.com/authors/smashicons) from [ww
Icons made by SimpleIcon from www.flaticon.com is licensed by CC 3.0 BY
-Heart Icon: https://iconmonstr.com/favorite-8-svg/ \ No newline at end of file +Heart Icon: https://iconmonstr.com/favorite-8-svg/ +Clip Icon: https://iconmonstr.com/paperclip-2-svg/ \ No newline at end of file diff --git a/src/components/post/PostLikeShareButtons.tsx b/src/components/post/PostLikeShareButtons.tsx index 986ae5a9..fb85307f 100644 --- a/src/components/post/PostLikeShareButtons.tsx +++ b/src/components/post/PostLikeShareButtons.tsx @@ -2,9 +2,11 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import styled, { css } from 'styled-components'; import palette from '../../lib/styles/palette'; import { getScrollTop } from '../../lib/utils'; -import { LikeIcon, ShareIcon } from '../../static/svg'; +import { LikeIcon, ShareIcon, ClipIcon } from '../../static/svg'; import { useSpring, animated, config } from 'react-spring'; import useBoolean from '../../lib/hooks/useBoolean'; +import { FaFacebook, FaTwitter } from 'react-icons/fa'; +import OutsideClickHandler from 'react-outside-click-handler'; const Wrapper = styled.div` position: relative; @@ -96,12 +98,14 @@ const ShareButton = styled(animated.div)` export interface PostLikeShareButtonsProps { onLikeToggle: () => any; + onShareClick: (type: 'facebook' | 'twitter' | 'clipboard') => void; likes: number; liked: boolean; } const PostLikeShareButtons: React.FC = ({ onLikeToggle, + onShareClick, likes, liked, }) => { @@ -143,8 +147,6 @@ const PostLikeShareButtons: React.FC = ({ const [prevLiked, setPrevLiked] = useState(liked); const [open, toggle] = useBoolean(false); - console.log(open); - useEffect(() => { setPrevLiked(liked); if (!prevLiked && liked) { @@ -167,8 +169,10 @@ const PostLikeShareButtons: React.FC = ({ config: config.gentle, }); - console.log(shareX); - + const onClickShareOutside = () => { + if (!open) return; + toggle(); + }; return ( @@ -205,7 +209,9 @@ const PostLikeShareButtons: React.FC = ({ ), }} > - + onShareClick('facebook')}> + + = ({ .interpolate(shareX => `translate(${shareX * 72}px)`), }} > - + onShareClick('twitter')}> + + = ({ ), }} > - + onShareClick('clipboard')}> + +

- - - + + + + + diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 313872fd..a57ab855 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -18,6 +18,7 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { prepareEdit } from '../../modules/write'; import PostLikeShareButtons from '../../components/post/PostLikeShareButtons'; import gql from 'graphql-tag'; +import { shareFacebook, shareTwitter, copyText } from '../../lib/share'; export interface PostViewerOwnProps { username: string; @@ -31,6 +32,7 @@ const PostViewer: React.FC = ({ username, urlSlug, history, + match, }) => { const userId = useUserId(); const dispatch = useDispatch(); @@ -147,6 +149,31 @@ const PostViewer: React.FC = ({ } }; + const onShareClick = (type: 'facebook' | 'twitter' | 'clipboard') => { + const { url } = match; + const link = `https://velog.io${url}`; + + switch (type) { + case 'facebook': + shareFacebook(link); + break; + case 'twitter': + const ownPost = post.user.id === userId; + const message = ownPost + ? `제가 velog 에 게재한 "${post.title}" 포스트를 읽어보세요.` + : `${post.user.username}님이 velog 에 게재한 "${ + post.title + }" 포스트를 읽어보세요.`; + + shareTwitter(link, message); + break; + case 'clipboard': + copyText(link); + break; + default: + } + }; + if (loading) return null; if (!data) return null; @@ -169,6 +196,7 @@ const PostViewer: React.FC = ({ shareButtons={ diff --git a/src/index.tsx b/src/index.tsx index 7850c595..acbdaafa 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -54,6 +54,26 @@ if (process.env.NODE_ENV === 'production') { ); } +(window as any).fbAsyncInit = function() { + (window as any).FB.init({ + appId: '203040656938507', + autoLogAppEvents: true, + xfbml: true, + version: 'v3.0', + }); +}; + +// Load facebook SDK +(function(d, s, id) { + var js, + fjs = d.getElementsByTagName(s)[0] as any; + if (d.getElementById(id)) return; + js = d.createElement(s) as any; + js.id = id; + js.src = 'https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v3.0'; + fjs.parentNode.insertBefore(js, fjs); +})(document, 'script', 'facebook-jssdk'); + // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA diff --git a/src/lib/share.ts b/src/lib/share.ts new file mode 100644 index 00000000..e96aa18a --- /dev/null +++ b/src/lib/share.ts @@ -0,0 +1,31 @@ +export const shareFacebook = (href: string) => { + const FB = (window as any).FB; + if (!FB) return; + + FB.ui({ + method: 'share', + href, + }); +}; + +export const shareTwitter = (href: string, text: string) => { + window.open( + `https://twitter.com/share?url=${encodeURI( + encodeURI(href), + )}&text=${text}&hashtags=velog`, + 'sharer', + 'toolbar=0,status=0,width=626,height=436', + ); +}; + +export const copyText = (text: string) => { + const tempInput = document.createElement('input'); + tempInput.type = 'text'; + tempInput.value = text; + if (!document.body) return; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand('Copy'); + if (!document.body) return; + document.body.removeChild(tempInput); +}; diff --git a/src/static/svg/icon-clip.svg b/src/static/svg/icon-clip.svg new file mode 100644 index 00000000..384d3797 --- /dev/null +++ b/src/static/svg/icon-clip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/svg/index.ts b/src/static/svg/index.ts index 8baf3506..2d344c58 100644 --- a/src/static/svg/index.ts +++ b/src/static/svg/index.ts @@ -11,3 +11,4 @@ export { ReactComponent as PlusBoxIcon } from './icon-plus-box.svg'; export { ReactComponent as SeriesImage } from './image-series.svg'; export { ReactComponent as LikeIcon } from './icon-like.svg'; export { ReactComponent as ShareIcon } from './icon-share.svg'; +export { ReactComponent as ClipIcon } from './icon-clip.svg'; From 5354b527a0d9fce2225e8c10d6db72cb70043746 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 28 Jul 2019 23:10:25 +0900 Subject: [PATCH 064/736] Set z-index for ShareButtons --- src/components/post/PostLikeShareButtons.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/post/PostLikeShareButtons.tsx b/src/components/post/PostLikeShareButtons.tsx index fb85307f..ff1ba4f4 100644 --- a/src/components/post/PostLikeShareButtons.tsx +++ b/src/components/post/PostLikeShareButtons.tsx @@ -83,6 +83,7 @@ const LikeCount = styled.div` const ShareButtons = styled.div` position: relative; width: 100%; + z-index: 5; .positioner { position: absolute; } From 68569622b5113f3e9a407d146d783ffe41e0dca0 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 29 Jul 2019 07:44:00 +0900 Subject: [PATCH 065/736] Prepare TOC --- package.json | 2 + sample.js | 28 +++++++++++ src/components/common/MarkdownRender.tsx | 1 + src/index.tsx | 3 ++ src/sample.ts | 27 +++++++++++ src/types/missingTypes.d.ts | 1 + yarn.lock | 60 ++++++++++++++++++++++-- 7 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 sample.js create mode 100644 src/sample.ts diff --git a/package.json b/package.json index ecbb2e96..3b07d392 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "graphql.macro": "^1.3.4", "highlight.js": "^9.15.6", "html-webpack-plugin": "4.0.0-alpha.2", + "htmlparser2": "^3.10.1", "identity-obj-proxy": "3.0.0", "immer": "^2.1.1", "isomorphic-fetch": "^2.2.1", @@ -116,6 +117,7 @@ "remark-breaks": "^1.0.2", "remark-highlight.js": "^5.1.0", "remark-html": "^9.0.0", + "remark-slug": "^5.1.2", "resolve": "1.10.0", "sass-loader": "7.1.0", "snakecase-keys": "^2.1.0", diff --git a/sample.js b/sample.js new file mode 100644 index 00000000..c4cf1666 --- /dev/null +++ b/sample.js @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const htmlparser = require('htmlparser2'); +const html = `

헤딩 어쩌고 저쩌고


랄라랄라랄랄라


  • asdf
  • asdfasdf
  • asdfasdf
  • asdfasdf


djWJrh

asdf


console.log('a');↵

어쩌고 저쩌고 블라블라릅라라


어쩌고



궁시렁 궁시렁


궁시렁 궁시렁


안녕하세요


안녕하세요


안녕하세요


안녕하세요 한글 작성

한글 작성 시

한글 작성시

안녕


이상한데


뭔가가


이상한데


잘 되는거 맞나?


작성을 하게 되는 과정에서

이응이 가아아아아

아아아아아

아아아안다


이상하네.. 뭔가


뭐지

`; + +function convertAttributesToString(attributes) { + const entries = Object.entries(attributes); + return entries.reduce( + (acc, current) => acc + ` ${current[0]}="${current[1]}"`, + '', + ); +} + +const parser = new htmlparser.Parser({ + onopentag: (name, attributes, text) => { + const attributesStr = convertAttributesToString(attributes); + console.log({ text }); + console.log(`<${name}${attributesStr} id="@{1}">`); + }, + ontext: text => { + console.log(text); + }, + onclosetag: tagname => { + console.log(``); + }, +}); + +parser.write(html); +parser.end(); diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index 3f382642..15095501 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import styled from 'styled-components'; import remark from 'remark'; import htmlPlugin from 'remark-html'; +import slug from 'remark-slug'; import prismPlugin from '../../lib/remark/prismPlugin'; import prismThemes from '../../lib/styles/prismThemes'; import breaks from 'remark-breaks'; diff --git a/src/index.tsx b/src/index.tsx index acbdaafa..ae1bb8c3 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,6 +15,7 @@ import rootReducer from './modules'; import storage from './lib/storage'; import { setUser } from './modules/core'; import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks'; +import sample from './sample'; const store = createStore(rootReducer, composeWithDevTools()); @@ -78,3 +79,5 @@ if (process.env.NODE_ENV === 'production') { // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA serviceWorker.unregister(); + +sample(); diff --git a/src/sample.ts b/src/sample.ts new file mode 100644 index 00000000..d9c4d32d --- /dev/null +++ b/src/sample.ts @@ -0,0 +1,27 @@ +import { escapeForUrl } from './lib/utils'; + +const html = `

헤딩 어쩌고 저쩌고


랄라랄라랄랄라


  • asdf
  • asdfasdf
  • asdfasdf
  • asdfasdf


djWJrh

asdf


console.log('a');↵

어쩌고 저쩌고 블라블라릅라라


어쩌고



궁시렁 궁시렁


궁시렁 궁시렁


안녕하세요


안녕하세요


안녕하세요


안녕하세요 한글 작성

한글 작성 시

한글 작성시

안녕


이상한데


뭔가가


이상한데


잘 되는거 맞나?


작성을 하게 되는 과정에서

이응이 가아아아아

아아아아아

아아아안다


이상하네.. 뭔가


뭐지

`; + +export default function sample() { + const div = document.createElement('div'); + div.innerHTML = html; + + const h1 = div.querySelectorAll('h1'); + const h2 = div.querySelectorAll('h2'); + const h3 = div.querySelectorAll('h3'); + + const idList: string[] = []; + + const configureId = (element: HTMLHeadingElement) => { + const id = escapeForUrl(element.innerText); + const exists = idList.filter(existingId => existingId.indexOf(id) !== -1); + const uniqueId = `${id}${exists.length === 0 ? '' : `-${exists.length}`}`; + element.id = uniqueId; + }; + + h1.forEach(configureId); + h2.forEach(configureId); + h3.forEach(configureId); + + console.log(div.innerHTML); +} diff --git a/src/types/missingTypes.d.ts b/src/types/missingTypes.d.ts index 2a85e9ad..b39565c2 100644 --- a/src/types/missingTypes.d.ts +++ b/src/types/missingTypes.d.ts @@ -3,5 +3,6 @@ declare module 'remark'; declare module 'remark-html'; declare module 'remark-highlight.js'; declare module 'remark-breaks'; +declare module 'remark-slug'; declare module 'unist-util-visit'; declare module 'strip-markdown'; diff --git a/yarn.lock b/yarn.lock index 179d2da3..62135424 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3982,7 +3982,7 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1: +domelementtype@1, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -4006,6 +4006,13 @@ domhandler@2.1: dependencies: domelementtype "1" +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + domutils@1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" @@ -4021,7 +4028,7 @@ domutils@1.5.1: dom-serializer "0" domelementtype "1" -domutils@^1.7.0: +domutils@^1.5.1, domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -4097,6 +4104,11 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +"emoji-regex@>=6.0.0 <=6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" + integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4= + emoji-regex@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" @@ -4140,7 +4152,7 @@ enhanced-resolve@^4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" -entities@~1.1.1: +entities@^1.1.1, entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== @@ -5145,6 +5157,13 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-slugger@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.2.1.tgz#47e904e70bf2dccd0014748142d31126cfd49508" + integrity sha512-SsZUjg/P03KPzQBt7OxJPasGw6NRO5uOgiZ5RGXVud5iSIZ0eNZeNp5rTwCxtavrRUa/A77j8mePVc5lEvk0KQ== + dependencies: + emoji-regex ">=6.0.0 <=6.1.1" + glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -5602,6 +5621,18 @@ html-webpack-plugin@4.0.0-alpha.2: tapable "^1.0.0" util.promisify "1.0.0" +htmlparser2@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + htmlparser2@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" @@ -7466,6 +7497,11 @@ mdast-util-to-hast@^4.0.0: unist-util-visit "^1.1.0" xtend "^4.0.1" +mdast-util-to-string@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.0.6.tgz#7d85421021343b33de1552fc71cb8e5b4ae7536d" + integrity sha512-868pp48gUPmZIhfKrLbaDneuzGiw3OTDjHc5M1kAepR2CWBJ+HpEsm252K4aXdiP5coVZaJPOqGtVU6Po8xnXg== + mdn-data@~1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" @@ -9824,6 +9860,15 @@ readable-stream@^3.0.6: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -10051,6 +10096,15 @@ remark-parse@^6.0.0: vfile-location "^2.0.0" xtend "^4.0.1" +remark-slug@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-5.1.2.tgz#715ecdef8df1226786204b1887d31ab16aa24609" + integrity sha512-DWX+Kd9iKycqyD+/B+gEFO3jjnt7Yg1O05lygYSNTe5i5PIxxxPjp5qPBDxPIzp5wreF7+1ROCwRgjEcqmzr3A== + dependencies: + github-slugger "^1.0.0" + mdast-util-to-string "^1.0.0" + unist-util-visit "^1.0.0" + remark-stringify@^6.0.0: version "6.0.4" resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-6.0.4.tgz#16ac229d4d1593249018663c7bddf28aafc4e088" From 798f768b318f2f0e8db9548d4e539bbe6b6af976 Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 30 Jul 2019 23:20:30 +0900 Subject: [PATCH 066/736] Prepare functions for TOC --- package.json | 1 - src/components/common/MarkdownRender.tsx | 1 + .../write/PublishActionButtonsContainer.tsx | 6 +- src/index.tsx | 3 - src/lib/heading.ts | 57 +++++++++++++++++++ src/sample.ts | 27 --------- yarn.lock | 34 +---------- 7 files changed, 66 insertions(+), 63 deletions(-) create mode 100644 src/lib/heading.ts delete mode 100644 src/sample.ts diff --git a/package.json b/package.json index 3b07d392..dbda4706 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "graphql.macro": "^1.3.4", "highlight.js": "^9.15.6", "html-webpack-plugin": "4.0.0-alpha.2", - "htmlparser2": "^3.10.1", "identity-obj-proxy": "3.0.0", "immer": "^2.1.1", "isomorphic-fetch": "^2.2.1", diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index 15095501..bf9eabd4 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -61,6 +61,7 @@ const MarkdownRender: React.SFC = ({ .use(breaks) .use(prismPlugin) .use(htmlPlugin) + .use(slug) .process(markdown, (err: any, file: any) => { const html = String(file); setHtml(html); diff --git a/src/containers/write/PublishActionButtonsContainer.tsx b/src/containers/write/PublishActionButtonsContainer.tsx index fd4c821d..8f105ec9 100644 --- a/src/containers/write/PublishActionButtonsContainer.tsx +++ b/src/containers/write/PublishActionButtonsContainer.tsx @@ -13,6 +13,7 @@ import { pick } from 'ramda'; import { escapeForUrl, safe } from '../../lib/utils'; import { useMutation } from 'react-apollo-hooks'; import useRouter from 'use-react-router'; +import { setHeadingId } from '../../lib/heading'; type PublishActionButtonsContainerProps = {}; @@ -51,7 +52,10 @@ const PublishActionButtonsContainer: React.FC< const variables = { title: options.title, - body: options.mode === WriteMode.MARKDOWN ? options.markdown : options.html, + body: + options.mode === WriteMode.MARKDOWN + ? options.markdown + : setHeadingId(options.html), tags: options.tags, is_markdown: options.mode === WriteMode.MARKDOWN, is_temp: false, diff --git a/src/index.tsx b/src/index.tsx index ae1bb8c3..acbdaafa 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,7 +15,6 @@ import rootReducer from './modules'; import storage from './lib/storage'; import { setUser } from './modules/core'; import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks'; -import sample from './sample'; const store = createStore(rootReducer, composeWithDevTools()); @@ -79,5 +78,3 @@ if (process.env.NODE_ENV === 'production') { // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA serviceWorker.unregister(); - -sample(); diff --git a/src/lib/heading.ts b/src/lib/heading.ts new file mode 100644 index 00000000..e77a069e --- /dev/null +++ b/src/lib/heading.ts @@ -0,0 +1,57 @@ +import { escapeForUrl } from './utils'; + +/** + * Set unique id for each headings (only for h1, h2, h3) + * @param html + */ +export function setHeadingId(html: string) { + const div = document.createElement('div'); + div.innerHTML = html; + + const h1 = div.querySelectorAll('h1'); + const h2 = div.querySelectorAll('h2'); + const h3 = div.querySelectorAll('h3'); + + const idList: string[] = []; + + const setId = (element: HTMLHeadingElement) => { + const id = escapeForUrl(element.innerText); + const exists = idList.filter(existingId => existingId.indexOf(id) !== -1); + const uniqueId = `${id}${exists.length === 0 ? '' : `-${exists.length}`}`; + element.id = uniqueId; + idList.push(uniqueId); + }; + + [h1, h2, h3].forEach(elements => elements.forEach(setId)); + + return div.innerHTML; +} + +/** + * Parses heading for building TOC + * @param html + */ +export function parseHeadings(html: string) { + const div = document.createElement('div'); + div.innerHTML = html; + + const elements = Array.from(div.children); + + const headings = elements.filter(el => el.tagName.match(/H([1-3])/)); + + const headingsInfo = headings.map(heading => ({ + id: heading.id, + text: heading.textContent, + level: parseInt(heading.tagName.replace('H', ''), 10), + })); + + const minLevel = Math.min( + ...Array.from(headingsInfo.map(info => info.level)), + ); + + headingsInfo.forEach(info => { + info.level -= minLevel; + }); + + return headingsInfo; +} diff --git a/src/sample.ts b/src/sample.ts deleted file mode 100644 index d9c4d32d..00000000 --- a/src/sample.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { escapeForUrl } from './lib/utils'; - -const html = `

헤딩 어쩌고 저쩌고


랄라랄라랄랄라


  • asdf
  • asdfasdf
  • asdfasdf
  • asdfasdf


djWJrh

asdf


console.log('a');↵

어쩌고 저쩌고 블라블라릅라라


어쩌고



궁시렁 궁시렁


궁시렁 궁시렁


안녕하세요


안녕하세요


안녕하세요


안녕하세요 한글 작성

한글 작성 시

한글 작성시

안녕


이상한데


뭔가가


이상한데


잘 되는거 맞나?


작성을 하게 되는 과정에서

이응이 가아아아아

아아아아아

아아아안다


이상하네.. 뭔가


뭐지

`; - -export default function sample() { - const div = document.createElement('div'); - div.innerHTML = html; - - const h1 = div.querySelectorAll('h1'); - const h2 = div.querySelectorAll('h2'); - const h3 = div.querySelectorAll('h3'); - - const idList: string[] = []; - - const configureId = (element: HTMLHeadingElement) => { - const id = escapeForUrl(element.innerText); - const exists = idList.filter(existingId => existingId.indexOf(id) !== -1); - const uniqueId = `${id}${exists.length === 0 ? '' : `-${exists.length}`}`; - element.id = uniqueId; - }; - - h1.forEach(configureId); - h2.forEach(configureId); - h3.forEach(configureId); - - console.log(div.innerHTML); -} diff --git a/yarn.lock b/yarn.lock index 62135424..e324b5a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3982,7 +3982,7 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.1: +domelementtype@1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -4006,13 +4006,6 @@ domhandler@2.1: dependencies: domelementtype "1" -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - domutils@1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" @@ -4028,7 +4021,7 @@ domutils@1.5.1: dom-serializer "0" domelementtype "1" -domutils@^1.5.1, domutils@^1.7.0: +domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -4152,7 +4145,7 @@ enhanced-resolve@^4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" -entities@^1.1.1, entities@~1.1.1: +entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== @@ -5621,18 +5614,6 @@ html-webpack-plugin@4.0.0-alpha.2: tapable "^1.0.0" util.promisify "1.0.0" -htmlparser2@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - htmlparser2@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" @@ -9860,15 +9841,6 @@ readable-stream@^3.0.6: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^3.1.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" From aa04fc17bc858b0be432a94a6a7190603d1bea14 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 31 Jul 2019 00:03:39 +0900 Subject: [PATCH 067/736] Implement Sticky component and TOC basic stuff --- src/components/common/MarkdownRender.tsx | 7 ++- src/components/common/Sticky.tsx | 57 +++++++++++++++++ src/components/post/PostContent.tsx | 14 ++++- src/components/post/PostHead.tsx | 3 + src/components/post/PostLikeShareButtons.tsx | 47 ++------------ src/components/post/PostToc.tsx | 62 +++++++++++++++++++ src/components/post/PostViewerProvider.tsx | 18 +++++- .../post/__tests__/PostHead.test.tsx | 1 + src/containers/post/PostViewer.tsx | 2 + 9 files changed, 163 insertions(+), 48 deletions(-) create mode 100644 src/components/common/Sticky.tsx create mode 100644 src/components/post/PostToc.tsx diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index bf9eabd4..b5ed4bb0 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -11,6 +11,7 @@ import Typography from './Typography'; export interface MarkdownRenderProps { markdown: string; codeTheme?: string; + onConvertFinish?: (html: string) => any; } const MarkdownRenderBlock = styled.div` @@ -54,6 +55,7 @@ const { useState, useEffect } = React; const MarkdownRender: React.SFC = ({ markdown, codeTheme = 'atom-one-light', + onConvertFinish, }) => { const [html, setHtml] = useState(''); useEffect(() => { @@ -65,8 +67,11 @@ const MarkdownRender: React.SFC = ({ .process(markdown, (err: any, file: any) => { const html = String(file); setHtml(html); + if (onConvertFinish) { + onConvertFinish(html); + } }); - }, [markdown]); + }, [markdown, onConvertFinish]); const markup = { __html: html }; return ( diff --git a/src/components/common/Sticky.tsx b/src/components/common/Sticky.tsx new file mode 100644 index 00000000..1c740cc6 --- /dev/null +++ b/src/components/common/Sticky.tsx @@ -0,0 +1,57 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import styled from 'styled-components'; +import { getScrollTop } from '../../lib/utils'; + +const StickyBlock = styled.div``; + +export interface StickyProps { + top: number; + className?: string; +} + +const Sticky: React.FC = ({ className, top, children }) => { + const [y, setY] = useState(0); + const element = useRef(null); + const [fixed, setFixed] = useState(false); + + const setup = useCallback(() => { + if (!element.current) return; + const pos = element.current.getBoundingClientRect(); + setY(pos.top + getScrollTop()); + }, []); + + const onScroll = useCallback(() => { + const scrollTop = getScrollTop(); + const nextFixed = scrollTop + 112 > y; + if (fixed !== nextFixed) { + setFixed(nextFixed); + } + }, [fixed, y]); + + useEffect(() => { + setup(); + }, [setup]); + + // register scroll event + useEffect(() => { + window.addEventListener('scroll', onScroll); + return () => { + window.removeEventListener('scroll', onScroll); + }; + }, [onScroll]); + + return ( + + {children} + + ); +}; + +export default Sticky; diff --git a/src/components/post/PostContent.tsx b/src/components/post/PostContent.tsx index c6719d7d..9b61da30 100644 --- a/src/components/post/PostContent.tsx +++ b/src/components/post/PostContent.tsx @@ -1,7 +1,9 @@ -import * as React from 'react'; +import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import MarkdownRender from '../common/MarkdownRender'; import PostHtmlContent from './PostHtmlContent'; +import { parseHeadings } from '../../lib/heading'; +import { usePostViewerDispatch } from './PostViewerProvider'; const PostContentBlock = styled.div` width: 768px; @@ -15,10 +17,18 @@ export interface PostContentProps { } const PostContent: React.FC = ({ isMarkdown, body }) => { + const [html, setHtml] = useState(isMarkdown ? null : body); + const dispatch = usePostViewerDispatch(); + + useEffect(() => { + if (!html) return; + const toc = parseHeadings(html); + dispatch({ type: 'SET_TOC', payload: toc }); + }, [dispatch, html]); return ( {isMarkdown ? ( - + ) : ( )} diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index b788d11d..8df92298 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -87,6 +87,7 @@ export interface PostHeadProps { onRemove: () => any; onEdit: () => any; shareButtons: React.ReactNode; + toc: React.ReactNode; } const PostHead: React.FC = ({ @@ -102,6 +103,7 @@ const PostHead: React.FC = ({ onRemove, onEdit, shareButtons, + toc, }) => { const [askRemove, toggleAskRemove] = useToggle(false); @@ -127,6 +129,7 @@ const PostHead: React.FC = ({ {shareButtons} + {toc} {series && ( ` +const PostLikeShareButtonsBlock = styled(Sticky)` width: 4rem; background: ${palette.gray0}; border: 1px solid ${palette.gray1}; @@ -25,12 +26,6 @@ const PostLikeShareButtonsBlock = styled.div<{ fixed: boolean }>` display: flex; flex-direction: column; align-items: center; - ${props => - props.fixed && - css` - position: fixed; - top: 7rem; - `} `; const CircleButton = styled(animated.div)<{ active?: boolean }>` @@ -110,40 +105,6 @@ const PostLikeShareButtons: React.FC = ({ likes, liked, }) => { - const [top, setTop] = useState(0); - const [fixed, setFixed] = useState(false); - const element = useRef(null); - - const setup = useCallback(() => { - if (!element.current) return; - const pos = element.current.getBoundingClientRect(); - setTop(pos.top + getScrollTop()); - }, []); - - const onScroll = useCallback(() => { - const scrollTop = getScrollTop(); - console.log({ scrollTop, top }); - const nextFixed = scrollTop + 112 > top; - if (fixed !== nextFixed) { - setFixed(nextFixed); - console.log('switfh~'); - } - }, [fixed, top]); - - // setup - useEffect(() => { - if (!element.current) return; - setup(); - }, [setup]); - - // register scroll event - useEffect(() => { - window.addEventListener('scroll', onScroll); - return () => { - window.removeEventListener('scroll', onScroll); - }; - }, [onScroll]); - const [animateLike, setAnimateLike] = useState(false); const [prevLiked, setPrevLiked] = useState(liked); const [open, toggle] = useBoolean(false); @@ -175,9 +136,9 @@ const PostLikeShareButtons: React.FC = ({ toggle(); }; return ( - + - + = props => { + const { toc } = usePostViewerState(); + if (!toc) return null; + return ( + + + + {toc.map(item => ( + + {item.text} + + ))} + + + + ); +}; + +export default PostToc; diff --git a/src/components/post/PostViewerProvider.tsx b/src/components/post/PostViewerProvider.tsx index 2981b441..59486157 100644 --- a/src/components/post/PostViewerProvider.tsx +++ b/src/components/post/PostViewerProvider.tsx @@ -1,8 +1,16 @@ import React, { createContext, useReducer, Dispatch, useContext } from 'react'; +import { parseHeadings } from '../../lib/heading'; + +type Toc = ReturnType; + +type SetSeriesOpen = { type: 'SET_SERIES_OPEN'; payload: boolean }; +type SetToc = { type: 'SET_TOC'; payload: Toc }; + +type Action = SetSeriesOpen | SetToc; -type Action = { type: 'SET_SERIES_OPEN'; payload: boolean }; type PostViewerState = { seriesOpen: boolean; + toc: null | Toc; }; const PostViewerStateContext = createContext(null); @@ -15,8 +23,13 @@ function reducer(state: PostViewerState, action: Action): PostViewerState { ...state, seriesOpen: action.payload, }; + case 'SET_TOC': + return { + ...state, + toc: action.payload, + }; default: - throw new Error(`Unhandled action type: ${action.type}`); + throw new Error(`Unhandled action type`); } } @@ -39,6 +52,7 @@ export const usePostViewerDispatch = () => { const PostViewerProvider: React.FC = ({ children }) => { const [state, dispatch] = useReducer(reducer, { seriesOpen: false, + toc: null, }); return ( diff --git a/src/components/post/__tests__/PostHead.test.tsx b/src/components/post/__tests__/PostHead.test.tsx index b61cd481..bd999c75 100644 --- a/src/components/post/__tests__/PostHead.test.tsx +++ b/src/components/post/__tests__/PostHead.test.tsx @@ -19,6 +19,7 @@ describe('PostHead', () => { onRemove: () => {}, onEdit: () => {}, shareButtons: null, + toc: null, }; const utils = render( diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index a57ab855..986d961d 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -19,6 +19,7 @@ import { prepareEdit } from '../../modules/write'; import PostLikeShareButtons from '../../components/post/PostLikeShareButtons'; import gql from 'graphql-tag'; import { shareFacebook, shareTwitter, copyText } from '../../lib/share'; +import PostToc from '../../components/post/PostToc'; export interface PostViewerOwnProps { username: string; @@ -201,6 +202,7 @@ const PostViewer: React.FC = ({ liked={post.liked} /> } + toc={} /> Date: Mon, 5 Aug 2019 23:35:16 +0900 Subject: [PATCH 068/736] Implement TOC (with header position bug) --- src/components/common/Typography.tsx | 28 +++- src/components/post/PostLikeShareButtons.tsx | 9 +- src/components/post/PostToc.tsx | 98 +++++++++++++- .../post/__tests__/PostContent.test.tsx | 7 +- .../MarkdownPreview.test.tsx.snap | 2 +- .../__snapshots__/QuillEditor.test.tsx.snap | 2 +- src/containers/base/HeaderContainer.tsx | 127 +++++++++++++----- src/containers/base/unusedLogic | 51 +++++++ .../__snapshots__/ActiveEditor.test.tsx.snap | 2 +- 9 files changed, 277 insertions(+), 49 deletions(-) create mode 100644 src/containers/base/unusedLogic diff --git a/src/components/common/Typography.tsx b/src/components/common/Typography.tsx index 0b186c47..afb398f4 100644 --- a/src/components/common/Typography.tsx +++ b/src/components/common/Typography.tsx @@ -5,7 +5,7 @@ import palette from '../../lib/styles/palette'; const TypographyBlock = styled.div` font-size: 1.125rem; color: ${palette.gray7}; - line-height: 1.75; + line-height: 1.85; letter-spacing: -0.02em; word-break: keep-all; word-wrap: break-word; @@ -44,6 +44,32 @@ const TypographyBlock = styled.div` margin-bottom: 3rem; } } + + h1 { + font-size: 2.75em; + } + h2 { + font-size: 2em; + } + h3 { + font-size: 1.5em; + } + h4 { + font-size: 1.25em; + } + + h1, + h2, + h3, + h4 { + margin-bottom: 1rem; + } + p + h1, + p + h2, + p + h3, + p + h4 { + margin-top: 2.5rem; + } `; export interface TypographyProps {} diff --git a/src/components/post/PostLikeShareButtons.tsx b/src/components/post/PostLikeShareButtons.tsx index 057bd9f0..6ee6e756 100644 --- a/src/components/post/PostLikeShareButtons.tsx +++ b/src/components/post/PostLikeShareButtons.tsx @@ -1,7 +1,6 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import styled, { css } from 'styled-components'; import palette from '../../lib/styles/palette'; -import { getScrollTop } from '../../lib/utils'; import { LikeIcon, ShareIcon, ClipIcon } from '../../static/svg'; import { useSpring, animated, config } from 'react-spring'; import useBoolean from '../../lib/hooks/useBoolean'; @@ -28,7 +27,7 @@ const PostLikeShareButtonsBlock = styled(Sticky)` align-items: center; `; -const CircleButton = styled(animated.div)<{ active?: boolean }>` +const CircleButton = styled(animated.div)<{ active?: string }>` height: 3rem; width: 3rem; display: flex; @@ -53,7 +52,7 @@ const CircleButton = styled(animated.div)<{ active?: boolean }>` border-color: ${palette.gray9}; } ${props => - props.active && + props.active === 'true' && css` background: ${palette.teal5}; border-color: ${palette.teal5}; @@ -142,7 +141,7 @@ const PostLikeShareButtons: React.FC = ({ ` display: block; a { &:hover { @@ -34,6 +35,11 @@ const TocItem = styled.div` text-decoration: none; color: inherit; } + ${props => + props.active && + css` + color: ${palette.gray9}; + `} & + & { margin-top: 4px; } @@ -43,13 +49,93 @@ export interface PostTocProps {} const PostToc: React.FC = props => { const { toc } = usePostViewerState(); - if (!toc) return null; + const [activeId, setActiveId] = useState(null); + const [headingTops, setHeadingTops] = useState< + | null + | { + id: string; + top: number; + }[] + >(null); + + const updateTocPositions = useCallback(() => { + if (!toc) return; + const scrollTop = getScrollTop(); + const headingTops = toc.map(({ id }) => { + const el = document.getElementById(id); + if (!el) { + return { + id, + top: 0, + }; + } + const top = el.getBoundingClientRect().top + scrollTop; + return { + id, + top, + }; + }); + setHeadingTops(headingTops); + }, [toc]); + + useEffect(() => { + updateTocPositions(); + let prevScrollHeight = document.body.scrollHeight; + let timeoutId: number | null = null; + function checkScrollHeight() { + const scrollHeight = document.body.scrollHeight; + if (prevScrollHeight !== scrollHeight) { + updateTocPositions(); + } + prevScrollHeight = scrollHeight; + timeoutId = setTimeout(checkScrollHeight, 250); + } + timeoutId = setTimeout(checkScrollHeight, 250); + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [updateTocPositions]); + + const onScroll = useCallback(() => { + const scrollTop = getScrollTop(); + if (!headingTops) return; + const currentHeading = [...headingTops].reverse().find(headingTop => { + return scrollTop >= headingTop.top - 4; + }); + if (!currentHeading) { + setActiveId(null); + return; + } + + setActiveId(currentHeading.id); + }, [headingTops]); + + useEffect(() => { + window.addEventListener('scroll', onScroll); + return () => { + window.removeEventListener('scroll', onScroll); + }; + }, [onScroll]); + + // For post SSR + useEffect(() => { + onScroll(); + }, [onScroll]); + + if (!toc || !headingTops) return null; + return ( {toc.map(item => ( - + {item.text} ))} diff --git a/src/components/post/__tests__/PostContent.test.tsx b/src/components/post/__tests__/PostContent.test.tsx index ef1720cf..876bd439 100644 --- a/src/components/post/__tests__/PostContent.test.tsx +++ b/src/components/post/__tests__/PostContent.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render } from 'react-testing-library'; import PostContent, { PostContentProps } from '../PostContent'; +import PostViewerProvider from '../PostViewerProvider'; describe('PostContent', () => { const setup = (props: Partial = {}) => { @@ -8,7 +9,11 @@ describe('PostContent', () => { isMarkdown: true, body: '# Hello World!\n안녕하세요.', }; - const utils = render(); + const utils = render( + + + , + ); return { ...utils, }; diff --git a/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap b/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap index 6942311f..af123fed 100644 --- a/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap @@ -9,7 +9,7 @@ exports[`MarkdownPreview matches snapshot 1`] = ` class="sc-bxivhb cFXJHm" />
({ user: state.core.user, @@ -22,6 +22,50 @@ type StateProps = ReturnType; type DispatchProps = typeof mapDispatchToProps; type HeaderContainerProps = OwnProps & StateProps & DispatchProps; +type StartFloating = { + type: 'START_FLOATING'; +}; + +type ExitFloating = { + type: 'EXIT_FLOATING'; +}; + +type SetMargin = { + type: 'SET_MARGIN'; + margin: number; +}; + +type FloatingState = { + floating: boolean; + margin: number; +}; + +type Action = StartFloating | ExitFloating | SetMargin; + +function reducer(state: FloatingState, action: Action) { + switch (action.type) { + case 'START_FLOATING': + return { + ...state, + margin: -80, + floating: true, + }; + case 'EXIT_FLOATING': + return { + ...state, + floating: false, + margin: -80, + }; + case 'SET_MARGIN': + return { + ...state, + margin: action.margin, + }; + default: + throw new Error('Unhandled Action'); + } +} + const HeaderContainer: React.SFC = ({ showAuthModal, user, @@ -29,53 +73,70 @@ const HeaderContainer: React.SFC = ({ userLogo, velogUsername, }) => { - const lastY = useRef(0); - const direction = useRef(null); + const [{ floating, margin }, dispatch] = useReducer(reducer, { + floating: false, + margin: 0, + }); + const prevScrollTop = useRef(0); + const prevDirection = useRef<'DOWN' | 'UP'>('DOWN'); + const baseY = useRef(0); - const [floating, setFloating] = useState(false); - const [baseY, setBaseY] = useState(0); - const [floatingMargin, setFloatingMargin] = useState(-80); const onScroll = useCallback(() => { const scrollTop = getScrollTop(); - // turns floating OFF - if (floating && scrollTop === 0) { - setFloating(false); - setFloatingMargin(-80); - return; + if (scrollTop > 80) { + dispatch({ type: 'START_FLOATING' }); } - - if (floating) { - const calculated = -80 + baseY - scrollTop; - setFloatingMargin(calculated > 0 ? 0 : calculated); + if (scrollTop === 0) { + dispatch({ type: 'EXIT_FLOATING' }); + prevDirection.current = 'DOWN'; + prevScrollTop.current = 0; + baseY.current = 0; } - const d = scrollTop < lastY.current ? 'UP' : 'DOWN'; + let direction: 'UP' | 'DOWN' = + prevScrollTop.current > scrollTop ? 'UP' : 'DOWN'; + + let margin = -80 + baseY.current - scrollTop; - // Fixes flickering issue - if ( - d !== direction.current && - (floatingMargin === 0 || floatingMargin <= -80) - ) { - setBaseY(scrollTop + (d === 'DOWN' ? 80 : 0)); + // set limits for margin + if (margin < -80) { + margin = -80; } + if (margin > 0) { + margin = 0; + } + + // hide header when moved by TOC + // if ( + // prevDirection.current === 'DOWN' && + // scrollTop - prevScrollTop.current < -80 + // ) { + // // margin = -80; + // // direction = 'DOWN'; + // // baseY.current = 0; + // } - // turns floating ON - if (direction.current !== 'UP' && d === 'UP' && scrollTop > 120) { - setFloating(true); + // direction changes from down to up + if (prevDirection.current !== direction && [-80, 0].includes(margin)) { + baseY.current = prevScrollTop.current + (direction === 'DOWN' ? 80 : 0); } - direction.current = d; - lastY.current = scrollTop; - }, [baseY, floating, floatingMargin]); + dispatch({ + type: 'SET_MARGIN', + margin: margin, + }); + + prevDirection.current = direction; + prevScrollTop.current = scrollTop; + }, []); useEffect(() => { document.addEventListener('scroll', onScroll); - const reset = () => { + return () => { document.removeEventListener('scroll', onScroll); }; - return reset; - }, [floating, baseY, floatingMargin, onScroll]); + }, [onScroll]); const onLoginClick = () => { showAuthModal('LOGIN'); @@ -84,7 +145,7 @@ const HeaderContainer: React.SFC = ({ return (
(null); + + const [floating, setFloating] = useState(false); + const [baseY, setBaseY] = useState(0); + const [floatingMargin, setFloatingMargin] = useState(-80); + const onScroll = useCallback(() => { + const scrollTop = getScrollTop(); + + // turns floating OFF + if (floating && scrollTop === 0) { + setFloating(false); + setFloatingMargin(-80); + return; + } + + if (floating) { + const calculated = -80 + baseY - scrollTop; + setFloatingMargin(calculated > 0 ? 0 : calculated); + } + + const d = scrollTop < lastY.current ? 'UP' : 'DOWN'; + console.log(d); + + // Fixes flickering issue + if ( + d !== direction.current && + (floatingMargin === 0 || floatingMargin <= -80) + ) { + setBaseY(scrollTop + (d === 'DOWN' ? 80 : 0)); + } + + // turns floating ON + if (direction.current !== 'UP' && d === 'UP' && scrollTop > 120) { + setFloating(true); + } + + direction.current = d; + lastY.current = scrollTop; + }, [baseY, floating, floatingMargin]); + + useEffect(() => { + document.addEventListener('scroll', onScroll); + const reset = () => { + document.removeEventListener('scroll', onScroll); + }; + return reset; + }, [floating, baseY, floatingMargin, onScroll]); + + */ \ No newline at end of file diff --git a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap index fa0bd375..a464fba7 100644 --- a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap +++ b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap @@ -267,7 +267,7 @@ exports[`ActiveEditor matches snapshot 1`] = ` class="sc-jWBwVP boWCkI" >
Date: Mon, 5 Aug 2019 23:43:37 +0900 Subject: [PATCH 069/736] Refactor header container and reducer --- src/containers/base/HeaderContainer.tsx | 50 ++++++++++--------------- src/modules/header.ts | 41 +++++++++++--------- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src/containers/base/HeaderContainer.tsx b/src/containers/base/HeaderContainer.tsx index 102f5b04..dd2db29d 100644 --- a/src/containers/base/HeaderContainer.tsx +++ b/src/containers/base/HeaderContainer.tsx @@ -2,25 +2,12 @@ import React, { useReducer } from 'react'; import Header from '../../components/base/Header'; import { getScrollTop } from '../../lib/utils'; import { RootState } from '../../modules'; -import { connect } from 'react-redux'; +import { connect, useSelector, useDispatch } from 'react-redux'; import { showAuthModal } from '../../modules/core'; const { useEffect, useRef, useCallback } = React; -const mapStateToProps = (state: RootState) => ({ - user: state.core.user, - custom: state.header.custom, - userLogo: state.header.userLogo, - velogUsername: state.header.velogUsername, -}); - -const mapDispatchToProps = { - showAuthModal, -}; -interface OwnProps {} -type StateProps = ReturnType; -type DispatchProps = typeof mapDispatchToProps; -type HeaderContainerProps = OwnProps & StateProps & DispatchProps; +type HeaderContainerProps = {}; type StartFloating = { type: 'START_FLOATING'; @@ -66,14 +53,18 @@ function reducer(state: FloatingState, action: Action) { } } -const HeaderContainer: React.SFC = ({ - showAuthModal, - user, - custom, - userLogo, - velogUsername, -}) => { - const [{ floating, margin }, dispatch] = useReducer(reducer, { +const HeaderContainer: React.SFC = () => { + const { user, custom, userLogo, velogUsername } = useSelector( + (state: RootState) => ({ + user: state.core.user, + custom: state.header.custom, + userLogo: state.header.userLogo, + velogUsername: state.header.velogUsername, + }), + ); + const dispatch = useDispatch(); + + const [{ floating, margin }, localDispatch] = useReducer(reducer, { floating: false, margin: 0, }); @@ -85,10 +76,10 @@ const HeaderContainer: React.SFC = ({ const scrollTop = getScrollTop(); if (scrollTop > 80) { - dispatch({ type: 'START_FLOATING' }); + localDispatch({ type: 'START_FLOATING' }); } if (scrollTop === 0) { - dispatch({ type: 'EXIT_FLOATING' }); + localDispatch({ type: 'EXIT_FLOATING' }); prevDirection.current = 'DOWN'; prevScrollTop.current = 0; baseY.current = 0; @@ -122,7 +113,7 @@ const HeaderContainer: React.SFC = ({ baseY.current = prevScrollTop.current + (direction === 'DOWN' ? 80 : 0); } - dispatch({ + localDispatch({ type: 'SET_MARGIN', margin: margin, }); @@ -139,7 +130,7 @@ const HeaderContainer: React.SFC = ({ }, [onScroll]); const onLoginClick = () => { - showAuthModal('LOGIN'); + dispatch(showAuthModal('LOGIN')); }; return ( @@ -155,7 +146,4 @@ const HeaderContainer: React.SFC = ({ ); }; -export default connect( - mapStateToProps, - mapDispatchToProps, -)(HeaderContainer); +export default HeaderContainer; diff --git a/src/modules/header.ts b/src/modules/header.ts index ae3fb762..2ee3c520 100644 --- a/src/modules/header.ts +++ b/src/modules/header.ts @@ -1,5 +1,9 @@ -import { createReducer, updateKey } from '../lib/utils'; -import { createStandardAction } from 'typesafe-actions'; +import { updateKey } from '../lib/utils'; +import { + createStandardAction, + createReducer, + ActionType, +} from 'typesafe-actions'; const SET_CUSTOM = 'header/SET_CUSTOM'; const SET_USER_LOGO = 'header/SET_USER_LOGO'; @@ -22,9 +26,13 @@ export const setVelogUsername = createStandardAction(SET_VELOG_USERNAME)< string >(); -type SetCustom = ReturnType; -type SetUserLogo = ReturnType; -type SetVelogUsername = ReturnType; +export const headerActions = { + setCustom, + setUserLogo, + setVelogUsername, +}; + +type HeaderAction = ActionType; const initialState: HeaderState = { custom: false, @@ -32,19 +40,16 @@ const initialState: HeaderState = { velogUsername: null, }; -const header = createReducer( - { - [SET_CUSTOM]: (state, action: SetCustom) => { - return updateKey(state, 'custom', action.payload); - }, - [SET_USER_LOGO]: (state, { payload }: SetUserLogo) => { - return updateKey(state, 'userLogo', payload); - }, - [SET_VELOG_USERNAME]: (state, { payload }: SetVelogUsername) => { - return updateKey(state, 'velogUsername', payload); - }, +const header = createReducer(initialState, { + [SET_CUSTOM]: (state, action) => { + return updateKey(state, 'custom', action.payload); + }, + [SET_USER_LOGO]: (state, { payload }) => { + return updateKey(state, 'userLogo', payload); + }, + [SET_VELOG_USERNAME]: (state, { payload }) => { + return updateKey(state, 'velogUsername', payload); }, - initialState, -); +}); export default header; From 1483ea7a85f190c8d8867b8776a689408a378b01 Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 9 Aug 2019 23:14:03 +0900 Subject: [PATCH 070/736] Finishes TOC and modifies Typography --- src/components/common/Typography.tsx | 1 + src/components/post/PostToc.tsx | 14 ++++++--- src/containers/base/HeaderContainer.tsx | 22 +++++++++++++ src/containers/post/PostViewer.tsx | 17 ++++++++-- src/modules/__tests__/header.test.ts | 15 +++++++-- src/modules/header.ts | 41 +++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/components/common/Typography.tsx b/src/components/common/Typography.tsx index afb398f4..02eb6f56 100644 --- a/src/components/common/Typography.tsx +++ b/src/components/common/Typography.tsx @@ -7,6 +7,7 @@ const TypographyBlock = styled.div` color: ${palette.gray7}; line-height: 1.85; letter-spacing: -0.02em; + text-align: justify; word-break: keep-all; word-wrap: break-word; font-family: 'Spoqa Han Sans', -apple-system, BlinkMacSystemFont, diff --git a/src/components/post/PostToc.tsx b/src/components/post/PostToc.tsx index 37ac92f7..97114224 100644 --- a/src/components/post/PostToc.tsx +++ b/src/components/post/PostToc.tsx @@ -16,7 +16,7 @@ const Positioner = styled.div` const PostTocBlock = styled(Sticky)` width: 240px; - margin-left: 3rem; + margin-left: 5rem; border-left: 2px solid ${palette.gray2}; padding-left: 0.75rem; padding-top: 0.25rem; @@ -28,6 +28,7 @@ const PostTocBlock = styled(Sticky)` const TocItem = styled.div<{ active: boolean }>` display: block; + transition: 0.125s all ease-in; a { &:hover { color: ${palette.gray9}; @@ -39,15 +40,18 @@ const TocItem = styled.div<{ active: boolean }>` props.active && css` color: ${palette.gray9}; + transform: scale(1.05); `} & + & { margin-top: 4px; } `; -export interface PostTocProps {} +export interface PostTocProps { + hideHeader: () => void; +} -const PostToc: React.FC = props => { +const PostToc: React.FC = ({ hideHeader }) => { const { toc } = usePostViewerState(); const [activeId, setActiveId] = useState(null); const [headingTops, setHeadingTops] = useState< @@ -136,7 +140,9 @@ const PostToc: React.FC = props => { style={{ marginLeft: item.level * 12 }} active={activeId === item.id} > - {item.text} + + {item.text} + ))} diff --git a/src/containers/base/HeaderContainer.tsx b/src/containers/base/HeaderContainer.tsx index dd2db29d..533b76a7 100644 --- a/src/containers/base/HeaderContainer.tsx +++ b/src/containers/base/HeaderContainer.tsx @@ -72,7 +72,17 @@ const HeaderContainer: React.SFC = () => { const prevDirection = useRef<'DOWN' | 'UP'>('DOWN'); const baseY = useRef(0); + const scrolling = useRef(false); + const scrollingTimeoutId = useRef(null); + const onScroll = useCallback(() => { + if (scrollingTimeoutId.current) { + clearTimeout(scrollingTimeoutId.current); + } + scrollingTimeoutId.current = setTimeout(() => { + scrolling.current = false; + }, 500); + const scrollTop = getScrollTop(); if (scrollTop > 80) { @@ -98,6 +108,17 @@ const HeaderContainer: React.SFC = () => { margin = 0; } + // Scrolled by TOC + if (!scrolling.current && scrollTop - prevScrollTop.current < -80) { + localDispatch({ + type: 'SET_MARGIN', + margin: -80, + }); + prevScrollTop.current = scrollTop; + prevDirection.current = 'DOWN'; + baseY.current = 0; + return; + } // hide header when moved by TOC // if ( // prevDirection.current === 'DOWN' && @@ -120,6 +141,7 @@ const HeaderContainer: React.SFC = () => { prevDirection.current = direction; prevScrollTop.current = scrollTop; + scrolling.current = true; }, []); useEffect(() => { diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 986d961d..577bc78b 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { READ_POST, SinglePost, @@ -20,6 +20,9 @@ import PostLikeShareButtons from '../../components/post/PostLikeShareButtons'; import gql from 'graphql-tag'; import { shareFacebook, shareTwitter, copyText } from '../../lib/share'; import PostToc from '../../components/post/PostToc'; +import { setMargin, setDirection, setBaseY } from '../../modules/header'; +import { getScrollTop } from '../../lib/utils'; +import { RootState } from '../../modules'; export interface PostViewerOwnProps { username: string; @@ -36,6 +39,7 @@ const PostViewer: React.FC = ({ match, }) => { const userId = useUserId(); + const margin = useSelector((state: RootState) => state.header.margin); const dispatch = useDispatch(); const readPost = useQuery<{ post: SinglePost }>(READ_POST, { variables: { @@ -180,6 +184,15 @@ const PostViewer: React.FC = ({ const { post } = data; + const hideHeader = () => { + // dispatch(setMargin(-80)); + // dispatch(setDirection('DOWN')); + // dispatch(setBaseY(0)); + // setTimeout(() => { + // dispatch(setMargin(-80)); + // dispatch(setDirection('DOWN')); + // }); + }; return ( = ({ liked={post.liked} /> } - toc={} + toc={} /> { describe('reducer', () => { - const initialState = { + const initialState: HeaderState = { custom: false, userLogo: null, velogUsername: null, + floating: false, + margin: 0, + direction: 'DOWN', + baseY: 0, }; it('should have initialState', () => { - const state = header(undefined, {}); + const state = header(undefined, { type: 'INIT' } as any); expect(state).toEqual(initialState); }); it('handles SET_CUSTOM action', () => { diff --git a/src/modules/header.ts b/src/modules/header.ts index 2ee3c520..7caa5e00 100644 --- a/src/modules/header.ts +++ b/src/modules/header.ts @@ -9,6 +9,12 @@ const SET_CUSTOM = 'header/SET_CUSTOM'; const SET_USER_LOGO = 'header/SET_USER_LOGO'; const SET_VELOG_USERNAME = 'header/SET_VELOG_USERNAME'; +const START_FLOATING = 'header/START_FLOATING'; +const EXIT_FLOATING = 'header/EXIT_FLOATING'; +const SET_MARGIN = 'header/SET_MARGIN'; +const SET_DIRECTION = 'header/SET_DIRECTION'; +const SET_BASE_Y = 'header/SET_BASE_Y'; + export type UserLogo = { title: string | null; logo_image: string | null; @@ -18,6 +24,10 @@ export interface HeaderState { custom: boolean; userLogo: null | UserLogo; velogUsername: string | null; + floating: boolean; + margin: number; + direction: 'UP' | 'DOWN'; + baseY: number; } export const setCustom = createStandardAction(SET_CUSTOM)(); @@ -26,10 +36,23 @@ export const setVelogUsername = createStandardAction(SET_VELOG_USERNAME)< string >(); +export const startFloating = createStandardAction(START_FLOATING)(); +export const exitFloating = createStandardAction(EXIT_FLOATING)(); +export const setMargin = createStandardAction(SET_MARGIN)(); +export const setDirection = createStandardAction(SET_DIRECTION)< + 'UP' | 'DOWN' +>(); +export const setBaseY = createStandardAction(SET_BASE_Y)(); + export const headerActions = { setCustom, setUserLogo, setVelogUsername, + startFloating, + exitFloating, + setMargin, + setDirection, + setBaseY, }; type HeaderAction = ActionType; @@ -38,6 +61,10 @@ const initialState: HeaderState = { custom: false, userLogo: null, velogUsername: null, + floating: false, + margin: 0, + direction: 'DOWN', + baseY: 0, }; const header = createReducer(initialState, { @@ -50,6 +77,20 @@ const header = createReducer(initialState, { [SET_VELOG_USERNAME]: (state, { payload }) => { return updateKey(state, 'velogUsername', payload); }, + [START_FLOATING]: state => ({ + ...state, + margin: -80, + floating: true, + }), + [EXIT_FLOATING]: state => ({ + ...state, + floating: false, + margin: -80, + }), + [SET_MARGIN]: (state, action) => updateKey(state, 'margin', action.payload), + [SET_DIRECTION]: (state, action) => + updateKey(state, 'direction', action.payload), + [SET_BASE_Y]: (state, action) => updateKey(state, 'baseY', action.payload), }); export default header; From 05c54cff3560aa692dfaaeb1ca6aee76b4591f9f Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 9 Aug 2019 23:22:31 +0900 Subject: [PATCH 071/736] Remove unused code --- src/components/common/Typography.tsx | 13 ++++++++----- src/components/post/PostToc.tsx | 10 +++------- src/containers/base/HeaderContainer.tsx | 2 +- src/containers/post/PostViewer.tsx | 17 ++--------------- src/modules/__tests__/header.test.ts | 4 ---- src/modules/header.ts | 22 ---------------------- 6 files changed, 14 insertions(+), 54 deletions(-) diff --git a/src/components/common/Typography.tsx b/src/components/common/Typography.tsx index 02eb6f56..b9729607 100644 --- a/src/components/common/Typography.tsx +++ b/src/components/common/Typography.tsx @@ -7,9 +7,11 @@ const TypographyBlock = styled.div` color: ${palette.gray7}; line-height: 1.85; letter-spacing: -0.02em; - text-align: justify; word-break: keep-all; word-wrap: break-word; + p { + text-align: justify; + } font-family: 'Spoqa Han Sans', -apple-system, BlinkMacSystemFont, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', arial, 나눔고딕, 'Nanum Gothic', 돋움; @@ -47,22 +49,23 @@ const TypographyBlock = styled.div` } h1 { - font-size: 2.75em; + font-size: 3rem; } h2 { - font-size: 2em; + font-size: 2rem; } h3 { - font-size: 1.5em; + font-size: 1.5rem; } h4 { - font-size: 1.25em; + font-size: 1.125rem; } h1, h2, h3, h4 { + line-height: 1.35; margin-bottom: 1rem; } p + h1, diff --git a/src/components/post/PostToc.tsx b/src/components/post/PostToc.tsx index 97114224..dfac7390 100644 --- a/src/components/post/PostToc.tsx +++ b/src/components/post/PostToc.tsx @@ -47,11 +47,9 @@ const TocItem = styled.div<{ active: boolean }>` } `; -export interface PostTocProps { - hideHeader: () => void; -} +export interface PostTocProps {} -const PostToc: React.FC = ({ hideHeader }) => { +const PostToc: React.FC = () => { const { toc } = usePostViewerState(); const [activeId, setActiveId] = useState(null); const [headingTops, setHeadingTops] = useState< @@ -140,9 +138,7 @@ const PostToc: React.FC = ({ hideHeader }) => { style={{ marginLeft: item.level * 12 }} active={activeId === item.id} > - - {item.text} - + {item.text} ))} diff --git a/src/containers/base/HeaderContainer.tsx b/src/containers/base/HeaderContainer.tsx index 533b76a7..74e53b6c 100644 --- a/src/containers/base/HeaderContainer.tsx +++ b/src/containers/base/HeaderContainer.tsx @@ -2,7 +2,7 @@ import React, { useReducer } from 'react'; import Header from '../../components/base/Header'; import { getScrollTop } from '../../lib/utils'; import { RootState } from '../../modules'; -import { connect, useSelector, useDispatch } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { showAuthModal } from '../../modules/core'; const { useEffect, useRef, useCallback } = React; diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 577bc78b..986d961d 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { READ_POST, SinglePost, @@ -20,9 +20,6 @@ import PostLikeShareButtons from '../../components/post/PostLikeShareButtons'; import gql from 'graphql-tag'; import { shareFacebook, shareTwitter, copyText } from '../../lib/share'; import PostToc from '../../components/post/PostToc'; -import { setMargin, setDirection, setBaseY } from '../../modules/header'; -import { getScrollTop } from '../../lib/utils'; -import { RootState } from '../../modules'; export interface PostViewerOwnProps { username: string; @@ -39,7 +36,6 @@ const PostViewer: React.FC = ({ match, }) => { const userId = useUserId(); - const margin = useSelector((state: RootState) => state.header.margin); const dispatch = useDispatch(); const readPost = useQuery<{ post: SinglePost }>(READ_POST, { variables: { @@ -184,15 +180,6 @@ const PostViewer: React.FC = ({ const { post } = data; - const hideHeader = () => { - // dispatch(setMargin(-80)); - // dispatch(setDirection('DOWN')); - // dispatch(setBaseY(0)); - // setTimeout(() => { - // dispatch(setMargin(-80)); - // dispatch(setDirection('DOWN')); - // }); - }; return ( = ({ liked={post.liked} /> } - toc={} + toc={} /> { custom: false, userLogo: null, velogUsername: null, - floating: false, - margin: 0, - direction: 'DOWN', - baseY: 0, }; it('should have initialState', () => { const state = header(undefined, { type: 'INIT' } as any); diff --git a/src/modules/header.ts b/src/modules/header.ts index 7caa5e00..631aef13 100644 --- a/src/modules/header.ts +++ b/src/modules/header.ts @@ -24,10 +24,6 @@ export interface HeaderState { custom: boolean; userLogo: null | UserLogo; velogUsername: string | null; - floating: boolean; - margin: number; - direction: 'UP' | 'DOWN'; - baseY: number; } export const setCustom = createStandardAction(SET_CUSTOM)(); @@ -61,10 +57,6 @@ const initialState: HeaderState = { custom: false, userLogo: null, velogUsername: null, - floating: false, - margin: 0, - direction: 'DOWN', - baseY: 0, }; const header = createReducer(initialState, { @@ -77,20 +69,6 @@ const header = createReducer(initialState, { [SET_VELOG_USERNAME]: (state, { payload }) => { return updateKey(state, 'velogUsername', payload); }, - [START_FLOATING]: state => ({ - ...state, - margin: -80, - floating: true, - }), - [EXIT_FLOATING]: state => ({ - ...state, - floating: false, - margin: -80, - }), - [SET_MARGIN]: (state, action) => updateKey(state, 'margin', action.payload), - [SET_DIRECTION]: (state, action) => - updateKey(state, 'direction', action.payload), - [SET_BASE_Y]: (state, action) => updateKey(state, 'baseY', action.payload), }); export default header; From 8c2c043b2936c945988c1aa6228c84aa909a436d Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 9 Aug 2019 23:28:32 +0900 Subject: [PATCH 072/736] Remove unused file --- src/containers/base/unusedLogic | 51 --------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 src/containers/base/unusedLogic diff --git a/src/containers/base/unusedLogic b/src/containers/base/unusedLogic deleted file mode 100644 index 47016e15..00000000 --- a/src/containers/base/unusedLogic +++ /dev/null @@ -1,51 +0,0 @@ -/* - const lastY = useRef(0); - const direction = useRef(null); - - const [floating, setFloating] = useState(false); - const [baseY, setBaseY] = useState(0); - const [floatingMargin, setFloatingMargin] = useState(-80); - const onScroll = useCallback(() => { - const scrollTop = getScrollTop(); - - // turns floating OFF - if (floating && scrollTop === 0) { - setFloating(false); - setFloatingMargin(-80); - return; - } - - if (floating) { - const calculated = -80 + baseY - scrollTop; - setFloatingMargin(calculated > 0 ? 0 : calculated); - } - - const d = scrollTop < lastY.current ? 'UP' : 'DOWN'; - console.log(d); - - // Fixes flickering issue - if ( - d !== direction.current && - (floatingMargin === 0 || floatingMargin <= -80) - ) { - setBaseY(scrollTop + (d === 'DOWN' ? 80 : 0)); - } - - // turns floating ON - if (direction.current !== 'UP' && d === 'UP' && scrollTop > 120) { - setFloating(true); - } - - direction.current = d; - lastY.current = scrollTop; - }, [baseY, floating, floatingMargin]); - - useEffect(() => { - document.addEventListener('scroll', onScroll); - const reset = () => { - document.removeEventListener('scroll', onScroll); - }; - return reset; - }, [floating, baseY, floatingMargin, onScroll]); - - */ \ No newline at end of file From e20816f2ab9f19f26379dbfcf5433a501a401240 Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 9 Aug 2019 23:28:40 +0900 Subject: [PATCH 073/736] Modify readPost graphql --- src/lib/graphql/post.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 8f620c71..1637235b 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -68,6 +68,23 @@ export type SeriesPost = { }; }; +export interface LinkedPosts { + previous: LinkedPost | null; + next: LinkedPost | null; +} + +export interface LinkedPost { + id: string; + title: string; + url_slug: string; + user: User; +} + +export interface User { + id: string; + username: string; +} + export interface SinglePost { id: string; title: string; @@ -102,6 +119,7 @@ export interface SinglePost { } | null; liked: boolean; likes: number; + linked_posts: LinkedPosts; } export interface CommentWithReplies { @@ -194,6 +212,26 @@ export const READ_POST = gql` } } } + linked_posts { + previous { + id + title + url_slug + user { + id + username + } + } + next { + id + title + url_slug + user { + id + username + } + } + } } } `; From 5ed4144f344bb8723a4d1e0f13255ba73e15c799 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 10 Aug 2019 00:04:51 +0900 Subject: [PATCH 074/736] Partially done with linked post --- src/components/post/LinkedPostItem.tsx | 122 ++++++++++++++++++ src/components/post/LinkedPostList.tsx | 35 +++++ src/components/post/PostCommentsTemplate.tsx | 2 +- .../post/__tests__/LinkedPostItem.test.tsx | 16 +++ .../post/__tests__/LinkedPostList.test.tsx | 38 ++++++ src/containers/post/PostViewer.tsx | 2 + src/lib/graphql/post.ts | 10 +- 7 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 src/components/post/LinkedPostItem.tsx create mode 100644 src/components/post/LinkedPostList.tsx create mode 100644 src/components/post/__tests__/LinkedPostItem.test.tsx create mode 100644 src/components/post/__tests__/LinkedPostList.test.tsx diff --git a/src/components/post/LinkedPostItem.tsx b/src/components/post/LinkedPostItem.tsx new file mode 100644 index 00000000..485e5932 --- /dev/null +++ b/src/components/post/LinkedPostItem.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import styled, { css, keyframes } from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { MdArrowBack, MdArrowForward } from 'react-icons/md'; +import { LinkedPost } from '../../lib/graphql/post'; + +const bounceLeft = keyframes` + 0% { + transform: translateX(0px) + } + 50% { + transform: translateX(-8px) + } + 100% { + transform: translateX(0px) + } +`; + +const bounceRight = keyframes` + 0% { + transform: translateX(0px) + } + 50% { + transform: translateX(8px) + } + 100% { + transform: translateX(0px) + } +`; + +const Circle = styled.div<{ right?: boolean }>` + width: 32px; + height: 32px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid ${palette.teal5}; + font-size: 1.5rem; + color: ${palette.teal5}; + ${props => + props.right + ? css` + margin-left: 1rem; + ` + : css` + margin-right: 1rem; + `} +`; + +const LinkedPostItemBlock = styled.div<{ right?: boolean }>` + cursor: pointer; + background: ${palette.gray0}; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); + width: 100%; + padding-left: 1rem; + padding-right: 1rem; + height: 4rem; + display: flex; + align-items: center; + + ${props => + props.right && + css` + flex-direction: row-reverse; + `} + &:hover { + ${Circle} { + animation-duration: 0.35s; + animation-name: ${props => (props.right ? bounceRight : bounceLeft)}; + animation-fill-mode: forwards; + animation-timing-function: ease-out; + } + } +`; + +const Text = styled.div<{ right?: boolean }>` + flex: 1; + display: flex; + flex-direction: column; + align-items: ${props => (props.right ? 'flex-end' : 'flex-start')}; + line-height: 1; + .description { + font-size: 0.75rem; + font-weight: bold; + color: ${palette.gray7}; + } + h3 { + font-size: 1.125rem; + color: ${palette.gray7}; + line-height: 1; + margin: 0; + margin-top: 0.5rem; + } +`; + +export interface LinkedPostItemProps { + right?: boolean; + linkedPost: LinkedPost | null; +} + +const LinkedPostItem: React.FC = ({ + right, + linkedPost, +}) => { + if (!linkedPost) { + return null; + } + return ( + + + {right ? : } + + +
{right ? '다음' : '이전'} 포스트
+

{linkedPost.title}

+
+
+ ); +}; + +export default LinkedPostItem; diff --git a/src/components/post/LinkedPostList.tsx b/src/components/post/LinkedPostList.tsx new file mode 100644 index 00000000..41049f3d --- /dev/null +++ b/src/components/post/LinkedPostList.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import styled from 'styled-components'; +import LinkedPost from './LinkedPostItem'; +import { LinkedPosts } from '../../lib/graphql/post'; +import VelogResponsive from '../velog/VelogResponsive'; + +const LinkedPostsBlock = styled(VelogResponsive)` + margin-top: 3rem; + display: flex; +`; +const Wrapper = styled.div` + flex: 1; + & + & { + margin-left: 3rem; + } +`; + +export interface LinkedPostListProps { + linkedPosts: LinkedPosts; +} + +const LinkedPostList: React.FC = ({ linkedPosts }) => { + return ( + + + + + + + + + ); +}; + +export default LinkedPostList; diff --git a/src/components/post/PostCommentsTemplate.tsx b/src/components/post/PostCommentsTemplate.tsx index e8d941f5..5dec89a2 100644 --- a/src/components/post/PostCommentsTemplate.tsx +++ b/src/components/post/PostCommentsTemplate.tsx @@ -4,7 +4,7 @@ import VelogResponsive from '../velog/VelogResponsive'; import palette from '../../lib/styles/palette'; const PostCommentsTemplateBlock = styled(VelogResponsive)` - margin-top: 7.5rem; + margin-top: 3rem; color: ${palette.gray8}; h4 { font-size: 1.125rem; diff --git a/src/components/post/__tests__/LinkedPostItem.test.tsx b/src/components/post/__tests__/LinkedPostItem.test.tsx new file mode 100644 index 00000000..94dd117b --- /dev/null +++ b/src/components/post/__tests__/LinkedPostItem.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { render } from 'react-testing-library'; +import LinkedPostItem, { LinkedPostItemProps } from '../LinkedPostItem'; + +describe('LinkedPostItem', () => { + const setup = (props: Partial = {}) => { + const initialProps: LinkedPostItemProps = {}; + const utils = render(); + return { + ...utils, + }; + }; + it('renders properly', () => { + setup(); + }); +}); diff --git a/src/components/post/__tests__/LinkedPostList.test.tsx b/src/components/post/__tests__/LinkedPostList.test.tsx new file mode 100644 index 00000000..6ff26f94 --- /dev/null +++ b/src/components/post/__tests__/LinkedPostList.test.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { render } from 'react-testing-library'; +import LinkedPostList, { LinkedPostListProps } from '../LinkedPostList'; + +describe('LinkedPostList', () => { + const setup = (props: Partial = {}) => { + const initialProps: LinkedPostListProps = { + linkedPosts: { + previous: { + id: 'a5063709-945e-4d93-a494-16ad5a881a61', + title: '1', + url_slug: '1', + user: { + id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', + username: 'blablabla', + }, + }, + next: { + id: '6a996a6b-4a55-4049-bee2-a9a0b504283c', + title: '3', + url_slug: '3', + user: { + id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', + username: 'blablabla', + }, + }, + }, + }; + + const utils = render(); + return { + ...utils, + }; + }; + it('renders properly', () => { + setup(); + }); +}); diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 986d961d..fac27d4b 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -20,6 +20,7 @@ import PostLikeShareButtons from '../../components/post/PostLikeShareButtons'; import gql from 'graphql-tag'; import { shareFacebook, shareTwitter, copyText } from '../../lib/share'; import PostToc from '../../components/post/PostToc'; +import LinkedPostList from '../../components/post/LinkedPostList'; export interface PostViewerOwnProps { username: string; @@ -205,6 +206,7 @@ const PostViewer: React.FC = ({ toc={} /> + Date: Sun, 11 Aug 2019 00:06:45 +0900 Subject: [PATCH 075/736] Implement LinkedPost prefetch --- src/components/post/LinkedPostItem.tsx | 22 ++++++- src/components/post/LinkedPostList.tsx | 1 + src/components/post/PostSeriesInfo.tsx | 9 ++- src/components/post/PostViewerProvider.tsx | 28 ++++++-- .../post/__tests__/LinkedPostItem.test.tsx | 9 ++- .../post/__tests__/LinkedPostList.test.tsx | 7 +- .../post/__tests__/PostSeriesInfo.test.tsx | 2 +- src/containers/post/PostViewer.tsx | 65 ++++++++++++++++++- .../post/__tests__/PostViewer.test.tsx | 4 ++ 9 files changed, 129 insertions(+), 18 deletions(-) diff --git a/src/components/post/LinkedPostItem.tsx b/src/components/post/LinkedPostItem.tsx index 485e5932..7a9fb895 100644 --- a/src/components/post/LinkedPostItem.tsx +++ b/src/components/post/LinkedPostItem.tsx @@ -3,6 +3,8 @@ import styled, { css, keyframes } from 'styled-components'; import palette from '../../lib/styles/palette'; import { MdArrowBack, MdArrowForward } from 'react-icons/md'; import { LinkedPost } from '../../lib/graphql/post'; +import { Link } from 'react-router-dom'; +import { ellipsis } from '../../lib/styles/utils'; const bounceLeft = keyframes` 0% { @@ -48,7 +50,7 @@ const Circle = styled.div<{ right?: boolean }>` `} `; -const LinkedPostItemBlock = styled.div<{ right?: boolean }>` +const LinkedPostItemBlock = styled(Link)<{ right?: boolean }>` cursor: pointer; background: ${palette.gray0}; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); @@ -58,7 +60,7 @@ const LinkedPostItemBlock = styled.div<{ right?: boolean }>` height: 4rem; display: flex; align-items: center; - + text-decoration: none; ${props => props.right && css` @@ -80,17 +82,25 @@ const Text = styled.div<{ right?: boolean }>` flex-direction: column; align-items: ${props => (props.right ? 'flex-end' : 'flex-start')}; line-height: 1; + min-width: 0; .description { font-size: 0.75rem; font-weight: bold; color: ${palette.gray7}; } h3 { + ${props => + props.right && + css` + text-align: right; + `}; + width: 100%; font-size: 1.125rem; color: ${palette.gray7}; line-height: 1; margin: 0; margin-top: 0.5rem; + ${ellipsis}; } `; @@ -106,8 +116,14 @@ const LinkedPostItem: React.FC = ({ if (!linkedPost) { return null; } + const to = `/@${linkedPost.user.username}/${linkedPost.url_slug}`; + return ( - + window.scrollTo(0, 0)} + > {right ? : } diff --git a/src/components/post/LinkedPostList.tsx b/src/components/post/LinkedPostList.tsx index 41049f3d..e21adfa1 100644 --- a/src/components/post/LinkedPostList.tsx +++ b/src/components/post/LinkedPostList.tsx @@ -9,6 +9,7 @@ const LinkedPostsBlock = styled(VelogResponsive)` display: flex; `; const Wrapper = styled.div` + min-width: 0; flex: 1; & + & { margin-left: 3rem; diff --git a/src/components/post/PostSeriesInfo.tsx b/src/components/post/PostSeriesInfo.tsx index 170b1b34..7dbd8921 100644 --- a/src/components/post/PostSeriesInfo.tsx +++ b/src/components/post/PostSeriesInfo.tsx @@ -13,6 +13,7 @@ import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom'; import { usePostViewerState, usePostViewerDispatch, + usePostViewerPrefetch, } from './PostViewerProvider'; const PostSeriesInfoBlock = styled.div` @@ -168,6 +169,8 @@ const PostSeriesInfo: React.FC = ({ const dispatch = usePostViewerDispatch(); const state = usePostViewerState(); + const prefetch = usePostViewerPrefetch(); + useEffect(() => { dispatch({ type: 'SET_SERIES_OPEN', @@ -192,8 +195,12 @@ const PostSeriesInfo: React.FC = ({ history.push(`/@${username}/${nextPos.url_slug}`); }; + const onMouseEnter = () => { + prefetch(); + }; + return ( - +

{name}

{open && ( diff --git a/src/components/post/PostViewerProvider.tsx b/src/components/post/PostViewerProvider.tsx index 59486157..30aab4a9 100644 --- a/src/components/post/PostViewerProvider.tsx +++ b/src/components/post/PostViewerProvider.tsx @@ -15,6 +15,7 @@ type PostViewerState = { const PostViewerStateContext = createContext(null); const PostViewerDispatchContext = createContext | null>(null); +const PostViewerPrefetchContext = createContext(null); function reducer(state: PostViewerState, action: Action): PostViewerState { switch (action.type) { @@ -49,18 +50,31 @@ export const usePostViewerDispatch = () => { return dispatch; }; -const PostViewerProvider: React.FC = ({ children }) => { +export const usePostViewerPrefetch = () => { + const prefetch = useContext(PostViewerPrefetchContext); + if (!prefetch) { + throw new Error('not wrapped with PostViewerProvider'); + } + return prefetch; +}; + +const PostViewerProvider: React.FC<{ prefetchLinkedPosts: Function }> = ({ + children, + prefetchLinkedPosts, +}) => { const [state, dispatch] = useReducer(reducer, { seriesOpen: false, toc: null, }); return ( - - - + + + + + ); }; diff --git a/src/components/post/__tests__/LinkedPostItem.test.tsx b/src/components/post/__tests__/LinkedPostItem.test.tsx index 94dd117b..5b08260b 100644 --- a/src/components/post/__tests__/LinkedPostItem.test.tsx +++ b/src/components/post/__tests__/LinkedPostItem.test.tsx @@ -1,11 +1,16 @@ import * as React from 'react'; import { render } from 'react-testing-library'; import LinkedPostItem, { LinkedPostItemProps } from '../LinkedPostItem'; +import { MemoryRouter } from 'react-router'; describe('LinkedPostItem', () => { const setup = (props: Partial = {}) => { - const initialProps: LinkedPostItemProps = {}; - const utils = render(); + const initialProps: LinkedPostItemProps = { linkedPost: null }; + const utils = render( + + + , + ); return { ...utils, }; diff --git a/src/components/post/__tests__/LinkedPostList.test.tsx b/src/components/post/__tests__/LinkedPostList.test.tsx index 6ff26f94..6cfe5e26 100644 --- a/src/components/post/__tests__/LinkedPostList.test.tsx +++ b/src/components/post/__tests__/LinkedPostList.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render } from 'react-testing-library'; import LinkedPostList, { LinkedPostListProps } from '../LinkedPostList'; +import { MemoryRouter } from 'react-router'; describe('LinkedPostList', () => { const setup = (props: Partial = {}) => { @@ -27,7 +28,11 @@ describe('LinkedPostList', () => { }, }; - const utils = render(); + const utils = render( + + + , + ); return { ...utils, }; diff --git a/src/components/post/__tests__/PostSeriesInfo.test.tsx b/src/components/post/__tests__/PostSeriesInfo.test.tsx index 742d46fc..ef0a302f 100644 --- a/src/components/post/__tests__/PostSeriesInfo.test.tsx +++ b/src/components/post/__tests__/PostSeriesInfo.test.tsx @@ -89,7 +89,7 @@ describe('PostSeriesInfo', () => { }; const utils = render( - + {}}> , diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index fac27d4b..e54bd914 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { READ_POST, @@ -6,6 +6,7 @@ import { REMOVE_POST, LIKE_POST, UNLIKE_POST, + LinkedPost, } from '../../lib/graphql/post'; import PostHead from '../../components/post/PostHead'; import PostContent from '../../components/post/PostContent'; @@ -21,6 +22,7 @@ import gql from 'graphql-tag'; import { shareFacebook, shareTwitter, copyText } from '../../lib/share'; import PostToc from '../../components/post/PostToc'; import LinkedPostList from '../../components/post/LinkedPostList'; +import { getScrollTop } from '../../lib/utils'; export interface PostViewerOwnProps { username: string; @@ -45,6 +47,19 @@ const PostViewer: React.FC = ({ }, }); const client = useApolloClient(); + const prefetchPost = useCallback( + ({ username, urlSlug }: { username: string; urlSlug: string }) => { + client.query({ + query: READ_POST, + variables: { + username, + url_slug: urlSlug, + }, + }); + }, + [client], + ); + const prefetched = useRef(false); const [removePost] = useMutation(REMOVE_POST); const [likePost, { loading: loadingLike }] = useMutation(LIKE_POST); @@ -52,11 +67,55 @@ const PostViewer: React.FC = ({ const { loading, error, data } = readPost; + const prefetchLinkedPosts = useCallback(() => { + if (!data || !data.post) return; + if (prefetched.current) return; + const { linked_posts: linkedPosts } = data.post; + const { next, previous } = linkedPosts; + const getVariables = (post: LinkedPost) => ({ + username: post.user.username, + urlSlug: post.url_slug, + }); + if (next) { + prefetchPost(getVariables(next)); + } + if (previous) { + prefetchPost(getVariables(previous)); + } + }, [data, prefetchPost]); + + const onScroll = useCallback(() => { + const scrollTop = getScrollTop(); + const { scrollHeight } = document.body; + const { innerHeight } = window; + const percentage = ((scrollTop + innerHeight) / scrollHeight) * 100; + if (percentage > 50) { + prefetchLinkedPosts(); + } + }, [prefetchLinkedPosts]); + useEffect(() => { if (!data) return; if (!data.post) return; + + // schedule post prefetch + const timeoutId = setTimeout(prefetchLinkedPosts, 1000 * 5); + dispatch(postActions.setPostId(data.post.id)); - }, [data, dispatch]); + + prefetched.current = false; + + return () => { + clearTimeout(timeoutId); + }; + }, [data, dispatch, prefetchLinkedPosts]); + + useEffect(() => { + window.addEventListener('scroll', onScroll); + return () => { + window.removeEventListener('scroll', onScroll); + }; + }, [onScroll]); const onRemove = async () => { if (!data || !data.post) return; @@ -182,7 +241,7 @@ const PostViewer: React.FC = ({ const { post } = data; return ( - + { From 70bca8f0baae79fe4ed70e037433cb71d319d04f Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 11 Aug 2019 23:16:14 +0900 Subject: [PATCH 076/736] Fix LikeButton animate bug --- package.json | 1 + src/components/post/PostLikeShareButtons.tsx | 8 +- .../__tests__/PostLikeShareButtons.test.tsx | 2 + src/containers/post/PostViewer.tsx | 1 + yarn.lock | 179 ++++++++++++++++++ 5 files changed, 189 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index dbda4706..bc02b53d 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "react-spring": "^8.0.27", "react-testing-library": "^7.0.0", "react-textarea-autosize": "^7.1.0", + "react-use": "^10.5.0", "redux": "^4.0.1", "redux-devtools-extension": "^2.13.8", "redux-mock-store": "^1.5.3", diff --git a/src/components/post/PostLikeShareButtons.tsx b/src/components/post/PostLikeShareButtons.tsx index 6ee6e756..bcbcdd6e 100644 --- a/src/components/post/PostLikeShareButtons.tsx +++ b/src/components/post/PostLikeShareButtons.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { usePrevious } from 'react-use'; import styled, { css } from 'styled-components'; import palette from '../../lib/styles/palette'; import { LikeIcon, ShareIcon, ClipIcon } from '../../static/svg'; @@ -96,6 +97,7 @@ export interface PostLikeShareButtonsProps { onShareClick: (type: 'facebook' | 'twitter' | 'clipboard') => void; likes: number; liked: boolean; + postId: string; } const PostLikeShareButtons: React.FC = ({ @@ -103,17 +105,19 @@ const PostLikeShareButtons: React.FC = ({ onShareClick, likes, liked, + postId, }) => { const [animateLike, setAnimateLike] = useState(false); const [prevLiked, setPrevLiked] = useState(liked); const [open, toggle] = useBoolean(false); + const prevPostId = usePrevious(postId); useEffect(() => { setPrevLiked(liked); - if (!prevLiked && liked) { + if (!prevLiked && liked && prevPostId === postId) { setAnimateLike(true); } - }, [liked, prevLiked]); + }, [liked, postId, prevLiked, prevPostId]); const { x } = useSpring({ from: { x: 0 }, diff --git a/src/components/post/__tests__/PostLikeShareButtons.test.tsx b/src/components/post/__tests__/PostLikeShareButtons.test.tsx index 6d6e3a46..c10109c3 100644 --- a/src/components/post/__tests__/PostLikeShareButtons.test.tsx +++ b/src/components/post/__tests__/PostLikeShareButtons.test.tsx @@ -8,8 +8,10 @@ describe('PostLikeShareButtons', () => { const setup = (props: Partial = {}) => { const initialProps: PostLikeShareButtonsProps = { onLikeToggle: () => {}, + onShareClick: () => {}, likes: 0, liked: false, + postId: 'id', }; const utils = render(); return { diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index e54bd914..ea5eab49 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -260,6 +260,7 @@ const PostViewer: React.FC = ({ onShareClick={onShareClick} likes={post.likes} liked={post.liked} + postId={post.id} /> } toc={} diff --git a/yarn.lock b/yarn.lock index e324b5a7..acf57770 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1305,6 +1305,13 @@ dependencies: "@types/react" "*" +"@types/react-wait@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@types/react-wait/-/react-wait-0.3.0.tgz#6f7ef17571a17e72c7864ede8cf7d3aa525a005e" + integrity sha512-5jIfDcHRjqeE7QfZG7kCqOpfrPSvOM1E3/nlKuJ/NZrG/WrhLo/AFr0i72jhTWzyNRo4ex0pshBaiCHksZXH3A== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@16.8.2": version "16.8.2" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.2.tgz#3b7a7f7ea89d1c7d68b00849fb5de839011c077b" @@ -2529,6 +2536,11 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +bowser@^1.7.3: + version "1.9.4" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" + integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3269,6 +3281,13 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +copy-to-clipboard@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467" + integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w== + dependencies: + toggle-selection "^1.0.6" + core-js@2.6.4, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: version "2.6.4" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.4.tgz#b8897c062c4d769dd30a0ac5c73976c47f92ea0d" @@ -3413,6 +3432,14 @@ css-has-pseudo@^0.10.0: postcss "^7.0.6" postcss-selector-parser "^5.0.0-rc.4" +css-in-js-utils@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99" + integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA== + dependencies: + hyphenate-style-name "^1.0.2" + isobject "^3.0.1" + css-loader@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.0.tgz#9f46aaa5ca41dbe31860e3b62b8e23c42916bf56" @@ -3497,6 +3524,14 @@ css-tree@1.0.0-alpha.29: mdn-data "~1.1.0" source-map "^0.5.3" +css-tree@^1.0.0-alpha.28: + version "1.0.0-alpha.34" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.34.tgz#9b3a774cce553391604e62276670518e670c0b27" + integrity sha512-JMKJi4h8WkQ+HPjsCUvFnIhGF0I7Jr+J4a+NcHOApyGIBjvx4/hbhk+oKMXydv+OCmVyKBp0hqhHpj5Z61tyMg== + dependencies: + mdn-data "2.0.4" + source-map "^0.5.3" + css-unit-converter@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" @@ -3641,6 +3676,11 @@ csstype@^2.2.0: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== +csstype@^2.5.5: + version "2.6.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" + integrity sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg== + cycle@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" @@ -4169,6 +4209,13 @@ error-inject@^1.0.0: resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= +error-stack-parser@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d" + integrity sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw== + dependencies: + stackframe "^1.0.4" + es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.1, es-abstract@^1.7.0: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" @@ -4719,6 +4766,11 @@ fast-levenshtein@~2.0.4: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastest-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" + integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg= + fastparse@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -5696,6 +5748,11 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +hyphenate-style-name@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" + integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== + iconv-lite@0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" @@ -5860,6 +5917,14 @@ ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +inline-style-prefixer@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911" + integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg== + dependencies: + bowser "^1.7.3" + css-in-js-utils "^2.0.0" + inquirer@6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52" @@ -7483,6 +7548,11 @@ mdast-util-to-string@^1.0.0: resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.0.6.tgz#7d85421021343b33de1552fc71cb8e5b4ae7536d" integrity sha512-868pp48gUPmZIhfKrLbaDneuzGiw3OTDjHc5M1kAepR2CWBJ+HpEsm252K4aXdiP5coVZaJPOqGtVU6Po8xnXg== +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + mdn-data@~1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" @@ -7804,6 +7874,20 @@ nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== +nano-css@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.0.tgz#1d794db644139665e68ffaeba1ec6d7c66fc620e" + integrity sha512-DrkBUciWkEvrWZUIyhjQkyqiF1x2bnB4FWZZ9FCYSz1Okcq5fUs6P2e46UaHYcdljaUkQbK0aS0h1I2zObCTBg== + dependencies: + css-tree "^1.0.0-alpha.28" + csstype "^2.5.5" + fastest-stable-stringify "^1.0.1" + inline-style-prefixer "^4.0.0" + rtl-css-js "^1.9.0" + sourcemap-codec "^1.4.1" + stacktrace-js "^2.0.0" + stylis "3.5.0" + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9669,6 +9753,11 @@ react-error-overlay@^5.1.3: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.3.tgz#16fcbde75ed4dc6161dc6dc959b48e92c6ffa9ad" integrity sha512-GoqeM3Xadie7XUApXOjkY3Qhs8RkwB/Za4WMedBGrOKH1eTuKGyoAECff7jiVonJchOx6KZ9i8ILO5XIoHB+Tg== +react-fast-compare@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + react-icons@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-3.3.0.tgz#09409cad23c39149e419af5c928eee6f9f456b93" @@ -9765,6 +9854,25 @@ react-textarea-autosize@^7.1.0: "@babel/runtime" "^7.1.2" prop-types "^15.6.0" +react-use@^10.5.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-10.5.0.tgz#625e025c7c4eeaa8bc1401284b10aca864bccf09" + integrity sha512-sqpGs9e1aENXXYHJjEh3hctn0W845FGHovv3KXMi+kCXeqYNH0/UV4/9yCmWR4kO/TNdPVb7om5Be0eV0H8BeA== + dependencies: + "@types/react-wait" "^0.3.0" + copy-to-clipboard "^3.1.0" + nano-css "^5.1.0" + react-fast-compare "^2.0.4" + react-wait "^0.3.0" + screenfull "^4.1.0" + throttle-debounce "^2.0.1" + ts-easing "^0.2.0" + +react-wait@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/react-wait/-/react-wait-0.3.0.tgz#0cdd4d919012451a5bc3ab0a16d00c6fd9a8c10b" + integrity sha512-kB5x/kMKWcn0uVr9gBdNz21/oGbQwEQnF3P9p6E9yLfJ9DRcKS0fagbgYMFI0YFOoyKDj+2q6Rwax0kTYJF37g== + react@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" @@ -10308,6 +10416,13 @@ rsvp@^3.3.3: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== +rtl-css-js@^1.9.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.13.0.tgz#e97ebb67dd9fc6c4be1278730901967fc432ed9f" + integrity sha512-zUydBqLfSKEk6XF+OiK1fmReAsnoZz0MKIYPsAfP4DZuCdLewtH7nDwXpnqd8jbL7J1pE9uCaOLTkfQGAFfJRw== + dependencies: + "@babel/runtime" "^7.1.2" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -10411,6 +10526,11 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +screenfull@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-4.2.1.tgz#3245b7bc73d2b7c9a15bd8caaf6965db7cbc7f04" + integrity sha512-PLSp6f5XdhvjCCCO8OjavRfzkSGL3Qmdm7P82bxyU8HDDDBhDV3UckRaYcRa/NDNTYt8YBpzjoLWHUAejmOjLg== + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -10709,6 +10829,11 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -10719,6 +10844,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sourcemap-codec@^1.4.1: + version "1.4.6" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" + integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== + space-separated-tokens@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" @@ -10814,6 +10944,13 @@ stable@~0.1.6: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-generator@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.3.tgz#bb74385c67ffc4ccf3c4dee5831832d4e509c8a0" + integrity sha512-kdzGoqrnqsMxOEuXsXyQTmvWXZmG0f3Ql2GDx5NtmZs59sT2Bt9Vdyq0XdtxUi58q/+nxtbF9KOQ9HkV1QznGg== + dependencies: + stackframe "^1.0.4" + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -10824,6 +10961,28 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== +stackframe@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" + integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw== + +stacktrace-gps@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc" + integrity sha512-9o+nWhiz5wFnrB3hBHs2PTyYrS60M1vvpSzHxwxnIbtY2q9Nt51hZvhrG1+2AxD374ecwyS+IUwfkHRE/2zuGg== + dependencies: + source-map "0.5.6" + stackframe "^1.0.4" + +stacktrace-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58" + integrity sha1-d2ymRqlbxsayuQd2U2p/xyxt21g= + dependencies: + error-stack-parser "^2.0.1" + stack-generator "^2.0.1" + stacktrace-gps "^3.0.1" + state-toggle@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.1.tgz#c3cb0974f40a6a0f8e905b96789eb41afa1cde3a" @@ -11064,6 +11223,11 @@ stylis-rule-sheet@^0.0.10: resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== +stylis@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" + integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== + stylis@^3.5.0: version "3.5.4" resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" @@ -11211,6 +11375,11 @@ throat@^4.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= +throttle-debounce@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5" + integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg== + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -11327,6 +11496,11 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= + toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" @@ -11401,6 +11575,11 @@ tryer@^1.0.0: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== +ts-easing@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" + integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== + ts-invariant@^0.4.0: version "0.4.4" resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" From 6f8e038f59b0da3c0da1cb284244a9f97eff4523 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 11 Aug 2019 23:30:53 +0900 Subject: [PATCH 077/736] Write test code for LinkedPostItem --- src/components/common/PlainLink.tsx | 24 +++++++++++++ src/components/post/LinkedPostItem.tsx | 5 +-- .../post/__tests__/LinkedPostItem.test.tsx | 34 +++++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/components/common/PlainLink.tsx diff --git a/src/components/common/PlainLink.tsx b/src/components/common/PlainLink.tsx new file mode 100644 index 00000000..5083377a --- /dev/null +++ b/src/components/common/PlainLink.tsx @@ -0,0 +1,24 @@ +import React, { HTMLProps } from 'react'; +import { Link } from 'react-router-dom'; + +type PlainLinkProps = HTMLProps & { + to: string; +}; + +/** + * Needed when StyledLink has a custom props + */ +const PlainLink: React.FC = ({ + to, + className, + children, + onClick, +}) => { + return ( + + {children} + + ); +}; + +export default PlainLink; diff --git a/src/components/post/LinkedPostItem.tsx b/src/components/post/LinkedPostItem.tsx index 7a9fb895..b0d0c3cd 100644 --- a/src/components/post/LinkedPostItem.tsx +++ b/src/components/post/LinkedPostItem.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { HTMLProps } from 'react'; import styled, { css, keyframes } from 'styled-components'; import palette from '../../lib/styles/palette'; import { MdArrowBack, MdArrowForward } from 'react-icons/md'; import { LinkedPost } from '../../lib/graphql/post'; import { Link } from 'react-router-dom'; import { ellipsis } from '../../lib/styles/utils'; +import PlainLink from '../common/PlainLink'; const bounceLeft = keyframes` 0% { @@ -50,7 +51,7 @@ const Circle = styled.div<{ right?: boolean }>` `} `; -const LinkedPostItemBlock = styled(Link)<{ right?: boolean }>` +const LinkedPostItemBlock = styled(PlainLink)<{ right?: boolean }>` cursor: pointer; background: ${palette.gray0}; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); diff --git a/src/components/post/__tests__/LinkedPostItem.test.tsx b/src/components/post/__tests__/LinkedPostItem.test.tsx index 5b08260b..b6b841fe 100644 --- a/src/components/post/__tests__/LinkedPostItem.test.tsx +++ b/src/components/post/__tests__/LinkedPostItem.test.tsx @@ -5,7 +5,17 @@ import { MemoryRouter } from 'react-router'; describe('LinkedPostItem', () => { const setup = (props: Partial = {}) => { - const initialProps: LinkedPostItemProps = { linkedPost: null }; + const initialProps: LinkedPostItemProps = { + linkedPost: { + id: 'a5063709-945e-4d93-a494-16ad5a881a61', + title: 'Post Title', + url_slug: 'urlSlug', + user: { + id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', + username: 'blablabla', + }, + }, + }; const utils = render( @@ -16,6 +26,26 @@ describe('LinkedPostItem', () => { }; }; it('renders properly', () => { - setup(); + const { getByText } = setup(); + getByText('Post Title'); + getByText('이전 포스트'); + }); + it('has proper link href', () => { + const { container } = setup(); + const a = container.childNodes[0] as HTMLAnchorElement; + expect(a).toBeTruthy(); + expect(a).toHaveAttribute('href', '/@blablabla/urlSlug'); + }); + it('is next post', () => { + const { getByText } = setup({ + right: true, + }); + getByText('다음 포스트'); + }); + it('is missing', () => { + const { container } = setup({ + linkedPost: null, + }); + expect(container.hasChildNodes()).toBe(false); }); }); From 77547c90554920da75c487d3c2c385db6af63b23 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 11 Aug 2019 23:35:03 +0900 Subject: [PATCH 078/736] Write LinkedPostLIst test code --- .../post/__tests__/LinkedPostList.test.tsx | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/components/post/__tests__/LinkedPostList.test.tsx b/src/components/post/__tests__/LinkedPostList.test.tsx index 6cfe5e26..5b5bf085 100644 --- a/src/components/post/__tests__/LinkedPostList.test.tsx +++ b/src/components/post/__tests__/LinkedPostList.test.tsx @@ -9,7 +9,7 @@ describe('LinkedPostList', () => { linkedPosts: { previous: { id: 'a5063709-945e-4d93-a494-16ad5a881a61', - title: '1', + title: 'prevPost', url_slug: '1', user: { id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', @@ -18,7 +18,7 @@ describe('LinkedPostList', () => { }, next: { id: '6a996a6b-4a55-4049-bee2-a9a0b504283c', - title: '3', + title: 'nextPost', url_slug: '3', user: { id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', @@ -37,7 +37,57 @@ describe('LinkedPostList', () => { ...utils, }; }; - it('renders properly', () => { - setup(); + it('has both posts', () => { + const { getByText } = setup(); + getByText('prevPost'); + getByText('nextPost'); + }); + it('has no post', () => { + const { queryByText } = setup({ + linkedPosts: { + next: null, + previous: null, + }, + }); + const prev = queryByText('이전 포스트'); + const next = queryByText('다음 포스트'); + expect(prev).not.toBeInTheDocument(); + expect(next).not.toBeInTheDocument(); + }); + it('has prev post only', () => { + const { getByText, queryByText } = setup({ + linkedPosts: { + previous: { + id: 'a5063709-945e-4d93-a494-16ad5a881a61', + title: 'prevPost', + url_slug: '1', + user: { + id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', + username: 'blablabla', + }, + }, + next: null, + }, + }); + getByText('prevPost'); + expect(queryByText('nextPost')).not.toBeInTheDocument(); + }); + it('has next post only', () => { + const { getByText, queryByText } = setup({ + linkedPosts: { + previous: null, + next: { + id: 'a5063709-945e-4d93-a494-16ad5a881a61', + title: 'nextPost', + url_slug: '1', + user: { + id: '0bcdf3e5-a228-42c3-8b52-3f0dc118dfd8', + username: 'blablabla', + }, + }, + }, + }); + getByText('nextPost'); + expect(queryByText('prevPost')).not.toBeInTheDocument(); }); }); From 8fea726788a9e3d86a7b9124d06a707fb88c4ae1 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 11 Aug 2019 23:48:12 +0900 Subject: [PATCH 079/736] Update react-testing-library --- package.json | 12 +- .../auth/__tests__/AuthEmailSuccess.test.tsx | 2 +- .../auth/__tests__/AuthForm.test.tsx | 2 +- .../base/__tests__/HeaderLogo.test.tsx | 2 +- .../base/__tests__/HeaderUserIcon.test.tsx | 2 +- .../common/__tests__/PopupOKCancel.test.tsx | 2 +- .../common/__tests__/SelectableList.test.tsx | 2 +- .../post/__tests__/LinkedPostItem.test.tsx | 2 +- .../post/__tests__/LinkedPostList.test.tsx | 2 +- .../post/__tests__/PostCommentItem.test.tsx | 2 +- .../post/__tests__/PostCommentList.test.tsx | 2 +- .../__tests__/PostCommentsTemplate.test.tsx | 2 +- .../post/__tests__/PostCommentsWrite.test.tsx | 2 +- .../post/__tests__/PostContent.test.tsx | 2 +- .../post/__tests__/PostHead.test.tsx | 2 +- .../__tests__/PostLikeShareButtons.test.tsx | 2 +- .../post/__tests__/PostReplies.test.tsx | 2 +- .../post/__tests__/PostSeriesInfo.test.tsx | 2 +- .../post/__tests__/PostsTags.test.tsx | 2 +- .../register/__tests__/RegisterForm.test.tsx | 2 +- .../__tests__/RegisterTemplate.test.tsx | 2 +- .../__tests__/VelogPageTemplate.test.tsx | 2 +- .../write/__tests__/AskChangeEditor.test.tsx | 2 +- .../write/__tests__/EditorPanes.test.tsx | 2 +- .../write/__tests__/MarkdownEditor.test.tsx | 2 +- .../write/__tests__/MarkdownPreview.test.tsx | 2 +- .../__tests__/PublishActionButtons.test.tsx | 2 +- .../write/__tests__/PublishPreview.test.tsx | 2 +- .../__tests__/PublishPrivacySetting.test.tsx | 2 +- .../__tests__/PublishScreenTemplate.test.tsx | 2 +- .../write/__tests__/PublishSection.test.tsx | 2 +- .../PublishSeriesConfigButtons.test.tsx | 2 +- .../__tests__/PublishSeriesCreate.test.tsx | 2 +- .../__tests__/PublishSeriesSection.test.tsx | 2 +- .../__tests__/PublishURLSetting.test.tsx | 2 +- .../write/__tests__/QuillEditor.test.tsx | 2 +- .../write/__tests__/TagInput.test.tsx | 2 +- .../write/__tests__/Toolbar.test.tsx | 2 +- .../write/__tests__/WriteFooter.test.tsx | 2 +- .../MarkdownPreview.test.tsx.snap | 2 +- .../__snapshots__/QuillEditor.test.tsx.snap | 2 +- .../__tests__/AuthModalContainer.test.tsx | 2 +- .../post/__tests__/PostComments.test.tsx | 2 +- .../PostCommentsWriteContainer.test.tsx | 2 +- .../post/__tests__/PostViewer.test.tsx | 2 +- .../write/__tests__/ActiveEditor.test.tsx | 2 +- .../MarkdownEditorContainer.test.tsx | 2 +- .../PublishPreviewContainer.test.tsx | 2 +- .../PublishPrivacySettingContainer.test.tsx | 2 +- .../write/__tests__/PublishScreen.test.tsx | 2 +- .../PublishSeriesSectionContainer.test.tsx | 2 +- .../PublishURLSettingContainer.test.tsx | 2 +- .../__tests__/QuillEditorContainer.test.tsx | 2 +- .../__tests__/TagInputContainer.test.tsx | 2 +- .../__snapshots__/ActiveEditor.test.tsx.snap | 2 +- src/lib/renderWithApollo.tsx | 2 +- src/lib/renderWithProviders.tsx | 2 +- src/lib/renderWithRedux.tsx | 2 +- src/setupTests.ts | 3 +- yarn.lock | 222 ++++++++---------- 60 files changed, 166 insertions(+), 185 deletions(-) diff --git a/package.json b/package.json index bc02b53d..7d616863 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "@loadable/server": "^5.6.0", "@loadable/webpack-plugin": "^5.5.0", "@svgr/webpack": "4.1.0", + "@testing-library/jest-dom": "^4.0.0", + "@testing-library/react": "^9.1.0", "@types/codemirror": "^0.0.72", "@types/date-fns": "^2.6.0", "@types/graphql": "^14.0.7", @@ -39,6 +41,7 @@ "@types/turndown": "^5.0.0", "@typescript-eslint/eslint-plugin": "^1.9.0", "@typescript-eslint/parser": "^1.9.0", + "add": "^2.0.6", "apollo-boost": "^0.1.28", "apollo-link-mock": "^1.0.1", "axios": "^0.18.0", @@ -75,7 +78,6 @@ "immer": "^2.1.1", "isomorphic-fetch": "^2.2.1", "jest": "23.6.0", - "jest-dom": "^3.1.2", "jest-pnp-resolver": "1.0.2", "jest-resolve": "23.6.0", "jest-watch-typeahead": "^0.2.1", @@ -96,18 +98,17 @@ "quill": "^1.3.6", "quill-delta-to-html": "^0.10.7", "ramda": "^0.26.1", - "react": "^16.8.6", + "react": "^16.9.0", "react-apollo": "^2.4.1", "react-apollo-hooks": "^0.5.0", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.2", - "react-dom": "^16.8.6", + "react-dom": "^16.9.0", "react-icons": "^3.3.0", "react-outside-click-handler": "^1.2.3", "react-redux": "^7.1.0", "react-router-dom": "^5.0.0", "react-spring": "^8.0.27", - "react-testing-library": "^7.0.0", "react-textarea-autosize": "^7.1.0", "react-use": "^10.5.0", "redux": "^4.0.1", @@ -139,7 +140,8 @@ "webpack-dev-server": "3.1.14", "webpack-manifest-plugin": "2.0.4", "webpack-node-externals": "^1.7.2", - "workbox-webpack-plugin": "3.6.3" + "workbox-webpack-plugin": "3.6.3", + "yarn": "^1.17.3" }, "scripts": { "start": "node scripts/start.js", diff --git a/src/components/auth/__tests__/AuthEmailSuccess.test.tsx b/src/components/auth/__tests__/AuthEmailSuccess.test.tsx index 6d6f25ff..bbcf26cc 100644 --- a/src/components/auth/__tests__/AuthEmailSuccess.test.tsx +++ b/src/components/auth/__tests__/AuthEmailSuccess.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import AuthEmailSuccess, { AuthEmailSuccessProps } from '../AuthEmailSuccess'; describe('AuthEmailSuccess', () => { diff --git a/src/components/auth/__tests__/AuthForm.test.tsx b/src/components/auth/__tests__/AuthForm.test.tsx index c7e584c3..3c6e6993 100644 --- a/src/components/auth/__tests__/AuthForm.test.tsx +++ b/src/components/auth/__tests__/AuthForm.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import AuthForm, { AuthFormProps } from '../AuthForm'; import { AuthMode } from '../../../modules/core'; diff --git a/src/components/base/__tests__/HeaderLogo.test.tsx b/src/components/base/__tests__/HeaderLogo.test.tsx index 91aeb8c8..11a33cc1 100644 --- a/src/components/base/__tests__/HeaderLogo.test.tsx +++ b/src/components/base/__tests__/HeaderLogo.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import HeaderLogo, { HeaderLogoProps } from '../HeaderLogo'; import { MemoryRouter } from 'react-router-dom'; diff --git a/src/components/base/__tests__/HeaderUserIcon.test.tsx b/src/components/base/__tests__/HeaderUserIcon.test.tsx index 325112e1..51cd0aae 100644 --- a/src/components/base/__tests__/HeaderUserIcon.test.tsx +++ b/src/components/base/__tests__/HeaderUserIcon.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import HeaderUserIcon, { HeaderUserIconProps } from '../HeaderUserIcon'; describe('HeaderUserIcon', () => { diff --git a/src/components/common/__tests__/PopupOKCancel.test.tsx b/src/components/common/__tests__/PopupOKCancel.test.tsx index 820de983..886059ed 100644 --- a/src/components/common/__tests__/PopupOKCancel.test.tsx +++ b/src/components/common/__tests__/PopupOKCancel.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PopupOKCancel, { PopupOKCancelProps } from '../PopupOKCancel'; describe('PopupOKCancel', () => { diff --git a/src/components/common/__tests__/SelectableList.test.tsx b/src/components/common/__tests__/SelectableList.test.tsx index dac3bbba..e80c4c47 100644 --- a/src/components/common/__tests__/SelectableList.test.tsx +++ b/src/components/common/__tests__/SelectableList.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import SelectableList, { SelectableListProps } from '../SelectableList'; describe('SelectableList', () => { diff --git a/src/components/post/__tests__/LinkedPostItem.test.tsx b/src/components/post/__tests__/LinkedPostItem.test.tsx index b6b841fe..421088b1 100644 --- a/src/components/post/__tests__/LinkedPostItem.test.tsx +++ b/src/components/post/__tests__/LinkedPostItem.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import LinkedPostItem, { LinkedPostItemProps } from '../LinkedPostItem'; import { MemoryRouter } from 'react-router'; diff --git a/src/components/post/__tests__/LinkedPostList.test.tsx b/src/components/post/__tests__/LinkedPostList.test.tsx index 5b5bf085..2b25b2dd 100644 --- a/src/components/post/__tests__/LinkedPostList.test.tsx +++ b/src/components/post/__tests__/LinkedPostList.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import LinkedPostList, { LinkedPostListProps } from '../LinkedPostList'; import { MemoryRouter } from 'react-router'; diff --git a/src/components/post/__tests__/PostCommentItem.test.tsx b/src/components/post/__tests__/PostCommentItem.test.tsx index 6887ed99..e2a334b0 100644 --- a/src/components/post/__tests__/PostCommentItem.test.tsx +++ b/src/components/post/__tests__/PostCommentItem.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PostCommentItem, { PostCommentItemProps } from '../PostCommentItem'; import { Comment } from '../../../lib/graphql/post'; import { formatDate } from '../../../lib/utils'; diff --git a/src/components/post/__tests__/PostCommentList.test.tsx b/src/components/post/__tests__/PostCommentList.test.tsx index 1ac4b749..6ca2e65c 100644 --- a/src/components/post/__tests__/PostCommentList.test.tsx +++ b/src/components/post/__tests__/PostCommentList.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import PostCommentsList, { PostCommentsListProps } from '../PostCommentsList'; describe('PostCommentsList', () => { diff --git a/src/components/post/__tests__/PostCommentsTemplate.test.tsx b/src/components/post/__tests__/PostCommentsTemplate.test.tsx index be57bc29..f2f2470f 100644 --- a/src/components/post/__tests__/PostCommentsTemplate.test.tsx +++ b/src/components/post/__tests__/PostCommentsTemplate.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import PostCommentsTemplate, { PostCommentsTemplateProps, } from '../PostCommentsTemplate'; diff --git a/src/components/post/__tests__/PostCommentsWrite.test.tsx b/src/components/post/__tests__/PostCommentsWrite.test.tsx index 6b12375b..99862e06 100644 --- a/src/components/post/__tests__/PostCommentsWrite.test.tsx +++ b/src/components/post/__tests__/PostCommentsWrite.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PostCommentsWrite, { PostCommentsWriteProps, } from '../PostCommentsWrite'; diff --git a/src/components/post/__tests__/PostContent.test.tsx b/src/components/post/__tests__/PostContent.test.tsx index 876bd439..e6fb6eaf 100644 --- a/src/components/post/__tests__/PostContent.test.tsx +++ b/src/components/post/__tests__/PostContent.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import PostContent, { PostContentProps } from '../PostContent'; import PostViewerProvider from '../PostViewerProvider'; diff --git a/src/components/post/__tests__/PostHead.test.tsx b/src/components/post/__tests__/PostHead.test.tsx index bd999c75..26b05947 100644 --- a/src/components/post/__tests__/PostHead.test.tsx +++ b/src/components/post/__tests__/PostHead.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PostHead, { PostHeadProps } from '../PostHead'; import { MemoryRouter } from 'react-router'; diff --git a/src/components/post/__tests__/PostLikeShareButtons.test.tsx b/src/components/post/__tests__/PostLikeShareButtons.test.tsx index c10109c3..a89d51cc 100644 --- a/src/components/post/__tests__/PostLikeShareButtons.test.tsx +++ b/src/components/post/__tests__/PostLikeShareButtons.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PostLikeShareButtons, { PostLikeShareButtonsProps, } from '../PostLikeShareButtons'; diff --git a/src/components/post/__tests__/PostReplies.test.tsx b/src/components/post/__tests__/PostReplies.test.tsx index b7583a20..42e8b30d 100644 --- a/src/components/post/__tests__/PostReplies.test.tsx +++ b/src/components/post/__tests__/PostReplies.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PostReplies, { PostRepliesProps } from '../PostReplies'; import renderWithRedux from '../../../lib/renderWithRedux'; diff --git a/src/components/post/__tests__/PostSeriesInfo.test.tsx b/src/components/post/__tests__/PostSeriesInfo.test.tsx index ef0a302f..c90191c4 100644 --- a/src/components/post/__tests__/PostSeriesInfo.test.tsx +++ b/src/components/post/__tests__/PostSeriesInfo.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PostSeriesInfo, { PostSeriesInfoProps } from '../PostSeriesInfo'; import { MemoryRouter, RouteComponentProps } from 'react-router'; import PostViewerProvider from '../PostViewerProvider'; diff --git a/src/components/post/__tests__/PostsTags.test.tsx b/src/components/post/__tests__/PostsTags.test.tsx index 429d13ab..d3ea60f3 100644 --- a/src/components/post/__tests__/PostsTags.test.tsx +++ b/src/components/post/__tests__/PostsTags.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import PostTags, { PostTagsProps } from '../PostTags'; import { MemoryRouter } from 'react-router-dom'; diff --git a/src/components/register/__tests__/RegisterForm.test.tsx b/src/components/register/__tests__/RegisterForm.test.tsx index ca8cf88d..a6ed1e35 100644 --- a/src/components/register/__tests__/RegisterForm.test.tsx +++ b/src/components/register/__tests__/RegisterForm.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import RegisterForm, { RegisterFormProps } from '../RegisterForm'; import { MemoryRouter } from 'react-router'; diff --git a/src/components/register/__tests__/RegisterTemplate.test.tsx b/src/components/register/__tests__/RegisterTemplate.test.tsx index 87b30380..6e5c0f4d 100644 --- a/src/components/register/__tests__/RegisterTemplate.test.tsx +++ b/src/components/register/__tests__/RegisterTemplate.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import RegisterTemplate, { RegisterTemplateProps } from '../RegisterTemplate'; describe('RegisterTemplate', () => { diff --git a/src/components/velog/__tests__/VelogPageTemplate.test.tsx b/src/components/velog/__tests__/VelogPageTemplate.test.tsx index 3ca23da8..838bc50f 100644 --- a/src/components/velog/__tests__/VelogPageTemplate.test.tsx +++ b/src/components/velog/__tests__/VelogPageTemplate.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import VelogPageTemplate, { VelogPageTemplateProps, } from '../VelogPageTemplate'; diff --git a/src/components/write/__tests__/AskChangeEditor.test.tsx b/src/components/write/__tests__/AskChangeEditor.test.tsx index df8a5444..d6a83fe4 100644 --- a/src/components/write/__tests__/AskChangeEditor.test.tsx +++ b/src/components/write/__tests__/AskChangeEditor.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import AskChangeEditor, { AskChangeEditorProps } from '../AskChangeEditor'; import { WriteMode } from '../../../modules/write'; diff --git a/src/components/write/__tests__/EditorPanes.test.tsx b/src/components/write/__tests__/EditorPanes.test.tsx index 648a246e..4f3cd557 100644 --- a/src/components/write/__tests__/EditorPanes.test.tsx +++ b/src/components/write/__tests__/EditorPanes.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import EditorPanes, { EditorPanesProps } from '../EditorPanes'; describe('EditorPanes', () => { diff --git a/src/components/write/__tests__/MarkdownEditor.test.tsx b/src/components/write/__tests__/MarkdownEditor.test.tsx index 5ff6963e..154a8419 100644 --- a/src/components/write/__tests__/MarkdownEditor.test.tsx +++ b/src/components/write/__tests__/MarkdownEditor.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import MarkdownEditor, { MarkdownEditorProps } from '../MarkdownEditor'; describe('MarkdownEditor', () => { diff --git a/src/components/write/__tests__/MarkdownPreview.test.tsx b/src/components/write/__tests__/MarkdownPreview.test.tsx index 094316b2..55fcfb9c 100644 --- a/src/components/write/__tests__/MarkdownPreview.test.tsx +++ b/src/components/write/__tests__/MarkdownPreview.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import MarkdownPreview, { MarkdownPreviewProps } from '../MarkdownPreview'; describe('MarkdownPreview', () => { diff --git a/src/components/write/__tests__/PublishActionButtons.test.tsx b/src/components/write/__tests__/PublishActionButtons.test.tsx index 6e3bf7fa..1f46b9c0 100644 --- a/src/components/write/__tests__/PublishActionButtons.test.tsx +++ b/src/components/write/__tests__/PublishActionButtons.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PublishActionButtons, { PublishActionButtonsProps, } from '../PublishActionButtons'; diff --git a/src/components/write/__tests__/PublishPreview.test.tsx b/src/components/write/__tests__/PublishPreview.test.tsx index 7280f88c..e665dd37 100644 --- a/src/components/write/__tests__/PublishPreview.test.tsx +++ b/src/components/write/__tests__/PublishPreview.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PublishPreview, { PublishPreviewProps } from '../PublishPreview'; describe('PublishPreview', () => { diff --git a/src/components/write/__tests__/PublishPrivacySetting.test.tsx b/src/components/write/__tests__/PublishPrivacySetting.test.tsx index 1edeb619..5d4a9aec 100644 --- a/src/components/write/__tests__/PublishPrivacySetting.test.tsx +++ b/src/components/write/__tests__/PublishPrivacySetting.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PublishPrivacySetting, { PublishPrivacySettingProps, } from '../PublishPrivacySetting'; diff --git a/src/components/write/__tests__/PublishScreenTemplate.test.tsx b/src/components/write/__tests__/PublishScreenTemplate.test.tsx index 06ddfd55..eda02e60 100644 --- a/src/components/write/__tests__/PublishScreenTemplate.test.tsx +++ b/src/components/write/__tests__/PublishScreenTemplate.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import PublishScreenTemplate, { PublishScreenTemplateProps, } from '../PublishScreenTemplate'; diff --git a/src/components/write/__tests__/PublishSection.test.tsx b/src/components/write/__tests__/PublishSection.test.tsx index c069eaf2..2b30f358 100644 --- a/src/components/write/__tests__/PublishSection.test.tsx +++ b/src/components/write/__tests__/PublishSection.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import PublishSection, { PublishSectionProps } from '../PublishSection'; describe('PublishSection', () => { diff --git a/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx b/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx index 69fd67b3..3a5d75ae 100644 --- a/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx +++ b/src/components/write/__tests__/PublishSeriesConfigButtons.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PublishSeriesConfigButtons, { PublishSeriesConfigButtonsProps, } from '../PublishSeriesConfigButtons'; diff --git a/src/components/write/__tests__/PublishSeriesCreate.test.tsx b/src/components/write/__tests__/PublishSeriesCreate.test.tsx index aec07032..3e86106e 100644 --- a/src/components/write/__tests__/PublishSeriesCreate.test.tsx +++ b/src/components/write/__tests__/PublishSeriesCreate.test.tsx @@ -4,7 +4,7 @@ import { fireEvent, waitForElement, waitForElementToBeRemoved, -} from 'react-testing-library'; +} from '@testing-library/react'; import PublishSeriesCreate, { PublishSeriesCreateProps, } from '../PublishSeriesCreate'; diff --git a/src/components/write/__tests__/PublishSeriesSection.test.tsx b/src/components/write/__tests__/PublishSeriesSection.test.tsx index 0f1eadef..d2d348e9 100644 --- a/src/components/write/__tests__/PublishSeriesSection.test.tsx +++ b/src/components/write/__tests__/PublishSeriesSection.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PublishSeriesSection, { PublishSeriesSectionProps, } from '../PublishSeriesSection'; diff --git a/src/components/write/__tests__/PublishURLSetting.test.tsx b/src/components/write/__tests__/PublishURLSetting.test.tsx index dd45d1a7..7d370bb5 100644 --- a/src/components/write/__tests__/PublishURLSetting.test.tsx +++ b/src/components/write/__tests__/PublishURLSetting.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PublishURLSetting, { PublishURLSettingProps, } from '../PublishURLSetting'; diff --git a/src/components/write/__tests__/QuillEditor.test.tsx b/src/components/write/__tests__/QuillEditor.test.tsx index 84534161..12448a1c 100644 --- a/src/components/write/__tests__/QuillEditor.test.tsx +++ b/src/components/write/__tests__/QuillEditor.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import QuillEditor, { QuillEditorProps } from '../QuillEditor'; describe('QuillEditor', () => { diff --git a/src/components/write/__tests__/TagInput.test.tsx b/src/components/write/__tests__/TagInput.test.tsx index ef20e56b..cd116a99 100644 --- a/src/components/write/__tests__/TagInput.test.tsx +++ b/src/components/write/__tests__/TagInput.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import TagInput, { TagInputProps } from '../TagInput'; describe('TagInput', () => { diff --git a/src/components/write/__tests__/Toolbar.test.tsx b/src/components/write/__tests__/Toolbar.test.tsx index a8e80203..357a6810 100644 --- a/src/components/write/__tests__/Toolbar.test.tsx +++ b/src/components/write/__tests__/Toolbar.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import Toolbar, { ToolbarProps } from '../Toolbar'; describe('Toolbar', () => { diff --git a/src/components/write/__tests__/WriteFooter.test.tsx b/src/components/write/__tests__/WriteFooter.test.tsx index 3f2bdcca..55847a19 100644 --- a/src/components/write/__tests__/WriteFooter.test.tsx +++ b/src/components/write/__tests__/WriteFooter.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import WriteFooter, { WriteFooterProps } from '../WriteFooter'; describe('WriteFooter', () => { diff --git a/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap b/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap index af123fed..7cb3578b 100644 --- a/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap @@ -9,7 +9,7 @@ exports[`MarkdownPreview matches snapshot 1`] = ` class="sc-bxivhb cFXJHm" />
{ const setup = (props: Partial = {}) => { diff --git a/src/containers/write/__tests__/PublishURLSettingContainer.test.tsx b/src/containers/write/__tests__/PublishURLSettingContainer.test.tsx index f886d826..0c173278 100644 --- a/src/containers/write/__tests__/PublishURLSettingContainer.test.tsx +++ b/src/containers/write/__tests__/PublishURLSettingContainer.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import PublishURLSettingContainer, { PublishURLSettingContainerProps, } from '../PublishURLSettingContainer'; diff --git a/src/containers/write/__tests__/QuillEditorContainer.test.tsx b/src/containers/write/__tests__/QuillEditorContainer.test.tsx index 754c3642..301c39ae 100644 --- a/src/containers/write/__tests__/QuillEditorContainer.test.tsx +++ b/src/containers/write/__tests__/QuillEditorContainer.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, fireEvent } from 'react-testing-library'; +import { render, fireEvent } from '@testing-library/react'; import QuillEditorContainer, { QuillEditorContainerProps, } from '../QuillEditorContainer'; diff --git a/src/containers/write/__tests__/TagInputContainer.test.tsx b/src/containers/write/__tests__/TagInputContainer.test.tsx index 5a24e426..b9ee1eb0 100644 --- a/src/containers/write/__tests__/TagInputContainer.test.tsx +++ b/src/containers/write/__tests__/TagInputContainer.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, waitForElement, fireEvent } from 'react-testing-library'; +import { render, waitForElement, fireEvent } from '@testing-library/react'; import TagInputContainer, { TagInputContainerProps, } from '../TagInputContainer'; diff --git a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap index a464fba7..42d9663e 100644 --- a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap +++ b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap @@ -267,7 +267,7 @@ exports[`ActiveEditor matches snapshot 1`] = ` class="sc-jWBwVP boWCkI" >
Date: Mon, 12 Aug 2019 23:26:43 +0900 Subject: [PATCH 080/736] Upgrade react-apollo --- package.json | 10 +- src/components/post/LinkedPostItem.tsx | 3 +- src/containers/base/UserLoader.tsx | 3 +- src/containers/main/RecentPosts.tsx | 2 +- src/containers/post/PostComments.tsx | 2 +- .../post/PostCommentsWriteContainer.tsx | 2 +- src/containers/post/PostEditComment.tsx | 2 +- src/containers/post/PostRepliesContainer.tsx | 2 +- src/containers/post/PostViewer.tsx | 2 +- .../post/__tests__/PostComments.test.tsx | 2 +- .../post/__tests__/PostViewer.test.tsx | 5 +- src/containers/velog/ConfigLoader.tsx | 3 +- .../velog/__tests__/ConfigLoader.test.tsx | 2 +- .../write/PublishActionButtonsContainer.tsx | 6 +- src/containers/write/PublishSeriesConfig.tsx | 4 +- .../write/__tests__/PublishScreen.test.tsx | 2 +- src/index.tsx | 12 +- src/lib/renderWithApollo.tsx | 2 +- src/lib/renderWithProviders.tsx | 2 +- src/server.tsx | 3 +- yarn.lock | 291 ++++++++---------- 21 files changed, 166 insertions(+), 196 deletions(-) diff --git a/package.json b/package.json index 7d616863..3f55eb21 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,10 @@ "version": "0.1.0", "private": true, "dependencies": { + "@apollo/react-components": "^3.0.0", + "@apollo/react-hooks": "^3.0.0", + "@apollo/react-ssr": "^3.0.0", + "@apollo/react-testing": "^3.0.0", "@babel/core": "7.2.2", "@loadable/babel-plugin": "^5.6.0", "@loadable/component": "^5.6.0", @@ -42,7 +46,7 @@ "@typescript-eslint/eslint-plugin": "^1.9.0", "@typescript-eslint/parser": "^1.9.0", "add": "^2.0.6", - "apollo-boost": "^0.1.28", + "apollo-boost": "^0.4.4", "apollo-link-mock": "^1.0.1", "axios": "^0.18.0", "babel-core": "7.0.0-bridge.0", @@ -70,7 +74,7 @@ "file-loader": "2.0.0", "fork-ts-checker-webpack-plugin": "1.0.0-alpha.6", "fs-extra": "7.0.1", - "graphql": "^14.1.1", + "graphql": "^14.4.2", "graphql.macro": "^1.3.4", "highlight.js": "^9.15.6", "html-webpack-plugin": "4.0.0-alpha.2", @@ -99,8 +103,6 @@ "quill-delta-to-html": "^0.10.7", "ramda": "^0.26.1", "react": "^16.9.0", - "react-apollo": "^2.4.1", - "react-apollo-hooks": "^0.5.0", "react-app-polyfill": "^0.2.1", "react-dev-utils": "^7.0.2", "react-dom": "^16.9.0", diff --git a/src/components/post/LinkedPostItem.tsx b/src/components/post/LinkedPostItem.tsx index b0d0c3cd..464d01c4 100644 --- a/src/components/post/LinkedPostItem.tsx +++ b/src/components/post/LinkedPostItem.tsx @@ -1,9 +1,8 @@ -import React, { HTMLProps } from 'react'; +import React from 'react'; import styled, { css, keyframes } from 'styled-components'; import palette from '../../lib/styles/palette'; import { MdArrowBack, MdArrowForward } from 'react-icons/md'; import { LinkedPost } from '../../lib/graphql/post'; -import { Link } from 'react-router-dom'; import { ellipsis } from '../../lib/styles/utils'; import PlainLink from '../common/PlainLink'; diff --git a/src/containers/base/UserLoader.tsx b/src/containers/base/UserLoader.tsx index 44fe9e5c..a5a70116 100644 --- a/src/containers/base/UserLoader.tsx +++ b/src/containers/base/UserLoader.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useCallback } from 'react'; -import { Query, QueryResult } from 'react-apollo'; +import { Query } from '@apollo/react-components'; import { connect } from 'react-redux'; import { CurrentUser, GET_CURRENT_USER } from '../../lib/graphql/user'; import { RootState } from '../../modules'; import { setUser } from '../../modules/core'; +import { QueryResult } from '@apollo/react-common'; const DetectUserChange: React.FC<{ user: CurrentUser | null; diff --git a/src/containers/main/RecentPosts.tsx b/src/containers/main/RecentPosts.tsx index a0312b57..ddc25d6c 100644 --- a/src/containers/main/RecentPosts.tsx +++ b/src/containers/main/RecentPosts.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PostCardList from '../../components/common/PostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; -import { useQuery } from 'react-apollo-hooks'; +import { useQuery } from '@apollo/react-hooks'; import PaginateWithScroll from '../../components/common/PaginateWithScroll'; interface RecentPostsProps {} diff --git a/src/containers/post/PostComments.tsx b/src/containers/post/PostComments.tsx index 18b7dfa2..b3bdde02 100644 --- a/src/containers/post/PostComments.tsx +++ b/src/containers/post/PostComments.tsx @@ -9,7 +9,7 @@ import { import PostCommentsList from '../../components/post/PostCommentsList'; import styled from 'styled-components'; import { useUserId } from '../../lib/hooks/useUser'; -import { useMutation, useQuery } from 'react-apollo-hooks'; +import { useQuery, useMutation } from '@apollo/react-hooks'; import useBoolean from '../../lib/hooks/useBoolean'; import PopupOKCancel from '../../components/common/PopupOKCancel'; diff --git a/src/containers/post/PostCommentsWriteContainer.tsx b/src/containers/post/PostCommentsWriteContainer.tsx index 38323cd8..ccba40dc 100644 --- a/src/containers/post/PostCommentsWriteContainer.tsx +++ b/src/containers/post/PostCommentsWriteContainer.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import PostCommentsWrite from '../../components/post/PostCommentsWrite'; import { WRITE_COMMENT, RELOAD_COMMENTS } from '../../lib/graphql/post'; -import { useMutation, useQuery } from 'react-apollo-hooks'; +import { useMutation, useQuery } from '@apollo/react-hooks'; export interface PostCommentsWriteContainerProps { postId: string; diff --git a/src/containers/post/PostEditComment.tsx b/src/containers/post/PostEditComment.tsx index d66ad637..45b2eea7 100644 --- a/src/containers/post/PostEditComment.tsx +++ b/src/containers/post/PostEditComment.tsx @@ -2,7 +2,7 @@ import React from 'react'; import useInput from '../../lib/hooks/useInput'; import PostCommentsWrite from '../../components/post/PostCommentsWrite'; import { EDIT_COMMENT } from '../../lib/graphql/post'; -import { useMutation } from 'react-apollo-hooks'; +import { useMutation } from '@apollo/react-hooks'; export interface PostEditCommentProps { id: string; diff --git a/src/containers/post/PostRepliesContainer.tsx b/src/containers/post/PostRepliesContainer.tsx index e1be72f7..38343adc 100644 --- a/src/containers/post/PostRepliesContainer.tsx +++ b/src/containers/post/PostRepliesContainer.tsx @@ -9,7 +9,7 @@ import { import PostReplies from '../../components/post/PostReplies'; import { useSelector } from 'react-redux'; import { RootState } from '../../modules'; -import { useQuery, useMutation } from 'react-apollo-hooks'; +import { useQuery, useMutation } from '@apollo/react-hooks'; import useBoolean from '../../lib/hooks/useBoolean'; import PopupOKCancel from '../../components/common/PopupOKCancel'; diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index ea5eab49..6d8a2eb6 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -14,7 +14,7 @@ import PostComments from './PostComments'; import { postActions } from '../../modules/post'; import PostViewerProvider from '../../components/post/PostViewerProvider'; import { useUserId } from '../../lib/hooks/useUser'; -import { useQuery, useMutation, useApolloClient } from 'react-apollo-hooks'; +import { useQuery, useMutation, useApolloClient } from '@apollo/react-hooks'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { prepareEdit } from '../../modules/write'; import PostLikeShareButtons from '../../components/post/PostLikeShareButtons'; diff --git a/src/containers/post/__tests__/PostComments.test.tsx b/src/containers/post/__tests__/PostComments.test.tsx index 8aee5c5a..a8a061ea 100644 --- a/src/containers/post/__tests__/PostComments.test.tsx +++ b/src/containers/post/__tests__/PostComments.test.tsx @@ -14,7 +14,7 @@ import { import renderWithRedux from '../../../lib/renderWithRedux'; import { ApolloProvider } from 'react-apollo-hooks'; import { createClient } from '../../../lib/renderWithApollo'; -import { MockedProvider } from 'react-apollo/test-utils'; +import { MockedProvider } from '@apollo/react-testing'; import { setUser } from '../../../modules/core'; const sampleComments: Comment[] = [ diff --git a/src/containers/post/__tests__/PostViewer.test.tsx b/src/containers/post/__tests__/PostViewer.test.tsx index 85b4ed88..7295568e 100644 --- a/src/containers/post/__tests__/PostViewer.test.tsx +++ b/src/containers/post/__tests__/PostViewer.test.tsx @@ -1,14 +1,13 @@ import * as React from 'react'; -import { render, waitForElement } from '@testing-library/react'; +import { waitForElement } from '@testing-library/react'; import PostViewer, { PostViewerProps, PostViewerOwnProps } from '../PostViewer'; import { MemoryRouter } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; import { READ_POST } from '../../../lib/graphql/post'; import renderWithRedux from '../../../lib/renderWithRedux'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { ApolloClient } from 'apollo-client'; import { MockLink, MockedResponse } from 'apollo-link-mock'; -import { ApolloProvider } from 'react-apollo-hooks'; +import { ApolloProvider } from '@apollo/react-hooks'; function createClient(mocks: MockedResponse[]) { return new ApolloClient({ diff --git a/src/containers/velog/ConfigLoader.tsx b/src/containers/velog/ConfigLoader.tsx index bd6ea320..eed50909 100644 --- a/src/containers/velog/ConfigLoader.tsx +++ b/src/containers/velog/ConfigLoader.tsx @@ -1,9 +1,10 @@ import React, { useEffect } from 'react'; -import { Query, QueryResult } from 'react-apollo'; +import { Query } from '@apollo/react-components'; import { connect } from 'react-redux'; import { GET_VELOG_CONFIG, VelogConfig } from '../../lib/graphql/user'; import { setUserLogo, setCustom, setVelogUsername } from '../../modules/header'; import { RootState } from '../../modules'; +import { QueryResult } from '@apollo/react-common'; interface ConfigEffectProps { velogConfig: VelogConfig; diff --git a/src/containers/velog/__tests__/ConfigLoader.test.tsx b/src/containers/velog/__tests__/ConfigLoader.test.tsx index a9707908..648b25c0 100644 --- a/src/containers/velog/__tests__/ConfigLoader.test.tsx +++ b/src/containers/velog/__tests__/ConfigLoader.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import ConfigLoader, { ConfigLoaderProps } from '../ConfigLoader'; -import { MockedProvider } from 'react-apollo/test-utils'; +import { MockedProvider } from '@apollo/react-testing'; import { GET_VELOG_CONFIG } from '../../../lib/graphql/user'; import renderWithRedux from '../../../lib/renderWithRedux'; import waitUntil from '../../../lib/waitUntil'; diff --git a/src/containers/write/PublishActionButtonsContainer.tsx b/src/containers/write/PublishActionButtonsContainer.tsx index 8f105ec9..aceb7526 100644 --- a/src/containers/write/PublishActionButtonsContainer.tsx +++ b/src/containers/write/PublishActionButtonsContainer.tsx @@ -11,7 +11,7 @@ import { } from '../../lib/graphql/post'; import { pick } from 'ramda'; import { escapeForUrl, safe } from '../../lib/utils'; -import { useMutation } from 'react-apollo-hooks'; +import { useMutation } from '@apollo/react-hooks'; import useRouter from 'use-react-router'; import { setHeadingId } from '../../lib/heading'; @@ -70,7 +70,7 @@ const PublishActionButtonsContainer: React.FC< const response = await writePost({ variables: variables, }); - if (!response.data) return; + if (!response || !response.data) return; const { user, url_slug } = response.data.writePost; history.push(`/@${user.username}/${url_slug}`); }; @@ -82,7 +82,7 @@ const PublishActionButtonsContainer: React.FC< ...variables, }, }); - if (!response.data) return; + if (!response || !response.data) return; console.log(response.data); const { user, url_slug } = response.data.editPost; history.push(`/@${user.username}/${url_slug}`); diff --git a/src/containers/write/PublishSeriesConfig.tsx b/src/containers/write/PublishSeriesConfig.tsx index 6322d669..356f3136 100644 --- a/src/containers/write/PublishSeriesConfig.tsx +++ b/src/containers/write/PublishSeriesConfig.tsx @@ -4,7 +4,7 @@ import useUser from '../../lib/hooks/useUser'; import { safe, sleep } from '../../lib/utils'; import PublishSeriesConfigTemplate from '../../components/write/PublishSeriesConfigTemplate'; import PublishSeriesList from './PublishSeriesList'; -import { useQuery, useMutation } from 'react-apollo-hooks'; +import { useQuery, useMutation } from '@apollo/react-hooks'; import { GetSeriesListResponse, GET_SERIES_LIST, @@ -71,7 +71,7 @@ const PublishSeriesConfig: React.FC = props => { url_slug: urlSlug, }, }); - if (!result.data) return; + if (!result || !result.data) return; await sleep(150); await seriesList.refetch(); setSelectedId(result.data.createSeries.id); diff --git a/src/containers/write/__tests__/PublishScreen.test.tsx b/src/containers/write/__tests__/PublishScreen.test.tsx index a7288292..de1f2dee 100644 --- a/src/containers/write/__tests__/PublishScreen.test.tsx +++ b/src/containers/write/__tests__/PublishScreen.test.tsx @@ -10,7 +10,7 @@ import { setDefaultDescription, toggleEditSeries, } from '../../../modules/write'; -import { MockedProvider } from 'react-apollo/test-utils'; +import { MockedProvider } from '@apollo/react-testing'; import renderWithProviders from '../../../lib/renderWithProviders'; describe('PublishScreen', () => { diff --git a/src/index.tsx b/src/index.tsx index acbdaafa..0e716a67 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,8 @@ import App from './App'; import * as serviceWorker from './serviceWorker'; import { BrowserRouter } from 'react-router-dom'; import { loadableReady } from '@loadable/component'; -import { ApolloProvider } from 'react-apollo'; +import { ApolloProvider } from '@apollo/react-hooks'; + import { createStore } from 'redux'; import { Provider } from 'react-redux'; import { composeWithDevTools } from 'redux-devtools-extension'; @@ -14,7 +15,6 @@ import client from './lib/graphql/client'; import rootReducer from './modules'; import storage from './lib/storage'; import { setUser } from './modules/core'; -import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks'; const store = createStore(rootReducer, composeWithDevTools()); @@ -43,11 +43,9 @@ if (process.env.NODE_ENV === 'production') { ReactDOM.render( - - - - - + + + , document.getElementById('root'), diff --git a/src/lib/renderWithApollo.tsx b/src/lib/renderWithApollo.tsx index bc467563..ebc755ff 100644 --- a/src/lib/renderWithApollo.tsx +++ b/src/lib/renderWithApollo.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { MockedResponse, MockLink } from 'apollo-link-mock'; import { ApolloClient } from 'apollo-client'; -import { ApolloProvider } from 'react-apollo-hooks'; +import { ApolloProvider } from '@apollo/react-hooks'; import { InMemoryCache } from 'apollo-boost'; import { render } from '@testing-library/react'; diff --git a/src/lib/renderWithProviders.tsx b/src/lib/renderWithProviders.tsx index d8cced4e..d363824d 100644 --- a/src/lib/renderWithProviders.tsx +++ b/src/lib/renderWithProviders.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { MockedResponse, MockLink } from 'apollo-link-mock'; import { ApolloClient } from 'apollo-client'; -import { ApolloProvider } from 'react-apollo-hooks'; +import { ApolloProvider } from '@apollo/react-hooks'; import { InMemoryCache } from 'apollo-boost'; import { render } from '@testing-library/react'; import { createStore } from 'redux'; diff --git a/src/server.tsx b/src/server.tsx index 43c3fcbd..47aa6ce2 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -8,7 +8,8 @@ import serve from 'koa-static'; import Router from 'koa-router'; import path from 'path'; import App from './App'; -import { ApolloProvider, getDataFromTree } from 'react-apollo'; +import { ApolloProvider } from '@apollo/react-hooks'; +import { getDataFromTree } from '@apollo/react-ssr'; import { ApolloClient } from 'apollo-client'; import { getMatches } from './pages/getMatches'; import { createHttpLink } from 'apollo-link-http'; diff --git a/yarn.lock b/yarn.lock index 1a112fb1..24247e68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,53 @@ # yarn lockfile v1 +"@apollo/react-common@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.0.0.tgz#2357518c4b3bf1fd680ee2ac114f565f527ec55d" + integrity sha512-EqHASkcmxipy2hU8rja+lD1S1HoTdodKKyJZZ3dgewnAHXnzXnnC3rw1+lkrgXPFsI2n2d2N2LYisD79OCdPmw== + dependencies: + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-components@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@apollo/react-components/-/react-components-3.0.0.tgz#ca489ff8f70c6a7224f0c2a8a0baedda0448a815" + integrity sha512-IF5HZWT4Vc+6JXenFApjc+QsfZccd7UVSV1Z8l4Y5+EoXkLbmM3fuu8lUQamZqzdVVh6coA8bdI0gafB7PU+1A== + dependencies: + "@apollo/react-common" "^3.0.0" + "@apollo/react-hooks" "^3.0.0" + prop-types "^15.7.2" + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-hooks@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.0.0.tgz#5fe4ff7812020f3e1b91b8628af8c03276496d78" + integrity sha512-7kaV6rkx2WZjDYcBmp52oyhTxbNn5Jc4AUmsXZVEnDu9uuvNYURA8bLlJNF8yu4zS7ed8D+ZebC0y0bhrz8wIg== + dependencies: + "@apollo/react-common" "^3.0.0" + "@wry/equality" "^0.1.9" + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-ssr@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@apollo/react-ssr/-/react-ssr-3.0.0.tgz#a31455f90ec87af0b3b597a6468a9e2732ce738b" + integrity sha512-lgEnvP4lwgqOv4rZA2dDyIcOLdCYkRms54aGxvErHxh7GGfsJFLnx6pRoa+uUvFjr8/edOVQtaHADnaMbtAMbA== + dependencies: + "@apollo/react-common" "^3.0.0" + "@apollo/react-hooks" "^3.0.0" + tslib "^1.10.0" + +"@apollo/react-testing@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@apollo/react-testing/-/react-testing-3.0.0.tgz#8c8b1250570434edcbe168dfb2fd54ecf98e596c" + integrity sha512-AUhjWdIx3hgG/rwvg4Bf3GPw61BReh0feSO2Xa4mh9T+CqcKRb0OwmIwAdh/C0BoPf3zUKROBNd9IJhs80LPFA== + dependencies: + "@apollo/react-common" "^3.0.0" + fast-json-stable-stringify "^2.0.0" + tslib "^1.10.0" + "@babel/code-frame@7.0.0", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" @@ -1210,6 +1257,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.24.tgz#b13564af612a22a20b5d95ca40f1bffb3af315cf" integrity sha512-GWWbvt+z9G5otRBW8rssOFgRY87J9N/qbhqfjMZ+gUuL6zoL+Hm6gP/8qQBG4jjimqdaNLCehcVapZ/Fs2WjCQ== +"@types/node@>=6": + version "12.7.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.1.tgz#3b5c3a26393c19b400844ac422bd0f631a94d69d" + integrity sha512-aK9jxMypeSrhiYofWWBf/T7O+KwaiAHzM4sveCdWPn71lzUSMimRnKzhXDKfKwV1kWoBo2P1aGgaIYGLf9/ljw== + "@types/prismjs@^1.16.0": version "1.16.0" resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.16.0.tgz#4328c9f65698e59f4feade8f4e5d928c748fd643" @@ -1600,7 +1652,15 @@ "@webassemblyjs/wast-parser" "1.7.11" "@xtuc/long" "4.2.1" -"@wry/equality@^0.1.2": +"@wry/context@^0.4.0": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8" + integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag== + dependencies: + "@types/node" ">=6" + tslib "^1.9.3" + +"@wry/equality@^0.1.2", "@wry/equality@^0.1.9": version "0.1.9" resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ== @@ -1786,82 +1846,80 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-boost@^0.1.28: - version "0.1.28" - resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.1.28.tgz#a1f9a913f854408abd59a029a94d83c9dd58081f" - integrity sha512-WnOeFKyI+1FxWtIsIjqj5TC+xMxJyY1pw8e0Gyd99vKaGNNulx1+MBEy2qL3u7NiaGWj93vxu/y4r8tKNnNqyA== +apollo-boost@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.4.tgz#7c278dac6cb6fa3f2f710c56baddc6e3ae730651" + integrity sha512-ASngBvazmp9xNxXfJ2InAzfDwz65o4lswlEPrWoN35scXmCz8Nz4k3CboUXbrcN/G0IExkRf/W7o9Rg0cjEBqg== dependencies: - apollo-cache "^1.1.26" - apollo-cache-inmemory "^1.4.3" - apollo-client "^2.4.13" + apollo-cache "^1.3.2" + apollo-cache-inmemory "^1.6.3" + apollo-client "^2.6.4" apollo-link "^1.0.6" apollo-link-error "^1.0.3" apollo-link-http "^1.3.1" - apollo-link-state "^0.4.0" graphql-tag "^2.4.2" + ts-invariant "^0.4.0" tslib "^1.9.3" -apollo-cache-inmemory@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.4.3.tgz#aded4fb8b3de9e2fb2573a6c03591b07ef98ed36" - integrity sha512-p9KGtEZ9Mlb+FS0UEaxR8WvKOijYV0c+TXlhC/XZ3/ltYvP1zL3b1ozSOLGR9SawN2895Fc7QDV5nzPpihV0rA== +apollo-cache-inmemory@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d" + integrity sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg== dependencies: - apollo-cache "^1.1.26" - apollo-utilities "^1.1.3" - optimism "^0.6.9" + apollo-cache "^1.3.2" + apollo-utilities "^1.3.2" + optimism "^0.10.0" + ts-invariant "^0.4.0" tslib "^1.9.3" -apollo-cache@1.1.26, apollo-cache@^1.1.26: - version "1.1.26" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.26.tgz#5afe023270effbc2063d90f51d8e56bce274ab37" - integrity sha512-JKFHijwkhXpcQ3jOat+ctwiXyjDhQgy0p6GSaj7zG+or+ZSalPqUnPzFRgRwFLVbYxBKJgHCkWX+2VkxWTZzQQ== +apollo-cache@1.3.2, apollo-cache@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a" + integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg== dependencies: - apollo-utilities "^1.1.3" + apollo-utilities "^1.3.2" tslib "^1.9.3" -apollo-client@^2.4.13: - version "2.4.13" - resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.4.13.tgz#09829fcbd68e069de9840d0a10764d7c6a3d0787" - integrity sha512-7mBdW/CW1qHB8Mj4EFAG3MTtbRc6S8aUUntUdrKfRWV1rZdWa0NovxsgVD/R4HZWZjRQ2UOM4ENsHdM5g1uXOQ== +apollo-client@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140" + integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ== dependencies: "@types/zen-observable" "^0.8.0" - apollo-cache "1.1.26" + apollo-cache "1.3.2" apollo-link "^1.0.0" - apollo-link-dedup "^1.0.0" - apollo-utilities "1.1.3" + apollo-utilities "1.3.2" symbol-observable "^1.0.2" + ts-invariant "^0.4.0" tslib "^1.9.3" zen-observable "^0.8.0" -apollo-link-dedup@^1.0.0: - version "1.0.15" - resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.15.tgz#028148f1028e806a9ab2f4abf3bde9dc292565e9" - integrity sha512-14/+Tg7ogcYVrvZa8C7uBQIvX2B/dCKSnojI41yDYGp/t2eWD5ITCWdgjhciXpi0Ij6z+NRyMEebACz3EOwm4w== - dependencies: - apollo-link "^1.2.8" - apollo-link-error@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.7.tgz#6233a339d732def831af2dd417065b2ffd9feb5c" - integrity sha512-olPTKr3yFoavFHSXSLqC5QSWrRACN8TK3+E0pVL8uVR0zILJflUSCRb8HizKQmxZWtr9yM+D2gRLu9mStI8qTA== + version "1.1.11" + resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.11.tgz#7cd363179616fb90da7866cee85cb00ee45d2f3b" + integrity sha512-442DNqn3CNRikDaenMMkoDmCRmkoUx/XyUMlRTZBEFdTw3FYPQLsmDO3hzzC4doY5/BHcn9/jdYh9EeLx4HPsA== dependencies: - apollo-link "^1.2.8" - apollo-link-http-common "^0.2.10" + apollo-link "^1.2.12" + apollo-link-http-common "^0.2.14" + tslib "^1.9.3" -apollo-link-http-common@^0.2.10: - version "0.2.10" - resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.10.tgz#b5bbf502ff40a81cc00281ba3b8543b7ad866dfe" - integrity sha512-KY9nhpAurw3z48OIYV0sCZFXrzWp/wjECsveK+Q9GUhhSe1kEbbUjFfmi+qigg+iELgdp5V8ioRJhinl1vPojw== +apollo-link-http-common@^0.2.14: + version "0.2.14" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8" + integrity sha512-v6mRU1oN6XuX8beVIRB6OpF4q1ULhSnmy7ScnHnuo1qV6GaFmDcbdvXqxIkAV1Q8SQCo2lsv4HeqJOWhFfApOg== dependencies: - apollo-link "^1.2.8" + apollo-link "^1.2.12" + ts-invariant "^0.4.0" + tslib "^1.9.3" apollo-link-http@^1.3.1: - version "1.5.11" - resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.11.tgz#1f72a377d03e874a08bc9eadb1ce7ecb166f1e56" - integrity sha512-wDG+I9UmpfaZRPIvTYBgkvqiCgmz6yWgvuzW/S24Q4r4Xrfe6sLpg2FmarhtdP+hdN+IXTLbFNCZ+Trgfpifow== + version "1.5.15" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.15.tgz#106ab23bb8997bd55965d05855736d33119652cf" + integrity sha512-epZFhCKDjD7+oNTVK3P39pqWGn4LEhShAoA1Q9e2tDrBjItNfviiE33RmcLcCURDYyW5JA6SMgdODNI4Is8tvQ== dependencies: - apollo-link "^1.2.8" - apollo-link-http-common "^0.2.10" + apollo-link "^1.2.12" + apollo-link-http-common "^0.2.14" + tslib "^1.9.3" apollo-link-mock@^1.0.1: version "1.0.1" @@ -1872,22 +1930,7 @@ apollo-link-mock@^1.0.1: apollo-utilities "^1.0.25" lodash.isequal "^4.5.0" -apollo-link-state@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.2.tgz#ac00e9be9b0ca89eae0be6ba31fe904b80bbe2e8" - integrity sha512-xMPcAfuiPVYXaLwC6oJFIZrKgV3GmdO31Ag2eufRoXpvT0AfJZjdaPB4450Nu9TslHRePN9A3quxNueILlQxlw== - dependencies: - apollo-utilities "^1.0.8" - graphql-anywhere "^4.1.0-alpha.0" - -apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.8.tgz#0f252adefd5047ac1a9f35ba9439d216587dcd84" - integrity sha512-lfzGRxhK9RmiH3HPFi7TIEBhhDY9M5a2ZDnllcfy5QDk7cCQHQ1WQArcw1FK0g1B+mV4Kl72DSrlvZHZJEolrA== - dependencies: - zen-observable-ts "^0.8.15" - -apollo-link@^1.2.3: +apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.12, apollo-link@^1.2.3: version "1.2.12" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429" integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q== @@ -1897,15 +1940,7 @@ apollo-link@^1.2.3: tslib "^1.9.3" zen-observable-ts "^0.8.19" -apollo-utilities@1.1.3, apollo-utilities@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.1.3.tgz#a8883c0392f6b46eac0d366204ebf34be9307c87" - integrity sha512-pF9abhiClX5gfj/WFWZh8DiI33nOLGxRhXH9ZMquaM1V8bhq1WLFPt2QjShWH3kGQVeIGUK+FQefnhe+ZaaAYg== - dependencies: - fast-json-stable-stringify "^2.0.0" - tslib "^1.9.3" - -apollo-utilities@^1.0.25, apollo-utilities@^1.3.0: +apollo-utilities@1.3.2, apollo-utilities@^1.0.25, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== @@ -1915,14 +1950,6 @@ apollo-utilities@^1.0.25, apollo-utilities@^1.3.0: ts-invariant "^0.4.0" tslib "^1.9.3" -apollo-utilities@^1.0.8, apollo-utilities@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.1.2.tgz#aa5eca9d1f1eb721c381a22e0dde03559d856db3" - integrity sha512-EjDx8vToK+zkWIxc76ZQY/irRX52puNg04xf/w8R0kVTDAgHuVfnFVC01O5vE25kFnIaa5em0pFI0p9b6YMkhQ== - dependencies: - fast-json-stable-stringify "^2.0.0" - tslib "^1.9.3" - append-transform@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" @@ -3307,7 +3334,7 @@ copy-to-clipboard@^3.1.0: dependencies: toggle-selection "^1.0.6" -core-js@2.6.4, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: +core-js@2.6.4, core-js@^2.4.0, core-js@^2.5.0: version "2.6.4" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.4.tgz#b8897c062c4d769dd30a0ac5c73976c47f92ea0d" integrity sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A== @@ -4813,11 +4840,6 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs-css-vars@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" - integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== - fbjs@^0.8.0: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" @@ -4831,20 +4853,6 @@ fbjs@^0.8.0: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fbjs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-1.0.0.tgz#52c215e0883a3c86af2a7a776ed51525ae8e0a5a" - integrity sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA== - dependencies: - core-js "^2.4.1" - fbjs-css-vars "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.18" - figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -5320,14 +5328,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== -graphql-anywhere@^4.1.0-alpha.0: - version "4.1.27" - resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.27.tgz#7d013b4c51039ea6c46839a53abe924f8fffd4f3" - integrity sha512-ErASfs9siEMrmroHU0V4heh6cIdA8K/SoYpahJFgEM6YDAwUZuycTAKIrMaK8XJI37sHZWcujF/ySuYnIkP5vw== - dependencies: - apollo-utilities "^1.1.2" - tslib "^1.9.3" - graphql-tag@^2.10.1, graphql-tag@^2.4.2: version "2.10.1" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" @@ -5342,10 +5342,10 @@ graphql.macro@^1.3.4: babel-plugin-macros "^2.5.0" graphql-tag "^2.10.1" -graphql@^14.1.1: - version "14.1.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.1.1.tgz#d5d77df4b19ef41538d7215d1e7a28834619fac0" - integrity sha512-C5zDzLqvfPAgTtP8AUPIt9keDabrdRAqSWjj2OPRKrKxI9Fb65I36s1uCs1UUBFnSWTdO7hyHi7z1ZbwKMKF6Q== +graphql@^14.4.2: + version "14.4.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3" + integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ== dependencies: iterall "^1.2.2" @@ -5582,7 +5582,7 @@ hoek@4.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -5832,11 +5832,6 @@ immer@^2.1.1: resolved "https://registry.yarnpkg.com/immer/-/immer-2.1.1.tgz#13abfc251e742b7dbc23e7444a3bd288c7394e43" integrity sha512-+S4ktJM7ugEy1aIfpsjTVf7ZLMMuwz8QqSxpJRZaCM+0ujfeU4kWcj57P02qQ/KRGMw7DwUS4lCKNNROHasXkw== -immutable-tuple@^0.4.9: - version "0.4.10" - resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.10.tgz#e0b1625384f514084a7a84b749a3bb26e9179929" - integrity sha512-45jheDbc3Kr5Cw8EtDD+4woGRUV0utIrJBZT8XH0TPZRfm8tzT0/sLGGzyyCCFqFMG5Pv5Igf3WY/arn6+8V9Q== - import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -7340,11 +7335,6 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= -lodash.flowright@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/lodash.flowright/-/lodash.flowright-3.5.0.tgz#2b5fff399716d7e7dc5724fe9349f67065184d67" - integrity sha1-K1//OZcW1+fcVyT+k0n2cGUYTWc= - lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -8271,12 +8261,12 @@ opn@5.4.0, opn@^5.1.0: dependencies: is-wsl "^1.1.0" -optimism@^0.6.9: - version "0.6.9" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.9.tgz#19258ff8b3be0cea29ac35f06bff818e026e30bb" - integrity sha512-xoQm2lvXbCA9Kd7SCx6y713Y7sZ6fUc5R6VYpoL5M6svKJbTuvtNopexK8sO8K4s0EOUYHuPN2+yAEsNyRggkQ== +optimism@^0.10.0: + version "0.10.2" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.2.tgz#626b6fd28b0923de98ecb36a3fd2d3d4e5632dd9" + integrity sha512-zPfBIxFFWMmQboM9+Z4MSJqc1PXp82v1PFq/GfQaufI69mHKlup7ykGNnfuGIGssXJQkmhSodQ/k9EWwjd8O8A== dependencies: - immutable-tuple "^0.4.9" + "@wry/context" "^0.4.0" optimist@^0.6.1: version "0.6.1" @@ -9660,25 +9650,6 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-apollo-hooks@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/react-apollo-hooks/-/react-apollo-hooks-0.5.0.tgz#49607fbe9cdfd0be08ea5defd39085bf5f42e10e" - integrity sha512-Us5KqFe7/c6vY1NaiyfhnD2Pz4lPLTojQXLppShaBVYU/vYvJrRjmj4MzIPXnExXaSfnQ+K2bWDr4lP4efbsRQ== - dependencies: - lodash "^4.17.11" - -react-apollo@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.4.1.tgz#89db63ebacf01c1603553bb476f089492aaeab2c" - integrity sha512-fSaiwXzY5xRmqKuGCtkMON8paL4/rKf4pB2aSI/0Ot8tn+gJisFqZ0iz9Pxvl6MHnJm1/gjJD3X1J4KmbYN2Yw== - dependencies: - fbjs "^1.0.0" - hoist-non-react-statics "^3.0.0" - invariant "^2.2.2" - lodash.flowright "^3.5.0" - lodash.isequal "^4.5.0" - prop-types "^15.6.0" - react-app-polyfill@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-0.2.1.tgz#96c701a40b9671c8547f70bdbb4a47f4d5767790" @@ -11555,7 +11526,7 @@ ts-easing@^0.2.0: resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== -ts-invariant@^0.4.0: +ts-invariant@^0.4.0, ts-invariant@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== @@ -11572,6 +11543,11 @@ tslib@1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== +tslib@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -12650,13 +12626,6 @@ ylru@^1.2.0: resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ== -zen-observable-ts@^0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.15.tgz#6cf7df6aa619076e4af2f707ccf8a6290d26699b" - integrity sha512-sXKPWiw6JszNEkRv5dQ+lQCttyjHM2Iks74QU5NP8mMPS/NrzTlHDr780gf/wOBqmHkPO6NCLMlsa+fAQ8VE8w== - dependencies: - zen-observable "^0.8.0" - zen-observable-ts@^0.8.19: version "0.8.19" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694" @@ -12666,6 +12635,6 @@ zen-observable-ts@^0.8.19: zen-observable "^0.8.0" zen-observable@^0.8.0: - version "0.8.13" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.13.tgz#a9f1b9dbdfd2d60a08761ceac6a861427d44ae2e" - integrity sha512-fa+6aDUVvavYsefZw0zaZ/v3ckEtMgCFi30sn91SEZea4y/6jQp05E3omjkX91zV6RVdn15fqnFZ6RKjRGbp2g== + version "0.8.14" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7" + integrity sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g== From 12a8b02b815882ea40f32db3e4a0f4206b878c5e Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 16 Aug 2019 00:03:24 +0900 Subject: [PATCH 081/736] Initialize UserProfile --- src/components/auth/AuthSocialButton.tsx | 1 + src/components/common/UserProfile.tsx | 120 +++++++++++++++++++++++ src/containers/post/PostViewer.tsx | 5 + src/pages/velog/UserPage.tsx | 21 ++++ src/pages/velog/VelogPage.tsx | 2 + src/static/svg/icon-email.svg | 3 + src/static/svg/icon-facebook-square.svg | 3 + src/static/svg/icon-github.svg | 10 +- src/static/svg/icon-share-2.svg | 3 + src/static/svg/icon-twitter.svg | 10 ++ src/static/svg/index.ts | 6 ++ 11 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src/components/common/UserProfile.tsx create mode 100644 src/pages/velog/UserPage.tsx create mode 100644 src/static/svg/icon-email.svg create mode 100644 src/static/svg/icon-facebook-square.svg create mode 100644 src/static/svg/icon-share-2.svg create mode 100644 src/static/svg/icon-twitter.svg diff --git a/src/components/auth/AuthSocialButton.tsx b/src/components/auth/AuthSocialButton.tsx index dece5794..a601d3de 100644 --- a/src/components/auth/AuthSocialButton.tsx +++ b/src/components/auth/AuthSocialButton.tsx @@ -13,6 +13,7 @@ const AuthSocialButtonBlock = styled.button<{ border: boolean }>` outline: none; border: none; transition: 0.125s all ease-in; + color: white; ${props => props.border && css` diff --git a/src/components/common/UserProfile.tsx b/src/components/common/UserProfile.tsx new file mode 100644 index 00000000..fac94444 --- /dev/null +++ b/src/components/common/UserProfile.tsx @@ -0,0 +1,120 @@ +import React, { CSSProperties } from 'react'; +import styled, { css } from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { + GithubIcon, + TwitterIcon, + FacebookSquareIcon, + EmailIcon, +} from '../../static/svg'; + +const UserProfileBlock = styled.div<{ gray?: boolean }>` + ${props => + props.gray && + css` + background: ${palette.gray0}; + border-radius: 8px; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); + padding: 2rem 1.5rem; + `} +`; + +const Section = styled.div` + display: flex; + align-items: center; + img { + display: block; + width: 8rem; + height: 8rem; + border-radius: 50%; + object-fit: cover; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); + } +`; + +const UserInfo = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 1rem; + font-family: 'Spoqa Han Sans', -apple-system, BlinkMacSystemFont, + -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', + arial, 나눔고딕, 'Nanum Gothic', 돋움; + + .name { + font-size: 1.5rem; + line-height: 1.5; + font-weight: bold; + color: ${palette.gray9}; + } + .description { + white-space: pre-wrap; + font-size: 1.125rem; + line-height: 1.5; + margin-top: 0.25rem; + color: ${palette.gray7}; + letter-spacing: -0.02em; + } +`; + +const Separator = styled.div` + background: ${palette.gray2}; + width: 100%; + height: 1px; + margin-top: 2rem; + margin-bottom: 1.5rem; +`; + +const ProfileIcons = styled.div` + color: ${palette.gray5}; + svg { + cursor: pointer; + width: 2rem; + height: 2rem; + &:hover { + color: ${palette.gray8}; + } + } + svg + svg { + margin-left: 1rem; + } +`; + +export interface UserProfileProps { + className?: string; + gray?: boolean; + style?: CSSProperties; +} + +const UserProfile: React.FC = ({ + gray, + className, + style, +}) => { + return ( + +
+ profile + +
Minjun Kim
+
+ Frontend Engineer@RIDI Corp. 개발을 재미있게 이것 저것 하는 + 개발자입니다. +
+
+
+ + + + + + + +
+ ); +}; + +export default UserProfile; diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 6d8a2eb6..21b830b8 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -23,6 +23,8 @@ import { shareFacebook, shareTwitter, copyText } from '../../lib/share'; import PostToc from '../../components/post/PostToc'; import LinkedPostList from '../../components/post/LinkedPostList'; import { getScrollTop } from '../../lib/utils'; +import UserProfile from '../../components/common/UserProfile'; +import VelogResponsive from '../../components/velog/VelogResponsive'; export interface PostViewerOwnProps { username: string; @@ -266,6 +268,9 @@ const PostViewer: React.FC = ({ toc={} /> + + + = props => { + return ( + + + + ); +}; + +export default UserPage; diff --git a/src/pages/velog/VelogPage.tsx b/src/pages/velog/VelogPage.tsx index 1bafdc40..d4881d07 100644 --- a/src/pages/velog/VelogPage.tsx +++ b/src/pages/velog/VelogPage.tsx @@ -3,6 +3,7 @@ import VelogPageTemplate from '../../components/velog/VelogPageTemplate'; import { RouteComponentProps, Route } from 'react-router'; import ConfigLoader from '../../containers/velog/ConfigLoader'; import PostPage from './PostPage'; +import UserPage from './UserPage'; export interface VelogPageProps extends RouteComponentProps<{ @@ -14,6 +15,7 @@ const VelogPage: React.FC = ({ match }) => { return ( + ); diff --git a/src/static/svg/icon-email.svg b/src/static/svg/icon-email.svg new file mode 100644 index 00000000..45432330 --- /dev/null +++ b/src/static/svg/icon-email.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/svg/icon-facebook-square.svg b/src/static/svg/icon-facebook-square.svg new file mode 100644 index 00000000..c3cb2356 --- /dev/null +++ b/src/static/svg/icon-facebook-square.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/svg/icon-github.svg b/src/static/svg/icon-github.svg index 9b3da830..76a35566 100644 --- a/src/static/svg/icon-github.svg +++ b/src/static/svg/icon-github.svg @@ -1,8 +1,8 @@ - - - + + + - - + + diff --git a/src/static/svg/icon-share-2.svg b/src/static/svg/icon-share-2.svg new file mode 100644 index 00000000..20a1fba5 --- /dev/null +++ b/src/static/svg/icon-share-2.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/svg/icon-twitter.svg b/src/static/svg/icon-twitter.svg new file mode 100644 index 00000000..55cc7a7e --- /dev/null +++ b/src/static/svg/icon-twitter.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/static/svg/index.ts b/src/static/svg/index.ts index 2d344c58..41f8b627 100644 --- a/src/static/svg/index.ts +++ b/src/static/svg/index.ts @@ -1,5 +1,8 @@ export { ReactComponent as Logo } from './logo.svg'; export { ReactComponent as FacebookIcon } from './icon-facebook.svg'; +export { + ReactComponent as FacebookSquareIcon, +} from './icon-facebook-square.svg'; export { ReactComponent as GoogleIcon } from './icon-google.svg'; export { ReactComponent as GithubIcon } from './icon-github.svg'; export { ReactComponent as GlobeIcon } from './icon-globe.svg'; @@ -12,3 +15,6 @@ export { ReactComponent as SeriesImage } from './image-series.svg'; export { ReactComponent as LikeIcon } from './icon-like.svg'; export { ReactComponent as ShareIcon } from './icon-share.svg'; export { ReactComponent as ClipIcon } from './icon-clip.svg'; +export { ReactComponent as EmailIcon } from './icon-email.svg'; +export { ReactComponent as TwitterIcon } from './icon-twitter.svg'; +export { ReactComponent as ShareIcon2 } from './icon-share-2.svg'; From bf4518015523c19de29e5698b7e9d7eaf85282ee Mon Sep 17 00:00:00 2001 From: velopert Date: Fri, 16 Aug 2019 23:29:41 +0900 Subject: [PATCH 082/736] Remove gray props from UserProfile and load profile_links --- .../__snapshots__/AuthForm.test.tsx.snap | 6 ++-- src/components/common/UserProfile.tsx | 34 +++++++------------ .../AuthModalContainer.test.tsx.snap | 6 ++-- src/containers/post/PostViewer.tsx | 16 +++++++-- .../post/__tests__/PostComments.test.tsx | 2 +- .../post/__tests__/PostViewer.test.tsx | 3 ++ src/containers/write/ActiveEditor.tsx | 2 +- src/lib/graphql/post.ts | 4 ++- src/lib/graphql/user.ts | 10 +++++- src/pages/velog/UserPage.tsx | 6 +--- 10 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/components/auth/__tests__/__snapshots__/AuthForm.test.tsx.snap b/src/components/auth/__tests__/__snapshots__/AuthForm.test.tsx.snap index 6222a351..4609099e 100644 --- a/src/components/auth/__tests__/__snapshots__/AuthForm.test.tsx.snap +++ b/src/components/auth/__tests__/__snapshots__/AuthForm.test.tsx.snap @@ -40,7 +40,7 @@ exports[`AuthForm renders correctly 1`] = ` class="sc-htpNat jVlyIb" > + ) : ( + + + + + )} + + ); +}; + +export default SeriesActionButtons; diff --git a/src/components/velog/SeriesPostItem.tsx b/src/components/velog/SeriesPostItem.tsx index 6f6db6a7..a44b0ac8 100644 --- a/src/components/velog/SeriesPostItem.tsx +++ b/src/components/velog/SeriesPostItem.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import palette from '../../lib/styles/palette'; import { emptyThumbnail } from '../../static/images'; import { formatDate } from '../../lib/utils'; import PostLink from '../common/PostLink'; -const SeriesPostItemBlock = styled.div` +const SeriesPostItemBlock = styled.div<{ edit?: boolean }>` font-family: 'Spoqa Han Sans', -apple-system, BlinkMacSystemFont, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', arial, 나눔고딕, 'Nanum Gothic', 돋움; @@ -72,6 +72,15 @@ const SeriesPostItemBlock = styled.div` color: ${palette.gray5}; font-size: 0.875rem; } + + ${props => + props.edit && + css` + padding: 1rem; + background: white; + border-radius: 4px; + box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.06); + `} `; export interface SeriesPostItemProps { @@ -82,6 +91,7 @@ export interface SeriesPostItemProps { thumbnail: string | null; urlSlug: string; username: string; + edit?: boolean; } const SeriesPostItem: React.FC = ({ @@ -92,9 +102,10 @@ const SeriesPostItem: React.FC = ({ thumbnail, username, urlSlug, + edit, }) => { return ( - +

{index}. diff --git a/src/components/velog/__tests__/SeriesActionButtons.test.tsx b/src/components/velog/__tests__/SeriesActionButtons.test.tsx new file mode 100644 index 00000000..2d1c17e8 --- /dev/null +++ b/src/components/velog/__tests__/SeriesActionButtons.test.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import SeriesActionButtons, { + SeriesActionButtonsProps, +} from '../SeriesActionButtons'; + +describe('SeriesActionButtons', () => { + const setup = (props: Partial = {}) => { + const initialProps: SeriesActionButtonsProps = { + onEdit: () => {}, + onApply: () => {}, + editing: false, + }; + const utils = render(); + + return { + ...utils, + }; + }; + it('renders properly', () => { + setup(); + }); + it('calls onEdit', () => { + const onEdit = jest.fn(); + const { getByText } = setup({ + onEdit, + }); + const editButton = getByText('수정'); + fireEvent.click(editButton); + expect(onEdit).toBeCalled(); + }); + it('shows apply button when editing={true}', () => { + const { getByText } = setup({ + editing: true, + }); + getByText('적용'); + }); + it('calls onApply', () => { + const onApply = jest.fn(); + const { getByText } = setup({ + editing: true, + onApply, + }); + const applyButton = getByText('적용'); + fireEvent.click(applyButton); + expect(onApply).toBeCalled(); + }); +}); diff --git a/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap b/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap index 7cb3578b..932abd48 100644 --- a/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/MarkdownPreview.test.tsx.snap @@ -9,7 +9,7 @@ exports[`MarkdownPreview matches snapshot 1`] = ` class="sc-bxivhb cFXJHm" />
= ({ match, }) => { useMount(() => { - if (!window.scrollTo) return; + if (!window.scrollTo || process.env.NODE_ENV === 'test') return; + console.log(window.scrollTo); window.scrollTo(0, 0); }); const userId = useUserId(); diff --git a/src/containers/velog/SeriesPosts.tsx b/src/containers/velog/SeriesPosts.tsx index 198b5f56..9ce51327 100644 --- a/src/containers/velog/SeriesPosts.tsx +++ b/src/containers/velog/SeriesPosts.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import SeriesPostsTemplate from '../../components/velog/SeriesPostsTemplate'; import SeriesSorterAligner from '../../components/velog/SeriesSorterAligner'; import SorterButton from '../../components/common/SorterButton'; @@ -6,6 +6,8 @@ import SeriesPostList from '../../components/velog/SeriesPostList'; import { useQuery } from '@apollo/react-hooks'; import { GetSeriesResponse, GET_SERIES } from '../../lib/graphql/series'; import useToggle from '../../lib/hooks/useToggle'; +import SeriesActionButtons from '../../components/velog/SeriesActionButtons'; +import DraggableSeriesPostList from '../../components/velog/DraggableSeriesPostList'; export interface SeriesPostsProps { username: string; @@ -13,8 +15,10 @@ export interface SeriesPostsProps { } const SeriesPosts: React.FC = ({ username, urlSlug }) => { - const [asc, toggle] = useToggle(true); - const { data, loading, error } = useQuery(GET_SERIES, { + const [asc, toggleAsc] = useToggle(true); + const [editing, toggleEditing] = useToggle(false); + + const { data } = useQuery(GET_SERIES, { variables: { username, url_slug: urlSlug, @@ -22,20 +26,34 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { fetchPolicy: 'cache-and-network', }); + const onApply = useCallback(() => { + console.log(editing); + toggleEditing(); + }, [editing, toggleEditing]); + if (!data || !data.series) return null; return ( - - - - { + + {!editing && ( + + + + )} + {editing ? ( + + ) : ( - } + )} ); }; diff --git a/src/containers/velog/__tests__/SeriesListContainer.test.tsx b/src/containers/velog/__tests__/SeriesListContainer.test.tsx index 3943c01e..48963636 100644 --- a/src/containers/velog/__tests__/SeriesListContainer.test.tsx +++ b/src/containers/velog/__tests__/SeriesListContainer.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, waitForElement } from '@testing-library/react'; +import { waitForElement } from '@testing-library/react'; import SeriesListContainer, { SeriesListContainerProps, } from '../SeriesListContainer'; diff --git a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap index 42d9663e..e9050992 100644 --- a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap +++ b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap @@ -267,7 +267,7 @@ exports[`ActiveEditor matches snapshot 1`] = ` class="sc-jWBwVP boWCkI" >
Date: Wed, 4 Sep 2019 00:03:07 +0900 Subject: [PATCH 098/736] Prepare for series edit --- src/components/velog/DragSample.tsx | 54 +++++++++++++++++++ .../velog/DraggableSeriesPostList.tsx | 22 ++++++-- src/components/velog/SeriesPostsTemplate.tsx | 34 ++++++++++-- src/containers/velog/SeriesPosts.tsx | 23 +++++++- src/lib/graphql/series.ts | 34 +++++++++++- 5 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 src/components/velog/DragSample.tsx diff --git a/src/components/velog/DragSample.tsx b/src/components/velog/DragSample.tsx new file mode 100644 index 00000000..a2dfb9ad --- /dev/null +++ b/src/components/velog/DragSample.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from 'react-beautiful-dnd'; + +const initial = Array.from({ length: 10 }, (v, k) => k).map(k => ({ + id: k, + content: `Message ${k}`, +})); + +const reorder = ( + list: typeof initial, + startIndex: number, + endIndex: number, +) => { + const temp = [...list]; + const [removed] = temp.splice(startIndex, 1); + temp.splice(endIndex, 0, removed); + return temp; +}; + +const DragSampleBlock = styled.div``; + +export interface DragSampleProps {} + +const DragSample = (props: DragSampleProps) => { + const [list, setList] = useState(initial); + + const onDragEnd = (result: DropResult) => { + if (!result.destination) return; + if (result.destination.index === result.source.index) return; + setList(reorder(list, result.source.index, result.destination.index)); + }; + + return ( + + + {provided => ( +
+ )} + + + ); +}; + +export default DragSample; diff --git a/src/components/velog/DraggableSeriesPostList.tsx b/src/components/velog/DraggableSeriesPostList.tsx index 30379167..9a759df3 100644 --- a/src/components/velog/DraggableSeriesPostList.tsx +++ b/src/components/velog/DraggableSeriesPostList.tsx @@ -20,6 +20,7 @@ const DroppableBlock = styled.div<{ isDraggingOver: boolean }>` props.isDraggingOver ? palette.gray1 : palette.gray0}; border-radius: 4px; padding: 1.5rem; + padding-bottom: 0.5rem; `; const DraggableBlock = styled.div<{ isDragging: boolean }>` @@ -32,20 +33,30 @@ const DraggableBlock = styled.div<{ isDragging: boolean }>` : css` opacity: 1; `} - - & + & { - margin-top: 1rem; - } + margin-bottom: 1rem; `; export interface DraggableSeriesListProps { seriesPosts: SeriesPostPreview[]; } +function reorder(list: T[], startIndex: number, endIndex: number) { + const nextList = [...list]; + const [removed] = nextList.splice(startIndex, 1); + nextList.splice(endIndex, 0, removed); + return nextList; +} + const DraggableSeriesList = ({ seriesPosts }: DraggableSeriesListProps) => { const [tempPosts, setTempPosts] = useState(seriesPosts); - const onDragEnd = (result: DropResult, provided: ResponderProvided) => {}; + const onDragEnd = (result: DropResult, provided: ResponderProvided) => { + if (!result.destination) return; + if (result.destination.index === result.source.index) return; + setTempPosts(prevTempPosts => + reorder(prevTempPosts, result.source.index, result.destination!.index), + ); + }; return ( @@ -81,6 +92,7 @@ const DraggableSeriesList = ({ seriesPosts }: DraggableSeriesListProps) => { )} ))} + {provided.placeholder} ); }} diff --git a/src/components/velog/SeriesPostsTemplate.tsx b/src/components/velog/SeriesPostsTemplate.tsx index 6b71bc5c..8913b9b7 100644 --- a/src/components/velog/SeriesPostsTemplate.tsx +++ b/src/components/velog/SeriesPostsTemplate.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import styled from 'styled-components'; import palette from '../../lib/styles/palette'; @@ -18,25 +18,51 @@ const SeriesPostsTemplateBlock = styled.div` arial, 나눔고딕, 'Nanum Gothic', 돋움; margin-top: 1rem; line-height: 1.5; - padding-bottom: 1.5rem; - border-bottom: 1px solid ${palette.gray3}; + margin-bottom: 1.5rem; font-size: 2.5rem; color: ${palette.gray9}; + outline: none; } `; +const Separator = styled.div` + background: ${palette.gray3}; + height: 1px; + width: 100%; + margin-bottom: 1.5rem; +`; + export interface SeriesPostsTemplateProps { name: string; + nextName: string; + editing: boolean; + onInput: (e: React.FormEvent) => void; } const SeriesPostsTemplate: React.FC = ({ children, + editing, name, + onInput, }) => { + const titleRef = useRef(null); + + useEffect(() => { + if (!editing || !titleRef.current) return; + titleRef.current.focus(); + }, [editing]); return ( -

{name}

+

+ {name} +

+
{children}
); diff --git a/src/containers/velog/SeriesPosts.tsx b/src/containers/velog/SeriesPosts.tsx index 9ce51327..54e9f751 100644 --- a/src/containers/velog/SeriesPosts.tsx +++ b/src/containers/velog/SeriesPosts.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import SeriesPostsTemplate from '../../components/velog/SeriesPostsTemplate'; import SeriesSorterAligner from '../../components/velog/SeriesSorterAligner'; import SorterButton from '../../components/common/SorterButton'; @@ -8,6 +8,7 @@ import { GetSeriesResponse, GET_SERIES } from '../../lib/graphql/series'; import useToggle from '../../lib/hooks/useToggle'; import SeriesActionButtons from '../../components/velog/SeriesActionButtons'; import DraggableSeriesPostList from '../../components/velog/DraggableSeriesPostList'; +import useInput from '../../lib/hooks/useInput'; export interface SeriesPostsProps { username: string; @@ -17,6 +18,7 @@ export interface SeriesPostsProps { const SeriesPosts: React.FC = ({ username, urlSlug }) => { const [asc, toggleAsc] = useToggle(true); const [editing, toggleEditing] = useToggle(false); + const [nextName, setNextName] = useState(''); const { data } = useQuery(GET_SERIES, { variables: { @@ -31,10 +33,27 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { toggleEditing(); }, [editing, toggleEditing]); + useEffect(() => { + if (!data || !data.series) return; + setNextName(data.series.name); + }, [data]); + + const onChangeNextName = useCallback( + (e: React.FormEvent) => { + setNextName(e.currentTarget.innerText); + }, + [], + ); + if (!data || !data.series) return null; return ( - + Date: Thu, 5 Sep 2019 23:58:00 +0900 Subject: [PATCH 099/736] Prepare for user about --- asset-license.md | 3 +- src/components/common/Typography.tsx | 2 +- .../velog/DraggableSeriesPostList.tsx | 13 +++- src/components/velog/SeriesPostItem.tsx | 18 ++++-- src/components/velog/SeriesSorterAligner.tsx | 2 +- src/components/velog/VelogAboutContent.tsx | 59 +++++++++++++++++++ ...downEditor.tsx => WriteMarkdownEditor.tsx} | 4 +- .../write/__tests__/MarkdownEditor.test.tsx | 2 +- .../MarkdownEditor.test.tsx.snap | 2 +- .../MarkdownPreview.test.tsx.snap | 2 +- .../__snapshots__/QuillEditor.test.tsx.snap | 2 +- .../post/__tests__/PostComments.test.tsx | 3 + src/containers/velog/SeriesPosts.tsx | 43 ++++++++++---- src/containers/velog/VelogAbout.tsx | 33 +++++++++++ .../write/MarkdownEditorContainer.tsx | 2 +- .../__snapshots__/ActiveEditor.test.tsx.snap | 2 +- src/lib/graphql/post.ts | 3 + src/lib/graphql/series.ts | 2 +- src/lib/graphql/user.ts | 22 +++++++ src/pages/velog/UserPage.tsx | 2 + src/pages/velog/VelogPage.tsx | 2 +- src/pages/velog/tabs/AboutTab.tsx | 14 +++++ src/static/images/index.ts | 1 + src/static/images/undraw_empty.svg | 1 + 24 files changed, 208 insertions(+), 31 deletions(-) create mode 100644 src/components/velog/VelogAboutContent.tsx rename src/components/write/{MarkdownEditor.tsx => WriteMarkdownEditor.tsx} (99%) create mode 100644 src/containers/velog/VelogAbout.tsx create mode 100644 src/pages/velog/tabs/AboutTab.tsx create mode 100644 src/static/images/undraw_empty.svg diff --git a/asset-license.md b/asset-license.md index dd473534..2f1b50c6 100644 --- a/asset-license.md +++ b/asset-license.md @@ -7,4 +7,5 @@ Icons made by [Smashicons](https://www.flaticon.com/authors/smashicons) from [ww Heart Icon: https://iconmonstr.com/favorite-8-svg/ -Clip Icon: https://iconmonstr.com/paperclip-2-svg/ \ No newline at end of file +Clip Icon: https://iconmonstr.com/paperclip-2-svg/ + diff --git a/src/components/common/Typography.tsx b/src/components/common/Typography.tsx index 51ea2b2c..5bfc899c 100644 --- a/src/components/common/Typography.tsx +++ b/src/components/common/Typography.tsx @@ -65,7 +65,7 @@ const TypographyBlock = styled.div` h2, h3, h4 { - line-height: 1.35; + line-height: 1.5; margin-bottom: 1rem; } p + h1, diff --git a/src/components/velog/DraggableSeriesPostList.tsx b/src/components/velog/DraggableSeriesPostList.tsx index 9a759df3..30b92bc6 100644 --- a/src/components/velog/DraggableSeriesPostList.tsx +++ b/src/components/velog/DraggableSeriesPostList.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import styled, { css } from 'styled-components'; import palette from '../../lib/styles/palette'; import { SeriesPostPreview } from '../../lib/graphql/series'; @@ -38,6 +38,7 @@ const DraggableBlock = styled.div<{ isDragging: boolean }>` export interface DraggableSeriesListProps { seriesPosts: SeriesPostPreview[]; + onChangeSeriesOrder: (order: string[]) => void; } function reorder(list: T[], startIndex: number, endIndex: number) { @@ -47,7 +48,10 @@ function reorder(list: T[], startIndex: number, endIndex: number) { return nextList; } -const DraggableSeriesList = ({ seriesPosts }: DraggableSeriesListProps) => { +const DraggableSeriesList = ({ + seriesPosts, + onChangeSeriesOrder, +}: DraggableSeriesListProps) => { const [tempPosts, setTempPosts] = useState(seriesPosts); const onDragEnd = (result: DropResult, provided: ResponderProvided) => { @@ -57,6 +61,11 @@ const DraggableSeriesList = ({ seriesPosts }: DraggableSeriesListProps) => { reorder(prevTempPosts, result.source.index, result.destination!.index), ); }; + + useEffect(() => { + onChangeSeriesOrder(tempPosts.map(tp => tp.id)); + }, [onChangeSeriesOrder, tempPosts]); + return ( diff --git a/src/components/velog/SeriesPostItem.tsx b/src/components/velog/SeriesPostItem.tsx index a44b0ac8..166347ec 100644 --- a/src/components/velog/SeriesPostItem.tsx +++ b/src/components/velog/SeriesPostItem.tsx @@ -108,14 +108,22 @@ const SeriesPostItem: React.FC = ({

{index}. - - {title} - + {edit ? ( + {title} + ) : ( + + {title} + + )}

- + {edit ? ( post-thumbnail - + ) : ( + + post-thumbnail + + )}

{description}

{formatDate(date)}
diff --git a/src/components/velog/SeriesSorterAligner.tsx b/src/components/velog/SeriesSorterAligner.tsx index 3afc0d2a..c4fa00af 100644 --- a/src/components/velog/SeriesSorterAligner.tsx +++ b/src/components/velog/SeriesSorterAligner.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; const SeriesSorterAlignerBlock = styled.div` display: flex; justify-content: flex-end; - margin-top: 2.25rem; + margin-top: 1rem; `; export interface SeriesSorterAlignerProps {} diff --git a/src/components/velog/VelogAboutContent.tsx b/src/components/velog/VelogAboutContent.tsx new file mode 100644 index 00000000..421e6f3e --- /dev/null +++ b/src/components/velog/VelogAboutContent.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import styled from 'styled-components'; +import VelogResponsive from './VelogResponsive'; +import MarkdownRender from '../common/MarkdownRender'; +import palette from '../../lib/styles/palette'; +import { undrawEmpty } from '../../static/images'; +import Button from '../common/Button'; + +const VelogAboutContentBlock = styled(VelogResponsive)``; + +const EmptyAbout = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + img { + width: 20rem; + height: 20rem; + margin-bottom: 2rem; + display: block; + } + .message { + font-size: 2rem; + color: ${palette.gray4}; + margin-bottom: 2rem; + } +`; + +export interface VelogAboutContentProps { + markdown: string; + own: boolean; + onClickWrite: () => void; +} + +const VelogAboutContent = ({ + markdown, + own, + onClickWrite, +}: VelogAboutContentProps) => { + return ( + + {markdown ? ( + + ) : ( + + empty about +
소개가 작성되지 않았습니다.
+ {own && ( + + )} +
+ )} +
+ ); +}; + +export default VelogAboutContent; diff --git a/src/components/write/MarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx similarity index 99% rename from src/components/write/MarkdownEditor.tsx rename to src/components/write/WriteMarkdownEditor.tsx index 7be83567..1500b8b8 100644 --- a/src/components/write/MarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -28,6 +28,7 @@ export interface MarkdownEditorProps { onUpload: () => void; lastUploadedImage: string | null; } + type MarkdownEditorState = { shadow: boolean; addLink: { @@ -71,7 +72,6 @@ const MarkdownEditorBlock = styled.div` color: ${palette.gray8}; font-family: 'Fira Mono', 'Spoqa Han Sans', monospace; /* font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', */ - monospace; .cm-header { line-height: 2; color: ${palette.gray9}; @@ -121,7 +121,7 @@ const FooterWrapper = styled.div` width: 100%; z-index: ${zIndexes.WriteFooter}; `; -export default class MarkdownEditor extends React.Component< +export default class WriteMarkdownEditor extends React.Component< MarkdownEditorProps, MarkdownEditorState > { diff --git a/src/components/write/__tests__/MarkdownEditor.test.tsx b/src/components/write/__tests__/MarkdownEditor.test.tsx index 154a8419..31728fd9 100644 --- a/src/components/write/__tests__/MarkdownEditor.test.tsx +++ b/src/components/write/__tests__/MarkdownEditor.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import MarkdownEditor, { MarkdownEditorProps } from '../MarkdownEditor'; +import MarkdownEditor, { MarkdownEditorProps } from '../WriteMarkdownEditor'; describe('MarkdownEditor', () => { const setup = (props: Partial = {}) => { diff --git a/src/components/write/__tests__/__snapshots__/MarkdownEditor.test.tsx.snap b/src/components/write/__tests__/__snapshots__/MarkdownEditor.test.tsx.snap index d58468e0..0a1d0736 100644 --- a/src/components/write/__tests__/__snapshots__/MarkdownEditor.test.tsx.snap +++ b/src/components/write/__tests__/__snapshots__/MarkdownEditor.test.tsx.snap @@ -3,7 +3,7 @@ exports[`MarkdownEditor matches snapshot 1`] = `
= ({ username, urlSlug }) => { const [asc, toggleAsc] = useToggle(true); const [editing, toggleEditing] = useToggle(false); const [nextName, setNextName] = useState(''); + const [order, setOrder] = useState([]); + const [editSeries] = useMutation(EDIT_SERIES); + const user = useUser(); + const isOwnSeries = user && user.username === username; const { data } = useQuery(GET_SERIES, { variables: { @@ -28,14 +37,21 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { fetchPolicy: 'cache-and-network', }); - const onApply = useCallback(() => { - console.log(editing); + const onApply = useCallback(async () => { + await editSeries({ + variables: { + id: data!.series.id, + name: nextName, + series_order: order, + }, + }); toggleEditing(); - }, [editing, toggleEditing]); + }, [data, editSeries, nextName, order, toggleEditing]); useEffect(() => { if (!data || !data.series) return; setNextName(data.series.name); + setOrder(data.series.series_posts.map(sp => sp.id)); }, [data]); const onChangeNextName = useCallback( @@ -54,18 +70,23 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { editing={editing} onInput={onChangeNextName} > - + {isOwnSeries && ( + + )} {!editing && ( )} {editing ? ( - + ) : ( { + const { data, loading, error } = useQuery( + GET_USER_ABOUT, + { variables: { username } }, + ); + const user = useUser(); + const [edit, onToggleEdit] = useToggle(false); + + const own = (user && user.username === username) || false; + if (!data || !data.user) return null; + console.log(data); + + return edit ? null : ( + + ); +}; + +export default VelogAbout; diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index 81111527..cde4f8fc 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import MarkdownEditor from '../../components/write/MarkdownEditor'; +import MarkdownEditor from '../../components/write/WriteMarkdownEditor'; import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../modules'; import { diff --git a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap index e9050992..366cc0a4 100644 --- a/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap +++ b/src/containers/write/__tests__/__snapshots__/ActiveEditor.test.tsx.snap @@ -267,7 +267,7 @@ exports[`ActiveEditor matches snapshot 1`] = ` class="sc-jWBwVP boWCkI" >
= ({ match }) => { + ); }; diff --git a/src/pages/velog/VelogPage.tsx b/src/pages/velog/VelogPage.tsx index b9050a15..62bf5242 100644 --- a/src/pages/velog/VelogPage.tsx +++ b/src/pages/velog/VelogPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import VelogPageTemplate from '../../components/velog/VelogPageTemplate'; -import { RouteComponentProps, Route, Switch } from 'react-router'; +import { RouteComponentProps, Route, Switch, Router } from 'react-router'; import ConfigLoader from '../../containers/velog/ConfigLoader'; import PostPage from './PostPage'; import UserPage from './UserPage'; diff --git a/src/pages/velog/tabs/AboutTab.tsx b/src/pages/velog/tabs/AboutTab.tsx new file mode 100644 index 00000000..b0685fae --- /dev/null +++ b/src/pages/velog/tabs/AboutTab.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import VelogAbout from '../../../containers/velog/VelogAbout'; +import { RouteComponentProps } from 'react-router'; + +export interface AboutTabProps + extends RouteComponentProps<{ + username: string; + }> {} + +const AboutTab = ({ match }: AboutTabProps) => { + return ; +}; + +export default AboutTab; diff --git a/src/static/images/index.ts b/src/static/images/index.ts index bfa11230..8e482b76 100644 --- a/src/static/images/index.ts +++ b/src/static/images/index.ts @@ -2,3 +2,4 @@ export { default as userThumbnail } from './user-thumbnail.png'; export { default as plutoWelcome } from './pluto-welcome.png'; export { default as seriesThumbnail } from './series-thumbnail.svg'; export { default as emptyThumbnail } from './empty-thumbnail.svg'; +export { default as undrawEmpty } from './undraw_empty.svg'; diff --git a/src/static/images/undraw_empty.svg b/src/static/images/undraw_empty.svg new file mode 100644 index 00000000..0135d8f2 --- /dev/null +++ b/src/static/images/undraw_empty.svg @@ -0,0 +1 @@ +empty \ No newline at end of file From fa4f33eba8da0fc106ef380dbbb5b29b8696a971 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 7 Sep 2019 23:29:21 +0900 Subject: [PATCH 100/736] Implement about editing --- src/components/common/MarkdownEditor.tsx | 108 ++++++++++++++++++ .../{write => common}/atom-one-light.css | 0 src/components/velog/VelogAboutEdit.tsx | 26 +++++ .../velog/VelogAboutRightButton.tsx | 29 +++++ src/components/write/WriteMarkdownEditor.tsx | 4 +- .../MarkdownEditor.test.tsx.snap | 1 - src/containers/velog/VelogAbout.tsx | 48 ++++++-- src/lib/graphql/user.ts | 9 ++ 8 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 src/components/common/MarkdownEditor.tsx rename src/components/{write => common}/atom-one-light.css (100%) create mode 100644 src/components/velog/VelogAboutEdit.tsx create mode 100644 src/components/velog/VelogAboutRightButton.tsx diff --git a/src/components/common/MarkdownEditor.tsx b/src/components/common/MarkdownEditor.tsx new file mode 100644 index 00000000..4ae544c2 --- /dev/null +++ b/src/components/common/MarkdownEditor.tsx @@ -0,0 +1,108 @@ +import React, { + CSSProperties, + useRef, + useEffect, + useState, + useCallback, +} from 'react'; +import styled from 'styled-components'; +import CodeMirror, { EditorFromTextArea, Editor } from 'codemirror'; + +import 'codemirror/lib/codemirror.css'; +import './atom-one-light.css'; +import palette from '../../lib/styles/palette'; + +// conditional require later on +require('codemirror/mode/markdown/markdown'); +require('codemirror/addon/display/placeholder'); + +const MarkdownEditorBlock = styled.div` + .CodeMirror { + height: auto; + font-size: 1.125rem; + line-height: 1.5; + color: ${palette.gray8}; + font-family: 'Fira Mono', 'Spoqa Han Sans', monospace; + /* font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', */ + .cm-header { + line-height: 2; + color: ${palette.gray9}; + } + .cm-header-1 { + font-size: 2.5rem; + } + .cm-header-2 { + font-size: 2rem; + } + .cm-header-3 { + font-size: 1.5rem; + } + .cm-header-4, + .cm-header-5, + .cm-header-6 { + font-size: 1.3125rem; + } + .cm-strong, + .cm-em { + color: ${palette.gray9}; + } + .CodeMirror-placeholder { + color: ${palette.gray5}; + font-style: italic; + } + } +`; + +export interface MarkdownEditorProps { + style?: CSSProperties; + className?: string; + onChangeMarkdown: (markdown: string) => void; + initialMarkdown?: string; +} + +const MarkdownEditor = ({ + style, + className, + onChangeMarkdown, + initialMarkdown, +}: MarkdownEditorProps) => { + const textArea = useRef(null); + const codemirror = useRef(null); + + const onChange = useCallback( + (cm: Editor) => { + onChangeMarkdown(cm.getValue()); + }, + [onChangeMarkdown], + ); + + // initialize editor + useEffect(() => { + if (!textArea.current) return; + const cm = CodeMirror.fromTextArea(textArea.current, { + mode: 'markdown', + theme: 'one-light', + placeholder: '당신은 어떤 사람인가요? 당신에 대해서 알려주세요.', + lineWrapping: true, + }); + codemirror.current = cm; + cm.focus(); + cm.on('change', onChange); + + if (initialMarkdown) { + cm.setValue(initialMarkdown); + } + + return () => { + cm.toTextArea(); + }; + }, [initialMarkdown, onChange]); + + return ( + +