diff --git a/.eslintignore b/.eslintignore index 09abc3355..099006ba9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,4 +8,5 @@ next-env.d.ts *.png *.jpeg *.txt -*.md \ No newline at end of file +*.md +*.ico \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e0bb177b2..e07dd663f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "license": "Apache License 2.0", "dependencies": { "@babel/core": "^7.13.14", - "@groupher/react-editor": "^1.1.26", + "@groupher/react-editor": "^1.1.28", "@next/bundle-analyzer": "^9.4.4", "@sentry/browser": "5.17.0", "@sentry/node": "5.17.0", @@ -80,6 +80,7 @@ "resize-observer-polyfill": "^1.5.1", "response-time": "^2.3.2", "rxjs": "6.2.2", + "sanitize-html": "^2.5.2", "scroll-into-view-if-needed": "1.5.0", "sentry-testkit": "^2.1.0", "store": "^2.0.12", @@ -1406,8 +1407,9 @@ } }, "node_modules/@groupher/editor-inline-code": { - "version": "1.0.4", - "integrity": "sha512-R7fUsMo7raFjrGyuzc1Su7T68giUTY2G1UWP5B6sn6BtE0M2JQajLCsgJzBE6/sbfMds5kubuNkNrd7KoeNjrA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@groupher/editor-inline-code/-/editor-inline-code-1.0.5.tgz", + "integrity": "sha512-60wJuSs3sYSVTcZwuhs1ZdgzK9F4OXl0RARv+yg/wJRMBASGZeothWAuksW0qeV39k9mUhB2z3QE5H0HG+/95Q==" }, "node_modules/@groupher/editor-link": { "version": "2.1.6", @@ -1469,8 +1471,9 @@ } }, "node_modules/@groupher/editor-strike": { - "version": "0.1.3", - "integrity": "sha512-mL/E2DfKsyYdXvfo1MX6Xp4xLxOZozpSx2I+f02JY5tlqhKH/pamQYFh4h+9/2wseWwOwCwQTjp461MbSWu+fQ==" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@groupher/editor-strike/-/editor-strike-0.1.4.tgz", + "integrity": "sha512-e7Lkv8Frb7Xf6u+B3J0qbQ4JYxFqMtF7EIqPBZIVsoMO0jVS4KhU+19menI6ewMPf6Duclts9qk1Js51Y2LNKg==" }, "node_modules/@groupher/editor-table": { "version": "2.0.8", @@ -1496,9 +1499,9 @@ } }, "node_modules/@groupher/editor.js": { - "version": "2.19.38", - "resolved": "https://registry.npmjs.org/@groupher/editor.js/-/editor.js-2.19.38.tgz", - "integrity": "sha512-vxOkNZlqRKc7oRuWUFsgmzkN8KE8GwCG6s0vyEWM8ZX8wmUK0Fzvcs4lneodRCETESLOxnQzBj+cSHbaRF6Pew==", + "version": "2.19.39", + "resolved": "https://registry.npmjs.org/@groupher/editor.js/-/editor.js-2.19.39.tgz", + "integrity": "sha512-30f7hvD9OvSlvXTXQ+oPoKtchgvFXFDuMV+W0BrTr54ZIXJkSMlReMgujfBtHgow05UJHFDMqI3L5/FS/TSdeg==", "dependencies": { "caniuse-lite": "^1.0.30001164", "codex-notifier": "^1.1.2", @@ -1534,9 +1537,9 @@ } }, "node_modules/@groupher/react-editor": { - "version": "1.1.26", - "resolved": "https://registry.npmjs.org/@groupher/react-editor/-/react-editor-1.1.26.tgz", - "integrity": "sha512-bDDKLMMVQHfu3igQqn8C3LGxobjtcAfO+1dhfkXapUGsEiutbLeZK+5hmz+BTpjQiWtLgkxU0tKCQtoG5Vmv+A==", + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@groupher/react-editor/-/react-editor-1.1.28.tgz", + "integrity": "sha512-xKc7lxRELIZ6+Nsv1ktX/c5L14Bl2Nq9hcR237z9zz+I9uCOT2JnIUKvR9qzlE7infJrHG9Vcgpm2x1DuEUf1w==", "dependencies": { "@groupher/editor-alert": "^1.0.27", "@groupher/editor-code": "^1.0.30", @@ -1546,7 +1549,7 @@ "@groupher/editor-eventbus": "1.0.6", "@groupher/editor-header": "2.3.2", "@groupher/editor-image": "^1.0.21", - "@groupher/editor-inline-code": "^1.0.4", + "@groupher/editor-inline-code": "^1.0.5", "@groupher/editor-link": "2.1.6", "@groupher/editor-list": "^1.4.10", "@groupher/editor-lock": "0.1.2", @@ -1554,9 +1557,9 @@ "@groupher/editor-mention": "^1.0.8", "@groupher/editor-paragraph": "^1.0.17", "@groupher/editor-quote": "^2.3.9", - "@groupher/editor-strike": "^0.1.3", + "@groupher/editor-strike": "^0.1.4", "@groupher/editor-table": "^2.0.8", - "@groupher/editor.js": "^2.19.38", + "@groupher/editor.js": "^2.19.39", "editorjs-drag-drop": "^0.2.1", "editorjs-undo": "^0.3.0", "react": "^17.x", @@ -8269,7 +8272,6 @@ "node_modules/dom-serializer": { "version": "1.3.2", "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -8292,7 +8294,6 @@ "node_modules/domelementtype": { "version": "2.2.0", "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true, "funding": [ { "type": "github", @@ -8322,7 +8323,6 @@ "node_modules/domhandler": { "version": "4.2.2", "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", - "dev": true, "dependencies": { "domelementtype": "^2.2.0" }, @@ -8336,7 +8336,6 @@ "node_modules/domutils": { "version": "2.8.0", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -8591,7 +8590,6 @@ "node_modules/entities": { "version": "2.2.0", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -11663,7 +11661,6 @@ "node_modules/htmlparser2": { "version": "6.1.0", "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -14143,6 +14140,14 @@ "node": ">=6" } }, + "node_modules/klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.21", "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==" @@ -16720,6 +16725,11 @@ "node": ">=0.10.0" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + }, "node_modules/parse5": { "version": "6.0.1", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", @@ -18834,6 +18844,47 @@ "node": ">=0.10.0" } }, + "node_modules/sanitize-html": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.5.2.tgz", + "integrity": "sha512-sJ1rO2YixFIqs2kIcEUb6PTrCjvz8DMq1XqWWuy0kjgjrn58GNLK1DKSIRybFZDO1WNgsEgD+WiEzTEYS8xEug==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "klona": "^2.0.3", + "parse-srcset": "^1.0.2", + "postcss": "^8.0.2" + } + }, + "node_modules/sanitize-html/node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sanitize-html/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/saxes": { "version": "5.0.1", "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", @@ -23429,8 +23480,9 @@ } }, "@groupher/editor-inline-code": { - "version": "1.0.4", - "integrity": "sha512-R7fUsMo7raFjrGyuzc1Su7T68giUTY2G1UWP5B6sn6BtE0M2JQajLCsgJzBE6/sbfMds5kubuNkNrd7KoeNjrA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@groupher/editor-inline-code/-/editor-inline-code-1.0.5.tgz", + "integrity": "sha512-60wJuSs3sYSVTcZwuhs1ZdgzK9F4OXl0RARv+yg/wJRMBASGZeothWAuksW0qeV39k9mUhB2z3QE5H0HG+/95Q==" }, "@groupher/editor-link": { "version": "2.1.6", @@ -23494,8 +23546,9 @@ } }, "@groupher/editor-strike": { - "version": "0.1.3", - "integrity": "sha512-mL/E2DfKsyYdXvfo1MX6Xp4xLxOZozpSx2I+f02JY5tlqhKH/pamQYFh4h+9/2wseWwOwCwQTjp461MbSWu+fQ==" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@groupher/editor-strike/-/editor-strike-0.1.4.tgz", + "integrity": "sha512-e7Lkv8Frb7Xf6u+B3J0qbQ4JYxFqMtF7EIqPBZIVsoMO0jVS4KhU+19menI6ewMPf6Duclts9qk1Js51Y2LNKg==" }, "@groupher/editor-table": { "version": "2.0.8", @@ -23523,9 +23576,9 @@ } }, "@groupher/editor.js": { - "version": "2.19.38", - "resolved": "https://registry.npmjs.org/@groupher/editor.js/-/editor.js-2.19.38.tgz", - "integrity": "sha512-vxOkNZlqRKc7oRuWUFsgmzkN8KE8GwCG6s0vyEWM8ZX8wmUK0Fzvcs4lneodRCETESLOxnQzBj+cSHbaRF6Pew==", + "version": "2.19.39", + "resolved": "https://registry.npmjs.org/@groupher/editor.js/-/editor.js-2.19.39.tgz", + "integrity": "sha512-30f7hvD9OvSlvXTXQ+oPoKtchgvFXFDuMV+W0BrTr54ZIXJkSMlReMgujfBtHgow05UJHFDMqI3L5/FS/TSdeg==", "requires": { "caniuse-lite": "^1.0.30001164", "codex-notifier": "^1.1.2", @@ -23557,9 +23610,9 @@ } }, "@groupher/react-editor": { - "version": "1.1.26", - "resolved": "https://registry.npmjs.org/@groupher/react-editor/-/react-editor-1.1.26.tgz", - "integrity": "sha512-bDDKLMMVQHfu3igQqn8C3LGxobjtcAfO+1dhfkXapUGsEiutbLeZK+5hmz+BTpjQiWtLgkxU0tKCQtoG5Vmv+A==", + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@groupher/react-editor/-/react-editor-1.1.28.tgz", + "integrity": "sha512-xKc7lxRELIZ6+Nsv1ktX/c5L14Bl2Nq9hcR237z9zz+I9uCOT2JnIUKvR9qzlE7infJrHG9Vcgpm2x1DuEUf1w==", "requires": { "@groupher/editor-alert": "^1.0.27", "@groupher/editor-code": "^1.0.30", @@ -23569,7 +23622,7 @@ "@groupher/editor-eventbus": "1.0.6", "@groupher/editor-header": "2.3.2", "@groupher/editor-image": "^1.0.21", - "@groupher/editor-inline-code": "^1.0.4", + "@groupher/editor-inline-code": "^1.0.5", "@groupher/editor-link": "2.1.6", "@groupher/editor-list": "^1.4.10", "@groupher/editor-lock": "0.1.2", @@ -23577,9 +23630,9 @@ "@groupher/editor-mention": "^1.0.8", "@groupher/editor-paragraph": "^1.0.17", "@groupher/editor-quote": "^2.3.9", - "@groupher/editor-strike": "^0.1.3", + "@groupher/editor-strike": "^0.1.4", "@groupher/editor-table": "^2.0.8", - "@groupher/editor.js": "^2.19.38", + "@groupher/editor.js": "^2.19.39", "editorjs-drag-drop": "^0.2.1", "editorjs-undo": "^0.3.0", "react": "^17.x", @@ -28881,7 +28934,6 @@ "dom-serializer": { "version": "1.3.2", "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, "requires": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -28894,8 +28946,7 @@ }, "domelementtype": { "version": "2.2.0", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" }, "domexception": { "version": "2.0.1", @@ -28915,7 +28966,6 @@ "domhandler": { "version": "4.2.2", "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", - "dev": true, "requires": { "domelementtype": "^2.2.0" } @@ -28923,7 +28973,6 @@ "domutils": { "version": "2.8.0", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, "requires": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -29128,8 +29177,7 @@ }, "entities": { "version": "2.2.0", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" }, "enzyme": { "version": "3.11.0", @@ -31510,7 +31558,6 @@ "htmlparser2": { "version": "6.1.0", "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, "requires": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -33317,6 +33364,11 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" + }, "language-subtag-registry": { "version": "0.3.21", "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==" @@ -35283,6 +35335,11 @@ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + }, "parse5": { "version": "6.0.1", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", @@ -36881,6 +36938,37 @@ } } }, + "sanitize-html": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.5.2.tgz", + "integrity": "sha512-sJ1rO2YixFIqs2kIcEUb6PTrCjvz8DMq1XqWWuy0kjgjrn58GNLK1DKSIRybFZDO1WNgsEgD+WiEzTEYS8xEug==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "klona": "^2.0.3", + "parse-srcset": "^1.0.2", + "postcss": "^8.0.2" + }, + "dependencies": { + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, "saxes": { "version": "5.0.1", "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", diff --git a/package.json b/package.json index ab9522ac4..74c606bbe 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "dependencies": { "@babel/core": "^7.13.14", - "@groupher/react-editor": "^1.1.26", + "@groupher/react-editor": "^1.1.28", "@next/bundle-analyzer": "^9.4.4", "@sentry/browser": "5.17.0", "@sentry/node": "5.17.0", @@ -112,6 +112,7 @@ "resize-observer-polyfill": "^1.5.1", "response-time": "^2.3.2", "rxjs": "6.2.2", + "sanitize-html": "^2.5.2", "scroll-into-view-if-needed": "1.5.0", "sentry-testkit": "^2.1.0", "store": "^2.0.12", diff --git a/public/favicon.ico b/public/favicon.ico index 3646fced4..1566d7dc7 100755 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/server/helper.js b/server/helper.js index f860689b9..a8d2b2372 100644 --- a/server/helper.js +++ b/server/helper.js @@ -9,6 +9,7 @@ const renderAndCache = ({ req, res, path }) => { // do not cache in dev mode if (dev) { const pagePath = path || req.path + console.log('# pagePath: ', pagePath) return app.render(req, res, pagePath, { ...req.query, ...req.params, diff --git a/server/routes.js b/server/routes.js index dced7a289..e76b1d79f 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const router = require('express').Router() -const R = require('ramda') const app = require('./app') const { renderAndCache } = require('./helper') @@ -22,7 +21,7 @@ router.route('/oauth/').get((req, res) => renderAndCache({ req, res })) // 将首页重定向到 HOME_PAGE router .route('/') - .get((req, res) => renderAndCache({ req, res, path: '/community' })) + .get((req, res) => renderAndCache({ req, res, path: '/index' })) // 来一杯 router.route('/have-a-drink/:slug?').get((req, res) => { @@ -85,7 +84,8 @@ router.route('/user/:userId').get((req, res) => { // 帖子页 router.route('/post/:id').get((req, res) => { - return renderAndCache({ req, res, path: '/post' }) + const { id } = req.params + return renderAndCache({ req, res, path: `/post/${id}` }) }) // job 帖子页 @@ -99,28 +99,34 @@ router.route('/:community/video/:id').get((req, res) => { }) // repo 帖子页 -router.route('/:community/repo/:id').get((req, res) => { - return renderAndCache({ req, res, path: '/repo' }) -}) +// router.route('/:community/repo/:id').get((req, res) => { +// return renderAndCache({ req, res, path: '/repo' }) +// }) // 创建新社区 -router.route('/create/community').get((req, res) => { - return renderAndCache({ req, res, page: '/create/community' }) +router.route('/publish/community').get((req, res) => { + return renderAndCache({ req, res, page: '/publish/community' }) +}) + +// 创建新帖子 +router.route('/publish/post').get((req, res) => { + return renderAndCache({ req, res, page: '/publish/post' }) }) -// 创建新内容 -router.route('/create/article').get((req, res) => { - return renderAndCache({ req, res, page: '/create/article' }) +// 编辑新帖子 +router.route('/update/post/:id').get((req, res) => { + const { id } = req.params + return renderAndCache({ req, res, path: `/update/post/${id}` }) }) // 创建新博客 -router.route('/create/blog').get((req, res) => { - return renderAndCache({ req, res, page: '/create/blog' }) +router.route('/publish/blog').get((req, res) => { + return renderAndCache({ req, res, page: '/publish/blog' }) }) // 创建新作品 -router.route('/create/works').get((req, res) => { - return renderAndCache({ req, res, page: '/create/works' }) +router.route('/publish/works').get((req, res) => { + return renderAndCache({ req, res, page: '/publish/works' }) }) // 所有社区 @@ -137,16 +143,7 @@ router.route('/:community/help-center').get((req, res) => { // 社区主页 router.route('/:community/:thread').get((req, res) => { - if ( - R.has('preview', req.query) && - R.has('id', req.query) && - R.has('community', req.query) - ) { - const { community, preview, id } = req.query - return res.redirect(`/${community}/${preview}/${id}`) - } - - return renderAndCache({ req, res, path: '/community' }) + return renderAndCache({ req, res, path: '/index' }) }) router.route('*').get((req, res) => handle(req, res)) diff --git a/src/components/ArticleCard/Header.tsx b/src/components/ArticleCard/Header.tsx index 36c967144..4eb4dbcdd 100644 --- a/src/components/ArticleCard/Header.tsx +++ b/src/components/ArticleCard/Header.tsx @@ -4,7 +4,7 @@ import type { TJob, TRadar } from '@/spec' import { ICON } from '@/config' import { THREAD } from '@/constant' import { cutRest } from '@/utils/helper' -import InlineTags from '@/components/InlineTags' +import TagsList from '@/components/TagsList' import { Br } from '@/components/Common' import type { TProps as TIndex } from './index' @@ -33,7 +33,7 @@ const Header: FC = ({ data, thread }) => {
{cutRest(title, 100)}
- + ) } @@ -43,7 +43,7 @@ const Header: FC = ({ data, thread }) => { return ( - +
<ExtraInfo> diff --git a/src/components/ArticleEditToolbar/CopyrightSelector.js b/src/components/ArticleEditToolbar/CopyrightSelector.js deleted file mode 100755 index 38f781b56..000000000 --- a/src/components/ArticleEditToolbar/CopyrightSelector.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react' -import { path, reject, find, propEq } from 'ramda' -// import T from 'prop-types' - -import { ICON, ICON_CMD } from '@/config' -import { THREAD } from '@/constant' - -import Tooltip from '@/components/Tooltip' - -import { - Wrapper, - Selector, - CheckIcon, - CheckText, - ReprintIcon, - ReprintWrapper, - CopyRightText, - MoreIcon, -} from './styles/copyright_selector' - -const FullOptions = [ - { - title: '原创', - value: 'original', - }, - { - title: '转载', - value: 'reprint', - }, - { - title: '翻译', - value: 'translate', - }, -] - -const getOptions = (thread) => { - switch (thread) { - case THREAD.JOB: - return reject((o) => o.value === 'translate', FullOptions) - - case THREAD.RADAR: - return reject((o) => o.value === 'original', FullOptions) - - default: - return FullOptions - } -} - -const getCpTitle = (cptype) => - path(['title'], find(propEq('value', cptype), FullOptions)) - -const CopyrightContent = ({ active, thread, onCopyrightChange }) => ( - <Wrapper> - {getOptions(thread).map((opt) => ( - <Selector key={opt.value} onClick={() => onCopyrightChange(opt.value)}> - <CheckText>{opt.title}</CheckText> - <CheckIcon - src={`${ICON_CMD}/check2.svg`} - active={active === opt.value} - /> - </Selector> - ))} - </Wrapper> -) - -const CopyrightSelector = ({ copyRight, thread, onCopyrightChange }) => { - return ( - <Tooltip - content={ - <CopyrightContent - active={copyRight} - thread={thread} - onCopyrightChange={onCopyrightChange} - /> - } - placement="bottom" - > - <ReprintWrapper> - <ReprintIcon src={`${ICON_CMD}/${copyRight}.svg`} /> - <CopyRightText>{getCpTitle(copyRight)}</CopyRightText> - <MoreIcon src={`${ICON}/shape/arrow-solid.svg`} /> - </ReprintWrapper> - </Tooltip> - ) -} - -export default React.memo(CopyrightSelector) diff --git a/src/components/ArticleEditToolbar/index.js b/src/components/ArticleEditToolbar/index.js deleted file mode 100755 index a343ee1cc..000000000 --- a/src/components/ArticleEditToolbar/index.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * - * ArticleEditToolbar - * - */ - -import React from 'react' -import T from 'prop-types' - -import { SITE_URL } from '@/config' -import { THREAD } from '@/constant' -import { buildLog } from '@/utils/logger' - -import CopyrightSelector from './CopyrightSelector' - -import { - Wrapper, - LinkLabel, - LinkInput, - SourceLink, - CopyRightWrapper, -} from './styles' - -/* eslint-disable-next-line */ -const log = buildLog('c:ArticleEditToolbar:index') - -const ArticleEditToolbar = ({ - onLinkAddrChange, - onCopyrightChange, - thread, - editData: { copyRight, linkAddr }, -}) => ( - <Wrapper> - <CopyRightWrapper> - <CopyrightSelector - copyRight={copyRight} - thread={thread} - onCopyrightChange={onCopyrightChange} - /> - </CopyRightWrapper> - - {copyRight !== 'original' && ( - <SourceLink> - <LinkLabel>原文地址</LinkLabel> - <LinkInput - placeholder={`请填写url地址, 比如: ${SITE_URL}`} - value={linkAddr} - onChange={onLinkAddrChange} - /> - </SourceLink> - )} - </Wrapper> -) - -ArticleEditToolbar.propTypes = { - thread: T.oneOf([THREAD.POST, THREAD.JOB]), - editData: T.shape({ - copyRight: T.string, - linkAddr: T.string, - }).isRequired, - onLinkAddrChange: T.func, - onCopyrightChange: T.func, -} - -ArticleEditToolbar.defaultProps = { - thread: THREAD.POST, - onLinkAddrChange: log, - onCopyrightChange: log, -} - -export default React.memo(ArticleEditToolbar) diff --git a/src/components/ArticleEditToolbar/styles/copyright_selector.ts b/src/components/ArticleEditToolbar/styles/copyright_selector.ts deleted file mode 100755 index f731b29f6..000000000 --- a/src/components/ArticleEditToolbar/styles/copyright_selector.ts +++ /dev/null @@ -1,51 +0,0 @@ -import styled from 'styled-components' - -import type { TActive } from '@/spec' -import { theme } from '@/utils/themes' -import css from '@/utils/css' -import Img from '@/Img' - -export const Wrapper = styled.div` - padding-left: 5px; -` -export const Selector = styled.div` - ${css.flex('align-center')}; - margin-bottom: 5px; - &:hover { - cursor: pointer; - color: ${theme('editor.title')}; - } -` -export const CheckIcon = styled(Img)<TActive>` - fill: ${theme('editor.content')}; - ${css.size(18)}; - margin-top: 2px; - margin-left: 3px; - visibility: ${({ active }) => (active ? 'visible' : 'hidden')}; -` -export const CheckText = styled.div` - color: ${theme('editor.content')}; -` -export const CopyRightText = styled.div` - font-size: 13px; -` -export const ReprintWrapper = styled.div` - ${css.flex('align-center')}; - color: ${theme('editor.content')}; - cursor: pointer; -` -export const ReprintIcon = styled(Img)` - fill: ${theme('editor.content')}; - ${css.size(13)}; - margin-right: 5px; -` -export const MoreIcon = styled(Img)` - ${css.size(13)}; - margin-left: 3px; - fill: ${theme('editor.placeholder')}; - &:hover { - cursor: pointer; - } - - transform: rotate(90deg); -` diff --git a/src/components/ArticleEditToolbar/styles/index.ts b/src/components/ArticleEditToolbar/styles/index.ts deleted file mode 100755 index 02c94a324..000000000 --- a/src/components/ArticleEditToolbar/styles/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import styled from 'styled-components' - -import Input from '@/components/Input' - -// import Img from '@/Img' -import { theme } from '@/utils/themes' -import css from '@/utils/css' - -export const Wrapper = styled.div` - ${css.flex('align-center')}; - width: 100%; -` -export const CopyRightWrapper = styled.div` - ${css.flex()}; -` -export const SourceLink = styled.div` - ${css.flex('align-center')}; - width: 400px; - margin-left: 16px; -` -export const LinkInput = styled(Input)` - height: 20px; - line-height: 20px; - width: 300px; - font-size: 12px; - margin-top: -1px; - background: transparent; - padding-left: 2px; - border-radius: 0; - color: ${theme('editor.title')}; - border: none; - border-bottom: 1px solid transparent; - - ::placeholder { - color: ${theme('editor.placeholder')}; - } - - text-align: left; - &:hover { - border-color: ${theme('editor.headerBg')}; - border-bottom: 1px solid; - border-bottom-color: ${theme('editor.border')}; - color: ${theme('editor.title')}; - } - &:focus { - border-color: ${theme('editor.headerBg')}; - box-shadow: none; - border-bottom: 1px solid; - border-bottom-color: ${theme('editor.placeholder')}; - color: ${theme('editor.title')}; - text-align: left; - } -` -export const LinkLabel = styled.div` - font-size: 12px; - width: 60px; - margin-top: 2px; - color: ${theme('editor.placeholder')}; - ${SourceLink}:hover & { - color: ${theme('editor.title')}; - } - transition: color 0.2s; -` diff --git a/src/components/BlogItem/DesktopView/Header.tsx b/src/components/BlogItem/DesktopView/Header.tsx index 106f7f233..d180b5b4f 100644 --- a/src/components/BlogItem/DesktopView/Header.tsx +++ b/src/components/BlogItem/DesktopView/Header.tsx @@ -7,7 +7,7 @@ import { ICON_CMD } from '@/config' import { parseDomain } from '@/utils/route' import AvatarsRow from '@/components/AvatarsRow' -import InlineTags from '@/components/InlineTags' +import TagsList from '@/components/TagsList' import { Wrapper, @@ -36,7 +36,7 @@ const Header: FC<TProps> = ({ item }) => { </TitleLink> )} <TagListWrapper> - <InlineTags items={item.articleTags} /> + <TagsList items={item.articleTags} /> </TagListWrapper> </Brief> </Wrapper> diff --git a/src/components/BlogItem/MobileView/Header.tsx b/src/components/BlogItem/MobileView/Header.tsx index 6a3d5ee5d..df0ac969b 100644 --- a/src/components/BlogItem/MobileView/Header.tsx +++ b/src/components/BlogItem/MobileView/Header.tsx @@ -3,7 +3,7 @@ import TimeAgo from 'timeago-react' import type { TBlog, TAccount } from '@/spec' -import InlineTags from '@/components/InlineTags' +import TagsList from '@/components/TagsList' import DotDivider from '@/components/DotDivider' import ImgFallback from '@/components/ImgFallback' @@ -38,7 +38,7 @@ const Header: FC<TProps> = ({ item, onAuthorSelect }) => { </TimeStamp> </AuthorInfo> <TagListWrapper> - <InlineTags items={item.articleTags} /> + <TagsList items={item.articleTags} /> </TagListWrapper> </Wrapper> ) diff --git a/src/components/Buttons/Button.tsx b/src/components/Buttons/Button.tsx index 241ae7e86..ca0fad027 100644 --- a/src/components/Buttons/Button.tsx +++ b/src/components/Buttons/Button.tsx @@ -1,25 +1,15 @@ -import { FC, ReactNode, useEffect, useState, memo } from 'react' +import { FC, ReactNode, memo } from 'react' import type { TSIZE_TSM } from '@/spec' import { SIZE } from '@/constant' import { buildLog } from '@/utils/logger' -import { - Wrapper, - RedWrapper, - LoadingMask, - ChildrenWrapper, - RealChildren, - LoadingText, -} from './styles/button' +import { LavaLampLoading } from '@/components/dynamic' +import { Wrapper, RedWrapper, ChildrenWrapper } from './styles/button' /* eslint-disable-next-line */ const log = buildLog('c:Buttons:Button') -const clearTimerIfNeed = (timerId: number): void => { - if (timerId) clearTimeout(timerId) -} - type TProps = { children?: ReactNode className?: string @@ -27,8 +17,7 @@ type TProps = { type?: 'primary' | 'red' | 'ghost' size?: TSIZE_TSM onClick?: () => void - loading?: boolean | null - loadingText?: string + loading?: boolean noBorder?: boolean disabled?: boolean } @@ -40,41 +29,11 @@ const Button: FC<TProps> = ({ onClick = log, size = SIZE.MEDIUM, className = '', - loading = null, - loadingText = '发布中', + loading = false, noBorder = false, disabled = false, }) => { - const [loadingWidth, setLoadingWidth] = useState(0) // 0 || 20 || 65 || 90 || 100 - - useEffect(() => { - let timer0 - let timer1 - let timer2 - let timer3 - - if (loading) { - timer0 = setTimeout(() => setLoadingWidth(25), 200) - timer1 = setTimeout(() => setLoadingWidth(65), 600) - timer2 = setTimeout(() => setLoadingWidth(90), 1200) - timer3 = setTimeout(() => setLoadingWidth(100), 1800) - } else if (loading === false) { - setLoadingWidth(100) - } else { - setLoadingWidth(0) - } - - return () => { - if (loading) { - clearTimerIfNeed(timer0) - clearTimerIfNeed(timer1) - clearTimerIfNeed(timer2) - clearTimerIfNeed(timer3) - } - - setLoadingWidth(0) - } - }, [loading]) + if (loading) return <LavaLampLoading size="small" /> switch (type) { case 'red': { @@ -103,11 +62,7 @@ const Button: FC<TProps> = ({ noBorder={noBorder} disabled={disabled} > - <LoadingMask width={`${loadingWidth}%`} /> - <ChildrenWrapper size={size}> - <RealChildren $loading={loading}>{children}</RealChildren> - <LoadingText $loading={loading}>{loadingText}</LoadingText> - </ChildrenWrapper> + <ChildrenWrapper size={size}>{children}</ChildrenWrapper> </Wrapper> ) } diff --git a/src/components/Buttons/SubmitButton.tsx b/src/components/Buttons/SubmitButton.tsx new file mode 100644 index 000000000..04da5d0ab --- /dev/null +++ b/src/components/Buttons/SubmitButton.tsx @@ -0,0 +1,52 @@ +import { FC, memo } from 'react' + +import type { TSubmitState } from '@/spec' +import { buildLog } from '@/utils/logger' + +import YesOrNoButtons from './YesOrNoButtons' +import { DonwWrapper, DoneIcon, DoneHint } from './styles/submit_button' + +/* eslint-disable-next-line */ +const log = buildLog('c:Buttons:SubmitButton') + +type TProps = { + // onClick?: () => void + submitState?: TSubmitState + + okText?: string + cancelText?: string + onCancel?: () => void + onPublish?: () => void +} + +const SubmitButton: FC<TProps> = ({ + okText = '发 布', + cancelText = '取 消', + onCancel = log, + onPublish = log, + submitState = { publishing: false, publishDone: false, isReady: false }, +}) => { + const { publishing, publishDone, isReady } = submitState + + return ( + <div> + {publishDone ? ( + <DonwWrapper> + <DoneIcon /> + <DoneHint>已完成</DoneHint> + </DonwWrapper> + ) : ( + <YesOrNoButtons + cancelText={cancelText} + confirmText={okText} + onConfirm={onPublish} + loading={publishing} + disabled={!isReady} + onCancel={onCancel} + /> + )} + </div> + ) +} + +export default memo(SubmitButton) diff --git a/src/components/Buttons/YesOrNoButtons.tsx b/src/components/Buttons/YesOrNoButtons.tsx index 41abd99ee..1095eb0ef 100644 --- a/src/components/Buttons/YesOrNoButtons.tsx +++ b/src/components/Buttons/YesOrNoButtons.tsx @@ -9,6 +9,8 @@ type TProps = { align?: 'center' | 'right' cancelText?: string confirmText?: string + loading?: boolean + disabled?: boolean onCancel?: () => void onConfirm?: () => void } @@ -19,12 +21,23 @@ const YesOrNoButton: FC<TProps> = ({ confirmText = '确定', onCancel = console.log, onConfirm = console.log, + disabled = false, + loading = false, }) => { return ( <Wrapper align={align}> - <CancelBtn onClick={() => onCancel?.()}>{cancelText}</CancelBtn> + {!loading && ( + <CancelBtn onClick={() => onCancel?.()}>{cancelText}</CancelBtn> + )} + <Space left={5} right={10} /> - <Button size="small" type="primary" onClick={() => onConfirm?.()}> + <Button + size="small" + type="primary" + loading={loading} + disabled={disabled} + onClick={() => onConfirm?.()} + > {confirmText} </Button> </Wrapper> diff --git a/src/components/Buttons/styles/button.ts b/src/components/Buttons/styles/button.ts index ad0b25702..0d02c74bb 100644 --- a/src/components/Buttons/styles/button.ts +++ b/src/components/Buttons/styles/button.ts @@ -75,24 +75,6 @@ export const ChildrenWrapper = styled.div<{ size: TSIZE }>` position: relative; z-index: 2; ` -export const RealChildren = styled.div<{ $loading: boolean }>` - ${css.flex('align-both')}; - opacity: ${({ $loading }) => ($loading ? 0 : 1)}; -` -export const LoadingText = styled.div<{ $loading: boolean }>` - opacity: ${({ $loading }) => (!$loading ? 0 : 1)}; - position: absolute; -` -export const LoadingMask = styled.div<{ width: string }>` - position: absolute; - width: ${({ width }) => width}; - height: 100%; - top: 0; - left: 0; - background: ${({ width }) => (width === '100%' ? 'transparent' : '#2c6b94')}; - z-index: 1; - transition: width 0.1s; -` export const RedWrapper = styled(Wrapper)` color: ${({ ghost }) => (ghost ? theme('baseColor.red') : 'white')}; background-color: ${({ ghost }) => diff --git a/src/components/Buttons/styles/metircs/button.ts b/src/components/Buttons/styles/metircs/button.ts index 4b55ae65e..0777a1fd1 100644 --- a/src/components/Buttons/styles/metircs/button.ts +++ b/src/components/Buttons/styles/metircs/button.ts @@ -8,7 +8,7 @@ export const getColor = (ghost: boolean, disabled: boolean): TTheme => { return theme('button.primary') } if (disabled) { - return '#83a7bd' // TODO: same as dimOnIdle background + return theme('thread.articleDigest') } return theme('button.fg') @@ -23,7 +23,7 @@ export const getBackgroundColor = ( return 'transparent' } if (disabled) { - return '#124b5a' // TODO: same as dimOnIdle background + return '#0D363D' // TODO: same as dimOnIdle background } return hover ? theme('button.hoverBg') : theme('button.primary') diff --git a/src/components/Buttons/styles/submit_button.ts b/src/components/Buttons/styles/submit_button.ts new file mode 100644 index 000000000..ceca6cc8c --- /dev/null +++ b/src/components/Buttons/styles/submit_button.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components' + +import css from '@/utils/css' +import { theme } from '@/utils/themes' +import CheckedSVG from '@/icons/Checked' + +import animate from '@/utils/animations' + +export const Wrapper = styled.div`` + +export const PublishFooter = styled.div` + ${css.flex('align-center', 'justify-between')}; + width: 100%; + padding-left: 26px; + padding-right: 35px; +` +export const DonwWrapper = styled.div` + ${css.flex('align-center')}; + animation: ${animate.zoomIn} 0.3s linear; +` +export const DoneIcon = styled(CheckedSVG)` + ${css.size(16)}; + fill: ${theme('baseColor.green')}; + margin-right: 6px; +` +export const DoneHint = styled.div` + color: ${theme('baseColor.green')}; + font-size: 14px; +` diff --git a/src/components/Cards/CommunityCard.tsx b/src/components/Cards/CommunityCard.tsx index 0f7d169e2..6d04f57ae 100644 --- a/src/components/Cards/CommunityCard.tsx +++ b/src/components/Cards/CommunityCard.tsx @@ -7,13 +7,18 @@ import { FC, memo } from 'react' import type { TCommunity } from '@/spec' import { cutRest } from '@/utils/helper' +import DotDivider from '@/components/DotDivider' import { Wrapper, CommunityLogo, Info, - SubsCount, + SubInfo, + SubsInfo, + UserIcon, + UserCount, Header, Title, + Raw, Desc, } from './styles/community_card' @@ -27,10 +32,18 @@ const CommunityCard: FC<TProps> = ({ return ( <Wrapper key={id}> <Header> - <CommunityLogo src={logo} /> + <CommunityLogo src={logo} raw={raw} /> <Info> - <Title href={`/${raw}/posts`}>{title} - {subscribersCount} 人已加入 + {title} + + {raw} + + + + {/* {subscribersCount} */} + 74 + + {cutRest(desc, 50)} diff --git a/src/components/Cards/styles/community_card.ts b/src/components/Cards/styles/community_card.ts index c7b62e012..68b131b5d 100644 --- a/src/components/Cards/styles/community_card.ts +++ b/src/components/Cards/styles/community_card.ts @@ -2,41 +2,78 @@ import styled from 'styled-components' // import type { TTestable } from '@/spec' -import Img from '@/Img' +// import Img from '@/Img' import { theme } from '@/utils/themes' import css from '@/utils/css' +import UserSVG from '@/icons/User' +import CommunityFaceLogo from '@/components/CommunityFaceLogo' + export const Wrapper = styled.div` ${css.flexColumn()}; width: 200px; min-height: 100px; ` -export const CommunityLogo = styled(Img)` +export const CommunityLogo = styled(CommunityFaceLogo)` ${css.size(30)}; ` -export const SubsCount = styled.div` +export const SubsInfo = styled.div` + ${css.flex('align-center')}; color: ${theme('thread.articleDigest')}; - font-size: 12px; - margin-top: 2px; ` export const Info = styled.div` ${css.flexColumn()}; + flex-grow: 1; margin-left: 16px; ` export const Header = styled.div` ${css.flex('align-center')}; margin-bottom: 10px; ` -export const Title = styled.a` - text-decoration: none; +export const Title = styled.div` color: ${theme('thread.articleTitle')}; font-size: 16px; font-weight: bold; +` +export const SubInfo = styled.div` + width: 100%; + ${css.flex('align-center')}; + margin-top: 2px; +` +export const Raw = styled.a` + text-decoration: none; + color: ${theme('thread.articleDigest')}; + font-size: 14px; + opacity: 0.8; + margin-bottom: 1px; + position: relative; + padding-left: 6px; + + &:before { + content: '/'; + position: absolute; + top: 2px; + left: 0; + font-size: 12px; + margin-right: 2px; + } &:hover { text-decoration: underline; + color: ${theme('thread.articleDigest')}; + opacity: 1; } ` +export const UserIcon = styled(UserSVG)` + ${css.size(10)}; + fill: ${theme('thread.articleDigest')}; + margin-right: 3px; + opacity: 0.8; +` +export const UserCount = styled.div` + fill: ${theme('thread.articleTitle')}; + font-size: 13px; +` export const Desc = styled.div` color: ${theme('thread.articleDigest')}; font-size: 14px; diff --git a/src/components/Checker/index.tsx b/src/components/Checker/index.tsx index 66daff2a9..fdf50197d 100755 --- a/src/components/Checker/index.tsx +++ b/src/components/Checker/index.tsx @@ -7,11 +7,10 @@ import { FC, ReactNode, memo } from 'react' import type { TSIZE_SM } from '@/spec' -import { ICON } from '@/config' import { SIZE } from '@/constant' import { buildLog } from '@/utils/logger' -import { Wrapper, IconWrapper, Icon, ChildWrapper } from './styles' +import { Wrapper, IconWrapper, CheckIcon, ChildWrapper } from './styles' /* eslint-disable-next-line */ const log = buildLog('c:Checker:index') @@ -22,6 +21,7 @@ type TProps = { hiddenMode?: boolean size?: TSIZE_SM dimWhenIdle?: boolean + disabled?: boolean onChange?: (checked: boolean) => void } @@ -31,6 +31,7 @@ const Checker: FC = ({ hiddenMode = false, size = SIZE.MEDIUM, children = null, + disabled = false, dimWhenIdle = false, }) => { const show = checked || !hiddenMode @@ -39,12 +40,13 @@ const Checker: FC = ({ show && onChange(!checked)} + disabled={disabled} + onClick={() => show && !disabled && onChange(!checked)} > - - + + - + {children} diff --git a/src/components/Checker/styles/index.ts b/src/components/Checker/styles/index.ts index 413c45091..c77fc539c 100755 --- a/src/components/Checker/styles/index.ts +++ b/src/components/Checker/styles/index.ts @@ -1,15 +1,15 @@ import styled from 'styled-components' import type { TActive } from '@/spec' -import Img from '@/Img' import { theme } from '@/utils/themes' import css from '@/utils/css' +import CheckedSVG from '@/icons/Checked' import { getIconSize, getFontSize, getBorderRadius } from './metric' -type TItem = { checked: boolean; size: string } +type TItem = { checked: boolean; size: string; disabled?: boolean } -type TWrapper = { dimWhenIdle: boolean } & TActive +type TWrapper = { dimWhenIdle: boolean; disabled?: boolean } & TActive export const Wrapper = styled.div` ${css.flex('align-center')}; visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; @@ -18,7 +18,7 @@ export const Wrapper = styled.div` &:hover { fill: #00a59b; opacity: 1; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; } ` export const IconWrapper = styled.div` @@ -27,13 +27,15 @@ export const IconWrapper = styled.div` width: ${({ size }) => getIconSize(size)}; height: ${({ size }) => getIconSize(size)}; ${css.flex('align-both')}; - border: 1px solid; + + border: ${({ disabled }) => (!disabled ? '1px solid' : 'none')}; + border-color: ${({ checked }) => (checked ? '#246b8c' : '#1c5975')}; border-radius: ${({ size }) => getBorderRadius(size)}; transition: all 0.2s; ` -export const Icon = styled(Img)` +export const CheckIcon = styled(CheckedSVG)` position: absolute; fill: #327faf; display: ${({ checked }) => (checked ? 'block' : 'none')}; @@ -41,12 +43,12 @@ export const Icon = styled(Img)` height: ${({ size }) => getIconSize(size)}; top: -1px; left: -1px; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; ` export const ChildWrapper = styled.div` color: ${({ checked }) => checked ? theme('thread.articleTitle') : theme('thread.articleDigest')}; font-size: ${({ size }) => getFontSize(size)}; margin-left: 6px; - cursor: pointer; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; ` diff --git a/src/components/CommunityFaceLogo/index.tsx b/src/components/CommunityFaceLogo/index.tsx index 75d68fb97..2a2048806 100755 --- a/src/components/CommunityFaceLogo/index.tsx +++ b/src/components/CommunityFaceLogo/index.tsx @@ -22,6 +22,7 @@ type TProps = { raw?: string loading?: boolean | null className?: string + noLazy?: boolean } const CommunityFaceLogo: FC = ({ @@ -30,13 +31,20 @@ const CommunityFaceLogo: FC = ({ raw = HCN, loading, className = 'community-facelogo-class', + noLazy = false, }) => { if (raw === HCN || isEmpty(src)) { return } return ( - + ) } diff --git a/src/components/CommunityStatesPad/index.tsx b/src/components/CommunityStatesPad/index.tsx index a16b97e2c..531c10f61 100755 --- a/src/components/CommunityStatesPad/index.tsx +++ b/src/components/CommunityStatesPad/index.tsx @@ -56,7 +56,7 @@ const CommunityStatesPad: FC = ({ /> - + 内容 ` +export const NumberSection = styled.div` ${css.flexColumn('align-end')}; padding: 0 5px; - border-radius: 4px; - - &:hover { - background: ${({ readOnly }) => - readOnly ? '' : theme('banner.numberHoverBg')}; - } ` export const ContentSection = styled(NumberSection)` ${css.media.mobile`display: none`}; diff --git a/src/components/ErrorPage/index.tsx b/src/components/ErrorPage/index.tsx index fec3f5f26..e085fc953 100755 --- a/src/components/ErrorPage/index.tsx +++ b/src/components/ErrorPage/index.tsx @@ -10,7 +10,6 @@ import Link from 'next/link' import type { TMetric } from '@/spec' import { METRIC } from '@/constant' -import { ICON_BASE } from '@/config' import { buildLog } from '@/utils/logger' import SpinPlanet from './SpinPlanet' diff --git a/src/components/Header/UserAccount.tsx b/src/components/Header/UserAccount.tsx index 2459b25dd..c3949fc7d 100755 --- a/src/components/Header/UserAccount.tsx +++ b/src/components/Header/UserAccount.tsx @@ -89,7 +89,7 @@ const UserAccount: FC = () => { testid="header-unlogin-user" onClick={() => console.log('todo onLogin')} > - +
)} diff --git a/src/components/Header/styles/user_account.ts b/src/components/Header/styles/user_account.ts index 3e20aefb3..6ee65ff31 100755 --- a/src/components/Header/styles/user_account.ts +++ b/src/components/Header/styles/user_account.ts @@ -1,10 +1,11 @@ import styled from 'styled-components' import type { TTestable } from '@/spec' -import Img from '@/Img' import { theme } from '@/utils/themes' import css from '@/utils/css' +import UserSVG from '@/icons/User' + export const Wrapper = styled.div.attrs(({ testid }: TTestable) => ({ 'data-test-id': testid, }))` @@ -72,7 +73,7 @@ export const AvatarIcon = styled.img` border-radius: 3px; opacity: ${theme('avatar.opacity')}; ` -export const DefaultUserIcon = styled(Img)` +export const DefaultUserIcon = styled(UserSVG)` fill: ${theme('header.fg')}; ${css.size(16)}; cursor: pointer; diff --git a/src/components/Icons/Setting.tsx b/src/components/Icons/Setting.tsx index 7b5ef68e5..c6c7abb36 100755 --- a/src/components/Icons/Setting.tsx +++ b/src/components/Icons/Setting.tsx @@ -4,14 +4,12 @@ const SvgComponent: FC = (props) => ( - - - - + ) diff --git a/src/components/Icons/User.tsx b/src/components/Icons/User.tsx new file mode 100644 index 000000000..70e304f79 --- /dev/null +++ b/src/components/Icons/User.tsx @@ -0,0 +1,21 @@ +import { memo, SVGProps } from 'react' + +const User = (props: SVGProps) => { + return ( + + + + + + + ) +} + +export default memo(User) diff --git a/src/components/InlineTags/index.tsx b/src/components/InlineTags/index.tsx deleted file mode 100755 index df9c71f5a..000000000 --- a/src/components/InlineTags/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * - * InlineTags - * - */ - -import { FC, memo } from 'react' - -import type { TTag, TSIZE_TSM } from '@/spec' -import { SIZE } from '@/constant' - -import { sortByColor } from '@/utils/helper' -import { Trans } from '@/utils/i18n' -import { buildLog } from '@/utils/logger' -import Maybe from '@/components/Maybe' -import Tooltip from '@/components/Tooltip' - -import { Wrapper, Tag, HashSign, Title, MoreText, PopoverInfo } from './styles' - -/* eslint-disable-next-line */ -const log = buildLog('c:InlineTags:index') - -const FullList = ({ items, mLeft, size }) => { - return ( - - {sortByColor(items).map((tag) => ( - - - {Trans(tag.title)} - - ))} - - ) -} - -type TProps = { - items: TTag[] - max?: number - mLeft?: number - size?: TSIZE_TSM -} - -const InlineTags: FC = ({ - items, - max = 3, - mLeft = 8, - size = SIZE.TINY, -}) => { - if (items.length > max) { - return ( - - - - } - > - - {sortByColor(items) - .slice(0, max) - .map((tag) => ( - - - {Trans(tag.title)} - - ))} - ... - - - ) - } - - return ( - - - - ) -} - -export default memo(InlineTags) diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index d29230420..0e61e9f64 100755 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -44,6 +44,7 @@ const Modal: FC = ({ offsetTop = '20%', }) => { const { Portal } = usePortal() + // damn, i forgot why i set this state, fix LATER const [visibleOnPage, setVisibleOnPage] = useState(false) const handleClose = useCallback(() => { @@ -58,6 +59,9 @@ const Modal: FC = ({ if (visibleOnPage) { toggleGlobalBlur(true) } + if (visibleOnPage && !show) { + toggleGlobalBlur(false) + } }, [show, visibleOnPage]) return ( @@ -79,7 +83,7 @@ const Modal: FC = ({ show={showCloseBtn} onClick={handleClose} /> - ESC + Esc e.stopPropagation()}> {children} diff --git a/src/components/Modal/styles/index.ts b/src/components/Modal/styles/index.ts index aa6d7a098..e2646a664 100755 --- a/src/components/Modal/styles/index.ts +++ b/src/components/Modal/styles/index.ts @@ -73,7 +73,7 @@ export const EscHint = styled.div<{ mode: string }>` mode === 'default' ? theme('font') : theme('baseColor.red')}; opacity: 0.7; position: absolute; - top: 38px; - right: -46px; + top: 35px; + right: -44px; font-size: 13px; ` diff --git a/src/components/PostItem/DesktopView/Header.tsx b/src/components/PostItem/DesktopView/Header.tsx index 7c964e736..748ac974b 100644 --- a/src/components/PostItem/DesktopView/Header.tsx +++ b/src/components/PostItem/DesktopView/Header.tsx @@ -7,7 +7,7 @@ import { ICON_CMD } from '@/config' import { parseDomain } from '@/utils/route' import AvatarsRow from '@/components/AvatarsRow' -import InlineTags from '@/components/InlineTags' +import TagsList from '@/components/TagsList' import { Wrapper, @@ -37,7 +37,7 @@ const Header: FC = ({ item }) => { )} - + diff --git a/src/components/PostItem/MobileView/Header.tsx b/src/components/PostItem/MobileView/Header.tsx index d2bdf3de2..95ada43a6 100644 --- a/src/components/PostItem/MobileView/Header.tsx +++ b/src/components/PostItem/MobileView/Header.tsx @@ -3,7 +3,7 @@ import TimeAgo from 'timeago-react' import type { TPost, TAccount } from '@/spec' -import InlineTags from '@/components/InlineTags' +import TagsList from '@/components/TagsList' import DotDivider from '@/components/DotDivider' import ImgFallback from '@/components/ImgFallback' @@ -38,7 +38,7 @@ const Header: FC = ({ item, onAuthorSelect }) => { - + ) diff --git a/src/components/RepoItem/Header.js b/src/components/RepoItem/Header.js index 8a9964921..12554759c 100755 --- a/src/components/RepoItem/Header.js +++ b/src/components/RepoItem/Header.js @@ -5,7 +5,7 @@ import { ICON_CMD } from '@/config' import { cutRest, numberWithCommas } from '@/utils/helper' import Tooltip from '@/components/Tooltip' import { Space } from '@/components/Common' -import InlineTags from '@/components/InlineTags' +import TagsList from '@/components/TagsList' import { Wrapper, @@ -36,7 +36,7 @@ const Header = ({ entry, onPreview }) => { {entry.ownerName} / {cutRest(entry.title, 20)} - + diff --git a/src/components/TagsList/Setter.tsx b/src/components/TagsList/Setter.tsx new file mode 100644 index 000000000..0e9cdfa76 --- /dev/null +++ b/src/components/TagsList/Setter.tsx @@ -0,0 +1,38 @@ +import { FC, memo } from 'react' + +import type { TCommunity, TThread, TTag } from '@/spec' +import { setArticleTag } from '@/utils/helper' +import { + Wrapper, + EmptyWrapper, + SettingIcon, + HashIcon, + Title, +} from './styles/setter' + +type TProps = { + noEmpty?: boolean + community: TCommunity + thread: TThread + tags: TTag[] +} + +const Setter: FC = ({ community, thread, tags, noEmpty = false }) => { + if (noEmpty) { + return ( + setArticleTag(community, thread, tags)}> + + 设置 + + ) + } + + return ( + setArticleTag(community, thread, tags)}> + + 设置标签 + + ) +} + +export default memo(Setter) diff --git a/src/components/TagsList/index.tsx b/src/components/TagsList/index.tsx new file mode 100755 index 000000000..664f1299c --- /dev/null +++ b/src/components/TagsList/index.tsx @@ -0,0 +1,95 @@ +/* + * TagsList + */ + +import { FC, memo } from 'react' + +import type { TTag, TSIZE_TSM, TCommunity, TThread } from '@/spec' +import { SIZE, THREAD } from '@/constant' + +import { sortByColor } from '@/utils/helper' +import { Trans } from '@/utils/i18n' +import { buildLog } from '@/utils/logger' +import Tooltip from '@/components/Tooltip' + +import Setter from './Setter' + +import { Wrapper, Tag, HashSign, Title, More } from './styles' + +/* eslint-disable-next-line */ +const log = buildLog('c:TagsList:index') + +const FullList = ({ items, mLeft, size }) => { + return ( + + {sortByColor(items).map((tag) => ( + + + {Trans(tag.title)} + + ))} + + ) +} + +type TProps = { + items: TTag[] + max?: number + mLeft?: number + size?: TSIZE_TSM + withSetter?: boolean + + // if withSetter is set to true, MUST have community and thread + community?: TCommunity + thread?: TThread +} + +const TagsList: FC = ({ + items, + max = 2, + mLeft = 8, + size = SIZE.TINY, + withSetter = false, + community = { raw: 'home' }, + thread = THREAD.POST, +}) => { + if (items.length > max) { + return ( + + {sortByColor(items) + .slice(0, max) + .map((tag) => ( + + + {Trans(tag.title)} + + ))} + } + > + ... + + {withSetter && ( + + )} + + ) + } + + return ( + + {items.length > 0 && } + {withSetter && ( + 0} + /> + )} + + ) +} + +export default memo(TagsList) diff --git a/src/components/InlineTags/styles/index.ts b/src/components/TagsList/styles/index.ts similarity index 78% rename from src/components/InlineTags/styles/index.ts rename to src/components/TagsList/styles/index.ts index 1a31dd607..1a50ba327 100755 --- a/src/components/InlineTags/styles/index.ts +++ b/src/components/TagsList/styles/index.ts @@ -8,10 +8,10 @@ import css from '@/utils/css' import { getIconSize, getTitleSize } from './metric' -export const Wrapper = styled.div<{ mLeft: number }>` - ${css.flex()}; - align-items: flex-end; +export const Wrapper = styled.div<{ mLeft?: number }>` + ${css.flex('align-end')}; margin-left: ${({ mLeft }) => (mLeft ? `${mLeft}px` : 0)}; + position: relative; ` export const Tag = styled.div` ${css.flex('align-center')}; @@ -22,8 +22,12 @@ export const Title = styled.div<{ size: TSIZE_TSM }>` color: ${theme('thread.articleDigest')}; font-size: ${({ size }) => getTitleSize(size)}; margin-left: 3px; + word-break: keep-all; +` +export const More = styled.div` + font-weight: bold; + cursor: pointer; ` -export const MoreText = styled.div`` type THashSign = { color: string; size: TSIZE_TSM } @@ -34,8 +38,4 @@ export const HashSign = styled(HashTagSVG)` opacity: 0.9; ` -export const PopoverInfo = styled.div` - padding: 10px; - padding-top: 12px; - padding-bottom: 5px; -` +export const PopoverInfo = styled.div`` diff --git a/src/components/InlineTags/styles/metric.ts b/src/components/TagsList/styles/metric.ts similarity index 100% rename from src/components/InlineTags/styles/metric.ts rename to src/components/TagsList/styles/metric.ts diff --git a/src/components/TagsList/styles/setter.ts b/src/components/TagsList/styles/setter.ts new file mode 100644 index 000000000..eeeffad40 --- /dev/null +++ b/src/components/TagsList/styles/setter.ts @@ -0,0 +1,63 @@ +import styled from 'styled-components' + +import { theme } from '@/utils/themes' +import css from '@/utils/css' + +import HashTagSVG from '@/icons/HashTag' +import SettingSVG from '@/icons/Setting' + +import { Wrapper as ParentWrapper } from './index' + +export const Wrapper = styled.div` + position: absolute; + right: -50px; + ${css.flex('align-center')}; + color: ${theme('thread.articleDigest')}; + margin-left: 5px; + opacity: 0; + + ${ParentWrapper}:hover & { + opacity: 0.8; + cursor: pointer; + } + + transition: opacity 0.2s; +` + +export const EmptyWrapper = styled.div` + ${css.flex('align-center')}; + color: ${theme('thread.articleDigest')}; + opacity: 0.8; + &:hover { + opacity: 1; + cursor: pointer; + } + + transition: opacity 0.2s; +` +export const SettingIcon = styled(SettingSVG)` + ${css.size(11)}; + fill: ${theme('thread.articleDigest')}; + opacity: 0.6; + margin-right: 3px; + + ${Wrapper}:hover & { + opacity: 0.8; + } +` + +export const HashIcon = styled(HashTagSVG)` + ${css.size(13)}; + fill: ${theme('thread.articleDigest')}; + transform: rotate(18deg); + opacity: 0.6; + margin-right: 5px; + + ${Wrapper}:hover & { + opacity: 0.8; + } +` +export const Title = styled.div` + color: ${theme('thread.articleDigest')}; + font-size: 14px; +` diff --git a/src/components/InlineTags/tests/index.test.ts b/src/components/TagsList/tests/index.test.ts similarity index 67% rename from src/components/InlineTags/tests/index.test.ts rename to src/components/TagsList/tests/index.test.ts index a16875c8d..2aa6bc07c 100755 --- a/src/components/InlineTags/tests/index.test.ts +++ b/src/components/TagsList/tests/index.test.ts @@ -1,9 +1,9 @@ // import React from 'react' // import { shallow } from 'enzyme' -// import InlineTags from '../index' +// import TagsList from '../index' -describe('TODO ', () => { +describe('TODO ', () => { it('Expect to have unit tests specified', () => { expect(true).toEqual(true) }) diff --git a/src/components/WordsCounter/helper.ts b/src/components/WordsCounter/helper.ts new file mode 100644 index 000000000..4a42c478c --- /dev/null +++ b/src/components/WordsCounter/helper.ts @@ -0,0 +1,44 @@ +import sanitizeHtml from 'sanitize-html' + +const countChinese = (str: string): number => { + const m = str.match(/[\u4e00-\u9fff\uf900-\ufaff]/g) + return !m ? 0 : m.length +} + +const countEnglishWords = (str: string): number => { + return str.split(/\b\W+\b/).length +} + +const extractParagraph = ({ text }) => { + return sanitizeHtml(text, { + allowedTags: [], + allowedAttributes: {}, + }) +} +const extractWords = (body: string): string => { + // console.log('extractWords parse: ', JSON.parse(body)) + const { blocks } = JSON.parse(body) + if (!blocks) return '' + let textToCount = '' + + for (let i = 0; i < blocks.length; i += 1) { + const block = blocks[i] + + if (block.type === 'paragraph') { + // extractParagraph + textToCount = textToCount.concat(extractParagraph(block.data)) + } + } + + // console.log('extract words: ', textToCount) + return textToCount +} + +export const countWords = (body: string): number => { + const str = extractWords(body) + if (str.length === 0) return 0 + + return countChinese(str) + countEnglishWords(str) +} + +export const holder = 1 diff --git a/src/components/WordsCounter/index.tsx b/src/components/WordsCounter/index.tsx new file mode 100755 index 000000000..15b07c7a0 --- /dev/null +++ b/src/components/WordsCounter/index.tsx @@ -0,0 +1,43 @@ +import { FC, memo, useEffect } from 'react' + +import type { TSpace } from '@/spec' +import { countWords } from './helper' +import { Wrapper, Hint, Main, CurNum, Slash, TotalNum } from './styles' + +type TProps = TSpace & { + body: string + max?: number + min?: number + onChange?: (isValid: boolean) => void +} + +const WordsCounter: FC = ({ + body, + max = 2000, + min = 10, + onChange, + ...restProps +}) => { + const currentCount = countWords(body) + const invalid = currentCount < min || currentCount > max + + useEffect(() => { + const isValid = currentCount >= min && currentCount <= max + onChange?.(isValid) + }, [currentCount, onChange, min, max]) + + return ( + + 当前 +
+ {currentCount} /{' '} + {currentCount < min && {min}} + {currentCount >= min && {max}} +
+ {currentCount < min && 字最少} + {currentCount >= min && 字最多} +
+ ) +} + +export default memo(WordsCounter) diff --git a/src/components/WordsCounter/styles/index.ts b/src/components/WordsCounter/styles/index.ts new file mode 100755 index 000000000..1ff08f5af --- /dev/null +++ b/src/components/WordsCounter/styles/index.ts @@ -0,0 +1,39 @@ +import styled from 'styled-components' + +import type { TSpace } from '@/spec' +import { theme } from '@/utils/themes' +import css from '@/utils/css' + +export const Wrapper = styled.div` + ${css.flex('align-end')}; + margin-left: ${({ left }) => `${left}px` || 0}; + margin-right: ${({ right }) => `${right}px` || 0}; + margin-top: ${({ top }) => `${top || 0}px`}; + margin-bottom: ${({ bottom }) => `${bottom || 0}px`}; +` + +export const Hint = styled.div` + color: ${theme('thread.articleDigest')}; + font-size: 12px; + opacity: 0.8; +` +export const Main = styled.div` + ${css.flex('align-end')}; + margin-left: 4px; + margin-right: 4px; +` +export const CurNum = styled.div<{ invalid: boolean }>` + color: ${({ invalid }) => + invalid ? theme('baseColor.red') : theme('thread.articleTitle')}; + font-size: 14px; +` +export const Slash = styled.div` + color: ${theme('thread.articleDigest')}; + font-size: 10px; + margin-left: 6px; + margin-right: 5px; + margin-bottom: 2px; +` +export const TotalNum = styled.div` + font-size: 12px; +` diff --git a/src/components/ArticleEditToolbar/tests/index.test.ts b/src/components/WordsCounter/tests/index.test.ts similarity index 62% rename from src/components/ArticleEditToolbar/tests/index.test.ts rename to src/components/WordsCounter/tests/index.test.ts index 06792d57c..d5d9d1efd 100755 --- a/src/components/ArticleEditToolbar/tests/index.test.ts +++ b/src/components/WordsCounter/tests/index.test.ts @@ -1,9 +1,9 @@ // import React from 'react' // import { shallow } from 'enzyme' -// import ArticleEditToolbar from '../index' +// import WordsCounter from '../index' -describe('TODO ', () => { +describe('TODO ', () => { it('Expect to have unit tests specified', () => { expect(true).toEqual(true) }) diff --git a/src/containers/content/ExploreContent/logic.ts b/src/containers/content/ExploreContent/logic.ts index 6a902fdd5..d379abc47 100755 --- a/src/containers/content/ExploreContent/logic.ts +++ b/src/containers/content/ExploreContent/logic.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { isEmpty } from 'ramda' -import type { TID } from '@/spec' +import type { TID, TEditValue } from '@/spec' import { EVENT, ERR } from '@/constant' import { errRescue } from '@/utils/helper' @@ -69,7 +69,7 @@ export const changeSearchStatus = (status): void => store.mark({ ...status }) * @param {e} htmlEvent * @return {void} */ -export const searchOnChange = (e): void => { +export const searchOnChange = (e: TEditValue): void => { updateEditing(store, 'searchValue', e) searchCommunities() } diff --git a/src/containers/editor/ArticleEditor/CommunityBadge.tsx b/src/containers/editor/ArticleEditor/CommunityBadge.tsx index 8143c1624..f0e67e4a7 100644 --- a/src/containers/editor/ArticleEditor/CommunityBadge.tsx +++ b/src/containers/editor/ArticleEditor/CommunityBadge.tsx @@ -1,6 +1,10 @@ import { FC, memo } from 'react' -import { ICON_BASE } from '@/config' +import type { TCommunity, TEditMode } from '@/spec' +import { cutRest, selectCommunity } from '@/utils/helper' + +import Tooltip from '@/components/Tooltip' +import CommunityCard from '@/components/Cards/CommunityCard' import { Wrapper, @@ -13,18 +17,34 @@ import { ArrowLogo, } from './styles/community_badge' -const CommunityBadge: FC = () => { +type TProps = { + community: TCommunity + mode: TEditMode +} + +const CommunityBadge: FC = ({ community, mode }) => { return ( - 发布到子社区: + {mode === 'publish' ? ( + 发布到子社区: + ) : ( + 所属社区: + )} - <Logo src={`${ICON_BASE}/pl/javascript.png`} /> - <div>JavaScript</div> - <ChangeBtn> - 更换 <ArrowLogo /> - </ChangeBtn> + <Logo src={community.logo} raw={community.raw} /> + <Tooltip + content={<CommunityCard item={community} />} + placement="bottom" + > + <div>{cutRest(community.title || '--', 15)}</div> + </Tooltip> + {mode === 'publish' && ( + <ChangeBtn onClick={selectCommunity}> + 更换 <ArrowLogo /> + </ChangeBtn> + )} diff --git a/src/containers/editor/ArticleEditor/Footer.tsx b/src/containers/editor/ArticleEditor/Footer.tsx index 5728744a6..c880f7eb3 100644 --- a/src/containers/editor/ArticleEditor/Footer.tsx +++ b/src/containers/editor/ArticleEditor/Footer.tsx @@ -1,25 +1,85 @@ import { FC, memo } from 'react' +import type { + TCopyright, + TEditMode, + TTag, + TCommunity, + TSubmitState, +} from '@/spec' + +import { THREAD } from '@/constant' + +import TagsList from '@/components/TagsList' +import SubmitButton from '@/components/Buttons/SubmitButton' +import Copyright from '@/components/Copyright' import Checker from '@/components/Checker' -import YesOrNoButtons from '@/components/Buttons/YesOrNoButtons' +import { SpaceGrow } from '@/components/Common' +import WordsCounter from '@/components/WordsCounter' import { Wrapper, ArticleFooter, PublishFooter } from './styles/footer' +import { editOnChange, onPublish, onCancel, setWordsCountState } from './logic' + +type TProps = { + mode: TEditMode + body: string + tags: TTag[] + isQuestion: boolean + copyRight: string + submitState: TSubmitState + community: TCommunity +} + +const Footer: FC = ({ + mode, + body, + tags, + isQuestion, + copyRight, + submitState, + community, +}) => { + console.log('# footer tags -> ', tags) -const Footer: FC = () => { return ( - 社区 / Tags etc ... - - - 求助 / 问答类 + + + + editOnChange(v, 'isQuestion')} + > + 求助 / 提问 -
- -
+
+ + editOnChange(v, 'copyRight')} + /> + +
) diff --git a/src/containers/editor/ArticleEditor/PublishRules.tsx b/src/containers/editor/ArticleEditor/PublishRules.tsx index a40e69bcf..fe307ef98 100644 --- a/src/containers/editor/ArticleEditor/PublishRules.tsx +++ b/src/containers/editor/ArticleEditor/PublishRules.tsx @@ -9,15 +9,16 @@ const PublishRules: FC = () => { 发帖须知
  • 友善是一切有意义讨论的基础和前提。Don't be an asshole.
  • - 如果是求助,问答类帖子,请务必提供上下文、需求场景等必要信息。”如何看待 - xxx” / “xx 和 xx 哪个更 xx” 等炸鱼问题在这里不受欢迎。 + 如果是寻求解答,请务必提供上下文、需求场景等必要信息, 并勾选发布上方的 + “求助 / 提问” 选框。这里崇尚具体实际的问题,请慎用 “如何看待 xxx” + 开头。
  • 如果不确定内容该发布到哪个社区,可以先发布到 /draft,并 @ 相关社区的志愿者,他们很乐于给你反馈。
  • 请尊重自己和他人的时间,不要发布无意义的烂梗和 trash talk。
  • -
  • 禁止盗版,不谈国是,勿搞沸腾。
  • +
  • 严禁侵权,勿议国是,不搞沸腾。
  • 如有其他疑问或建议反馈,请发布到 /feetback#publish。
  • diff --git a/src/containers/editor/ArticleEditor/TitleInput/index.tsx b/src/containers/editor/ArticleEditor/TitleInput/index.tsx index c00a9c562..e0beb6bc6 100644 --- a/src/containers/editor/ArticleEditor/TitleInput/index.tsx +++ b/src/containers/editor/ArticleEditor/TitleInput/index.tsx @@ -1,11 +1,21 @@ import { FC, memo } from 'react' import { Wrapper, Inputer } from '../styles/title_input' +import { editOnChange } from '../logic' -const TitleInput: FC = () => { +type TProps = { + title: string +} + +const TitleInput: FC = ({ title }) => { return ( - + editOnChange(e, 'title')} + /> ) } diff --git a/src/containers/editor/ArticleEditor/constant.ts b/src/containers/editor/ArticleEditor/constant.ts deleted file mode 100755 index b6c4fa48d..000000000 --- a/src/containers/editor/ArticleEditor/constant.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const STEP = { - EDIT: 'edit', - SETTING: 'setting', -} - -export const holder = 1 diff --git a/src/containers/editor/ArticleEditor/index.tsx b/src/containers/editor/ArticleEditor/index.tsx index 207708abf..c963d2652 100755 --- a/src/containers/editor/ArticleEditor/index.tsx +++ b/src/containers/editor/ArticleEditor/index.tsx @@ -4,12 +4,13 @@ import { FC } from 'react' -import type { TMetric } from '@/spec' +import type { TMetric, TEditMode } from '@/spec' import { METRIC } from '@/constant' import { buildLog } from '@/utils/logger' import { pluggedIn } from '@/utils/mobx' +import CommunityTagSetter from '@/containers/tool/CommunityTagSetter' import RichEditor from '@/containers/editor/RichEditor' import TitleInput from './TitleInput' @@ -22,7 +23,7 @@ import PublishRules from './PublishRules' import type { TStore } from './store' import { Wrapper, InnerWrapper, ContentWrapper } from './styles' -import { useInit } from './logic' +import { useInit, editOnChange, changeCommunity, onTagSelect } from './logic' /* eslint-disable-next-line */ const log = buildLog('C:ArticleEditor') @@ -31,25 +32,59 @@ type TProps = { testid?: string articleEditor?: TStore metric?: TMetric + mode?: TEditMode } const ArticleEditorContainer: FC = ({ testid = 'article-editor', articleEditor: store, metric = METRIC.ARTICLE_EDITOR, + mode = 'publish', }) => { - useInit(store) + useInit(store, mode) + const { + title, + body, + copyRight, + isQuestion, + communityData, + submitState, + tagsData, + } = store + + const initEditor = mode === 'publish' || body !== '{}' return ( + {communityData.id && ( + + )} - - -