From 1006d7a0776008e549d4ca8fcacf2fbc110e9c28 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 02:04:32 +0900 Subject: [PATCH 001/449] Sanitize html and allow embeding --- package.json | 2 + src/components/common/MarkdownRender.tsx | 75 +++++++++++++++++-- src/components/write/MarkdownPreview.tsx | 2 +- src/components/write/WriteMarkdownEditor.tsx | 9 ++- src/lib/remark/embedPlugin.ts | 8 +- src/lib/styles/prismThemes.ts | 6 +- yarn.lock | 78 +++++++++++++++++++- 7 files changed, 163 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 95a5c64d..1b7ea214 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/react-router-dom": "^5.1.3", "@types/react-textarea-autosize": "^4.3.5", "@types/react-toastify": "^4.1.0", + "@types/sanitize-html": "^1.20.2", "@types/styled-components": "^4.4.1", "@types/throttle-debounce": "^2.1.0", "@typescript-eslint/eslint-plugin": "^2.8.0", @@ -128,6 +129,7 @@ "remark-slug": "^5.1.2", "resolve": "1.12.2", "resolve-url-loader": "3.1.1", + "sanitize-html": "^1.21.1", "sass-loader": "8.0.0", "semver": "6.3.0", "serverless-webpack": "^5.3.1", diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index 81232011..facf7bc0 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -12,11 +12,13 @@ import { loadScript, ssrEnabled } from '../../lib/utils'; import media from '../../lib/styles/media'; import parse from 'html-react-parser'; import { throttle } from 'throttle-debounce'; +import sanitize from 'sanitize-html'; export interface MarkdownRenderProps { markdown: string; codeTheme?: string; onConvertFinish?: (html: string) => any; + editing?: boolean; } const MarkdownRenderBlock = styled.div` @@ -59,19 +61,21 @@ const MarkdownRenderBlock = styled.div` margin-bottom: 1.5rem; } - .youtube { + iframe { width: 768px; height: 430px; max-width: 100%; background: black; display: block; margin: auto; + border: none; + border-radius: 4px; + overflow: hidden; } .twitter-wrapper { display: flex; justify-content: center; - height: 580px; align-items: center; blockquote { border-left: none; @@ -79,6 +83,62 @@ const MarkdownRenderBlock = styled.div` } `; +function filter(html: string) { + return sanitize(html, { + allowedTags: [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'p', + 'a', + 'ul', + 'ol', + 'nl', + 'li', + 'b', + 'i', + 'strong', + 'em', + 'strike', + 'code', + 'hr', + 'br', + 'div', + 'table', + 'thead', + 'caption', + 'tbody', + 'tr', + 'th', + 'td', + 'pre', + 'iframe', + 'span', + ], + allowedAttributes: { + a: ['href', 'name', 'target'], + img: ['src'], + iframe: ['src', 'allow', 'allowfullscreen', 'scrolling', 'class'], + '*': ['class', 'id'], + }, + allowedStyles: { + '*': { + // Match HEX and RGB + color: [ + /^#(0x)?[0-9a-f]+$/i, + /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/, + ], + 'text-align': [/^left$/, /^right$/, /^center$/], + }, + }, + allowedIframeHostnames: ['www.youtube.com', 'codesandbox.io', 'codepen.io'], + }); +} + const { useState, useEffect } = React; type RenderedElement = @@ -91,12 +151,15 @@ const MarkdownRender: React.FC = ({ markdown, codeTheme = 'atom-one-light', onConvertFinish, + editing, }) => { const initialHtml = ssrEnabled ? remark() .use(breaks) .use(prismPlugin) - .use(htmlPlugin) + .use(htmlPlugin, { + sanitize: true, + }) .use(embedPlugin) .use(slug) .processSync(markdown) @@ -104,7 +167,7 @@ const MarkdownRender: React.FC = ({ : ''; const [element, setElement] = useState( - ssrEnabled ? parse(initialHtml) : null, + ssrEnabled ? parse(filter(initialHtml)) : null, ); const applyElement = React.useMemo(() => { @@ -131,11 +194,11 @@ const MarkdownRender: React.FC = ({ // if (window && (window as any).twttr) return; loadScript('https://platform.twitter.com/widgets.js'); } - const el = parse(html); + const el = parse(editing ? html : filter(html)); applyElement(el); }); - }, [applyElement, markdown, onConvertFinish]); + }, [applyElement, editing, markdown, onConvertFinish]); return ( diff --git a/src/components/write/MarkdownPreview.tsx b/src/components/write/MarkdownPreview.tsx index 73369929..738fe9d4 100644 --- a/src/components/write/MarkdownPreview.tsx +++ b/src/components/write/MarkdownPreview.tsx @@ -31,7 +31,7 @@ const MarkdownPreview: React.FC = ({ return ( {title} - + ); }; diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index ac692a77..921c8d7c 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -170,11 +170,18 @@ const checker = { return pathMatch[1]; }, codesandbox: (text: string) => { - const regex = /^`, + ``, twitter: (code: string) => ``, codesandbox: (code: string) => - ``, + ``, + codepen: (code: string) => + ``, }; type ConverterKey = keyof typeof converters; diff --git a/src/lib/styles/prismThemes.ts b/src/lib/styles/prismThemes.ts index 2edf7d90..f5777de0 100644 --- a/src/lib/styles/prismThemes.ts +++ b/src/lib/styles/prismThemes.ts @@ -316,7 +316,7 @@ const prismThemes = { code[class*='language-'], pre[class*='language-'] { color: #f8f8f2; - text-shadow: 0 1px rgba(0, 0, 0, 0.3); // direction: ltr; + text-shadow: 0 1px rgba(0, 0, 0, 0.3); } :not(pre) > code[class*='language-'], @@ -326,7 +326,7 @@ const prismThemes = { pre { color: #f8f8f2; - text-shadow: 0 1px rgba(0, 0, 0, 0.3); // direction: ltr; + text-shadow: 0 1px rgba(0, 0, 0, 0.3); background: #272822; } @@ -429,7 +429,7 @@ http://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascr code[class*='language-'], pre[class*='language-'] { color: #ccc; - background: rgb(40, 41, 54); // text-shadow: none; + background: rgb(40, 41, 54); } pre[class*='language-']::-moz-selection, diff --git a/yarn.lock b/yarn.lock index 39cd27b6..95b3f3d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1501,11 +1501,18 @@ dependencies: date-fns "*" -"@types/domhandler@2.4.1": +"@types/domhandler@*", "@types/domhandler@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/domhandler/-/domhandler-2.4.1.tgz#7b3b347f7762180fbcb1ece1ce3dd0ebbb8c64cf" integrity sha512-cfBw6q6tT5sa1gSPFSRKzF/xxYrrmeiut7E0TxNBObiLSBTuFEHibcfEe3waQPEDbqBsq+ql/TOniw65EyDFMA== +"@types/domutils@*": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@types/domutils/-/domutils-1.7.2.tgz#89422e579c165994ad5c09ce90325da596cc105d" + integrity sha512-Nnwy1Ztwq42SSNSZSh9EXBJGrOZPR+PQ2sRT4VZy8hnsFXfCil7YlKO2hd2360HyrtFz2qwnKQ13ENrgXNxJbw== + dependencies: + "@types/domhandler" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1560,6 +1567,15 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/htmlparser2@*": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@types/htmlparser2/-/htmlparser2-3.10.1.tgz#1e65ba81401d53f425c1e2ba5a3d05c90ab742c7" + integrity sha512-fCxmHS4ryCUCfV9+CJZY1UjkbR+6Al/EQdX5Jh03qBj9gdlPG5q+7uNoDgE/ZNXb3XNWSAQgqKIWnbRCbOyyWA== + dependencies: + "@types/domhandler" "*" + "@types/domutils" "*" + "@types/node" "*" + "@types/http-assert@*": version "1.5.1" resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" @@ -1834,6 +1850,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/sanitize-html@^1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.20.2.tgz#59777f79f015321334e3a9f28882f58c0a0d42b8" + integrity sha512-SrefiiBebGIhxEFkpbbYOwO1S6+zQLWAC4s4tipchlHq1aO9bp0xiapM7Zm0ml20MF+3OePWYdksB1xtneKPxg== + dependencies: + "@types/htmlparser2" "*" + "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -2514,7 +2537,7 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" -array-uniq@^1.0.1: +array-uniq@^1.0.1, array-uniq@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= @@ -6362,7 +6385,7 @@ html-webpack-plugin@4.0.0-beta.5: tapable "^1.1.0" util.promisify "1.0.0" -htmlparser2@3.10.1, htmlparser2@^3.3.0: +htmlparser2@3.10.1, htmlparser2@^3.10.0, htmlparser2@^3.3.0: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -8018,21 +8041,46 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.mergewith@^4.6.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -11337,6 +11385,22 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" +sanitize-html@^1.21.1: + version "1.21.1" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.21.1.tgz#1647d15c0c672901aa41eac1b86d0c38146d30ce" + integrity sha512-W6enXSVphVaVbmVbzVngBthR5f5sMmhq3EfPfBlzBzp2WnX8Rnk7NGpP7KmHUc0Y3MVk9tv/+CbpdHchX9ai7g== + dependencies: + chalk "^2.4.1" + htmlparser2 "^3.10.0" + lodash.clonedeep "^4.5.0" + lodash.escaperegexp "^4.1.2" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.mergewith "^4.6.1" + postcss "^7.0.5" + srcset "^1.0.0" + xtend "^4.0.1" + sanitize.css@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-10.0.0.tgz#b5cb2547e96d8629a60947544665243b1dc3657a" @@ -11834,6 +11898,14 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +srcset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" + integrity sha1-pWad4StC87HV6D7QPHEEb8SPQe8= + dependencies: + array-uniq "^1.0.2" + number-is-nan "^1.0.0" + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" From d1700a12a5d3eb7d67e9380fdb8a95899105046a Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 03:07:48 +0900 Subject: [PATCH 002/449] Set styles for blockquote - fix #16 --- src/components/common/Typography.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/common/Typography.tsx b/src/components/common/Typography.tsx index a53df533..df73dc61 100644 --- a/src/components/common/Typography.tsx +++ b/src/components/common/Typography.tsx @@ -5,7 +5,7 @@ import media from '../../lib/styles/media'; const TypographyBlock = styled.div` font-size: 1.125rem; - color: ${palette.gray7}; + color: ${palette.gray8}; line-height: 1.85; letter-spacing: -0.02em; word-break: keep-all; @@ -121,6 +121,26 @@ const TypographyBlock = styled.div` margin-top: 2rem; } } + + blockquote { + margin-top: 2rem; + margin-bottom: 2rem; + border-left: 4px solid ${palette.teal5}; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + background: ${palette.gray0}; + margin-left: 0; + margin-right: 0; + padding: 1rem; + padding-left: 2rem; + color: ${palette.gray9}; + p:first-child { + margin-top: 0; + } + p:last-child { + margin-bottom: 0; + } + } `; export interface TypographyProps {} From 235be29e5b241713169d877d7d93f0e96be92b5a Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 03:13:41 +0900 Subject: [PATCH 003/449] Use div instead of p in PostCommentItem - Fix #57 --- src/components/post/PostCommentItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index ec476f20..0c159b6c 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -91,7 +91,7 @@ const CommentHead = styled.div` } `; -const CommentText = styled.p<{ deleted: boolean }>` +const CommentText = styled.div<{ deleted: boolean }>` h1, h2 { font-size: 1.75rem; From 8f75ac86a26c3b587e3903c5cf4f7ab386494d34 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 03:20:19 +0900 Subject: [PATCH 004/449] Update style for blockquote --- src/components/common/Typography.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/common/Typography.tsx b/src/components/common/Typography.tsx index df73dc61..54db296d 100644 --- a/src/components/common/Typography.tsx +++ b/src/components/common/Typography.tsx @@ -134,10 +134,13 @@ const TypographyBlock = styled.div` padding: 1rem; padding-left: 2rem; color: ${palette.gray9}; - p:first-child { + ul, ol { + padding-left: 1rem; + } + *:first-child { margin-top: 0; } - p:last-child { + *:last-child { margin-bottom: 0; } } From a1aa5e7719ec02881ca1ef07216c6acde5f36a6c Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 03:24:34 +0900 Subject: [PATCH 005/449] Fixes unconsistent margin at postviewer --- src/components/velog/VelogResponsive.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/velog/VelogResponsive.tsx b/src/components/velog/VelogResponsive.tsx index f7f2a29e..896c69b9 100644 --- a/src/components/velog/VelogResponsive.tsx +++ b/src/components/velog/VelogResponsive.tsx @@ -4,7 +4,8 @@ import media from '../../lib/styles/media'; const VelogResponsiveBlock = styled.div` width: 768px; - margin: 0 auto; + margin-left: auto; + margin-right: auto; ${media.small} { width: 100%; } From 098932911f48cffaed3964e4e7b4d3c698d013bc Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 03:27:27 +0900 Subject: [PATCH 006/449] Fix welcome overflow --- src/components/auth/AuthModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/auth/AuthModal.tsx b/src/components/auth/AuthModal.tsx index 7749d419..3c7128e6 100644 --- a/src/components/auth/AuthModal.tsx +++ b/src/components/auth/AuthModal.tsx @@ -61,7 +61,6 @@ const AuthModalBlock = styled.div<{ visible: boolean }>` color: ${palette.gray7}; text-align: center; font-weight: 600; - font-size: 2rem; } } .white-block { From 19bbdfc156deac027269781ee93aaf332f1d0099 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 03:44:17 +0900 Subject: [PATCH 007/449] Finally fixes #52 --- src/components/write/WriteMarkdownEditor.tsx | 1 + src/containers/write/MarkdownEditorContainer.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index 921c8d7c..2fa4431b 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -367,6 +367,7 @@ export default class WriteMarkdownEditor extends React.Component< const { lastUploadedImage, initialBody } = this.props; if (initialBody !== prevProps.initialBody) { if (!this.codemirror) return; + if (this.codemirror.getValue() === this.props.initialBody) return; this.codemirror.setValue(this.props.initialBody); } if ( diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index 49cbdc31..f81b101c 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -249,7 +249,7 @@ const MarkdownEditorContainer: React.FC = () => { } }, [title, postId, onTempSave, lastSavedData, markdown]); - useSaveHotKey(onTempSave); + useSaveHotKey(() => onTempSave(true)); return ( <> From b0754e64131eb617d57f2f4a36121aacd2b240ea Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 04:01:53 +0900 Subject: [PATCH 008/449] Fix #67 --- src/components/write/AddLink.tsx | 7 +++++-- src/components/write/WriteMarkdownEditor.tsx | 22 +++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/components/write/AddLink.tsx b/src/components/write/AddLink.tsx index 50dbf0e4..5a4bc861 100644 --- a/src/components/write/AddLink.tsx +++ b/src/components/write/AddLink.tsx @@ -55,7 +55,8 @@ const AddLinkBlock = styled.div` interface AddLinkProps { left: number; - top: number; + top: number | null; + bottom: number | null; stickToRight?: boolean; onConfirm: (link: string) => void; onClose: () => void; @@ -68,6 +69,7 @@ const { useCallback, useRef, useEffect } = React; const AddLink: React.FC = ({ left, top, + bottom, stickToRight, onConfirm, onClose, @@ -92,7 +94,8 @@ const AddLink: React.FC = ({ diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index 2fa4431b..041aaddc 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -34,7 +34,8 @@ export interface MarkdownEditorProps { type MarkdownEditorState = { shadow: boolean; addLink: { - top: number; + top: number | null; + bottom: number | null; left: number; visible: boolean; stickToRight: boolean; @@ -235,6 +236,7 @@ export default class WriteMarkdownEditor extends React.Component< shadow: false, addLink: { top: 0, + bottom: 0, left: 0, visible: false, stickToRight: false, @@ -404,15 +406,20 @@ export default class WriteMarkdownEditor extends React.Component< const cursorPos = this.codemirror.cursorCoords(cursor); if (!this.block.current) return; const stickToRight = cursorPos.left > this.block.current.clientWidth - 341; - + const calculatedTop = + this.block.current.scrollTop + + cursorPos.top + + this.codemirror.defaultTextHeight() / 2 + + 1; + + const isAtBottom = calculatedTop + 173 > this.block.current?.clientHeight; + const pos = isAtBottom + ? { top: null, bottom: 64 } + : { top: calculatedTop, bottom: null }; this.setState({ addLink: { visible: true, - top: - this.block.current.scrollTop + - cursorPos.top + - this.codemirror.defaultTextHeight() / 2 + - 1, + ...pos, left: cursorPos.left, stickToRight, }, @@ -838,6 +845,7 @@ ${selected} defaultValue="" left={addLink.left} top={addLink.top} + bottom={addLink.bottom} stickToRight={addLink.stickToRight} onConfirm={this.handleConfirmAddLink} onClose={this.handleCancelAddLink} From dd3db26512d91aafed362de05760c279a36ca5f1 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 04:04:06 +0900 Subject: [PATCH 009/449] Fix #54, #61 --- src/components/post/PostCommentItem.tsx | 1 + src/components/setting/SettingUserProfile.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/post/PostCommentItem.tsx b/src/components/post/PostCommentItem.tsx index 0c159b6c..e5e412ed 100644 --- a/src/components/post/PostCommentItem.tsx +++ b/src/components/post/PostCommentItem.tsx @@ -34,6 +34,7 @@ const CommentHead = styled.div` height: 3.375rem; display: block; border-radius: 50%; + object-fit: cover; ${media.small} { width: 2.5rem; height: 2.5rem; diff --git a/src/components/setting/SettingUserProfile.tsx b/src/components/setting/SettingUserProfile.tsx index 7d4cef97..28aff9a2 100644 --- a/src/components/setting/SettingUserProfile.tsx +++ b/src/components/setting/SettingUserProfile.tsx @@ -97,6 +97,7 @@ const Section = styled.section` display: flex; flex-direction: column; img { + object-fit: cover; width: 8rem; height: 8rem; border-radius: 50%; From 8de7cb5d0b13a1f1832c6a24220be5b8c5b317b8 Mon Sep 17 00:00:00 2001 From: velopert Date: Sat, 15 Feb 2020 16:57:57 +0900 Subject: [PATCH 010/449] Hotfix: allow img tag - Fix #69 --- src/components/common/MarkdownRender.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index facf7bc0..ffdc258f 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -118,6 +118,7 @@ function filter(html: string) { 'pre', 'iframe', 'span', + 'img', ], allowedAttributes: { a: ['href', 'name', 'target'], From ec821386c00d88a8f282d8843be86f2ace7e0e3e Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 16 Feb 2020 19:16:09 +0900 Subject: [PATCH 011/449] Fix #52 Post was initialized every time when user tempsaves the post. Wrong initialization happens when user types something in the middle of tempsave process. Fixed by setting a mutable `initialized` value --- src/containers/write/ActiveEditor.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/containers/write/ActiveEditor.tsx b/src/containers/write/ActiveEditor.tsx index 5a98a264..65590881 100644 --- a/src/containers/write/ActiveEditor.tsx +++ b/src/containers/write/ActiveEditor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { RootState } from '../../modules'; import { @@ -31,6 +31,7 @@ const ActiveEditor: React.FC = () => { const mode = useSelector((state: RootState) => state.write.mode); const postId = useSelector((state: RootState) => state.write.postId); const [askLoadTemp, setAskLoadTemp] = useState(false); + const initialized = useRef(false); const dispatch = useDispatch(); const location = useLocation(); @@ -75,6 +76,7 @@ const ActiveEditor: React.FC = () => { const post = safe(() => readPostForEdit.data!.post); useEffect(() => { if (!post) return; + if (initialized.current) return; dispatch( prepareEdit({ id: post.id, @@ -91,6 +93,7 @@ const ActiveEditor: React.FC = () => { }), ); dispatch(setInitialBody(post.body)); + initialized.current = true; }, [dispatch, post]); const lastPostHistory = safe(() => getLastPostHistory.data!.lastPostHistory); @@ -105,6 +108,7 @@ const ActiveEditor: React.FC = () => { useEffect(() => { if (!lastPostHistory) return; if (!post) return; + const equals = shallowEqual( { title: lastPostHistory.title, From 8a9d5cb3541dd4e4dddf3e1a61f779db41fcbfba Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 16 Feb 2020 19:18:24 +0900 Subject: [PATCH 012/449] Fix #71 --- src/components/common/MarkdownRender.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index ffdc258f..6bb0a39e 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -77,9 +77,9 @@ const MarkdownRenderBlock = styled.div` display: flex; justify-content: center; align-items: center; - blockquote { - border-left: none; - } + border-left: none; + background: none; + padding: none; } `; From 28024c9bd06d915b75298468209e0981c6a51b59 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 16 Feb 2020 21:19:25 +0900 Subject: [PATCH 013/449] Set default background to SeriesButton --- src/components/write/PublishSeriesSection.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/write/PublishSeriesSection.tsx b/src/components/write/PublishSeriesSection.tsx index 770d73ad..d4824d87 100644 --- a/src/components/write/PublishSeriesSection.tsx +++ b/src/components/write/PublishSeriesSection.tsx @@ -8,6 +8,7 @@ import { ellipsis } from '../../lib/styles/utils'; const PublishSeriesSectionBlock = styled(PublishSection)``; const SeriesButton = styled.button` + background: white; height: 3rem; width: 100%; border-radius: 4px; From 6fb7fd444e9f6be70554db637c83ba6a1b7e843c Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 17 Feb 2020 16:53:32 +0900 Subject: [PATCH 014/449] Fix suspected bug for linkedpost crash --- src/components/common/MarkdownRender.tsx | 44 +++++++++++++++--------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index 6bb0a39e..2da91ba2 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -154,23 +154,23 @@ const MarkdownRender: React.FC = ({ onConvertFinish, editing, }) => { - const initialHtml = ssrEnabled - ? remark() - .use(breaks) - .use(prismPlugin) - .use(htmlPlugin, { - sanitize: true, - }) - .use(embedPlugin) - .use(slug) - .processSync(markdown) - .toString() - : ''; - - const [element, setElement] = useState( - ssrEnabled ? parse(filter(initialHtml)) : null, + const [html, setHtml] = useState( + ssrEnabled + ? remark() + .use(breaks) + .use(prismPlugin) + .use(htmlPlugin, { + sanitize: true, + }) + .use(embedPlugin) + .use(slug) + .processSync(markdown) + .toString() + : '', ); + const [element, setElement] = useState(null); + const applyElement = React.useMemo(() => { return throttle(250, (el: any) => { setElement(el); @@ -195,6 +195,12 @@ const MarkdownRender: React.FC = ({ // if (window && (window as any).twttr) return; loadScript('https://platform.twitter.com/widgets.js'); } + + if (!editing) { + setHtml(html); + return; + } + const el = parse(editing ? html : filter(html)); applyElement(el); @@ -203,7 +209,13 @@ const MarkdownRender: React.FC = ({ return ( - {element} + {editing ? ( + + {element} + + ) : ( + + )} ); }; From 0b0a24d3ed2d786cb3e87fedb878ecf15a3804e8 Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 17 Feb 2020 23:39:55 +0900 Subject: [PATCH 015/449] Fix render crash on write page --- src/components/write/Toolbar.tsx | 2 -- src/components/write/WriteMarkdownEditor.tsx | 33 +++----------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/src/components/write/Toolbar.tsx b/src/components/write/Toolbar.tsx index 32efadfd..3f7b4a51 100644 --- a/src/components/write/Toolbar.tsx +++ b/src/components/write/Toolbar.tsx @@ -21,12 +21,10 @@ const ToolbarBlock = styled.div<{ forMarkdown: boolean; }>` width: 100%; - position: sticky; top: 0; display: flex; align-items: center; margin-bottom: 1rem; - position: sticky; width: 100%; background: white; z-index: ${zIndexes.Toolbar}; diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index 041aaddc..5d274e44 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -32,7 +32,6 @@ export interface MarkdownEditorProps { } type MarkdownEditorState = { - shadow: boolean; addLink: { top: number | null; bottom: number | null; @@ -208,7 +207,7 @@ function WriterHead({ children: React.ReactNode; }) { const screenHeight = - (typeof window !== 'undefined' && window.screen.height) || 0; + (typeof window !== 'undefined' && window.screen.height) || 508; const springStyle = useSpring({ maxHeight: hide ? 0 : screenHeight * 0.5, @@ -233,7 +232,6 @@ export default class WriteMarkdownEditor extends React.Component< editorElement = React.createRef(); toolbarTop = 0; state = { - shadow: false, addLink: { top: 0, bottom: 0, @@ -333,25 +331,6 @@ export default class WriteMarkdownEditor extends React.Component< } }; - handleScroll = (e: React.UIEvent) => { - const { shadow } = this.state; - const nextShadow = e.currentTarget.scrollTop > this.toolbarTop; - // console.log(e.currentTarget.scrollTop, this.toolbarTop); - if (shadow !== nextShadow) { - this.setState({ - shadow: nextShadow, - }); - } - if (this.block.current) { - const { clientWidth } = this.block.current; - if (clientWidth !== this.state.clientWidth) { - this.setState({ - clientWidth, - }); - } - } - }; - handleResize = () => { if (this.block.current) { this.setState({ @@ -810,14 +789,10 @@ ${selected} }; public render() { - const { shadow, addLink, clientWidth } = this.state; + const { addLink, clientWidth } = this.state; const { title, tagInput, footer } = this.props; return ( - +
{ssrEnabled ? ( @@ -833,7 +808,7 @@ ${selected} {tagInput} Date: Tue, 18 Feb 2020 00:44:48 +0900 Subject: [PATCH 016/449] Fix syntax highlight it was disabled by mistake --- src/components/common/MarkdownRender.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index 2da91ba2..00d5de7d 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -214,7 +214,10 @@ const MarkdownRender: React.FC = ({ {element} ) : ( - + )} ); From e1ecd8c843fc4202d76f977f44c0686fb857c060 Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 18 Feb 2020 01:00:07 +0900 Subject: [PATCH 017/449] Implement user tag view (only for desktop for now) Mobile view will be implemented tomorrow. #59 --- src/components/search/SearchInput.tsx | 5 + src/components/velog/SideArea.tsx | 33 ++++++ src/components/velog/UserTagVerticalList.tsx | 102 +++++++++++++++++++ src/components/velog/UserTags.tsx | 27 +++++ src/components/velog/hooks/useUserTags.ts | 17 ++++ src/components/write/Toolbar.tsx | 2 - src/containers/velog/UserPosts.tsx | 7 +- src/lib/graphql/tags.ts | 22 ++++ src/pages/velog/UserPage.tsx | 18 +++- src/pages/velog/tabs/UserPostsTab.tsx | 14 ++- 10 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 src/components/velog/SideArea.tsx create mode 100644 src/components/velog/UserTagVerticalList.tsx create mode 100644 src/components/velog/UserTags.tsx create mode 100644 src/components/velog/hooks/useUserTags.ts diff --git a/src/components/search/SearchInput.tsx b/src/components/search/SearchInput.tsx index f5f54eb3..da3a4b07 100644 --- a/src/components/search/SearchInput.tsx +++ b/src/components/search/SearchInput.tsx @@ -104,6 +104,7 @@ function SearchInput({ }: SearchInputProps) { const [focus, toggleFocus] = useToggle(false); const [value, onChange] = useInput(initial); + const mounted = useRef(false); const inputRef = useRef(null); @@ -124,6 +125,10 @@ function SearchInput({ }; useEffect(() => { + if (!mounted.current) { + mounted.current = true; + return; + } if (searchAsYouType) { debouncedSearch(value); } diff --git a/src/components/velog/SideArea.tsx b/src/components/velog/SideArea.tsx new file mode 100644 index 00000000..fa865122 --- /dev/null +++ b/src/components/velog/SideArea.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import styled from 'styled-components'; +import media from '../../lib/styles/media'; +import palette from '../../lib/styles/palette'; + +export type SideAreaProps = { + children: React.ReactNode; +}; + +function SideArea({ children }: SideAreaProps) { + return ( + + {children} + + ); +} + +const Wrapper = styled.div` + position: relative; +`; + +const Block = styled.div` + position: absolute; + margin-top: 4.25rem; + width: 11.5rem; + left: -13.5rem; + + ${media.large} { + display: none; + } +`; + +export default SideArea; diff --git a/src/components/velog/UserTagVerticalList.tsx b/src/components/velog/UserTagVerticalList.tsx new file mode 100644 index 00000000..2d1dfedc --- /dev/null +++ b/src/components/velog/UserTagVerticalList.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import styled from 'styled-components'; +import palette from '../../lib/styles/palette'; +import SideArea from './SideArea'; +import { Tag } from '../../lib/graphql/tags'; +import { Link } from 'react-router-dom'; +import { escapeForUrl } from '../../lib/utils'; + +export type UserTagVerticalListProps = { + active: string | null; + tags: Tag[]; + postsCount: number; + username: string; +}; + +function UserTagVerticalList({ + tags, + active, + postsCount, + username, +}: UserTagVerticalListProps) { + return ( + + +
태그 목록
+
    + + 전체보기 + ({postsCount}) + + {tags.map(tag => ( + + + {tag.name} + + ({tag.posts_count}) + + ))} +
+
+
+ ); +} + +const Block = styled.div` + .title { + font-size: 1rem; + line-height: 1.5; + padding-bottom: 0.5rem; + border-bottom: 1px solid ${palette.gray5}; + margin-bottom: 1rem; + color: ${palette.gray7}; + font-weight: bold; + } + ul { + list-style: none; + /* margin-left: 0; */ + padding-left: 0; + } +`; + +const ListItem = styled.li<{ active?: boolean }>` + color: ${palette.gray8}; + font-size: 0.875rem; + line-height: 1.5; + + a { + color: inherit; + text-decoration: none; + &:hover { + color: ${palette.gray9}; + ${props => + props.active && + ` + color: ${palette.teal5}; + `} + text-decoration: underline; + span { + text-decoration: none; + } + } + } + + ${props => + props.active && + ` + color: ${palette.teal5}; + font-weight: bold; + `} + + span { + margin-left: 0.5rem; + color: ${palette.gray6}; + font-weight: normal; + } + + & + & { + margin-top: 0.25rem; + } +`; + +export default UserTagVerticalList; diff --git a/src/components/velog/UserTags.tsx b/src/components/velog/UserTags.tsx new file mode 100644 index 00000000..14cad9e0 --- /dev/null +++ b/src/components/velog/UserTags.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import UserTagVerticalList from './UserTagVerticalList'; +import useUserTags from './hooks/useUserTags'; + +export type UserTagsProps = { + username: string; + tag: string | null; +}; + +function UserTags({ username, tag }: UserTagsProps) { + const { data, loading } = useUserTags(username); + if (!data || loading) return null; + + return ( + <> + + + ); +} + +export default UserTags; diff --git a/src/components/velog/hooks/useUserTags.ts b/src/components/velog/hooks/useUserTags.ts new file mode 100644 index 00000000..76d24f93 --- /dev/null +++ b/src/components/velog/hooks/useUserTags.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@apollo/react-hooks'; +import { GET_USER_TAGS, GetUserTagsResponse } from '../../../lib/graphql/tags'; + +export default function useUserTags(username: string) { + const { data, loading } = useQuery(GET_USER_TAGS, { + variables: { + username, + }, + }); + + return { + data: data + ? { tags: data.userTags.tags, postsCount: data.userTags.posts_count } + : null, + loading, + }; +} diff --git a/src/components/write/Toolbar.tsx b/src/components/write/Toolbar.tsx index 3f7b4a51..e9e8cfea 100644 --- a/src/components/write/Toolbar.tsx +++ b/src/components/write/Toolbar.tsx @@ -9,11 +9,9 @@ import { MdFormatQuote, MdImage, MdCode, - MdRemoveRedEye, } from 'react-icons/md'; import palette from '../../lib/styles/palette'; import zIndexes from '../../lib/styles/zIndexes'; -import { FaMarkdown } from 'react-icons/fa'; // box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.09); const ToolbarBlock = styled.div<{ diff --git a/src/containers/velog/UserPosts.tsx b/src/containers/velog/UserPosts.tsx index 75180b3b..74e9ad47 100644 --- a/src/containers/velog/UserPosts.tsx +++ b/src/containers/velog/UserPosts.tsx @@ -11,12 +11,14 @@ import styled from 'styled-components'; interface UserPostsProps { username: string; + tag: string | null; } -const UserPosts: React.FC = ({ username }) => { +const UserPosts: React.FC = ({ username, tag }) => { const getPostList = useQuery<{ posts: PartialPost[] }>(GET_POST_LIST, { variables: { username, + tag, }, notifyOnNetworkStatusChange: true, }); @@ -29,6 +31,7 @@ const UserPosts: React.FC = ({ username }) => { variables: { cursor, username, + tag, }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult) return prev; @@ -38,7 +41,7 @@ const UserPosts: React.FC = ({ username }) => { }, }); }, - [getPostList, username], + [getPostList, tag, username], ); if (!data || !data.posts) return ; diff --git a/src/lib/graphql/tags.ts b/src/lib/graphql/tags.ts index 8188e21d..1e47eb3e 100644 --- a/src/lib/graphql/tags.ts +++ b/src/lib/graphql/tags.ts @@ -20,6 +20,21 @@ export const GET_TAGS = gql` } `; +export const GET_USER_TAGS = gql` + query UserTags($username: String) { + userTags(username: $username) { + tags { + id + name + description + posts_count + thumbnail + } + posts_count + } + } +`; + export const GET_TAG = gql` query Tag($name: String!) { tag(name: $name) { @@ -36,6 +51,13 @@ export type GetTagsResponse = { tags: Tag[]; }; +export type GetUserTagsResponse = { + userTags: { + tags: Tag[]; + posts_count: number; + }; +}; + export type GetTagResponse = { tag: Tag; }; diff --git a/src/pages/velog/UserPage.tsx b/src/pages/velog/UserPage.tsx index 7ea63ab6..a2bdd0e8 100644 --- a/src/pages/velog/UserPage.tsx +++ b/src/pages/velog/UserPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import VelogResponsive from '../../components/velog/VelogResponsive'; import UserProfileContainer from '../../containers/velog/UserProfileContainer'; @@ -9,20 +9,34 @@ import SeriesTab from './tabs/SeriesTab'; import AboutTab from './tabs/AboutTab'; import palette from '../../lib/styles/palette'; import media from '../../lib/styles/media'; +import UserTags from '../../components/velog/UserTags'; +import qs from 'qs'; +import { usePrevious } from 'react-use'; const UserPageBlock = styled(VelogResponsive)``; export interface UserPageProps extends RouteComponentProps<{ username: string; tab?: 'series' | 'about' }> {} -const UserPage: React.FC = ({ match }) => { +const UserPage: React.FC = ({ match, location }) => { const { username, tab } = match.params; + const { tag } = qs.parse(location.search, { + ignoreQueryPrefix: true, + }) as { tag: string | undefined }; + + const prevTag = usePrevious(tag); + useEffect(() => { + if (prevTag !== tag) { + window.scrollTo(0, 0); + } + }, [prevTag, tag]); return ( + diff --git a/src/pages/velog/tabs/UserPostsTab.tsx b/src/pages/velog/tabs/UserPostsTab.tsx index 1f47bd87..b96bc0bc 100644 --- a/src/pages/velog/tabs/UserPostsTab.tsx +++ b/src/pages/velog/tabs/UserPostsTab.tsx @@ -19,9 +19,15 @@ const UserPostsTab: React.FC = ({ location, history, }) => { - const { q }: { q: string | undefined } = qs.parse(location.search, { - ignoreQueryPrefix: true, - }); + const { + q, + tag, + }: { q: string | undefined; tag: string | undefined } = qs.parse( + location.search, + { + ignoreQueryPrefix: true, + }, + ); const { username } = match.params; usePreserveScroll('user/posts'); @@ -47,7 +53,7 @@ const UserPostsTab: React.FC = ({ {q ? ( ) : ( - + )} From 9a34252c0314d7f33b240bd54412d79cb9bd327b Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 18 Feb 2020 01:14:53 +0900 Subject: [PATCH 018/449] Use network-only fetch policy for user tags --- src/components/velog/hooks/useUserTags.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/velog/hooks/useUserTags.ts b/src/components/velog/hooks/useUserTags.ts index 76d24f93..695abb5e 100644 --- a/src/components/velog/hooks/useUserTags.ts +++ b/src/components/velog/hooks/useUserTags.ts @@ -6,6 +6,7 @@ export default function useUserTags(username: string) { variables: { username, }, + fetchPolicy: 'network-only', }); return { From e4a2fc4d504de3e27575894a07fc35a7c86923e1 Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 18 Feb 2020 01:41:55 +0900 Subject: [PATCH 019/449] Remove velog-title from post title --- src/containers/post/PostViewer.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 5204b565..58601fb9 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -301,9 +301,7 @@ const PostViewer: React.FC = ({ return ( - - {post.title} - {velogTitle} - + {post.title} {post.short_description && ( )} From cc4ecba1f6dbe3ccf7feb6dd6903efff4e78f127 Mon Sep 17 00:00:00 2001 From: velopert Date: Tue, 18 Feb 2020 23:19:42 +0900 Subject: [PATCH 020/449] Fix #80 --- .env | 4 ++-- docker/redis/docker-compose.yml | 1 - src/components/velog/SideArea.tsx | 2 +- src/pages/velog/UserPage.tsx | 14 -------------- src/pages/velog/tabs/UserPostsTab.tsx | 21 ++++++++++++++++++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.env b/.env index 786c4376..90607079 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -REACT_APP_API_HOST=https://v2dev.velog.io/ -PUBLIC_URL=https://static.velog.io/ \ No newline at end of file +REACT_APP_API_HOST=http://localhost:5000/ +PUBLIC_URL=/ \ No newline at end of file diff --git a/docker/redis/docker-compose.yml b/docker/redis/docker-compose.yml index ca92b923..a67981a5 100644 --- a/docker/redis/docker-compose.yml +++ b/docker/redis/docker-compose.yml @@ -6,7 +6,6 @@ services: environment: # ALLOW_EMPTY_PASSWORD is recommended only for development. - ALLOW_EMPTY_PASSWORD=yes - - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL ports: - '6379:6379' volumes: diff --git a/src/components/velog/SideArea.tsx b/src/components/velog/SideArea.tsx index fa865122..f34de13a 100644 --- a/src/components/velog/SideArea.tsx +++ b/src/components/velog/SideArea.tsx @@ -21,7 +21,7 @@ const Wrapper = styled.div` const Block = styled.div` position: absolute; - margin-top: 4.25rem; + width: 11.5rem; left: -13.5rem; diff --git a/src/pages/velog/UserPage.tsx b/src/pages/velog/UserPage.tsx index a2bdd0e8..32310c64 100644 --- a/src/pages/velog/UserPage.tsx +++ b/src/pages/velog/UserPage.tsx @@ -9,9 +9,6 @@ import SeriesTab from './tabs/SeriesTab'; import AboutTab from './tabs/AboutTab'; import palette from '../../lib/styles/palette'; import media from '../../lib/styles/media'; -import UserTags from '../../components/velog/UserTags'; -import qs from 'qs'; -import { usePrevious } from 'react-use'; const UserPageBlock = styled(VelogResponsive)``; @@ -20,23 +17,12 @@ export interface UserPageProps const UserPage: React.FC = ({ match, location }) => { const { username, tab } = match.params; - const { tag } = qs.parse(location.search, { - ignoreQueryPrefix: true, - }) as { tag: string | undefined }; - - const prevTag = usePrevious(tag); - useEffect(() => { - if (prevTag !== tag) { - window.scrollTo(0, 0); - } - }, [prevTag, tag]); return ( - diff --git a/src/pages/velog/tabs/UserPostsTab.tsx b/src/pages/velog/tabs/UserPostsTab.tsx index b96bc0bc..f5854d66 100644 --- a/src/pages/velog/tabs/UserPostsTab.tsx +++ b/src/pages/velog/tabs/UserPostsTab.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import UserPosts from '../../../containers/velog/UserPosts'; import { RouteComponentProps } from 'react-router'; import qs from 'qs'; @@ -8,6 +8,8 @@ import styled from 'styled-components'; import media from '../../../lib/styles/media'; import usePreserveScroll from '../../../lib/hooks/usePreserveScroll'; import { Helmet } from 'react-helmet-async'; +import { usePrevious } from 'react-use'; +import UserTags from '../../../components/velog/UserTags'; export interface UserPostsTabProps extends RouteComponentProps<{ username: string }> { @@ -32,8 +34,15 @@ const UserPostsTab: React.FC = ({ const { username } = match.params; usePreserveScroll('user/posts'); + const prevTag = usePrevious(tag); + useEffect(() => { + if (prevTag !== tag) { + window.scrollTo(0, 0); + } + }, [prevTag, tag]); + return ( - <> +
= ({ username={match.params.username} /> + + + {q ? ( @@ -56,7 +68,7 @@ const UserPostsTab: React.FC = ({ )} - +
); }; @@ -73,4 +85,7 @@ const Block = styled.div` } `; +const TagWrapper = styled.div` + position: relative; +`; export default UserPostsTab; From e199983e3e20c9af999ff353057eca5633318b0a Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 19 Feb 2020 01:52:26 +0900 Subject: [PATCH 021/449] Implement horizontal list --- src/components/common/TagItem.tsx | 2 +- .../velog/UserTagHorizontalList.tsx | 95 +++++++++++++++++++ src/components/velog/UserTags.tsx | 7 ++ src/pages/velog/tabs/UserPostsTab.tsx | 7 +- 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/components/velog/UserTagHorizontalList.tsx diff --git a/src/components/common/TagItem.tsx b/src/components/common/TagItem.tsx index 8efe9539..2d6a5791 100644 --- a/src/components/common/TagItem.tsx +++ b/src/components/common/TagItem.tsx @@ -34,7 +34,7 @@ const tagStyle = css` ${media.small} { height: 1.5rem; font-size: 0.75rem; - border-radius: 0.625rem; + border-radius: 0.75rem; padding-left: 0.75rem; padding-right: 0.75rem; margin-right: 0.5rem; diff --git a/src/components/velog/UserTagHorizontalList.tsx b/src/components/velog/UserTagHorizontalList.tsx new file mode 100644 index 00000000..7678d73f --- /dev/null +++ b/src/components/velog/UserTagHorizontalList.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import media from '../../lib/styles/media'; +import { Tag } from '../../lib/graphql/tags'; +import palette from '../../lib/styles/palette'; +import { Link } from 'react-router-dom'; +import { escapeForUrl } from '../../lib/utils'; + +export type UserTagHorizontalListProps = { + active: string | null; + tags: Tag[]; + postsCount: number; + username: string; +}; + +function UserTagHorizontalList({ + active, + tags, + postsCount, + username, +}: UserTagHorizontalListProps) { + return ( + + + 전체보기 ({postsCount}) + + {tags.map(tag => ( + + {tag.name} + ({tag.posts_count}) + + ))} + + ); +} + +const Block = styled.div` + display: none; + ${media.large} { + display: flex; + } + overflow-x: auto; + margin-top: -1.5rem; + padding-top: 1rem; + padding-bottom: 1rem; + + ${media.small} { + padding-left: 1rem; + padding-right: 1rem; + } + margin-bottom: 0.5rem; +`; + +const TagItem = styled(Link)<{ active?: boolean }>` + flex-shrink: 0; + height: 1.5rem; + font-size: 0.75rem; + border-radius: 0.75rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + background: ${palette.gray1}; + color: ${palette.gray8}; + display: flex; + align-items: center; + line-height: 1.5; + + span { + margin-left: 0.25rem; + color: ${palette.gray6}; + font-size: 0.75rem; + } + + ${props => + props.active && + css` + background: ${palette.teal6}; + color: white; + span { + color: white; + opacity: 0.8; + } + `} + + text-decoration: none; + + & + & { + margin-left: 0.5rem; + } +`; + +export default UserTagHorizontalList; diff --git a/src/components/velog/UserTags.tsx b/src/components/velog/UserTags.tsx index 14cad9e0..03ed7ffe 100644 --- a/src/components/velog/UserTags.tsx +++ b/src/components/velog/UserTags.tsx @@ -2,6 +2,7 @@ import React from 'react'; import UserTagVerticalList from './UserTagVerticalList'; import useUserTags from './hooks/useUserTags'; +import UserTagHorizontalList from './UserTagHorizontalList'; export type UserTagsProps = { username: string; @@ -20,6 +21,12 @@ function UserTags({ username, tag }: UserTagsProps) { postsCount={data.postsCount} username={username} /> + ); } diff --git a/src/pages/velog/tabs/UserPostsTab.tsx b/src/pages/velog/tabs/UserPostsTab.tsx index f5854d66..5a738b2b 100644 --- a/src/pages/velog/tabs/UserPostsTab.tsx +++ b/src/pages/velog/tabs/UserPostsTab.tsx @@ -10,6 +10,7 @@ import usePreserveScroll from '../../../lib/hooks/usePreserveScroll'; import { Helmet } from 'react-helmet-async'; import { usePrevious } from 'react-use'; import UserTags from '../../../components/velog/UserTags'; +import { getScrollTop } from '../../../lib/utils'; export interface UserPostsTabProps extends RouteComponentProps<{ username: string }> { @@ -36,7 +37,11 @@ const UserPostsTab: React.FC = ({ const prevTag = usePrevious(tag); useEffect(() => { - if (prevTag !== tag) { + if ( + prevTag !== tag && + window.screen.width > 768 && + getScrollTop() > window.screen.height * 0.6 + ) { window.scrollTo(0, 0); } }, [prevTag, tag]); From 10c96a0997284c1cdf956663af8d6593dc108f78 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 19 Feb 2020 23:43:00 +0900 Subject: [PATCH 022/449] Fix drag drop image upload issue - #84, #86 --- src/components/common/DragDropUpload.tsx | 30 ++++++++++++++++---- src/components/write/WriteMarkdownEditor.tsx | 6 ++++ src/lib/jazzbar/Jazzbar.tsx | 10 +++++-- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/common/DragDropUpload.tsx b/src/components/common/DragDropUpload.tsx index cd692a38..04bbf630 100644 --- a/src/components/common/DragDropUpload.tsx +++ b/src/components/common/DragDropUpload.tsx @@ -31,10 +31,9 @@ const DragDropUpload: React.FC = ({ onUpload }) => { const [dragging, setDragging] = useState(false); useEffect(() => { - const onDrop = (e: any) => { + const onDrop = (e: DragEvent) => { e.preventDefault(); - const { files } = e.dataTransfer; - console.log(files); + const { files } = e.dataTransfer || { files: null }; if (!files) return; if (!files[0]) return; onUpload(files[0]); @@ -57,6 +56,17 @@ const DragDropUpload: React.FC = ({ onUpload }) => { setDragging(true); } }; + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; + } + if (!dragging) { + setDragging(true); + } + }; + const onDragLeave = () => { if (down.current) return; dragIndex.current -= 1; @@ -65,20 +75,30 @@ const DragDropUpload: React.FC = ({ onUpload }) => { } }; + const onMouseLeave = () => { + if (dragging) { + setDragging(false); + } + }; + window.addEventListener('drop', onDrop); + window.addEventListener('dragover', onDragOver); window.addEventListener('mousedown', onMouseDown); window.addEventListener('mouseup', onMouseUp); window.addEventListener('dragenter', onDragEnter); window.addEventListener('dragleave', onDragLeave); + document.addEventListener('mouseleave', onMouseLeave); return () => { window.removeEventListener('drop', onDrop); + window.removeEventListener('dragover', onDragOver); window.removeEventListener('mousedown', onMouseDown); window.removeEventListener('mouseup', onMouseUp); - window.addEventListener('dragenter', onDragEnter); + window.removeEventListener('dragenter', onDragEnter); window.removeEventListener('dragleave', onDragLeave); + document.removeEventListener('mouseleave', onMouseLeave); }; - }, [onUpload]); + }, [dragging, onUpload]); return dragging ? ( diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index 5d274e44..145a992b 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -285,6 +285,12 @@ export default class WriteMarkdownEditor extends React.Component< } }); + this.codemirror.on('dragover', (cm, e) => { + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; + } + }); + this.codemirror.on('mousewheel', cm => { console.log(cm.getScrollInfo()); }); diff --git a/src/lib/jazzbar/Jazzbar.tsx b/src/lib/jazzbar/Jazzbar.tsx index bba7b2df..f403a6be 100644 --- a/src/lib/jazzbar/Jazzbar.tsx +++ b/src/lib/jazzbar/Jazzbar.tsx @@ -4,6 +4,7 @@ import { JazzbarContext } from '.'; export interface JazzbarProps {} +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const Jazzbar: React.FC = props => { const jazzbar = useContext(JazzbarContext); const [visible, setVisible] = useState(false); @@ -14,10 +15,13 @@ const Jazzbar: React.FC = props => { const hide = useCallback(() => { setHiding(true); - setTimeout(() => { - setHiding(false); + const run = async () => { + await sleep(200); setZero(true); - }, 200); + await sleep(400); + setHiding(false); + }; + run(); }, []); useEffect(() => { From 772c613a32d7d4b3b0c413a949458f413683d117 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 19 Feb 2020 23:50:31 +0900 Subject: [PATCH 023/449] Apply enhancement #85 Autosave even when postId does not exist (title must be valid & markdown length > 30) --- src/containers/write/MarkdownEditorContainer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index f81b101c..7aa9a25e 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -239,8 +239,8 @@ const MarkdownEditorContainer: React.FC = () => { const changed = !shallowEqual(lastSavedData, { title, body: markdown }); if (changed) { const timeoutId = setTimeout(() => { - if (!postId) return; - onTempSave(); + if (!postId && !title && markdown.length < 30) return; + onTempSave(true); }, 10 * 1000); return () => { From f326c4813212f28bd0361acb096bab51981a99fe Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 19 Feb 2020 23:59:45 +0900 Subject: [PATCH 024/449] Show TOC on medium size desktop view --- src/components/post/PostToc.tsx | 7 +++++-- src/lib/styles/media.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/post/PostToc.tsx b/src/components/post/PostToc.tsx index 85150487..4539d691 100644 --- a/src/components/post/PostToc.tsx +++ b/src/components/post/PostToc.tsx @@ -4,12 +4,12 @@ import palette from '../../lib/styles/palette'; import Sticky from '../common/Sticky'; import { usePostViewerState } from './PostViewerProvider'; import { getScrollTop } from '../../lib/utils'; -import media from '../../lib/styles/media'; +import media, { mediaQuery } from '../../lib/styles/media'; const Wrapper = styled.div` position: relative; margin-top: 2rem; - ${media.xlarge} { + ${mediaQuery(1365)} { display: none; } `; @@ -21,6 +21,9 @@ const Positioner = styled.div` const PostTocBlock = styled(Sticky)` width: 240px; margin-left: 5rem; + ${media.xlarge} { + margin-left: 3rem; + } border-left: 2px solid ${palette.gray2}; padding-left: 0.75rem; padding-right: 0.75rem; diff --git a/src/lib/styles/media.ts b/src/lib/styles/media.ts index 8b434038..5394df47 100644 --- a/src/lib/styles/media.ts +++ b/src/lib/styles/media.ts @@ -1,4 +1,4 @@ -const mediaQuery = (maxWidth: number) => ` +export const mediaQuery = (maxWidth: number) => ` @media (max-width: ${maxWidth}px) `; From f1fc3970a82330555c41d306aabd247bc5c410e3 Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 20 Feb 2020 23:55:48 +0900 Subject: [PATCH 025/449] Fix postcard crash on search --- src/components/common/PostCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index 30334d08..d75b0ebb 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -151,7 +151,7 @@ const PostCard = ({ post, hideUser }: PostCardProps) => { thumbnail Date: Mon, 24 Feb 2020 23:09:46 +0900 Subject: [PATCH 026/449] Temporarily disable redis cache --- src/server/serverRender.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/server/serverRender.tsx b/src/server/serverRender.tsx index 95ecc4f7..1ccb00a7 100644 --- a/src/server/serverRender.tsx +++ b/src/server/serverRender.tsx @@ -37,14 +37,16 @@ const serverRender = async ({ url, loggedIn, cookie }: SSROption) => { const store = createStore(rootReducer); // prepare apollo client - if (!loggedIn && process.env.STAGE !== 'true') { - const cachedPage = await cacheManager.get(url); - if (cachedPage) { - return { - html: cachedPage, - statusCode: 200, - }; - } + if (!loggedIn && process.env.STAGE !== 'true' && false) { + try { + const cachedPage = await cacheManager.get(url); + if (cachedPage) { + return { + html: cachedPage, + statusCode: 200, + }; + } + } catch (e) {} } const client = new ApolloClient({ @@ -115,7 +117,7 @@ const serverRender = async ({ url, loggedIn, cookie }: SSROption) => { )}`; try { - if (!loggedIn && process.env.STAGE !== 'true') { + if (!loggedIn && process.env.STAGE !== 'true' && false) { await cacheManager.set(url, pageHtml); } } catch (e) { From 8ac0aa86769a5702968c966bbde90550deb84daa Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 26 Feb 2020 00:01:47 +0900 Subject: [PATCH 027/449] Initialize UI for PostCard Legacy PostCard is renamed to FlatPostCard --- src/App.tsx | 8 +- src/components/common/FlatPostCard.tsx | 299 ++++++++++++++ ...{PostCardList.tsx => FlatPostCardList.tsx} | 2 +- src/components/common/PostCard.tsx | 379 +++++------------- src/components/common/PostCardGrid.tsx | 28 ++ src/components/home/HomeHeader.tsx | 40 ++ src/components/home/HomeLayout.tsx | 30 ++ src/components/home/HomeResponsive.tsx | 19 + src/components/home/HomeTab.tsx | 77 ++++ src/components/home/HomeTemplate.tsx | 27 ++ src/containers/main/RecentPosts.tsx | 2 +- src/containers/main/TrendingPosts.tsx | 2 +- src/containers/search/SearchResult.tsx | 2 +- src/containers/tags/TagDetailContainer.tsx | 2 +- src/containers/velog/UserPosts.tsx | 2 +- src/lib/graphql/post.ts | 2 + src/pages/home/HomePage.tsx | 37 ++ src/pages/home/RecentPostsPage.tsx | 9 + src/pages/home/TrendingPostsPage.tsx | 13 + src/pages/home/hooks/useTrendingPosts.ts | 13 + 20 files changed, 710 insertions(+), 283 deletions(-) create mode 100644 src/components/common/FlatPostCard.tsx rename src/components/common/{PostCardList.tsx => FlatPostCardList.tsx} (94%) create mode 100644 src/components/common/PostCardGrid.tsx create mode 100644 src/components/home/HomeHeader.tsx create mode 100644 src/components/home/HomeLayout.tsx create mode 100644 src/components/home/HomeResponsive.tsx create mode 100644 src/components/home/HomeTab.tsx create mode 100644 src/components/home/HomeTemplate.tsx create mode 100644 src/pages/home/HomePage.tsx create mode 100644 src/pages/home/RecentPostsPage.tsx create mode 100644 src/pages/home/TrendingPostsPage.tsx create mode 100644 src/pages/home/hooks/useTrendingPosts.ts diff --git a/src/App.tsx b/src/App.tsx index 84d8e42d..d0346fd3 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import VelogPageFallback from './containers/velog/VelogPageFallback'; import ErrorBoundary from './components/error/ErrorBoundary'; import NotFoundPage from './pages/NotFoundPage'; import { Helmet } from 'react-helmet-async'; +import HomePage from './pages/home/HomePage'; const loadableConfig = { fallback: , @@ -57,12 +58,13 @@ const App: React.FC = props => {
- - + + + {/* */} diff --git a/src/components/common/FlatPostCard.tsx b/src/components/common/FlatPostCard.tsx new file mode 100644 index 00000000..4a0a1e5b --- /dev/null +++ b/src/components/common/FlatPostCard.tsx @@ -0,0 +1,299 @@ +import React, { useRef } from 'react'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { userThumbnail } from '../../static/images'; +import Tag from './TagItem'; +import { PartialPost } from '../../lib/graphql/post'; +import { formatDate } from '../../lib/utils'; +import usePrefetchPost from '../../lib/hooks/usePrefetchPost'; +import Skeleton from './Skeleton'; +import SkeletonTexts from './SkeletonTexts'; +import RatioImage from './RatioImage'; +import media from '../../lib/styles/media'; +import PrivatePostLabel from './PrivatePostLabel'; +import optimizeImage from '../../lib/optimizeImage'; + +const PostCardBlock = styled.div` + padding-top: 4rem; + padding-bottom: 4rem; + ${media.small} { + padding-top: 2rem; + padding-bottom: 2rem; + } + + & > a { + color: inherit; + text-decoration: none; + } + &:first-child { + padding-top: 0; + } + .user-info { + display: flex; + align-items: center; + img { + width: 3rem; + height: 3rem; + display: block; + margin-right: 1rem; + background: ${palette.gray0}; + object-fit: cover; + border-radius: 1.5rem; + box-shadow: 0px 0 8px rgba(0, 0, 0, 0.1); + ${media.small} { + width: 2rem; + height: 2rem; + border-radius: 1rem; + } + } + .username { + font-size: 0.875rem; + color: ${palette.gray9}; + font-weight: bold; + a { + color: inherit; + text-decoration: none; + &:hover { + color: ${palette.gray8}; + } + } + } + margin-bottom: 1.5rem; + ${media.small} { + margin-bottom: 0.75rem; + } + } + .post-thumbnail { + margin-bottom: 1rem; + ${media.small} { + } + } + line-height: 1.5; + h2 { + font-size: 1.5rem; + margin: 0; + color: ${palette.gray9}; + word-break: keep-all; + ${media.small} { + font-size: 1rem; + } + } + p { + margin-bottom: 2rem; + margin-top: 0.5rem; + font-size: 1rem; + color: ${palette.gray7}; + word-break: keep-all; + overflow-wrap: break-word; + ${media.small} { + font-size: 0.875rem; + margin-bottom: 1.5rem; + } + } + .subinfo { + display: flex; + align-items: center; + margin-top: 1rem; + color: ${palette.gray6}; + font-size: 0.875rem; + ${media.small} { + font-size: 0.75rem; + } + span { + } + .separator { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + } + .tags-wrapper { + margin-bottom: -0.875rem; + ${media.small} { + margin-bottom: -0.5rem; + } + } + + & + & { + border-top: 1px solid ${palette.gray2}; + } +`; + +interface PostCardProps { + post: PartialPost; + hideUser?: boolean; +} + +const FlatPostCard = ({ post, hideUser }: PostCardProps) => { + const prefetch = usePrefetchPost(post.user.username, post.url_slug); + const prefetchTimeoutId = useRef(null); + + const onMouseEnter = () => { + prefetchTimeoutId.current = setTimeout(prefetch, 2000); + }; + + const onMouseLeave = () => { + if (prefetchTimeoutId.current) { + clearTimeout(prefetchTimeoutId.current); + } + }; + + const url = `/@${post.user.username}/${post.url_slug}`; + const velogUrl = `/@${post.user.username}`; + + if (!post.user.profile) { + console.log(post); + } + return ( + + {!hideUser && ( +
+ + thumbnail + +
+ {post.user.username} +
+
+ )} + {post.thumbnail && ( + + + + )} + +

{post.title}

+ +

{post.short_description}

+
+ {post.tags.map(tag => ( + + ))} +
+
+ {formatDate(post.released_at)} +
·
+ {post.comments_count}개의 댓글 + {post.is_private && ( + <> +
·
+ + + + + )} +
+
+ ); +}; + +export type PostCardSkeletonProps = { + hideUser?: boolean; +}; + +export function PostCardSkeleton({ hideUser }: PostCardSkeletonProps) { + return ( + + {!hideUser && ( +
+ +
+ +
+
+ )} +
+
+ +
+
+

+ +

+
+
+ +
+
+ +
+
+ +
+
+
+ + + +
+
+ + +
+
+ ); +} + +const SkeletonBlock = styled(PostCardBlock)` + h2 { + display: flex; + margin-top: 1.375rem; + margin-bottom: 0.375rem; + } + .user-thumbnail-skeleton { + width: 3rem; + height: 3rem; + ${media.small} { + width: 2rem; + height: 2rem; + } + } + .thumbnail-skeleton-wrapper { + width: 100%; + padding-top: 52.35%; + position: relative; + .skeleton { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + .short-description { + margin-bottom: 2rem; + margin-top: 1rem; + font-size: 1rem; + .line { + display: flex; + } + .line + .line { + margin-top: 0.5rem; + } + } + .tags-skeleton { + line-height: 1; + font-size: 2rem; + ${media.small} { + font-size: 1.25rem; + } + } +`; + +export default React.memo(FlatPostCard); diff --git a/src/components/common/PostCardList.tsx b/src/components/common/FlatPostCardList.tsx similarity index 94% rename from src/components/common/PostCardList.tsx rename to src/components/common/FlatPostCardList.tsx index 6c3e047b..4d6cc919 100644 --- a/src/components/common/PostCardList.tsx +++ b/src/components/common/FlatPostCardList.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styled from 'styled-components'; -import PostCard, { PostCardSkeleton } from './PostCard'; +import PostCard, { PostCardSkeleton } from './FlatPostCard'; import { PartialPost } from '../../lib/graphql/post'; import palette from '../../lib/styles/palette'; diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index d75b0ebb..97ec8089 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -1,299 +1,130 @@ -import React, { useRef } from 'react'; -import { Link } from 'react-router-dom'; +import React from 'react'; import styled from 'styled-components'; -import palette from '../../lib/styles/palette'; -import { userThumbnail } from '../../static/images'; -import Tag from './TagItem'; -import { PartialPost } from '../../lib/graphql/post'; -import { formatDate } from '../../lib/utils'; -import usePrefetchPost from '../../lib/hooks/usePrefetchPost'; -import Skeleton from './Skeleton'; -import SkeletonTexts from './SkeletonTexts'; import RatioImage from './RatioImage'; -import media from '../../lib/styles/media'; -import PrivatePostLabel from './PrivatePostLabel'; -import optimizeImage from '../../lib/optimizeImage'; +import { ellipsis } from '../../lib/styles/utils'; +import palette from '../../lib/styles/palette'; +import { LikeIcon } from '../../static/svg'; -const PostCardBlock = styled.div` - padding-top: 4rem; - padding-bottom: 4rem; - ${media.small} { - padding-top: 2rem; - padding-bottom: 2rem; - } +export type PostCardProps = {}; - & > a { - color: inherit; - text-decoration: none; - } - &:first-child { - padding-top: 0; - } - .user-info { - display: flex; - align-items: center; - img { - width: 3rem; - height: 3rem; - display: block; - margin-right: 1rem; - background: ${palette.gray0}; - object-fit: cover; - border-radius: 1.5rem; - box-shadow: 0px 0 8px rgba(0, 0, 0, 0.1); - ${media.small} { - width: 2rem; - height: 2rem; - border-radius: 1rem; - } - } - .username { - font-size: 0.875rem; - color: ${palette.gray9}; - font-weight: bold; - a { - color: inherit; - text-decoration: none; - &:hover { - color: ${palette.gray8}; - } - } - } - margin-bottom: 1.5rem; - ${media.small} { - margin-bottom: 0.75rem; - } - } - .post-thumbnail { - margin-bottom: 1rem; - ${media.small} { - } - } - line-height: 1.5; - h2 { - font-size: 1.5rem; +function PostCard(props: PostCardProps) { + return ( + + + +

벨로그 v2 업데이트 안내

+

+ Node.js, PHP 등 익숙한 언어들을 던지고 생뚱맞은 장고를 택한 이유는 단 + 하나였습니다. 장고를 사용하시는 분들이 가장 많이 이야기하는, + 생산성입니다 내용이 더 길어지면 엉떻게 되니닌 +

+
+ 2020년 2월 10일 + · + 64개의 댓글 +
+
+
+
+ post-thumbnail + + by velopert + +
+
+ + 65 +
+
+
+ ); +} + +const Block = styled.div` + width: 20rem; + background: white; + border-radius: 4px; + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.04); + margin: 1rem; + overflow: hidden; +`; + +const Content = styled.div` + padding: 1rem; + h4 { + font-size: 1rem; margin: 0; + margin-bottom: 0.25rem; + line-height: 1.5; + ${ellipsis} color: ${palette.gray9}; - word-break: keep-all; - ${media.small} { - font-size: 1rem; - } } p { - margin-bottom: 2rem; - margin-top: 0.5rem; - font-size: 1rem; - color: ${palette.gray7}; + margin: 0; word-break: keep-all; overflow-wrap: break-word; - ${media.small} { - font-size: 0.875rem; - margin-bottom: 1.5rem; - } + font-size: 0.875rem; + line-height: 1.5; + height: 3.9375rem; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + color: ${palette.gray7}; + margin-bottom: 1.5rem; } - .subinfo { - display: flex; - align-items: center; - margin-top: 1rem; + .sub-info { + font-size: 0.75rem; + line-height: 1.5; color: ${palette.gray6}; - font-size: 0.875rem; - ${media.small} { - font-size: 0.75rem; - } - span { - } .separator { - margin-left: 0.5rem; - margin-right: 0.5rem; - } - } - .tags-wrapper { - margin-bottom: -0.875rem; - ${media.small} { - margin-bottom: -0.5rem; + margin-left: 0.25rem; + margin-right: 0.25rem; } } - - & + & { - border-top: 1px solid ${palette.gray2}; - } `; -interface PostCardProps { - post: PartialPost; - hideUser?: boolean; -} - -const PostCard = ({ post, hideUser }: PostCardProps) => { - const prefetch = usePrefetchPost(post.user.username, post.url_slug); - const prefetchTimeoutId = useRef(null); - - const onMouseEnter = () => { - prefetchTimeoutId.current = setTimeout(prefetch, 2000); - }; - - const onMouseLeave = () => { - if (prefetchTimeoutId.current) { - clearTimeout(prefetchTimeoutId.current); - } - }; - - const url = `/@${post.user.username}/${post.url_slug}`; - const velogUrl = `/@${post.user.username}`; - - if (!post.user.profile) { - console.log(post); - } - return ( - - {!hideUser && ( -
- - thumbnail - -
- {post.user.username} -
-
- )} - {post.thumbnail && ( - - - - )} - -

{post.title}

- -

{post.short_description}

-
- {post.tags.map(tag => ( - - ))} -
-
- {formatDate(post.released_at)} -
·
- {post.comments_count}개의 댓글 - {post.is_private && ( - <> -
·
- - - - - )} -
-
- ); -}; - -export type PostCardSkeletonProps = { - hideUser?: boolean; -}; - -export function PostCardSkeleton({ hideUser }: PostCardSkeletonProps) { - return ( - - {!hideUser && ( -
- -
- -
-
- )} -
-
- -
-
-

- -

-
-
- -
-
- -
-
- -
-
-
- - - -
-
- - -
-
- ); -} - -const SkeletonBlock = styled(PostCardBlock)` - h2 { +const Footer = styled.div` + padding: 0.625rem 1rem; + border-top: 1px solid ${palette.gray0}; + display: flex; + font-size: 0.75rem; + line-height: 1.5; + justify-content: space-between; + .userinfo { display: flex; - margin-top: 1.375rem; - margin-bottom: 0.375rem; - } - .user-thumbnail-skeleton { - width: 3rem; - height: 3rem; - ${media.small} { - width: 2rem; - height: 2rem; - } - } - .thumbnail-skeleton-wrapper { - width: 100%; - padding-top: 52.35%; - position: relative; - .skeleton { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - } - .short-description { - margin-bottom: 2rem; - margin-top: 1rem; - font-size: 1rem; - .line { - display: flex; + align-items: center; + img { + border-radius: 50%; + width: 1.5rem; + height: 1.5rem; + display: block; + margin-right: 0.5rem; } - .line + .line { - margin-top: 0.5rem; + span { + color: ${palette.gray6}; + b { + color: ${palette.gray8}; + } } } - .tags-skeleton { - line-height: 1; - font-size: 2rem; - ${media.small} { - font-size: 1.25rem; + .likes { + display: flex; + align-items: center; + svg { + width: 0.75rem; + height: 0.75rem; + margin-right: 0.5rem; } } `; -export default React.memo(PostCard); +export default PostCard; diff --git a/src/components/common/PostCardGrid.tsx b/src/components/common/PostCardGrid.tsx new file mode 100644 index 00000000..9b5bbff5 --- /dev/null +++ b/src/components/common/PostCardGrid.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import styled from 'styled-components'; +import PostCard from './PostCard'; + +export type PostCardGridProps = {}; + +function PostCardGrid(props: PostCardGridProps) { + return ( + + + + + + + + + + + ); +} + +const Block = styled.div` + display: flex; + margin: -1rem; + flex-wrap: wrap; +`; + +export default PostCardGrid; diff --git a/src/components/home/HomeHeader.tsx b/src/components/home/HomeHeader.tsx new file mode 100644 index 00000000..94bc4fd6 --- /dev/null +++ b/src/components/home/HomeHeader.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Logo } from '../../static/svg'; +import RoundButton from '../common/RoundButton'; +import HomeResponsive from './HomeResponsive'; + +export type HomeHeaderProps = {}; + +function HomeHeader(props: HomeHeaderProps) { + return ( + + + + + {}}> + 로그인 + + + + + ); +} + +const Block = styled.div` + height: 4rem; +`; + +const Inner = styled(HomeResponsive)` + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; +`; + +const Right = styled.div` + display: flex; + align-items: center; +`; + +export default HomeHeader; diff --git a/src/components/home/HomeLayout.tsx b/src/components/home/HomeLayout.tsx new file mode 100644 index 00000000..f1bf4427 --- /dev/null +++ b/src/components/home/HomeLayout.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styled from 'styled-components'; + +export type HomeLayoutProps = { + main: React.ReactNode; + side: React.ReactNode; +}; + +function HomeLayout({ main, side }: HomeLayoutProps) { + return ( + +
{main}
+ {side} +
+ ); +} + +const Block = styled.div` + display: flex; + margin-top: 2rem; +`; +const Main = styled.main` + flex: 1; +`; +const Side = styled.aside` + margin-left: 6rem; + width: 16rem; +`; + +export default HomeLayout; diff --git a/src/components/home/HomeResponsive.tsx b/src/components/home/HomeResponsive.tsx new file mode 100644 index 00000000..eec78504 --- /dev/null +++ b/src/components/home/HomeResponsive.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import styled from 'styled-components'; + +export type HomeResponsiveProps = { + className?: string; + children: React.ReactNode; +}; + +function HomeResponsive({ className, children }: HomeResponsiveProps) { + return {children}; +} + +const Block = styled.div` + width: 1728px; + margin-left: auto; + margin-right: auto; +`; + +export default HomeResponsive; diff --git a/src/components/home/HomeTab.tsx b/src/components/home/HomeTab.tsx new file mode 100644 index 00000000..78b04935 --- /dev/null +++ b/src/components/home/HomeTab.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import styled from 'styled-components'; +import { NavLink, useLocation } from 'react-router-dom'; +import palette from '../../lib/styles/palette'; +import { MdTrendingUp, MdAccessTime } from 'react-icons/md'; +import { useSpring, animated } from 'react-spring'; + +export type HomeTabProps = {}; + +function HomeTab(props: HomeTabProps) { + const location = useLocation(); + + const isRecent = location.pathname === '/recent'; + + const springStyle = useSpring({ + left: isRecent ? '50%' : '0%', + config: { + friction: 16, + tensiton: 160, + }, + }); + + return ( + + { + return ['/', '/trending'].indexOf(location.pathname) !== -1; + }} + > + + 트렌딩 + + + + 최신 + + + + ); +} + +const Block = styled.div` + margin-top: 1.5rem; + display: flex; + position: relative; + width: 14rem; + a { + width: 7rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.125rem; + text-decoration: none; + color: ${palette.gray6}; + height: 2.875rem; + svg { + font-size: 1.5rem; + margin-right: 0.5rem; + } + &.active { + color: ${palette.gray8}; + font-weight: bold; + } + } +`; + +const Indicator = styled(animated.div)` + width: 50%; + height: 2px; + position: absolute; + bottom: 0px; + background: ${palette.gray8}; +`; + +export default HomeTab; diff --git a/src/components/home/HomeTemplate.tsx b/src/components/home/HomeTemplate.tsx new file mode 100644 index 00000000..5ace3bdb --- /dev/null +++ b/src/components/home/HomeTemplate.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled, { createGlobalStyle } from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { Link } from 'react-router-dom'; + +const BackgroundStyle = createGlobalStyle` + body { + background: ${palette.gray0}; + } +`; + +export type HomeTemplateProps = { + children: React.ReactNode; +}; + +function HomeTemplate({ children }: HomeTemplateProps) { + return ( + <> + + {children} + + ); +} + +const Block = styled.div``; + +export default HomeTemplate; diff --git a/src/containers/main/RecentPosts.tsx b/src/containers/main/RecentPosts.tsx index 57008c1e..26147bd4 100644 --- a/src/containers/main/RecentPosts.tsx +++ b/src/containers/main/RecentPosts.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; import { useQuery } from '@apollo/react-hooks'; import useScrollPagination from '../../lib/hooks/useScrollPagination'; diff --git a/src/containers/main/TrendingPosts.tsx b/src/containers/main/TrendingPosts.tsx index 5011d646..43d1567b 100644 --- a/src/containers/main/TrendingPosts.tsx +++ b/src/containers/main/TrendingPosts.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import { GET_TRENDING_POSTS, GetTrendingPostsResponse, diff --git a/src/containers/search/SearchResult.tsx b/src/containers/search/SearchResult.tsx index 2386ffdb..eadc000b 100644 --- a/src/containers/search/SearchResult.tsx +++ b/src/containers/search/SearchResult.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import SearchResultInfo from '../../components/search/SearchResultInfo'; -import PostCardList from '../../components/common/PostCardList'; +import PostCardList from '../../components/common/FlatPostCardList'; import { useQuery } from '@apollo/react-hooks'; import { SearchPostsResponse, SEARCH_POSTS } from '../../lib/graphql/post'; import useScrollPagination from '../../lib/hooks/useScrollPagination'; diff --git a/src/containers/tags/TagDetailContainer.tsx b/src/containers/tags/TagDetailContainer.tsx index 107dcdaf..4e7f2545 100644 --- a/src/containers/tags/TagDetailContainer.tsx +++ b/src/containers/tags/TagDetailContainer.tsx @@ -7,7 +7,7 @@ import { safe, ssrEnabled } from '../../lib/utils'; import useScrollPagination from '../../lib/hooks/useScrollPagination'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import useNotFound from '../../lib/hooks/useNotFound'; import { Helmet } from 'react-helmet-async'; diff --git a/src/containers/velog/UserPosts.tsx b/src/containers/velog/UserPosts.tsx index 74e9ad47..cc18a4e3 100644 --- a/src/containers/velog/UserPosts.tsx +++ b/src/containers/velog/UserPosts.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; import { useQuery } from '@apollo/react-hooks'; import PaginateWithScroll from '../../components/common/PaginateWithScroll'; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 9a9b8bd3..9b5b333a 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -54,6 +54,7 @@ export type PartialPost = { updated_at: string; tags: string[]; comments_count: number; + likes: number; }; // Generated by https://quicktype.io @@ -172,6 +173,7 @@ export const GET_TRENDING_POSTS = gql` title short_description thumbnail + likes user { id username diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx new file mode 100644 index 00000000..e8250447 --- /dev/null +++ b/src/pages/home/HomePage.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import HomeTemplate from '../../components/home/HomeTemplate'; +import HomeHeader from '../../components/home/HomeHeader'; +import HomeTab from '../../components/home/HomeTab'; +import HomeResponsive from '../../components/home/HomeResponsive'; +import HomeLayout from '../../components/home/HomeLayout'; +import { Route } from 'react-router-dom'; +import TrendingPostsPage from './TrendingPostsPage'; +import RecentPostsPage from './RecentPostsPage'; + +export type HomePageProps = {}; + +function HomePage(props: HomePageProps) { + return ( + + + + + + + + + } + side={
Hello
} + /> +
+
+ ); +} + +export default HomePage; diff --git a/src/pages/home/RecentPostsPage.tsx b/src/pages/home/RecentPostsPage.tsx new file mode 100644 index 00000000..8361183a --- /dev/null +++ b/src/pages/home/RecentPostsPage.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export type RecentPostsPageProps = {}; + +function RecentPostsPage(props: RecentPostsPageProps) { + return
몰라용
; +} + +export default RecentPostsPage; diff --git a/src/pages/home/TrendingPostsPage.tsx b/src/pages/home/TrendingPostsPage.tsx new file mode 100644 index 00000000..65b5ebe6 --- /dev/null +++ b/src/pages/home/TrendingPostsPage.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PostCardGrid from '../../components/common/PostCardGrid'; +import useTrendingPosts from './hooks/useTrendingPosts'; + +export type TrendingPageProps = {}; + +function TrendingPage(props: TrendingPageProps) { + const { data, loading } = useTrendingPosts(); + + return ; +} + +export default TrendingPage; diff --git a/src/pages/home/hooks/useTrendingPosts.ts b/src/pages/home/hooks/useTrendingPosts.ts new file mode 100644 index 00000000..563d976c --- /dev/null +++ b/src/pages/home/hooks/useTrendingPosts.ts @@ -0,0 +1,13 @@ +import { + GET_TRENDING_POSTS, + GetTrendingPostsResponse, +} from '../../../lib/graphql/post'; +import { useQuery } from '@apollo/react-hooks'; + +export default function useTrendingPosts() { + const { data, loading } = useQuery( + GET_TRENDING_POSTS, + ); + + return { data, loading }; +} From 30e0200efad2b7fa55074c74aa31c5d9483a6fd7 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 26 Feb 2020 00:32:18 +0900 Subject: [PATCH 028/449] Resolve issue #74 Set fixed height to PublishSeriesList --- src/containers/write/PublishSeriesList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/write/PublishSeriesList.tsx b/src/containers/write/PublishSeriesList.tsx index 8ab10801..ac2f4e02 100644 --- a/src/containers/write/PublishSeriesList.tsx +++ b/src/containers/write/PublishSeriesList.tsx @@ -2,7 +2,8 @@ import SelectableList from '../../components/common/SelectableList'; import styled from 'styled-components'; const PublishSeriesList = styled(SelectableList)` - flex: 1; + height: 16.5625rem; + overflow-y: auto; `; export default PublishSeriesList; From ecd41df5fb611692e631596225ce6505469c80c7 Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 27 Feb 2020 23:06:06 +0900 Subject: [PATCH 029/449] Implement post loading --- src/components/common/PostCard.tsx | 78 +++++++++++++++--------- src/components/common/PostCardGrid.tsx | 18 +++--- src/components/home/HomeSidebar.tsx | 24 ++++++++ src/lib/graphql/post.ts | 1 + src/pages/home/HomePage.tsx | 3 +- src/pages/home/RecentPostsPage.tsx | 7 ++- src/pages/home/TrendingPostsPage.tsx | 3 +- src/pages/home/hooks/useRecentPosts.ts | 37 +++++++++++ src/pages/home/hooks/useTrendingPosts.ts | 40 +++++++++++- 9 files changed, 169 insertions(+), 42 deletions(-) create mode 100644 src/components/home/HomeSidebar.tsx create mode 100644 src/pages/home/hooks/useRecentPosts.ts diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index 97ec8089..fd1d2f2f 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -1,46 +1,55 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import RatioImage from './RatioImage'; import { ellipsis } from '../../lib/styles/utils'; import palette from '../../lib/styles/palette'; import { LikeIcon } from '../../static/svg'; +import { PartialPost } from '../../lib/graphql/post'; +import { formatDate } from '../../lib/utils'; +import { userThumbnail } from '../../static/images'; +import optimizeImage from '../../lib/optimizeImage'; -export type PostCardProps = {}; +export type PostCardProps = { + post: PartialPost; +}; -function PostCard(props: PostCardProps) { +function PostCard({ post }: PostCardProps) { return ( - - -

벨로그 v2 업데이트 안내

-

- Node.js, PHP 등 익숙한 언어들을 던지고 생뚱맞은 장고를 택한 이유는 단 - 하나였습니다. 장고를 사용하시는 분들이 가장 많이 이야기하는, - 생산성입니다 내용이 더 길어지면 엉떻게 되니닌 -

+ {post.thumbnail && ( + + )} + +

{post.title}

+
+

+ {post.short_description} + {post.short_description.length === 150 && '...'} +

+
- 2020년 2월 10일 + {formatDate(post.released_at)} · - 64개의 댓글 + {post.comments_count}개의 댓글
post-thumbnail - by velopert + by {post.user.username}
- 65 + {post.likes}
@@ -54,10 +63,15 @@ const Block = styled.div` box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.04); margin: 1rem; overflow: hidden; + display: flex; + flex-direction: column; `; -const Content = styled.div` +const Content = styled.div<{ clamp: boolean }>` padding: 1rem; + display: flex; + flex: 1; + flex-direction: column; h4 { font-size: 1rem; margin: 0; @@ -66,18 +80,26 @@ const Content = styled.div` ${ellipsis} color: ${palette.gray9}; } + .description-wrapper { + flex: 1; + } p { margin: 0; word-break: keep-all; overflow-wrap: break-word; font-size: 0.875rem; line-height: 1.5; - height: 3.9375rem; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; + ${props => + props.clamp && + css` + height: 3.9375rem; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + `} + color: ${palette.gray7}; margin-bottom: 1.5rem; } diff --git a/src/components/common/PostCardGrid.tsx b/src/components/common/PostCardGrid.tsx index 9b5bbff5..04fc6458 100644 --- a/src/components/common/PostCardGrid.tsx +++ b/src/components/common/PostCardGrid.tsx @@ -1,20 +1,18 @@ import React from 'react'; import styled from 'styled-components'; import PostCard from './PostCard'; +import { PartialPost } from '../../lib/graphql/post'; -export type PostCardGridProps = {}; +export type PostCardGridProps = { + posts: PartialPost[]; +}; -function PostCardGrid(props: PostCardGridProps) { +function PostCardGrid({ posts }: PostCardGridProps) { return ( - - - - - - - - + {posts.map(post => ( + + ))} ); } diff --git a/src/components/home/HomeSidebar.tsx b/src/components/home/HomeSidebar.tsx new file mode 100644 index 00000000..9b165d80 --- /dev/null +++ b/src/components/home/HomeSidebar.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import styled from 'styled-components'; +import MainNoticeWidgetContainer from '../../containers/main/MainNoticeWidgetContainer'; +import MainTagWidgetContainer from '../../containers/main/MainTagWidgetContainer'; +import MainRightFooter from '../main/MainRightFooter'; +import Sticky from '../common/Sticky'; + +export type HomeSidebarProps = {}; + +function HomeSidebar(props: HomeSidebarProps) { + return ( + + + + + + + + ); +} + +const Block = styled.div``; + +export default HomeSidebar; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 9b5b333a..ed2af1b5 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -162,6 +162,7 @@ export const GET_POST_LIST = gql` comments_count tags is_private + likes } } `; diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index e8250447..eddc06e0 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -7,6 +7,7 @@ import HomeLayout from '../../components/home/HomeLayout'; import { Route } from 'react-router-dom'; import TrendingPostsPage from './TrendingPostsPage'; import RecentPostsPage from './RecentPostsPage'; +import HomeSidebar from '../../components/home/HomeSidebar'; export type HomePageProps = {}; @@ -27,7 +28,7 @@ function HomePage(props: HomePageProps) { } - side={
Hello
} + side={} /> diff --git a/src/pages/home/RecentPostsPage.tsx b/src/pages/home/RecentPostsPage.tsx index 8361183a..c21d57ee 100644 --- a/src/pages/home/RecentPostsPage.tsx +++ b/src/pages/home/RecentPostsPage.tsx @@ -1,9 +1,14 @@ import React from 'react'; +import useRecentPosts from './hooks/useRecentPosts'; +import PostCardGrid from '../../components/common/PostCardGrid'; export type RecentPostsPageProps = {}; function RecentPostsPage(props: RecentPostsPageProps) { - return
몰라용
; + const { data, loading } = useRecentPosts(); + + if (!data) return null; + return ; } export default RecentPostsPage; diff --git a/src/pages/home/TrendingPostsPage.tsx b/src/pages/home/TrendingPostsPage.tsx index 65b5ebe6..ac0e4702 100644 --- a/src/pages/home/TrendingPostsPage.tsx +++ b/src/pages/home/TrendingPostsPage.tsx @@ -7,7 +7,8 @@ export type TrendingPageProps = {}; function TrendingPage(props: TrendingPageProps) { const { data, loading } = useTrendingPosts(); - return ; + if (!data) return null; + return ; } export default TrendingPage; diff --git a/src/pages/home/hooks/useRecentPosts.ts b/src/pages/home/hooks/useRecentPosts.ts new file mode 100644 index 00000000..0e898c31 --- /dev/null +++ b/src/pages/home/hooks/useRecentPosts.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@apollo/react-hooks'; +import { GET_POST_LIST, PartialPost } from '../../../lib/graphql/post'; +import { useCallback } from 'react'; +import useScrollPagination from '../../../lib/hooks/useScrollPagination'; + +export default function useRecentPosts() { + const { data, loading, fetchMore } = useQuery<{ posts: PartialPost[] }>( + GET_POST_LIST, + {}, + ); + + const onLoadMore = useCallback( + (cursor: string) => { + fetchMore({ + variables: { + cursor, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return { + posts: [...prev.posts, ...fetchMoreResult.posts], + }; + }, + }); + }, + [fetchMore], + ); + + const cursor = data?.posts[data?.posts.length - 1]?.id; + + useScrollPagination({ + cursor, + onLoadMore, + }); + + return { data, loading }; +} diff --git a/src/pages/home/hooks/useTrendingPosts.ts b/src/pages/home/hooks/useTrendingPosts.ts index 563d976c..afa21055 100644 --- a/src/pages/home/hooks/useTrendingPosts.ts +++ b/src/pages/home/hooks/useTrendingPosts.ts @@ -3,11 +3,49 @@ import { GetTrendingPostsResponse, } from '../../../lib/graphql/post'; import { useQuery } from '@apollo/react-hooks'; +import { useCallback } from 'react'; +import useScrollPagination from '../../../lib/hooks/useScrollPagination'; export default function useTrendingPosts() { - const { data, loading } = useQuery( + const { data, loading, fetchMore } = useQuery( GET_TRENDING_POSTS, ); + const onLoadMoreByOffset = useCallback( + (offset: number) => { + fetchMore({ + variables: { + offset, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + + // filter unique posts + const idMap: Record = prev.trendingPosts.reduce( + (acc, current) => { + Object.assign(acc, { [current.id]: true }); + return acc; + }, + {}, + ); + + const uniquePosts = fetchMoreResult.trendingPosts.filter( + post => !idMap[post.id], + ); + + return { + trendingPosts: [...prev.trendingPosts, ...uniquePosts], + }; + }, + }); + }, + [fetchMore], + ); + + useScrollPagination({ + offset: data?.trendingPosts.length, + onLoadMoreByOffset, + }); + return { data, loading }; } From c63f99a25ba48042bd8b0beab86d046a3959b9c5 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 1 Mar 2020 17:52:01 +0900 Subject: [PATCH 030/449] Implement responsive design to grid --- package.json | 2 + src/components/base/Header.tsx | 109 +++++++-------- src/components/common/PostCard.tsx | 147 ++++++++++++++++++--- src/components/common/PostCardGrid.tsx | 16 ++- src/components/home/FloatingHomeHeader.tsx | 93 +++++++++++++ src/components/home/HomeHeader.tsx | 30 ++++- src/components/home/HomeLayout.tsx | 8 ++ src/components/home/HomeResponsive.tsx | 16 +++ src/components/home/HomeSidebar.tsx | 2 +- src/components/home/hooks/useHeader.ts | 25 ++++ src/components/main/MainRightFooter.tsx | 4 + src/containers/main/TrendingPosts.tsx | 2 +- src/lib/graphql/post.ts | 2 + src/lib/hooks/useScrollPagination.ts | 23 +++- src/pages/home/HomePage.tsx | 2 + src/pages/home/RecentPostsPage.tsx | 13 +- src/pages/home/TrendingPostsPage.tsx | 16 ++- src/pages/home/hooks/useRecentPosts.ts | 17 ++- src/pages/home/hooks/useTrendingPosts.ts | 16 ++- yarn.lock | 34 ++++- 20 files changed, 479 insertions(+), 98 deletions(-) create mode 100644 src/components/home/FloatingHomeHeader.tsx create mode 100644 src/components/home/hooks/useHeader.ts diff --git a/package.json b/package.json index 1b7ea214..fc4d1ea5 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/react-router-dom": "^5.1.3", "@types/react-textarea-autosize": "^4.3.5", "@types/react-toastify": "^4.1.0", + "@types/react-virtualized": "^9.21.8", "@types/sanitize-html": "^1.20.2", "@types/styled-components": "^4.4.1", "@types/throttle-debounce": "^2.1.0", @@ -120,6 +121,7 @@ "react-textarea-autosize": "^7.1.2", "react-toastify": "^5.5.0", "react-use": "^13.12.2", + "react-virtualized": "^9.21.2", "redux": "^4.0.4", "redux-devtools-extension": "^2.13.8", "remark": "^11.0.2", diff --git a/src/components/base/Header.tsx b/src/components/base/Header.tsx index 336c281d..7ee2766e 100644 --- a/src/components/base/Header.tsx +++ b/src/components/base/Header.tsx @@ -12,15 +12,16 @@ import HeaderLogo from './HeaderLogo'; import media from '../../lib/styles/media'; import { SearchIcon2 } from '../../static/svg'; import { Link } from 'react-router-dom'; +import HomeResponsive from '../home/HomeResponsive'; const HeaderBlock = styled.div<{ floating: boolean }>` width: 100%; - > .wrapper { - width: 1200px; + .wrapper { + /* width: 1200px; */ + width: 100%; height: 4rem; - margin: 0 auto; - padding-left: 1rem; - padding-right: 1rem; + /* padding-left: 1rem; + padding-right: 1rem; */ display: flex; justify-content: space-between; align-items: center; @@ -36,10 +37,10 @@ const HeaderBlock = styled.div<{ floating: boolean }>` } ${media.large} { - width: 1024px; + /* width: 1024px; */ } ${media.medium} { - width: 100%; + /* width: 100%; */ .write-button { display: none; } @@ -122,63 +123,65 @@ const Header: React.FC = ({ style={{ marginTop: floating ? floatingMargin : 0 }} data-testid="Header" > -
-
- -
-
- {/* {velogUsername ? ( + +
+
+ +
+
+ {/* {velogUsername ? ( ) : ( )} */} - {!isSearch && ( - - - - )} - {user ? ( -
+ {!isSearch && ( + + + + )} + {user ? ( +
+ + 새 글 작성 + + + +
+ ) : ( - 새 글 작성 + 로그인 - - -
- ) : ( - - 로그인 - - )} + )} +
-
+ {floating && } diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index fd1d2f2f..e12e4b56 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -8,29 +8,39 @@ import { PartialPost } from '../../lib/graphql/post'; import { formatDate } from '../../lib/utils'; import { userThumbnail } from '../../static/images'; import optimizeImage from '../../lib/optimizeImage'; +import SkeletonTexts from './SkeletonTexts'; +import Skeleton from './Skeleton'; +import { mediaQuery } from '../../lib/styles/media'; +import { Link } from 'react-router-dom'; export type PostCardProps = { post: PartialPost; }; function PostCard({ post }: PostCardProps) { + const url = `/@${post.user.username}/${post.url_slug}`; + return ( {post.thumbnail && ( - + + + )} -

{post.title}

-
-

- {post.short_description} - {post.short_description.length === 150 && '...'} -

-
+ +

{post.title}

+
+

+ {post.short_description.replace(/:/g, ':')} + {post.short_description.length === 150 && '...'} +

+
+
{formatDate(post.released_at)} · @@ -38,7 +48,7 @@ function PostCard({ post }: PostCardProps) {