Skip to content

Commit 5c2df32

Browse files
authored
Merge pull request #501 from velopert/feature/turnstile
Feature/turnstile
2 parents 4cf0129 + f04ce88 commit 5c2df32

17 files changed

+308
-112
lines changed

public/index.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@
3535
window.dataLayer = window.dataLayer || [];
3636
function gtag(){dataLayer.push(arguments);}
3737
gtag('js', new Date());
38-
3938
gtag('config', 'G-8D0MD2S4PK');
4039
</script>
40+
<!-- Cloudflare (turnstile) -->
41+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onAppReady" defer></script>
4142

4243

4344
<title>React App</title>

src/components/base/HeaderLogo.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const HeaderLogo: React.FC<HeaderLogoProps> = ({
3737
<VelogIcon />
3838
</VelogLogoLink>
3939
<VLink to={velogPath} className="user-logo">
40-
{userLogo.title || createFallbackTitle(username)}
40+
<span>{userLogo.title || createFallbackTitle(username)}</span>
4141
</VLink>
4242
</HeaderLogoBlock>
4343
);
@@ -69,7 +69,7 @@ const HeaderLogoBlock = styled.div`
6969
7070
.user-logo {
7171
display: block;
72-
max-width: calc(100vw - 200px);
72+
max-width: calc(100vw - 250px);
7373
${ellipsis};
7474
}
7575
`;

src/components/write/PublishActionButtons.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import media from '../../lib/styles/media';
66
const PublishActionButtonsBlock = styled.div`
77
display: flex;
88
justify-content: flex-end;
9+
margin-top: 0.5rem;
910
${media.custom(767)} {
1011
margin-top: 2rem;
1112
}
@@ -15,12 +16,14 @@ export interface PublishActionButtonsProps {
1516
onCancel: () => void;
1617
onPublish: () => void;
1718
edit: boolean;
19+
isLoading: boolean;
1820
}
1921

2022
const PublishActionButtons: React.FC<PublishActionButtonsProps> = ({
2123
onCancel,
2224
onPublish,
2325
edit,
26+
isLoading,
2427
}) => {
2528
return (
2629
<PublishActionButtonsBlock>
@@ -32,7 +35,12 @@ const PublishActionButtons: React.FC<PublishActionButtonsProps> = ({
3235
>
3336
취소
3437
</Button>
35-
<Button size="large" data-testid="publish" onClick={onPublish}>
38+
<Button
39+
size="large"
40+
data-testid="publish"
41+
onClick={onPublish}
42+
disabled={isLoading}
43+
>
3644
{edit ? '수정하기' : '출간하기'}
3745
</Button>
3846
</PublishActionButtonsBlock>

src/components/write/PublishSeriesCreate.tsx

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, FormEvent } from 'react';
1+
import React, { useState, useEffect, FormEvent, useRef } from 'react';
22
import styled, { css, keyframes } from 'styled-components';
33
import { themedPalette } from '../../lib/styles/themes';
44
import OutsideClickHandler from 'react-outside-click-handler';
@@ -110,8 +110,8 @@ const PublishSeriesCreate: React.FC<PublishSeriesCreateProps> = ({
110110
urlSlug: '',
111111
});
112112
const [editing, setEditing] = useState<boolean>(false);
113-
114113
const [defaultUrlSlug, setDefaultUrlSlug] = useState('');
114+
const hideTimeoutId = useRef<NodeJS.Timeout | null>(null);
115115

116116
useEffect(() => {
117117
let timeoutId: ReturnType<typeof setTimeout> | null = null;
@@ -137,15 +137,33 @@ const PublishSeriesCreate: React.FC<PublishSeriesCreateProps> = ({
137137
setEditing(true);
138138
}, [form.urlSlug]);
139139

140+
useEffect(() => {
141+
return () => {
142+
if (hideTimeoutId.current) {
143+
clearTimeout(hideTimeoutId.current);
144+
}
145+
};
146+
}, [hideTimeoutId]);
147+
140148
const onHide = () => {
141149
setDisappear(true);
142-
setTimeout(() => {
150+
const timeout = setTimeout(() => {
143151
setOpen(false);
144152
setDisappear(false);
145153
setShowOpenBlock(false);
146154
}, 125);
155+
const timeoutId = timeout;
156+
hideTimeoutId.current = timeoutId;
147157
};
148158

159+
useEffect(() => {
160+
return () => {
161+
if (hideTimeoutId.current) {
162+
clearTimeout(hideTimeoutId.current);
163+
}
164+
};
165+
}, []);
166+
149167
const submit = (e: FormEvent) => {
150168
e.preventDefault();
151169
if (form.name.trim() === '') {

src/containers/write/ActiveEditor.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ const ActiveEditor: React.FC<ActiveEditorProps> = () => {
132132
}, [dispatch, lastPostHistory, post]);
133133

134134
if (
135+
id &&
135136
!newPost &&
136137
((!readPostForEdit.loading && post === null) ||
137138
(post && post.user.id !== userId))

src/containers/write/MarkdownEditorContainer.tsx

+33-17
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,21 @@ const MarkdownEditorContainer: React.FC<MarkdownEditorContainerProps> = () => {
6161
tags,
6262
} = useSelector((state: RootState) => state.write);
6363
const uncachedClient = useUncachedApolloClient();
64-
const [writePost] = useMutation<WritePostResponse>(WRITE_POST, {
65-
client: uncachedClient,
66-
});
64+
const [writePost, { loading: writePostLoading }] =
65+
useMutation<WritePostResponse>(WRITE_POST, {
66+
client: uncachedClient,
67+
});
6768

6869
const bodyRef = useRef(initialBody);
6970
const titleRef = useRef(title);
7071
const [createPostHistory] =
7172
useMutation<CreatePostHistoryResponse>(CREATE_POST_HISTORY);
72-
const [editPost] = useMutation<EditPostResult>(EDIT_POST, {
73-
client: uncachedClient,
74-
});
73+
const [editPost, { loading: editPostLoading }] = useMutation<EditPostResult>(
74+
EDIT_POST,
75+
{
76+
client: uncachedClient,
77+
},
78+
);
7579

7680
const [lastSavedData, setLastSavedData] = useState({
7781
title: initialTitle,
@@ -148,6 +152,7 @@ const MarkdownEditorContainer: React.FC<MarkdownEditorContainerProps> = () => {
148152

149153
const onTempSave = useCallback(
150154
async (notify?: boolean) => {
155+
if (writePostLoading || editPostLoading) return;
151156
if (!title || !markdown) {
152157
toast.error('제목 또는 내용이 비어있습니다.');
153158
return;
@@ -171,14 +176,17 @@ const MarkdownEditorContainer: React.FC<MarkdownEditorContainerProps> = () => {
171176
thumbnail: null,
172177
meta: {},
173178
series_id: null,
179+
token: null,
174180
},
175181
});
176-
if (!response || !response.data) return;
182+
183+
if (!response.data?.writePost) return;
177184
const { id } = response.data.writePost;
178185
dispatch(setWritePostId(id));
179186
history.replace(`/write?id=${id}`);
180187
notifySuccess();
181188
}
189+
182190
// tempsaving unreleased post:
183191
if (isTemp) {
184192
await editPost({
@@ -194,6 +202,7 @@ const MarkdownEditorContainer: React.FC<MarkdownEditorContainerProps> = () => {
194202
meta: {},
195203
series_id: null,
196204
tags,
205+
token: null,
197206
},
198207
});
199208
notifySuccess();
@@ -205,19 +214,22 @@ const MarkdownEditorContainer: React.FC<MarkdownEditorContainerProps> = () => {
205214
if (shallowEqual(lastSavedData, { title, body: markdown })) {
206215
return;
207216
}
208-
await createPostHistory({
209-
variables: {
210-
post_id: postId,
211-
title,
212-
body: markdown,
213-
is_markdown: true,
214-
},
215-
});
217+
218+
if (postId) {
219+
await createPostHistory({
220+
variables: {
221+
post_id: postId,
222+
title,
223+
body: markdown,
224+
is_markdown: true,
225+
},
226+
});
227+
}
228+
216229
setLastSavedData({
217230
title,
218231
body: markdown,
219232
});
220-
notifySuccess();
221233
},
222234
[
223235
createPostHistory,
@@ -231,6 +243,8 @@ const MarkdownEditorContainer: React.FC<MarkdownEditorContainerProps> = () => {
231243
tags,
232244
title,
233245
writePost,
246+
writePostLoading,
247+
editPostLoading,
234248
],
235249
);
236250

@@ -259,9 +273,11 @@ const MarkdownEditorContainer: React.FC<MarkdownEditorContainerProps> = () => {
259273
thumbnail: null,
260274
meta: {},
261275
series_id: null,
276+
token: null,
262277
},
263278
});
264-
if (!response || !response.data) return;
279+
280+
if (!response.data?.writePost) return;
265281
id = response.data.writePost.id;
266282
dispatch(setWritePostId(id));
267283
history.replace(`/write?id=${id}`);

src/containers/write/PublishActionButtonsContainer.tsx

+61-18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { setHeadingId } from '../../lib/heading';
1717
import { useHistory } from 'react-router';
1818
import { toast } from 'react-toastify';
1919
import { useUncachedApolloClient } from '../../lib/graphql/UncachedApolloContext';
20+
import useTurnstile from '../../lib/hooks/useTurnstile';
2021

2122
type PublishActionButtonsContainerProps = {};
2223

@@ -25,6 +26,10 @@ const PublishActionButtonsContainer: React.FC<
2526
> = () => {
2627
const history = useHistory();
2728
const client = useApolloClient();
29+
const user = useSelector((state: RootState) => state.core.user);
30+
31+
const isTurnstileEnabled = !!user && !user.is_trusted;
32+
const { isLoading, token } = useTurnstile(isTurnstileEnabled);
2833

2934
const options = useSelector((state: RootState) =>
3035
pick(
@@ -54,12 +59,16 @@ const PublishActionButtonsContainer: React.FC<
5459

5560
const uncachedClient = useUncachedApolloClient();
5661

57-
const [writePost] = useMutation<WritePostResponse>(WRITE_POST, {
58-
client: uncachedClient,
59-
});
60-
const [editPost] = useMutation<EditPostResult>(EDIT_POST, {
61-
client: uncachedClient,
62-
});
62+
const [writePost, { loading: writePostLoading }] =
63+
useMutation<WritePostResponse>(WRITE_POST, {
64+
client: uncachedClient,
65+
});
66+
const [editPost, { loading: editPostLoading }] = useMutation<EditPostResult>(
67+
EDIT_POST,
68+
{
69+
client: uncachedClient,
70+
},
71+
);
6372

6473
const variables = {
6574
title: options.title,
@@ -77,44 +86,78 @@ const PublishActionButtonsContainer: React.FC<
7786
short_description: options.description,
7887
},
7988
series_id: safe(() => options.selectedSeries!.id),
89+
token,
8090
};
8191

8292
const onPublish = async () => {
93+
if (writePostLoading) {
94+
toast.info('포스트 작성 중입니다.');
95+
return;
96+
}
97+
8398
if (options.title.trim() === '') {
8499
toast.error('제목이 비어있습니다.');
85100
return;
86101
}
102+
87103
try {
88104
const response = await writePost({
89105
variables: variables,
90106
});
91-
if (!response || !response.data) return;
107+
108+
if (!response.data?.writePost) {
109+
toast.error('포스트 작성 실패');
110+
return;
111+
}
112+
92113
const { user, url_slug } = response.data.writePost;
93114
await client.resetStore();
94115
history.push(`/@${user.username}/${url_slug}`);
95-
} catch (e) {
116+
} catch (error) {
117+
console.log('write post failed', error);
96118
toast.error('포스트 작성 실패');
97119
}
98120
};
99121

100122
const onEdit = async () => {
101-
const response = await editPost({
102-
variables: {
103-
id: options.postId,
104-
...variables,
105-
},
106-
});
107-
if (!response || !response.data) return;
108-
const { user, url_slug } = response.data.editPost;
109-
await client.resetStore();
110-
history.push(`/@${user.username}/${url_slug}`);
123+
if (editPostLoading) {
124+
toast.info('포스트 수정 중입니다.');
125+
return;
126+
}
127+
128+
if (options.title.trim() === '') {
129+
toast.error('제목이 비어있습니다.');
130+
return;
131+
}
132+
133+
try {
134+
const response = await editPost({
135+
variables: {
136+
id: options.postId,
137+
...variables,
138+
},
139+
});
140+
141+
if (!response.data?.editPost) {
142+
toast.error('포스트 수정 실패');
143+
return;
144+
}
145+
146+
const { user, url_slug } = response.data.editPost;
147+
await client.resetStore();
148+
history.push(`/@${user.username}/${url_slug}`);
149+
} catch (error) {
150+
console.log('edit post failed', error);
151+
toast.error('포스트 수정 실패');
152+
}
111153
};
112154

113155
return (
114156
<PublishActionButtons
115157
onCancel={onCancel}
116158
onPublish={options.postId ? onEdit : onPublish}
117159
edit={!!options.postId && !options.isTemp}
160+
isLoading={isLoading}
118161
/>
119162
);
120163
};

0 commit comments

Comments
 (0)