From 51d2310c1fc48997a2e61893f21b2cb084198cc1 Mon Sep 17 00:00:00 2001 From: Pavel Mineev Date: Sun, 5 Jan 2020 23:58:10 +0300 Subject: [PATCH] Nice inputs and buttons (#510) * Set default font in examle * prettify file * add ui elements * Use UIButton at AuthPanel * sort deps * remove unsed getUserTitle method * move UserId inside AuthPanel * tests * use UIButton and UInput in Anonymous login * Use UIButton and UIInput in email login * Replace Button to UIButton * fix context for onTitleClick in dropdown * rearrage class props in dropdown * TODO: change finding over DOM to using ref * Use UIButton in input * rearrage deps * Use UIButton in comment * test * Focus and Input buttons * custom focus style for inputs and buttons * the same focus style for comment input * align buttons by top line in input * Token dropdown & hover fix * disable hover when button disabled * make token dropdown markup in new style * Fix trailing comma * update prettier * move prettier config to json format because it is more hendy for settings (for example vscode can suggest rules) * prettier fixed trailing comman in ejs * Remove unnecessary conditions * Files was formatted by Prettier * fix cursor pointer on collapse button * Move components and right naming * ui-button -> button * ui-input -> input * input -> comment-form * Change cursor behavior on disabled state * Change button style * font-weight: normal by default * add small border-radius * Change another one button to component * Fix autofocus on username in email login form * use preact inbuild autoFocus * fix autofocus on back from token step * sleep for 0s enough to wait next render before focus * use more specific name for username input * fix line-height at auth line on mobile * fix className --- frontend/.eslintrc.js | 1 + frontend/.prettierrc | 15 + frontend/.prettierrc.js | 9 - .../app/@types/bem-react-helper/index.d.ts | 2 +- frontend/app/common/types.ts | 2 +- .../auth-panel__anonymous-login-form.scss | 15 +- .../auth-panel__anonymous-login-form.tsx | 19 +- .../auth-panel__dropdown-provider.scss | 3 - .../auth-panel__email-login-form.scss | 37 +- .../auth-panel__email-login-form.tsx | 86 ++-- .../auth-panel__pseudo-link.scss | 13 - .../__sign-out/auth-panel__sign-out.scss | 3 - .../__user-id/auth-panel__user-id.scss | 11 +- .../__user-id/auth-panel__user-id.tsx | 15 - .../components/auth-panel/__user-id/index.ts | 3 - .../app/components/auth-panel/auth-panel.scss | 1 + .../components/auth-panel/auth-panel.test.tsx | 13 +- .../app/components/auth-panel/auth-panel.tsx | 71 ++- frontend/app/components/auth-panel/index.ts | 4 - .../button/_focused/button_focused.scss | 3 - .../button/_kind/_link/button_kind_link.scss | 6 +- .../_kind/_primary/button_kind_primary.scss | 12 + .../_secondary/button_kind_secondary.scss | 8 + .../button/_kind/_text/button_kind_text.scss | 5 - .../_size/_large/button_size_large.scss | 5 + .../_size/_middle/button_size_middle.scss | 4 + .../_theme/_dark/button_theme_dark.scss | 4 + frontend/app/components/button/button.scss | 21 +- frontend/app/components/button/button.tsx | 109 +---- frontend/app/components/button/index.ts | 8 +- .../__actions/comment-form__actions.scss} | 2 +- .../comment-form__button_type_preview.scss} | 2 +- .../comment-form__button_type_send.scss} | 2 +- .../__button/comment-form__button.scss | 4 + .../comment-form__control-panel.scss} | 2 +- .../__counter/comment-form__counter.scss} | 2 +- .../__error/comment-form__error.scss} | 2 +- .../comment-form__field-wrapper.scss | 3 + .../__field/comment-form__field.scss} | 5 +- .../comment-form__markdown-link.scss} | 2 +- .../comment-form__markdown-toolbar.scss} | 12 +- .../__markdown/comment-form__markdown.scss | 3 + .../comment-form__preview-wrapper.scss | 3 + .../__preview/comment-form__preview.scss} | 2 +- .../__rss-link/comment-form__rss-link.scss} | 2 +- .../__rss/comment-form__rss.scss} | 2 +- .../_simple/comment-form_simple.scss} | 2 +- .../_dark/comment-form_theme_dark.scss} | 20 +- .../_light/comment-form_theme_light.scss} | 16 +- .../components/comment-form/comment-form.scss | 8 + .../comment-form/comment-form.test.tsx | 15 + .../components/comment-form/comment-form.tsx | 453 +++++++++++++++++ frontend/app/components/comment-form/index.ts | 29 ++ .../markdown-toolbar-icons/bold-icon.tsx | 9 +- .../markdown-toolbar-icons/code-icon.tsx | 9 +- .../markdown-toolbar-icons/header-icon.tsx | 9 +- .../markdown-toolbar-icons/image-icon.tsx | 2 +- .../markdown-toolbar-icons/italic-icon.tsx | 9 +- .../markdown-toolbar-icons/link-icon.tsx | 9 +- .../ordered-list-icon.tsx | 9 +- .../markdown-toolbar-icons/quote-icon.tsx | 9 +- .../unordered-list-icon.tsx | 9 +- .../markdown-toolbar.tsx | 36 +- .../textarea-autosize.tsx | 0 .../comment__action_type_collapse.scss | 1 + .../_delete/comment__action_type_delete.scss | 5 - .../_edit/comment__action_type_edit.scss | 5 - .../comment/__action/comment__action.scss | 8 +- .../comment/__control/comment__control.scss | 5 - .../_theme/_dark/comment_theme_dark.scss | 5 +- .../_theme/_light/comment_theme_light.scss | 5 +- .../app/components/comment/comment.test.tsx | 5 +- frontend/app/components/comment/comment.tsx | 47 +- frontend/app/components/comment/styles.ts | 2 - .../dropdown/__item/dropdown__item.scss | 8 +- .../dropdown/__title/dropdown__title.scss | 6 - .../_theme/_dark/dropdown_theme_dark.scss | 4 - .../_theme/_light/dropdown_theme_light.scss | 4 - frontend/app/components/dropdown/dropdown.tsx | 24 +- .../input/__button/input__button.scss | 14 - .../__field-wrapper/input__field-wrapper.scss | 3 - .../input/__markdown/input__markdown.scss | 3 - .../input__preview-wrapper.scss | 3 - frontend/app/components/input/index.ts | 2 + frontend/app/components/input/input.scss | 17 +- frontend/app/components/input/input.test.tsx | 15 - frontend/app/components/input/input.tsx | 463 +----------------- frontend/app/components/input/styles.ts | 26 - .../root__pinned-comments.scss | 2 +- .../root/__show-more/root__show-more.scss | 16 - frontend/app/components/root/root.tsx | 9 +- frontend/app/components/settings/settings.tsx | 4 +- frontend/app/components/thread/thread.tsx | 2 +- frontend/app/hooks/useAction.ts | 11 +- frontend/app/store/comments/reducers.ts | 26 +- frontend/app/store/comments/types.ts | 2 +- frontend/app/store/thread/types.ts | 2 +- frontend/app/store/thread/utils.ts | 2 +- frontend/index.ejs | 219 +++++---- frontend/package-lock.json | 6 +- frontend/package.json | 2 +- 101 files changed, 1081 insertions(+), 1113 deletions(-) create mode 100644 frontend/.prettierrc delete mode 100644 frontend/.prettierrc.js delete mode 100644 frontend/app/components/auth-panel/__dropdown-provider/auth-panel__dropdown-provider.scss delete mode 100644 frontend/app/components/auth-panel/__pseudo-link/auth-panel__pseudo-link.scss delete mode 100644 frontend/app/components/auth-panel/__sign-out/auth-panel__sign-out.scss delete mode 100644 frontend/app/components/auth-panel/__user-id/auth-panel__user-id.tsx delete mode 100644 frontend/app/components/auth-panel/__user-id/index.ts delete mode 100644 frontend/app/components/button/_focused/button_focused.scss create mode 100644 frontend/app/components/button/_kind/_primary/button_kind_primary.scss create mode 100644 frontend/app/components/button/_kind/_secondary/button_kind_secondary.scss delete mode 100644 frontend/app/components/button/_kind/_text/button_kind_text.scss create mode 100644 frontend/app/components/button/_size/_large/button_size_large.scss create mode 100644 frontend/app/components/button/_size/_middle/button_size_middle.scss create mode 100644 frontend/app/components/button/_theme/_dark/button_theme_dark.scss rename frontend/app/components/{input/__actions/input__actions.scss => comment-form/__actions/comment-form__actions.scss} (71%) rename frontend/app/components/{input/__button/_type/_preview/input__button_type_preview.scss => comment-form/__button/_type/_preview/comment-form__button_type_preview.scss} (69%) rename frontend/app/components/{input/__button/_type/_send/input__button_type_send.scss => comment-form/__button/_type/_send/comment-form__button_type_send.scss} (72%) create mode 100644 frontend/app/components/comment-form/__button/comment-form__button.scss rename frontend/app/components/{input/__control-panel/input__control-panel.scss => comment-form/__control-panel/comment-form__control-panel.scss} (58%) rename frontend/app/components/{input/__counter/input__counter.scss => comment-form/__counter/comment-form__counter.scss} (81%) rename frontend/app/components/{input/__error/input__error.scss => comment-form/__error/comment-form__error.scss} (77%) create mode 100644 frontend/app/components/comment-form/__field-wrapper/comment-form__field-wrapper.scss rename frontend/app/components/{input/__field/input__field.scss => comment-form/__field/comment-form__field.scss} (86%) rename frontend/app/components/{input/__markdown-link/input__markdown-link.scss => comment-form/__markdown-link/comment-form__markdown-link.scss} (81%) rename frontend/app/components/{input/__markdown-toolbar/input__markdown-toolbar.scss => comment-form/__markdown-toolbar/comment-form__markdown-toolbar.scss} (66%) create mode 100644 frontend/app/components/comment-form/__markdown/comment-form__markdown.scss create mode 100644 frontend/app/components/comment-form/__preview-wrapper/comment-form__preview-wrapper.scss rename frontend/app/components/{input/__preview/input__preview.scss => comment-form/__preview/comment-form__preview.scss} (85%) rename frontend/app/components/{input/__rss-link/input__rss-link.scss => comment-form/__rss-link/comment-form__rss-link.scss} (84%) rename frontend/app/components/{input/__rss/input__rss.scss => comment-form/__rss/comment-form__rss.scss} (83%) rename frontend/app/components/{input/_simple/input_simple.scss => comment-form/_simple/comment-form_simple.scss} (51%) rename frontend/app/components/{input/_theme/_dark/input_theme_dark.scss => comment-form/_theme/_dark/comment-form_theme_dark.scss} (62%) rename frontend/app/components/{input/_theme/_light/input_theme_light.scss => comment-form/_theme/_light/comment-form_theme_light.scss} (62%) create mode 100644 frontend/app/components/comment-form/comment-form.scss create mode 100644 frontend/app/components/comment-form/comment-form.test.tsx create mode 100644 frontend/app/components/comment-form/comment-form.tsx create mode 100644 frontend/app/components/comment-form/index.ts rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/bold-icon.tsx (73%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/code-icon.tsx (62%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/header-icon.tsx (71%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/image-icon.tsx (83%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/italic-icon.tsx (66%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/link-icon.tsx (76%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/ordered-list-icon.tsx (86%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/quote-icon.tsx (76%) rename frontend/app/components/{input => comment-form}/markdown-toolbar-icons/unordered-list-icon.tsx (81%) rename frontend/app/components/{input => comment-form}/markdown-toolbar.tsx (64%) rename frontend/app/components/{input => comment-form}/textarea-autosize.tsx (100%) delete mode 100644 frontend/app/components/comment/__action/_type/_delete/comment__action_type_delete.scss delete mode 100644 frontend/app/components/comment/__action/_type/_edit/comment__action_type_edit.scss delete mode 100644 frontend/app/components/input/__button/input__button.scss delete mode 100644 frontend/app/components/input/__field-wrapper/input__field-wrapper.scss delete mode 100644 frontend/app/components/input/__markdown/input__markdown.scss delete mode 100644 frontend/app/components/input/__preview-wrapper/input__preview-wrapper.scss delete mode 100644 frontend/app/components/input/input.test.tsx delete mode 100644 frontend/app/components/input/styles.ts diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 5db70ec1..e06205db 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { tsconfigRootDir: __dirname, }, rules: { + 'jsx-a11y/no-autofocus': 0, // disabling because typescipt uses it's own lint (see next rule) 'no-unused-vars': 0, // allow Rust-like var starting with _underscore diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..d4d8cfc4 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,15 @@ +{ + "printWidth": 120, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "avoid", + "overrides": [{ + "files": ["*.ejs"], + "options": { + "trailingComma": "none" + } + }] +} diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js deleted file mode 100644 index a677742f..00000000 --- a/frontend/.prettierrc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - printWidth: 120, - useTabs: false, - semi: true, - singleQuote: true, - trailingComma: 'es5', - bracketSpacing: true, - arrowParens: 'avoid', -}; diff --git a/frontend/app/@types/bem-react-helper/index.d.ts b/frontend/app/@types/bem-react-helper/index.d.ts index dd8f1b81..459fe670 100644 --- a/frontend/app/@types/bem-react-helper/index.d.ts +++ b/frontend/app/@types/bem-react-helper/index.d.ts @@ -2,7 +2,7 @@ declare module 'bem-react-helper' { export interface Mods { [key: string]: string | number | boolean | undefined | null; } - export type Mix = string[] | string; + export type Mix = Array | string; export default function b( classname: string, props?: { diff --git a/frontend/app/common/types.ts b/frontend/app/common/types.ts index d0f55a24..58ed9a8d 100644 --- a/frontend/app/common/types.ts +++ b/frontend/app/common/types.ts @@ -105,7 +105,7 @@ export interface Config { max_comment_size: number; admins: string[]; admin_email: string; - auth_providers: (AuthProvider['name'])[]; + auth_providers: AuthProvider['name'][]; low_score: number; critical_score: number; positive_score: boolean; diff --git a/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.scss b/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.scss index 1cfaa798..d0d26b41 100644 --- a/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.scss +++ b/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.scss @@ -6,7 +6,7 @@ } .auth-panel-anonymous-login-form__input { - width: 15em; + width: 9em; } .auth-panel-anonymous-login-form__remember-me { @@ -14,16 +14,5 @@ } .auth-panel-anonymous-login-form__submit { - background: none; - border: none; - padding: 0; - margin: 0 0.5em; - color: currentColor; - font: inherit; - cursor: pointer; -} - -.auth-panel-anonymous-login-form__submit:disabled { - opacity: 0.4; - cursor: not-allowed; + margin-left: 0.5em; } diff --git a/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.tsx b/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.tsx index 856ae95d..cc1783ef 100644 --- a/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.tsx +++ b/frontend/app/components/auth-panel/__anonymous-login-form/auth-panel__anonymous-login-form.tsx @@ -3,6 +3,9 @@ import { createElement, Component, createRef } from 'preact'; import b from 'bem-react-helper'; import { Theme } from '@app/common/types'; +import { Input } from '@app/components/input'; +import { Button } from '@app/components/button'; + interface Props { onSubmit(username: string): Promise; theme: Theme; @@ -76,10 +79,9 @@ export class AnonymousLoginForm extends Component { return (
- { onChange={this.onCheckedChange} checked={this.state.honeyPotValue} /> - + > + Log in +
); } diff --git a/frontend/app/components/auth-panel/__dropdown-provider/auth-panel__dropdown-provider.scss b/frontend/app/components/auth-panel/__dropdown-provider/auth-panel__dropdown-provider.scss deleted file mode 100644 index 599ec6af..00000000 --- a/frontend/app/components/auth-panel/__dropdown-provider/auth-panel__dropdown-provider.scss +++ /dev/null @@ -1,3 +0,0 @@ -.auth-panel__dropdown-provider { - padding: 0.2rem 0.4rem; -} diff --git a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss index 6ff96ca5..2a80648b 100644 --- a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss +++ b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.scss @@ -13,35 +13,33 @@ .auth-panel-email-login-form__token-input { resize: vertical; - font: inherit; - font-weight: normal; + border: 1px solid #c4c4c4; + padding: 4px; + font-family: inherit; font-size: 0.8em; + font-weight: normal; + line-height: 1.5; + + &:focus { + box-shadow: 0 0 0 2px rgba(37, 156, 154, 0.4); + border-color: #259c9a; + outline: none; + } } .auth-panel-email-login-form__submit { - background: none; - border: none; - padding: 0; - padding: 0.1em; - margin-top: 0.2em; - color: currentColor; - font: inherit; - cursor: pointer; -} - -.auth-panel-email-login-form__submit:disabled { - opacity: 0.4; - cursor: not-allowed; + margin: 0.3rem 0.15rem 0.15rem; } .auth-panel-email-login-form__back-button { - color: #259c9a; - cursor: pointer; + text-align: left; margin-left: 0.1rem; margin-bottom: 0.5rem; - &:hover { - opacity: 0.8; + &:before { + content: '◄'; + display: inline-block; + margin-right: 3px; } } @@ -49,4 +47,5 @@ color: #9a0000; text-align: center; margin-top: 1em; + font-weight: normal; } diff --git a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx index 3f7a1b7c..2a7f1a17 100644 --- a/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx +++ b/frontend/app/components/auth-panel/__email-login-form/auth-panel__email-login-form.tsx @@ -8,7 +8,9 @@ import { sendEmailVerificationRequest } from '@app/common/api'; import { extractErrorMessageFromResponse } from '@app/utils/errorUtils'; import { getHandleClickProps } from '@app/common/accessibility'; import { sleep } from '@app/utils/sleep'; -import TextareaAutosize from '@app/components/input/textarea-autosize'; +import TextareaAutosize from '@app/components/comment-form/textarea-autosize'; +import { Input } from '@app/components/input'; +import { Button } from '@app/components/button'; const mapStateToProps = () => ({ sendEmailVerification: sendEmailVerificationRequest, @@ -36,7 +38,7 @@ export class EmailLoginForm extends Component { static usernameRegex = /^[a-zA-Z][\w ]+$/; static emailRegex = /[^@]+@[^.]+\..+/; - inputRef = createRef(); + usernameInputRef = createRef(); tokenRef = createRef(); constructor(props: Props) { @@ -62,8 +64,8 @@ export class EmailLoginForm extends Component { async focus() { await sleep(100); - if (this.inputRef.current) { - this.inputRef.current.focus(); + if (this.usernameInputRef.current) { + this.usernameInputRef.current.focus(); return; } this.tokenRef.current && this.tokenRef.current.textareaRef && this.tokenRef.current.textareaRef.select(); @@ -115,15 +117,23 @@ export class EmailLoginForm extends Component { this.setState({ error: null, tokenValue: (e.target as HTMLInputElement).value }); } - goBack() { + async goBack() { + // Wait for finding back button in DOM by dropbox + // It prevents dropdown from closing, because if dropdown doesn't find clicked element it closes + await sleep(0); + this.setState({ tokenValue: '', error: null, verificationSent: false, }); - setTimeout(() => { - this.inputRef.current && this.inputRef.current.focus(); - }, 100); + + // Wait for rendering username+email step to find user input + await sleep(0); + + if (this.usernameInputRef.current) { + this.usernameInputRef.current.focus(); + } } getForm1InvalidReason(): string | null { @@ -142,12 +152,6 @@ export class EmailLoginForm extends Component { return null; } - componentDidMount() { - setTimeout(() => { - this.inputRef.current && this.inputRef.current.focus(); - }, 100); - } - render(props: Props) { // TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname let className = b('auth-panel-email-login-form', {}, { theme: props.theme }); @@ -160,42 +164,31 @@ export class EmailLoginForm extends Component { if (!this.state.verificationSent) return (
- {/* - * We adding hidden span element to bear with DropDown's onOutSideClick handler. - * This function checks if element that was clicked is a children of it's root component. - * And the problem is that by the time handler gets executed our target element is not a - * part of a dom, so handler suggests that we clicked somewhere outside and hides dropdown - */} - - {'< Back'} - - - - + > + Send Verification + {this.state.error &&
{this.state.error}
}
); @@ -204,9 +197,9 @@ export class EmailLoginForm extends Component { return (
- - {'< Back'} - + { spellcheck={false} autocomplete="off" /> - + > + Confirm + {this.state.error &&
{this.state.error}
} ); diff --git a/frontend/app/components/auth-panel/__pseudo-link/auth-panel__pseudo-link.scss b/frontend/app/components/auth-panel/__pseudo-link/auth-panel__pseudo-link.scss deleted file mode 100644 index dfdf7367..00000000 --- a/frontend/app/components/auth-panel/__pseudo-link/auth-panel__pseudo-link.scss +++ /dev/null @@ -1,13 +0,0 @@ -.auth-panel__pseudo-link { - display: inline-block; - font-weight: 700; - white-space: nowrap; - cursor: pointer; - /* important so "anonymous" label in Dropdown component - will not be overriden by `.dropdown_theme_light .dropdown__title` style */ - color: #0aa !important; - - &:hover { - color: #06c5c5 !important; - } -} diff --git a/frontend/app/components/auth-panel/__sign-out/auth-panel__sign-out.scss b/frontend/app/components/auth-panel/__sign-out/auth-panel__sign-out.scss deleted file mode 100644 index 17f5627c..00000000 --- a/frontend/app/components/auth-panel/__sign-out/auth-panel__sign-out.scss +++ /dev/null @@ -1,3 +0,0 @@ -.auth-panel__sign-out { - margin-left: 5px; -} diff --git a/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.scss b/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.scss index 9d6db960..5ff2ec52 100644 --- a/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.scss +++ b/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.scss @@ -1,6 +1,15 @@ .auth-panel__user-id { overflow: hidden; text-overflow: ellipsis; - padding: 5px 15px; + padding: 5px; cursor: pointer; } + +.auth-panel__user-dropdown-title { + color: inherit; + + &:hover { + color: inherit; + opacity: 0.9; + } +} diff --git a/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.tsx b/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.tsx deleted file mode 100644 index 60962192..00000000 --- a/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** @jsx createElement */ -import { createElement, JSX, FunctionComponent } from 'preact'; -import b from 'bem-react-helper'; -import { Theme } from '@app/common/types'; - -type Props = { - id: string; - theme: Theme; -} & JSX.HTMLAttributes; - -export const UserID: FunctionComponent = ({ id, theme, ...props }) => ( -
- {id} -
-); diff --git a/frontend/app/components/auth-panel/__user-id/index.ts b/frontend/app/components/auth-panel/__user-id/index.ts deleted file mode 100644 index ae781725..00000000 --- a/frontend/app/components/auth-panel/__user-id/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { UserID } from './auth-panel__user-id'; - -import './auth-panel__user-id.scss'; diff --git a/frontend/app/components/auth-panel/auth-panel.scss b/frontend/app/components/auth-panel/auth-panel.scss index 9988a8a7..49779cf3 100644 --- a/frontend/app/components/auth-panel/auth-panel.scss +++ b/frontend/app/components/auth-panel/auth-panel.scss @@ -4,4 +4,5 @@ font-size: 14px; line-height: 16px; align-items: baseline; + padding: 2px 0; } diff --git a/frontend/app/components/auth-panel/auth-panel.test.tsx b/frontend/app/components/auth-panel/auth-panel.test.tsx index f0712e3d..cf5ca97e 100644 --- a/frontend/app/components/auth-panel/auth-panel.test.tsx +++ b/frontend/app/components/auth-panel/auth-panel.test.tsx @@ -1,8 +1,11 @@ /** @jsx createElement */ import { createElement } from 'preact'; import { mount } from 'enzyme'; + +import { Button } from '@app/components/button'; +import { User, PostInfo } from '@app/common/types'; + import { Props, AuthPanel } from './auth-panel'; -import { User, PostInfo } from '../../common/types'; const DefaultProps: Partial = { sort: '-score', @@ -29,7 +32,7 @@ describe('', () => { expect(authForm.text()).toEqual(expect.stringContaining('Sign in to comment using')); - const providerLinks = authForm.find('.auth-panel__pseudo-link'); + const providerLinks = authForm.find(Button); expect(providerLinks.at(0).text()).toEqual('Google'); expect(providerLinks.at(1).text()).toEqual('GitHub'); @@ -49,7 +52,7 @@ describe('', () => { const providerLinks = element .find('.auth-panel__column') .first() - .find('.auth-panel__pseudo-link'); + .find(Button); expect(providerLinks.at(0).text()).toEqual('GitHub'); expect(providerLinks.at(1).text()).toEqual('Google'); @@ -69,7 +72,7 @@ describe('', () => { const providerLinks = element .find('.auth-panel__column') .first() - .find('.auth-panel__pseudo-link'); + .find(Button); expect(providerLinks.at(0).text()).toEqual('Google'); expect(providerLinks.at(1).text()).toEqual('GitHub'); @@ -94,7 +97,7 @@ describe('', () => { expect(authForm.text()).toEqual(expect.stringContaining('Sign in using Google or GitHub')); - const providerLinks = authForm.find('.auth-panel__pseudo-link'); + const providerLinks = authForm.find(Button); expect(providerLinks.at(0).text()).toEqual('Google'); expect(providerLinks.at(1).text()).toEqual('GitHub'); diff --git a/frontend/app/components/auth-panel/auth-panel.tsx b/frontend/app/components/auth-panel/auth-panel.tsx index 94181689..4eb0e09f 100644 --- a/frontend/app/components/auth-panel/auth-panel.tsx +++ b/frontend/app/components/auth-panel/auth-panel.tsx @@ -7,15 +7,15 @@ import { requestDeletion } from '@app/utils/email'; import { getHandleClickProps } from '@app/common/accessibility'; import { User, AuthProvider, Sorting, Theme, PostInfo } from '@app/common/types'; -import Dropdown, { DropdownItem } from '@app/components/dropdown'; -import { Button } from '@app/components/button'; -import { UserID } from './__user-id'; -import { AnonymousLoginForm } from './__anonymous-login-form'; -import { EmailLoginFormConnected } from './__email-login-form'; -import { StoreState } from '@app/store'; -import { ProviderState } from '@app/store/provider/reducers'; import debounce from '@app/utils/debounce'; import postMessage from '@app/utils/postMessage'; +import { StoreState } from '@app/store'; +import { ProviderState } from '@app/store/provider/reducers'; +import Dropdown, { DropdownItem } from '@app/components/dropdown'; +import { Button } from '@app/components/button'; + +import { AnonymousLoginForm } from './__anonymous-login-form'; +import { EmailLoginFormConnected } from './__email-login-form'; import { EmailLoginFormRef } from './__email-login-form/auth-panel__email-login-form'; export interface Props { @@ -25,7 +25,7 @@ export interface Props { isCommentsDisabled: boolean; theme: Theme; postInfo: PostInfo; - providers: (AuthProvider['name'])[]; + providers: AuthProvider['name'][]; provider: ProviderState; onSortChange(s: Sorting): Promise; @@ -129,11 +129,6 @@ export class AuthPanel extends Component { } } - getUserTitle() { - const { user } = this.props; - return {user!.name}; - } - /** wrapper function to handle both oauth and anonymous providers*/ onSignIn(provider: AuthProvider) { this.props.onSignIn(provider); @@ -153,7 +148,7 @@ export class AuthPanel extends Component { } renderAuthorized = () => { - const { user, onSignOut } = this.props; + const { user, onSignOut, theme } = this.props; if (!user) return null; const isUserAnonymous = user && user.id.substr(0, 10) === 'anonymous_'; @@ -161,20 +156,26 @@ export class AuthPanel extends Component { return (
You signed in as{' '} - + - +
+ {user.id} +
{!isUserAnonymous && ( - )}
{' '} -
@@ -186,7 +187,7 @@ export class AuthPanel extends Component { return ( @@ -203,7 +204,7 @@ export class AuthPanel extends Component { return ( @@ -220,25 +221,21 @@ export class AuthPanel extends Component { } return ( - {PROVIDER_NAMES[provider]} - + ); }; - renderOther = (providers: (AuthProvider['name'])[]) => { + renderOther = (providers: AuthProvider['name'][]) => { return ( - + {providers.map(provider => ( {this.renderProvider(provider, true)} ))} @@ -323,26 +320,28 @@ export class AuthPanel extends Component { renderSettingsLabel = () => { return ( - this.toggleBlockedVisibility())} role="link" > {this.state.isBlockedVisible ? 'Hide' : 'Show'} settings - + ); }; renderReadOnlySwitch = () => { const { isCommentsDisabled } = this.props; return ( - this.toggleCommentsAvailability())} role="link" > {isCommentsDisabled ? 'Enable' : 'Disable'} comments - + ); }; diff --git a/frontend/app/components/auth-panel/index.ts b/frontend/app/components/auth-panel/index.ts index 779d12c4..861405b4 100644 --- a/frontend/app/components/auth-panel/index.ts +++ b/frontend/app/components/auth-panel/index.ts @@ -5,18 +5,14 @@ import './auth-panel.scss'; import './__readonly-label/auth-panel__readonly-label.scss'; import './__column/auth-panel__column.scss'; -import './__pseudo-link/auth-panel__pseudo-link.scss'; import './__select/auth-panel__select.scss'; import './__select-label/auth-panel__select-label.scss'; import './__select-label-value/auth-panel__select-label-value.scss'; import './__sort/auth-panel__sort.scss'; import './__user-id/auth-panel__user-id.scss'; -import './__sign-out/auth-panel__sign-out.scss'; import './_theme/_dark/auth-panel_theme_dark.scss'; import './_theme/_light/auth-panel_theme_light.scss'; import './_logged-in/auth-panel_logged-in.scss'; - -import './__dropdown-provider/auth-panel__dropdown-provider.scss'; diff --git a/frontend/app/components/button/_focused/button_focused.scss b/frontend/app/components/button/_focused/button_focused.scss deleted file mode 100644 index 098f02aa..00000000 --- a/frontend/app/components/button/_focused/button_focused.scss +++ /dev/null @@ -1,3 +0,0 @@ -.button_focused:not(.button_clicked) { - outline-width: 5px; -} diff --git a/frontend/app/components/button/_kind/_link/button_kind_link.scss b/frontend/app/components/button/_kind/_link/button_kind_link.scss index b973fb8a..46a5505a 100644 --- a/frontend/app/components/button/_kind/_link/button_kind_link.scss +++ b/frontend/app/components/button/_kind/_link/button_kind_link.scss @@ -1,10 +1,6 @@ .button_kind_link { - border: none; background: transparent; - padding: 0; - - font-weight: 700; - white-space: nowrap; + font-weight: bold; color: #0aa; &:hover { diff --git a/frontend/app/components/button/_kind/_primary/button_kind_primary.scss b/frontend/app/components/button/_kind/_primary/button_kind_primary.scss new file mode 100644 index 00000000..0fbef096 --- /dev/null +++ b/frontend/app/components/button/_kind/_primary/button_kind_primary.scss @@ -0,0 +1,12 @@ +.button_kind_primary { + background: #259c9a; + color: #fff; + + &:hover { + background: #06c5c5; + } + + &:hover&:disabled { + background: #259c9a; + } +} diff --git a/frontend/app/components/button/_kind/_secondary/button_kind_secondary.scss b/frontend/app/components/button/_kind/_secondary/button_kind_secondary.scss new file mode 100644 index 00000000..b46c2c4d --- /dev/null +++ b/frontend/app/components/button/_kind/_secondary/button_kind_secondary.scss @@ -0,0 +1,8 @@ +.button_kind_secondary { + background: #fff; + color: inherit; + + &:hover { + box-shadow: inset 0 0 0 2px #06c5c5; + } +} diff --git a/frontend/app/components/button/_kind/_text/button_kind_text.scss b/frontend/app/components/button/_kind/_text/button_kind_text.scss deleted file mode 100644 index 30bfeb2e..00000000 --- a/frontend/app/components/button/_kind/_text/button_kind_text.scss +++ /dev/null @@ -1,5 +0,0 @@ -.button_kind_text { - border: none; - background: transparent; - padding: 0; -} diff --git a/frontend/app/components/button/_size/_large/button_size_large.scss b/frontend/app/components/button/_size/_large/button_size_large.scss new file mode 100644 index 00000000..f721bd3c --- /dev/null +++ b/frontend/app/components/button/_size/_large/button_size_large.scss @@ -0,0 +1,5 @@ +.button_size_large { + height: 2rem; + padding: 0 12px; + font-size: 16px; +} diff --git a/frontend/app/components/button/_size/_middle/button_size_middle.scss b/frontend/app/components/button/_size/_middle/button_size_middle.scss new file mode 100644 index 00000000..73b58c83 --- /dev/null +++ b/frontend/app/components/button/_size/_middle/button_size_middle.scss @@ -0,0 +1,4 @@ +.button_size_middle { + height: 2rem; + padding: 0 12px; +} diff --git a/frontend/app/components/button/_theme/_dark/button_theme_dark.scss b/frontend/app/components/button/_theme/_dark/button_theme_dark.scss new file mode 100644 index 00000000..a3431cc5 --- /dev/null +++ b/frontend/app/components/button/_theme/_dark/button_theme_dark.scss @@ -0,0 +1,4 @@ +.button_theme_dark.button_kind_secondary { + background: #22201c; + color: #ddd; +} diff --git a/frontend/app/components/button/button.scss b/frontend/app/components/button/button.scss index cba7381d..2080eca3 100644 --- a/frontend/app/components/button/button.scss +++ b/frontend/app/components/button/button.scss @@ -1,6 +1,21 @@ .button { - cursor: pointer; - outline-width: 0; - font-size: inherit; + background: none; + border: 0; + padding: 0; + margin: 0; + border-radius: 2px; font-family: inherit; + font-size: inherit; + cursor: pointer; + white-space: nowrap; + + &:focus { + box-shadow: 0 0 0 2px rgba(37, 156, 154, 0.4); + outline: none; + } + + &:disabled { + opacity: 0.4; + cursor: default; + } } diff --git a/frontend/app/components/button/button.tsx b/frontend/app/components/button/button.tsx index cbff6e06..c08e992a 100644 --- a/frontend/app/components/button/button.tsx +++ b/frontend/app/components/button/button.tsx @@ -1,107 +1,26 @@ /** @jsx createElement */ -import { createElement, JSX, Component } from 'preact'; -import b from 'bem-react-helper'; - -import noop from '@app/utils/noop'; +import { createElement, JSX } from 'preact'; +import { forwardRef } from 'preact/compat'; +import b, { Mods, Mix } from 'bem-react-helper'; import { Theme } from '@app/common/types'; -type Props = { +interface Props extends Omit { + kind?: 'primary' | 'secondary' | 'link'; + size?: 'middle' | 'large'; + theme?: Theme; + mods?: Mods; + mix?: Mix; type?: string; - kind?: string; - theme: Theme; - mix?: string; - - onClick?: (e: MouseEvent) => void; - onFocus?: (e: FocusEvent) => void; - onBlur?: (e: FocusEvent) => void; -} & JSX.HTMLAttributes; - -interface State { - isClicked: boolean; - isFocused: boolean; } -export class Button extends Component { - constructor(props: Props) { - super(props); - - this.state = { - isClicked: false, - isFocused: false, - }; - - this.onMouseDown = this.onMouseDown.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - } - - onMouseDown() { - this.setState({ - isClicked: true, - }); - } - - onClick(e: MouseEvent) { - this.props.onClick!(e); - } - - onBlur(e: FocusEvent) { - this.setState({ - isClicked: false, - isFocused: false, - }); - - this.props.onBlur!(e); - } - - onFocus(e: FocusEvent) { - this.setState({ - isFocused: true, - }); - - this.props.onFocus!(e); - } - - render(props: Props, state: State) { - const { children, className } = props; - const { isClicked, isFocused } = state; - - let rclassName = b( - 'button', - { mix: props.mix }, - { theme: props.theme, type: props.type, kind: props.kind, clicked: isClicked, focused: isFocused } - ); - if (className) { - rclassName += - ' ' + - b( - className, - {}, - { theme: props.theme, type: props.type, kind: props.kind, clicked: isClicked, focused: isFocused } - ); - } - - const localProps = { ...props }; - delete localProps.children; - delete localProps.mix; +export const Button = forwardRef( + ({ children, theme, mods, mix, kind, type = 'button', size, ...props }) => { + const className = b('button', { mods: { kind, size }, mix }, { theme, ...mods }); return ( - ); } -} - -Button.defaultProps = { - type: 'button', - onClick: noop, - onBlur: noop, - onFocus: noop, -}; +); diff --git a/frontend/app/components/button/index.ts b/frontend/app/components/button/index.ts index 14ce2c9d..98ff80d3 100644 --- a/frontend/app/components/button/index.ts +++ b/frontend/app/components/button/index.ts @@ -3,6 +3,10 @@ export { Button } from './button'; import './button.scss'; import './_kind/_link/button_kind_link.scss'; -import './_kind/_text/button_kind_text.scss'; +import './_kind/_primary/button_kind_primary.scss'; +import './_kind/_secondary/button_kind_secondary.scss'; -import './_focused/button_focused.scss'; +import './_size/_large/button_size_large.scss'; +import './_size/_middle/button_size_middle.scss'; + +import './_theme/_dark/button_theme_dark.scss'; diff --git a/frontend/app/components/input/__actions/input__actions.scss b/frontend/app/components/comment-form/__actions/comment-form__actions.scss similarity index 71% rename from frontend/app/components/input/__actions/input__actions.scss rename to frontend/app/components/comment-form/__actions/comment-form__actions.scss index 36c1e30c..00571f71 100644 --- a/frontend/app/components/input/__actions/input__actions.scss +++ b/frontend/app/components/comment-form/__actions/comment-form__actions.scss @@ -1,4 +1,4 @@ -.input__actions { +.comment-form__actions { display: flex; align-items: center; padding-top: 8px; diff --git a/frontend/app/components/input/__button/_type/_preview/input__button_type_preview.scss b/frontend/app/components/comment-form/__button/_type/_preview/comment-form__button_type_preview.scss similarity index 69% rename from frontend/app/components/input/__button/_type/_preview/input__button_type_preview.scss rename to frontend/app/components/comment-form/__button/_type/_preview/comment-form__button_type_preview.scss index 5a1c55bb..8c7e5425 100644 --- a/frontend/app/components/input/__button/_type/_preview/input__button_type_preview.scss +++ b/frontend/app/components/comment-form/__button/_type/_preview/comment-form__button_type_preview.scss @@ -1,4 +1,4 @@ -.input__button_type_preview { +.comment-form__button_type_preview { &:hover, &:focus { box-shadow: inset 0 0 0 2px #0aa; diff --git a/frontend/app/components/input/__button/_type/_send/input__button_type_send.scss b/frontend/app/components/comment-form/__button/_type/_send/comment-form__button_type_send.scss similarity index 72% rename from frontend/app/components/input/__button/_type/_send/input__button_type_send.scss rename to frontend/app/components/comment-form/__button/_type/_send/comment-form__button_type_send.scss index cf24b329..32727828 100644 --- a/frontend/app/components/input/__button/_type/_send/input__button_type_send.scss +++ b/frontend/app/components/comment-form/__button/_type/_send/comment-form__button_type_send.scss @@ -1,4 +1,4 @@ -.input__button_type_send { +.comment-form__button_type_send { background: #259c9a; color: #fff; diff --git a/frontend/app/components/comment-form/__button/comment-form__button.scss b/frontend/app/components/comment-form/__button/comment-form__button.scss new file mode 100644 index 00000000..eea1c1e5 --- /dev/null +++ b/frontend/app/components/comment-form/__button/comment-form__button.scss @@ -0,0 +1,4 @@ +.comment-form__button { + margin: 8px 8px 0 0; + align-self: flex-start; +} diff --git a/frontend/app/components/input/__control-panel/input__control-panel.scss b/frontend/app/components/comment-form/__control-panel/comment-form__control-panel.scss similarity index 58% rename from frontend/app/components/input/__control-panel/input__control-panel.scss rename to frontend/app/components/comment-form/__control-panel/comment-form__control-panel.scss index dbe5e17b..194f7bde 100644 --- a/frontend/app/components/input/__control-panel/input__control-panel.scss +++ b/frontend/app/components/comment-form/__control-panel/comment-form__control-panel.scss @@ -1,4 +1,4 @@ -.input__control-panel { +.comment-form__control-panel { height: 30px; background-color: #eee; } diff --git a/frontend/app/components/input/__counter/input__counter.scss b/frontend/app/components/comment-form/__counter/comment-form__counter.scss similarity index 81% rename from frontend/app/components/input/__counter/input__counter.scss rename to frontend/app/components/comment-form/__counter/comment-form__counter.scss index 31b8a7d8..23f5eb21 100644 --- a/frontend/app/components/input/__counter/input__counter.scss +++ b/frontend/app/components/comment-form/__counter/comment-form__counter.scss @@ -1,4 +1,4 @@ -.input__counter { +.comment-form__counter { position: absolute; right: 4px; bottom: 4px; diff --git a/frontend/app/components/input/__error/input__error.scss b/frontend/app/components/comment-form/__error/comment-form__error.scss similarity index 77% rename from frontend/app/components/input/__error/input__error.scss rename to frontend/app/components/comment-form/__error/comment-form__error.scss index 5f71bc64..36f49315 100644 --- a/frontend/app/components/input/__error/input__error.scss +++ b/frontend/app/components/comment-form/__error/comment-form__error.scss @@ -1,4 +1,4 @@ -.input__error { +.comment-form__error { margin: 0; padding: 10px 12px; font-size: 14px; diff --git a/frontend/app/components/comment-form/__field-wrapper/comment-form__field-wrapper.scss b/frontend/app/components/comment-form/__field-wrapper/comment-form__field-wrapper.scss new file mode 100644 index 00000000..ef5fc64e --- /dev/null +++ b/frontend/app/components/comment-form/__field-wrapper/comment-form__field-wrapper.scss @@ -0,0 +1,3 @@ +.comment-form__field-wrapper { + position: relative; +} diff --git a/frontend/app/components/input/__field/input__field.scss b/frontend/app/components/comment-form/__field/comment-form__field.scss similarity index 86% rename from frontend/app/components/input/__field/input__field.scss rename to frontend/app/components/comment-form/__field/comment-form__field.scss index 6531b563..37a21647 100644 --- a/frontend/app/components/input/__field/input__field.scss +++ b/frontend/app/components/comment-form/__field/comment-form__field.scss @@ -1,4 +1,4 @@ -.input__field { +.comment-form__field { $lineHeight: 1.4; $fontSize: 16px; $paddingVrt: 10px; @@ -20,7 +20,8 @@ transform: translateZ(0); /* let's try to fix blinking in Safari, again */ &:focus { - box-shadow: inset 0 0 0 2px #259c9a; + box-shadow: 0 0 0 2px rgba(37, 156, 154, 0.4); + border-color: #259c9a; outline: none; } diff --git a/frontend/app/components/input/__markdown-link/input__markdown-link.scss b/frontend/app/components/comment-form/__markdown-link/comment-form__markdown-link.scss similarity index 81% rename from frontend/app/components/input/__markdown-link/input__markdown-link.scss rename to frontend/app/components/comment-form/__markdown-link/comment-form__markdown-link.scss index 08fb0b53..268e85c0 100644 --- a/frontend/app/components/input/__markdown-link/input__markdown-link.scss +++ b/frontend/app/components/comment-form/__markdown-link/comment-form__markdown-link.scss @@ -1,4 +1,4 @@ -.input__markdown-link { +.comment-form__markdown-link { font-weight: 700; white-space: nowrap; text-decoration: none; diff --git a/frontend/app/components/input/__markdown-toolbar/input__markdown-toolbar.scss b/frontend/app/components/comment-form/__markdown-toolbar/comment-form__markdown-toolbar.scss similarity index 66% rename from frontend/app/components/input/__markdown-toolbar/input__markdown-toolbar.scss rename to frontend/app/components/comment-form/__markdown-toolbar/comment-form__markdown-toolbar.scss index fd58dca5..729aa27f 100644 --- a/frontend/app/components/input/__markdown-toolbar/input__markdown-toolbar.scss +++ b/frontend/app/components/comment-form/__markdown-toolbar/comment-form__markdown-toolbar.scss @@ -1,9 +1,9 @@ -.input__toolbar { +.comment-form__toolbar { display: block; float: right; } -.input__toolbar-file-input { +.comment-form__toolbar-file-input { position: absolute; width: 1px; height: 1px; @@ -11,7 +11,7 @@ clip: rect(0 0 0 0); } -.input__toolbar-item { +.comment-form__toolbar-item { background: none; border: 0; color: #586069; @@ -24,16 +24,16 @@ } } -.input__toolbar-icon { +.comment-form__toolbar-icon { display: inline-block; fill: currentColor; } -.input__toolbar-group { +.comment-form__toolbar-group { display: inline-block; margin-left: 20px; } -.input__toolbar-group:first-child { +.comment-form__toolbar-group:first-child { margin-left: 0; } diff --git a/frontend/app/components/comment-form/__markdown/comment-form__markdown.scss b/frontend/app/components/comment-form/__markdown/comment-form__markdown.scss new file mode 100644 index 00000000..8288a500 --- /dev/null +++ b/frontend/app/components/comment-form/__markdown/comment-form__markdown.scss @@ -0,0 +1,3 @@ +.comment-form__markdown { + margin-bottom: 5px; +} diff --git a/frontend/app/components/comment-form/__preview-wrapper/comment-form__preview-wrapper.scss b/frontend/app/components/comment-form/__preview-wrapper/comment-form__preview-wrapper.scss new file mode 100644 index 00000000..7b64c089 --- /dev/null +++ b/frontend/app/components/comment-form/__preview-wrapper/comment-form__preview-wrapper.scss @@ -0,0 +1,3 @@ +.comment-form__preview-wrapper { + overflow: hidden; +} diff --git a/frontend/app/components/input/__preview/input__preview.scss b/frontend/app/components/comment-form/__preview/comment-form__preview.scss similarity index 85% rename from frontend/app/components/input/__preview/input__preview.scss rename to frontend/app/components/comment-form/__preview/comment-form__preview.scss index 4a97624f..8c4efa49 100644 --- a/frontend/app/components/input/__preview/input__preview.scss +++ b/frontend/app/components/comment-form/__preview/comment-form__preview.scss @@ -1,4 +1,4 @@ -.input__preview { +.comment-form__preview { margin-top: 8px; padding: 7px 11px; overflow: hidden; diff --git a/frontend/app/components/input/__rss-link/input__rss-link.scss b/frontend/app/components/comment-form/__rss-link/comment-form__rss-link.scss similarity index 84% rename from frontend/app/components/input/__rss-link/input__rss-link.scss rename to frontend/app/components/comment-form/__rss-link/comment-form__rss-link.scss index e35e602b..39ddb646 100644 --- a/frontend/app/components/input/__rss-link/input__rss-link.scss +++ b/frontend/app/components/comment-form/__rss-link/comment-form__rss-link.scss @@ -1,4 +1,4 @@ -.input__rss-link { +.comment-form__rss-link { font-weight: 700; white-space: nowrap; text-decoration: none; diff --git a/frontend/app/components/input/__rss/input__rss.scss b/frontend/app/components/comment-form/__rss/comment-form__rss.scss similarity index 83% rename from frontend/app/components/input/__rss/input__rss.scss rename to frontend/app/components/comment-form/__rss/comment-form__rss.scss index c4da29a4..96dbaafb 100644 --- a/frontend/app/components/input/__rss/input__rss.scss +++ b/frontend/app/components/comment-form/__rss/comment-form__rss.scss @@ -1,4 +1,4 @@ -.input__rss { +.comment-form__rss { margin-left: auto; align-self: flex-end; font-size: 12px; diff --git a/frontend/app/components/input/_simple/input_simple.scss b/frontend/app/components/comment-form/_simple/comment-form_simple.scss similarity index 51% rename from frontend/app/components/input/_simple/input_simple.scss rename to frontend/app/components/comment-form/_simple/comment-form_simple.scss index dd696ce0..eb0b8b19 100644 --- a/frontend/app/components/input/_simple/input_simple.scss +++ b/frontend/app/components/comment-form/_simple/comment-form_simple.scss @@ -1,3 +1,3 @@ -.input_simple { +.comment-form_simple { border-width: 12px; } diff --git a/frontend/app/components/input/_theme/_dark/input_theme_dark.scss b/frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.scss similarity index 62% rename from frontend/app/components/input/_theme/_dark/input_theme_dark.scss rename to frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.scss index 4a0c1e73..3e7e1403 100644 --- a/frontend/app/components/input/_theme/_dark/input_theme_dark.scss +++ b/frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.scss @@ -1,49 +1,49 @@ -.input_theme_dark { +.comment-form_theme_dark { border-color: #333; background: #22201c; /* try to fix textarea blinking in Safari */ - .input__actions { + .comment-form__actions { background: #333; } - .input__button_type_preview { + .comment-form__button_type_preview { background: #22201c; color: #ddd; } - .input__button_type_send { + .comment-form__button_type_send { color: #ddd; } - .input__error { + .comment-form__error { border-top: 8px solid #333; background: #672323; color: #f98989; } - .input__field { + .comment-form__field { background: #22201c; color: #eee; } - .input__preview { + .comment-form__preview { border-color: #22201c; background: #22201c; color: #ddd; } - .input__preview-wrapper { + .comment-form__preview-wrapper { background: #333; } - .input__toolbar-item { + .comment-form__toolbar-item { color: #ddd; &:hover { color: #0aa; } } - .input__control-panel { + .comment-form__control-panel { background-color: #333; } } diff --git a/frontend/app/components/input/_theme/_light/input_theme_light.scss b/frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.scss similarity index 62% rename from frontend/app/components/input/_theme/_light/input_theme_light.scss rename to frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.scss index efb89b0b..f3104b5f 100644 --- a/frontend/app/components/input/_theme/_light/input_theme_light.scss +++ b/frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.scss @@ -1,38 +1,38 @@ -.input_theme_light { +.comment-form_theme_light { border-color: #eee; background: #fff; /* try to fix textarea blinking in Safari */ - .input__actions { + .comment-form__actions { background: #eee; } - .input__button_type_preview { + .comment-form__button_type_preview { background: #fff; color: #000; } - .input__button_type_send { + .comment-form__button_type_send { color: #fff; } - .input__error { + .comment-form__error { border-top: 8px solid #eee; background: #ffd7d7; color: #9a0000; } - .input__field { + .comment-form__field { background: #fff; color: #000; } - .input__preview { + .comment-form__preview { border-color: #eee; background: #fff; color: #333; } - .input__preview-wrapper { + .comment-form__preview-wrapper { background: #eee; } } diff --git a/frontend/app/components/comment-form/comment-form.scss b/frontend/app/components/comment-form/comment-form.scss new file mode 100644 index 00000000..3e635bf9 --- /dev/null +++ b/frontend/app/components/comment-form/comment-form.scss @@ -0,0 +1,8 @@ +.comment-form { + position: relative; + display: block; + font-size: 0; + border-style: solid; + border-width: 6px 12px 12px 12px; + border-radius: 2px; +} diff --git a/frontend/app/components/comment-form/comment-form.test.tsx b/frontend/app/components/comment-form/comment-form.test.tsx new file mode 100644 index 00000000..e144c5d7 --- /dev/null +++ b/frontend/app/components/comment-form/comment-form.test.tsx @@ -0,0 +1,15 @@ +/** @jsx createElement */ +import { createElement } from 'preact'; +import { shallow } from 'enzyme'; + +import { CommentForm, Props } from './comment-form'; + +describe('', () => { + it('shoud render without control panel, preview button, and rss links in "simple view" mode', () => { + const element = shallow(); + + expect(element.exists('.comment-form__control-panel')).toEqual(false); + expect(element.exists('.comment-form__button_type_preview')).toEqual(false); + expect(element.exists('.comment-form__rss')).toEqual(false); + }); +}); diff --git a/frontend/app/components/comment-form/comment-form.tsx b/frontend/app/components/comment-form/comment-form.tsx new file mode 100644 index 00000000..ed7b4d8a --- /dev/null +++ b/frontend/app/components/comment-form/comment-form.tsx @@ -0,0 +1,453 @@ +/** @jsx createElement */ +import { createElement, Component, createRef } from 'preact'; +import b, { Mix } from 'bem-react-helper'; + +import { User, Theme, Image, ApiError } from '@app/common/types'; +import { BASE_URL, API_BASE } from '@app/common/constants'; +import { StaticStore } from '@app/common/static_store'; +import { siteId, url, pageTitle } from '@app/common/settings'; +import { extractErrorMessageFromResponse } from '@app/utils/errorUtils'; +import { sleep } from '@app/utils/sleep'; +import { replaceSelection } from '@app/utils/replaceSelection'; +import { Button } from '@app/components/button'; + +import MarkdownToolbar from './markdown-toolbar'; +import TextareaAutosize from './textarea-autosize'; + +const RSS_THREAD_URL = `${BASE_URL}${API_BASE}/rss/post?site=${siteId}&url=${url}`; +const RSS_SITE_URL = `${BASE_URL}${API_BASE}/rss/site?site=${siteId}`; +const RSS_REPLIES_URL = `${BASE_URL}${API_BASE}/rss/reply?site=${siteId}&user=`; + +let textareaId = 0; + +export interface Props { + /** user id for rss link generation */ + userId?: User['id']; + errorMessage?: string; + value?: string; + mix?: Mix; + mode?: 'main' | 'edit' | 'reply'; + theme: Theme; + simpleView?: boolean; + autofocus?: boolean; + + onSubmit(text: string, pageTitle: string): Promise; + getPreview(text: string): Promise; + /** action on cancel. optional as root input has no cancel option */ + onCancel?: () => void; + uploadImage?: (image: File) => Promise; +} + +interface State { + preview: string | null; + isErrorShown: boolean; + /** error message, if contains newlines, it will be splitted to multiple errors */ + errorMessage: string | null; + /** prevents error hiding on input event */ + errorLock: boolean; + isDisabled: boolean; + maxLength: number; + /** main input value */ + text: string; + /** override main button text */ + buttonText: null | string; +} + +const Labels = { + main: 'Send', + edit: 'Save', + reply: 'Reply', +}; + +const ImageMimeRegex = /image\//i; + +export class CommentForm extends Component { + /** reference to textarea element */ + textAreaRef = createRef(); + textareaId: string; + + constructor(props: Props) { + super(props); + textareaId = textareaId + 1; + this.textareaId = `textarea_${textareaId}`; + this.state = { + preview: null, + isErrorShown: false, + errorMessage: null, + errorLock: false, + isDisabled: false, + maxLength: StaticStore.config.max_comment_size, + text: props.value || '', + buttonText: null, + }; + + this.send = this.send.bind(this); + this.getPreview = this.getPreview.bind(this); + this.onInput = this.onInput.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onDragOver = this.onDragOver.bind(this); + this.onDrop = this.onDrop.bind(this); + this.appendError = this.appendError.bind(this); + this.uploadImage = this.uploadImage.bind(this); + this.uploadImages = this.uploadImages.bind(this); + this.onPaste = this.onPaste.bind(this); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.value !== this.props.value) { + this.setState({ text: nextProps.value || '' }); + this.props.autofocus && this.textAreaRef.current && this.textAreaRef.current.focus(); + } + } + + shouldComponentUpdate(nextProps: Props, nextState: State) { + return ( + nextProps.mode !== this.props.mode || + nextProps.theme !== this.props.theme || + nextProps.userId !== this.props.userId || + nextProps.value !== this.props.value || + nextProps.errorMessage !== this.props.errorMessage || + nextState !== this.state + ); + } + + onKeyDown(e: KeyboardEvent) { + // send on cmd+enter / ctrl+enter + if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) { + this.send(e); + } + } + + onInput(e: Event) { + if (this.state.errorLock) { + this.setState({ + preview: null, + text: (e.target as HTMLInputElement).value, + }); + return; + } + this.setState({ + isErrorShown: false, + errorMessage: null, + preview: null, + text: (e.target as HTMLInputElement).value, + }); + } + + async onPaste(e: ClipboardEvent) { + if (!(e.clipboardData && e.clipboardData.files.length > 0)) { + return; + } + e.preventDefault(); + const files = Array.from(e.clipboardData.files); + await this.uploadImages(files); + } + + send(e: Event) { + const text = this.state.text; + const props = this.props; + + if (e) e.preventDefault(); + + if (!text || !text.trim()) return; + + if (text === this.props.value) { + this.props.onCancel && this.props.onCancel(); + this.setState({ preview: null, text: '' }); + } + + this.setState({ isDisabled: true, isErrorShown: false }); + + props + .onSubmit(text, pageTitle || document.title) + .then(() => { + this.setState({ preview: null, text: '' }); + }) + .catch(e => { + console.error(e); // eslint-disable-line no-console + const errorMessage = extractErrorMessageFromResponse(e); + this.setState({ isErrorShown: true, errorMessage }); + }) + .finally(() => this.setState({ isDisabled: false })); + } + + getPreview() { + const text = this.state.text; + + if (!text || !text.trim()) return; + + this.setState({ isErrorShown: false, errorMessage: null }); + + this.props + .getPreview(text) + .then(preview => this.setState({ preview })) + .catch(() => { + this.setState({ isErrorShown: true, errorMessage: null }); + }); + } + + /** appends error to input's error block */ + appendError(...errors: string[]) { + if (!this.state.errorMessage) { + this.setState({ + errorMessage: errors.join('\n'), + isErrorShown: true, + }); + return; + } + this.setState({ + errorMessage: this.state.errorMessage + '\n' + errors.join('\n'), + isErrorShown: true, + }); + } + + onDragOver(e: DragEvent) { + if (!this.props.uploadImage) return; + if (StaticStore.config.max_image_size === 0) return; + if (!this.textAreaRef) return; + if (!e.dataTransfer) return; + const items = Array.from(e.dataTransfer.items); + if (Array.from(items).filter(i => i.kind === 'file' && ImageMimeRegex.test(i.type)).length === 0) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + } + + onDrop(e: DragEvent) { + if (!this.props.uploadImage) return; + if (StaticStore.config.max_image_size === 0) return; + if (!e.dataTransfer) return; + + const data = Array.from(e.dataTransfer.files).filter(f => ImageMimeRegex.test(f.type)); + if (data.length === 0) return; + + e.preventDefault(); + + this.uploadImages(data); + } + + /** wrapper with error handling for props.uploadImage */ + uploadImage(file: File): Promise { + return this.props.uploadImage!(file).catch( + (e: ApiError | string) => + new Error( + typeof e === 'string' + ? `${file.name} upload failed with "${e}"` + : `${file.name} upload failed with "${e.error}"` + ) + ); + } + + /** performs upload process */ + async uploadImages(files: File[]) { + if (!this.props.uploadImage) return; + if (!this.textAreaRef.current) return; + + /** Human readable image size limit, i.e 5MB */ + const maxImageSizeString = (StaticStore.config.max_image_size / 1024 / 1024).toFixed(2) + 'MB'; + /** upload delay to avoid server rate limiter */ + const uploadDelay = 5000; + + const isSelectionSupported = this.textAreaRef.current.isSelectionSupported(); + + this.setState({ + errorLock: true, + errorMessage: null, + isErrorShown: false, + isDisabled: true, + buttonText: 'Uploading...', + }); + + // fallback for ie < 9 + if (!isSelectionSupported) { + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const isFirst = i === 0; + const placeholderStart = this.state.text.length === 0 ? '' : '\n'; + + if (file.size > StaticStore.config.max_image_size) { + this.appendError(`${file.name} exceeds size limit of ${maxImageSizeString}`); + continue; + } + + !isFirst && (await sleep(uploadDelay)); + + const result = await this.uploadImage(file); + + if (result instanceof Error) { + this.appendError(result.message); + continue; + } + + const markdownString = `${placeholderStart}![${result.name}](${result.url})`; + this.setState({ + text: this.state.text + markdownString, + }); + } + + this.setState({ errorLock: false, isDisabled: false, buttonText: null }); + return; + } + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const isFirst = i === 0; + const placeholderStart = this.state.text.length === 0 ? '' : '\n'; + + const uploadPlaceholder = `${placeholderStart}![uploading ${file.name}...]()`; + const uploadPlaceholderLength = uploadPlaceholder.length; + const selection = this.textAreaRef.current.getSelection(); + /** saved selection in case of error */ + const originalText = this.state.text; + const restoreSelection = async () => { + this.setState({ + text: originalText, + }); + /** sleeping awhile so textarea catch state change and its selection */ + await sleep(100); + this.textAreaRef.current!.setSelection(selection); + }; + + if (file.size > StaticStore.config.max_image_size) { + this.appendError(`${file.name} exceeds size limit of ${maxImageSizeString}`); + continue; + } + + this.setState({ + text: replaceSelection(this.state.text, selection, uploadPlaceholder), + }); + + !isFirst && (await sleep(uploadDelay)); + + const result = await this.uploadImage(file); + + if (result instanceof Error) { + this.appendError(result.message); + await restoreSelection(); + continue; + } + + const markdownString = `${placeholderStart}![${result.name}](${result.url})`; + this.setState({ + text: replaceSelection(this.state.text, [selection[0], selection[0] + uploadPlaceholderLength], markdownString), + }); + /** sleeping awhile so textarea catch state change and its selection */ + await sleep(100); + const selectionPointer = selection[0] + markdownString.length; + this.textAreaRef.current.setSelection([selectionPointer, selectionPointer]); + } + + this.setState({ errorLock: false, isDisabled: false, buttonText: null }); + } + + render(props: Props, { isDisabled, isErrorShown, errorMessage, preview, maxLength, text, buttonText }: State) { + const charactersLeft = maxLength - text.length; + errorMessage = props.errorMessage || errorMessage; + const label = buttonText || Labels[props.mode || 'main']; + + return ( +
+ {!props.simpleView && ( +
+ +
+ )} +
+ + + {charactersLeft < 100 && {charactersLeft}} +
+ + {(isErrorShown || !!errorMessage) && + (errorMessage || 'Something went wrong. Please try again a bit later.').split('\n').map(e => ( +

+ {e} +

+ ))} + +
+ {!props.simpleView && ( + + )} + + + {!props.simpleView && props.mode === 'main' && ( +
+
+ Styling with{' '} + + Markdown + {' '} + is supported +
+ Subscribe to the{' '} + + Thread + + {', '} + + Site + {' '} + or  + + Replies + {' '} + by RSS +
+ )} +
+ + {// TODO: it can be more elegant; + // for example it can render full comment component here (or above textarea on mobile) + !!preview && ( +
+
+
+ )} + + ); + } +} diff --git a/frontend/app/components/comment-form/index.ts b/frontend/app/components/comment-form/index.ts new file mode 100644 index 00000000..f44db006 --- /dev/null +++ b/frontend/app/components/comment-form/index.ts @@ -0,0 +1,29 @@ +export { CommentForm } from './comment-form'; + +import '@app/components/raw-content'; + +import './comment-form.scss'; + +import './__actions/comment-form__actions.scss'; + +import './__button/comment-form__button.scss'; +import './__button/_type/_preview/comment-form__button_type_preview.scss'; +import './__button/_type/_send/comment-form__button_type_send.scss'; + +import './__control-panel/comment-form__control-panel.scss'; +import './__counter/comment-form__counter.scss'; +import './__error/comment-form__error.scss'; +import './__field/comment-form__field.scss'; +import './__field-wrapper/comment-form__field-wrapper.scss'; +import './__preview/comment-form__preview.scss'; +import './__preview-wrapper/comment-form__preview-wrapper.scss'; +import './__rss/comment-form__rss.scss'; +import './__rss-link/comment-form__rss-link.scss'; +import './__markdown/comment-form__markdown.scss'; +import './__markdown-link/comment-form__markdown-link.scss'; +import './__markdown-toolbar/comment-form__markdown-toolbar.scss'; + +import './_theme/_dark/comment-form_theme_dark.scss'; +import './_theme/_light/comment-form_theme_light.scss'; + +import './_simple/comment-form_simple.scss'; diff --git a/frontend/app/components/input/markdown-toolbar-icons/bold-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/bold-icon.tsx similarity index 73% rename from frontend/app/components/input/markdown-toolbar-icons/bold-icon.tsx rename to frontend/app/components/comment-form/markdown-toolbar-icons/bold-icon.tsx index c237908c..64be5802 100644 --- a/frontend/app/components/input/markdown-toolbar-icons/bold-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/bold-icon.tsx @@ -3,7 +3,14 @@ import { createElement } from 'preact'; export default function BoldIcon() { return ( -
- + +
+ - + - +
-
- +
+ - + - + {this.props.allowUpload ? ( -
-
- +
+ - +
diff --git a/frontend/app/components/input/textarea-autosize.tsx b/frontend/app/components/comment-form/textarea-autosize.tsx similarity index 100% rename from frontend/app/components/input/textarea-autosize.tsx rename to frontend/app/components/comment-form/textarea-autosize.tsx diff --git a/frontend/app/components/comment/__action/_type/_collapse/comment__action_type_collapse.scss b/frontend/app/components/comment/__action/_type/_collapse/comment__action_type_collapse.scss index ce74b6a6..0a1e80f4 100644 --- a/frontend/app/components/comment/__action/_type/_collapse/comment__action_type_collapse.scss +++ b/frontend/app/components/comment/__action/_type/_collapse/comment__action_type_collapse.scss @@ -11,6 +11,7 @@ text-align: center; border: 1px solid; border-radius: 2px; + cursor: pointer; &:hover { border-color: #0aa; diff --git a/frontend/app/components/comment/__action/_type/_delete/comment__action_type_delete.scss b/frontend/app/components/comment/__action/_type/_delete/comment__action_type_delete.scss deleted file mode 100644 index 511c5da9..00000000 --- a/frontend/app/components/comment/__action/_type/_delete/comment__action_type_delete.scss +++ /dev/null @@ -1,5 +0,0 @@ -.comment__action_type_delete { - &:hover { - color: #31c7c5; - } -} diff --git a/frontend/app/components/comment/__action/_type/_edit/comment__action_type_edit.scss b/frontend/app/components/comment/__action/_type/_edit/comment__action_type_edit.scss deleted file mode 100644 index 2621badb..00000000 --- a/frontend/app/components/comment/__action/_type/_edit/comment__action_type_edit.scss +++ /dev/null @@ -1,5 +0,0 @@ -.comment__action_type_edit { - &:hover { - color: #31c7c5; - } -} diff --git a/frontend/app/components/comment/__action/comment__action.scss b/frontend/app/components/comment/__action/comment__action.scss index 19a3323e..69cb8293 100644 --- a/frontend/app/components/comment/__action/comment__action.scss +++ b/frontend/app/components/comment/__action/comment__action.scss @@ -1,13 +1,7 @@ .comment__action { font-size: 14px; vertical-align: middle; - cursor: pointer; - user-select: none; - color: #259c9a; - - &:hover { - color: #31c7c5; - } + font-weight: normal; + .comment__action { margin-left: 8px; diff --git a/frontend/app/components/comment/__control/comment__control.scss b/frontend/app/components/comment/__control/comment__control.scss index 53caeef3..3996951c 100644 --- a/frontend/app/components/comment/__control/comment__control.scss +++ b/frontend/app/components/comment/__control/comment__control.scss @@ -1,13 +1,8 @@ .comment__control { margin-right: 8px; - cursor: pointer; color: #8cd4d4; &:last-child { margin-right: 0; } - - &:hover { - color: #0aa; - } } diff --git a/frontend/app/components/comment/_theme/_dark/comment_theme_dark.scss b/frontend/app/components/comment/_theme/_dark/comment_theme_dark.scss index d6b17955..93a9c6cf 100644 --- a/frontend/app/components/comment/_theme/_dark/comment_theme_dark.scss +++ b/frontend/app/components/comment/_theme/_dark/comment_theme_dark.scss @@ -9,10 +9,7 @@ } } - .comment__action_type_delete { - color: #6a6a6a; - } - + .comment__action_type_delete, .comment__action_type_edit { color: #6a6a6a; } diff --git a/frontend/app/components/comment/_theme/_light/comment_theme_light.scss b/frontend/app/components/comment/_theme/_light/comment_theme_light.scss index 7c4f35ff..33bc5dab 100644 --- a/frontend/app/components/comment/_theme/_light/comment_theme_light.scss +++ b/frontend/app/components/comment/_theme/_light/comment_theme_light.scss @@ -9,10 +9,7 @@ } } - .comment__action_type_delete { - color: #a6a6a6; - } - + .comment__action_type_delete, .comment__action_type_edit { color: #a6a6a6; } diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index 99c3f6cf..1ddfc38e 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -182,7 +182,8 @@ describe('', () => { ); - const controls = element.find('.comment__controls > span'); + const controls = element.find('.comment__controls').children(); + expect(controls.length).toBe(5); expect(controls.at(0).text()).toEqual('Copy'); expect(controls.at(1).text()).toEqual('Pin'); @@ -196,7 +197,7 @@ describe('', () => { ); - const controls = element.find('.comment__controls > span'); + const controls = element.find('.comment__controls').children(); expect(controls.length).toBe(1); expect(controls.at(0).text()).toEqual('Hide'); }); diff --git a/frontend/app/components/comment/comment.tsx b/frontend/app/components/comment/comment.tsx index 06131a9b..1576ed12 100644 --- a/frontend/app/components/comment/comment.tsx +++ b/frontend/app/components/comment/comment.tsx @@ -15,9 +15,10 @@ import { Theme, BlockTTL, Comment as CommentType, PostInfo, User, CommentMode } import { extractErrorMessageFromResponse, FetcherError } from '@app/utils/errorUtils'; import { isUserAnonymous } from '@app/utils/isUserAnonymous'; -import { Input } from '@app/components/input'; +import { CommentForm } from '@app/components/comment-form'; import { AvatarIcon } from '@app/components/avatar-icon'; -import Countdown from '../countdown'; +import { Button } from '@app/components/button'; +import Countdown from '@app/components/countdown'; import { boundActions } from './connected-comment'; import { getPreview, uploadImage } from '@app/common/api'; import postMessage from '@app/utils/postMessage'; @@ -388,33 +389,33 @@ export class Comment extends Component { this.state.isCopied ? ( Copied! ) : ( - + ) ); controls.push( - + ); } if (!isCurrentUser) { controls.push( - + ); } if (isAdmin) { if (this.props.isUserBanned) { controls.push( - + ); } @@ -437,9 +438,9 @@ export class Comment extends Component { if (!this.props.data.delete) { controls.push( - + ); } } @@ -678,9 +679,9 @@ export class Comment extends Component { {(!props.collapsed || props.view === 'pinned') && (
{!props.data.delete && !props.isCommentsDisabled && !props.disabled && !isGuest && props.view === 'main' && ( - + )} {!props.data.delete && !props.disabled && @@ -688,19 +689,21 @@ export class Comment extends Component { isCurrentUser && (editable || isEditing) && props.view === 'main' && [ - {isEditing ? 'Cancel' : 'Edit'} - , + , !isAdmin && ( - Delete - + ), state.editDeadline && ( {
{isReplying && props.view === 'main' && ( - { )} {isEditing && props.view === 'main' && ( - button, & > a, - & > button { - display: block; - width: 100%; + & > .dropdown { + display: inline-block; text-align: left; - padding: 5px 15px; + padding: 5px; } &_separator { diff --git a/frontend/app/components/dropdown/__title/dropdown__title.scss b/frontend/app/components/dropdown/__title/dropdown__title.scss index aaa7b97d..f92b205e 100644 --- a/frontend/app/components/dropdown/__title/dropdown__title.scss +++ b/frontend/app/components/dropdown/__title/dropdown__title.scss @@ -1,10 +1,4 @@ .dropdown__title { - border: none; - background: none; - font-weight: bold; - padding: 0; - margin: 0; - &::after { content: '\25BE'; margin-left: 2px; diff --git a/frontend/app/components/dropdown/_theme/_dark/dropdown_theme_dark.scss b/frontend/app/components/dropdown/_theme/_dark/dropdown_theme_dark.scss index 98265b49..b24b8fa1 100644 --- a/frontend/app/components/dropdown/_theme/_dark/dropdown_theme_dark.scss +++ b/frontend/app/components/dropdown/_theme/_dark/dropdown_theme_dark.scss @@ -1,8 +1,4 @@ .dropdown_theme_dark { - .dropdown__title { - color: #ddd; - } - .dropdown__content { background-color: #22201c; } diff --git a/frontend/app/components/dropdown/_theme/_light/dropdown_theme_light.scss b/frontend/app/components/dropdown/_theme/_light/dropdown_theme_light.scss index d35c1a03..0a1474e3 100644 --- a/frontend/app/components/dropdown/_theme/_light/dropdown_theme_light.scss +++ b/frontend/app/components/dropdown/_theme/_light/dropdown_theme_light.scss @@ -1,8 +1,4 @@ .dropdown_theme_light { - .dropdown__title { - color: #000; - } - .dropdown__content { background-color: #fff; } diff --git a/frontend/app/components/dropdown/dropdown.tsx b/frontend/app/components/dropdown/dropdown.tsx index 36406bc6..24b5fa72 100644 --- a/frontend/app/components/dropdown/dropdown.tsx +++ b/frontend/app/components/dropdown/dropdown.tsx @@ -2,9 +2,9 @@ import { createElement, Component, createRef } from 'preact'; import b from 'bem-react-helper'; -import { Button } from '@app/components/button'; import { Theme } from '@app/common/types'; import { sleep } from '@app/utils/sleep'; +import { Button } from '@app/components/button'; interface Props { title: string; @@ -25,6 +25,9 @@ interface State { export default class Dropdown extends Component { rootNode = createRef(); + storedDocumentHeight: string | null = null; + storedDocumentHeightSet: boolean = false; + checkInterval: number | undefined = undefined; constructor(props: Props) { super(props); @@ -40,7 +43,7 @@ export default class Dropdown extends Component { this.__onClose = this.__onClose.bind(this); } - onTitleClick() { + onTitleClick = () => { const isActive = !this.state.isActive; const contentTranslateX = isActive ? this.state.contentTranslateX : 0; this.setState( @@ -63,11 +66,7 @@ export default class Dropdown extends Component { } } ); - } - - storedDocumentHeight: string | null = null; - storedDocumentHeightSet: boolean = false; - checkInterval: number | undefined = undefined; + }; __onOpen() { const isChildOfDropDown = (() => { @@ -90,6 +89,7 @@ export default class Dropdown extends Component { if (!this.rootNode.current || !this.state.isActive) return; const windowHeight = window.innerHeight; const dcBottom = (() => { + // TODO: use ref const dc = Array.from(this.rootNode.current.children).find(c => c.classList.contains('dropdown__content')); if (!dc) return 0; const rect = dc.getBoundingClientRect(); @@ -112,6 +112,7 @@ export default class Dropdown extends Component { async __adjustDropDownContent() { if (!this.rootNode.current) return; + // TODO: use ref const dc = this.rootNode.current.querySelector('.dropdown__content'); if (!dc) return; await sleep(10); @@ -185,11 +186,10 @@ export default class Dropdown extends Component { diff --git a/frontend/app/components/input/__button/input__button.scss b/frontend/app/components/input/__button/input__button.scss deleted file mode 100644 index 92c6948b..00000000 --- a/frontend/app/components/input/__button/input__button.scss +++ /dev/null @@ -1,14 +0,0 @@ -.input__button { - box-sizing: border-box; - margin: 0 8px 0 0; - padding: 8px 12px; - font-size: 16px; - border: 0; - border-radius: 2px; - cursor: pointer; - - &:disabled { - opacity: 0.5; - pointer-events: none; - } -} diff --git a/frontend/app/components/input/__field-wrapper/input__field-wrapper.scss b/frontend/app/components/input/__field-wrapper/input__field-wrapper.scss deleted file mode 100644 index 1a6cde54..00000000 --- a/frontend/app/components/input/__field-wrapper/input__field-wrapper.scss +++ /dev/null @@ -1,3 +0,0 @@ -.input__field-wrapper { - position: relative; -} diff --git a/frontend/app/components/input/__markdown/input__markdown.scss b/frontend/app/components/input/__markdown/input__markdown.scss deleted file mode 100644 index 8abd80e6..00000000 --- a/frontend/app/components/input/__markdown/input__markdown.scss +++ /dev/null @@ -1,3 +0,0 @@ -.input__markdown { - margin-bottom: 5px; -} diff --git a/frontend/app/components/input/__preview-wrapper/input__preview-wrapper.scss b/frontend/app/components/input/__preview-wrapper/input__preview-wrapper.scss deleted file mode 100644 index e76ebcae..00000000 --- a/frontend/app/components/input/__preview-wrapper/input__preview-wrapper.scss +++ /dev/null @@ -1,3 +0,0 @@ -.input__preview-wrapper { - overflow: hidden; -} diff --git a/frontend/app/components/input/index.ts b/frontend/app/components/input/index.ts index f376f8cd..697ffbc4 100644 --- a/frontend/app/components/input/index.ts +++ b/frontend/app/components/input/index.ts @@ -1 +1,3 @@ export { Input } from './input'; + +import './input.scss'; diff --git a/frontend/app/components/input/input.scss b/frontend/app/components/input/input.scss index 8d43f7a6..78ea6a37 100644 --- a/frontend/app/components/input/input.scss +++ b/frontend/app/components/input/input.scss @@ -1,8 +1,13 @@ .input { - position: relative; - display: block; - font-size: 0; - border-style: solid; - border-width: 6px 12px 12px 12px; - border-radius: 2px; + font-size: 16px; + font-family: inherit; + padding: 4px 8px; + border: 1px solid #c4c4c4; + margin: 0; + + &:focus { + box-shadow: 0 0 0 2px rgba(37, 156, 154, 0.4); + border-color: #259c9a; + outline: none; + } } diff --git a/frontend/app/components/input/input.test.tsx b/frontend/app/components/input/input.test.tsx deleted file mode 100644 index 8a656538..00000000 --- a/frontend/app/components/input/input.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; -import { shallow } from 'enzyme'; - -import { Input, Props } from './input'; - -describe('', () => { - it('shoud render without control panel, preview button, and rss links in "simple view" mode', () => { - const element = shallow(); - - expect(element.exists('.input__control-panel')).toEqual(false); - expect(element.exists('.input__button_type_preview')).toEqual(false); - expect(element.exists('.input__rss')).toEqual(false); - }); -}); diff --git a/frontend/app/components/input/input.tsx b/frontend/app/components/input/input.tsx index 8688ceef..500ba050 100644 --- a/frontend/app/components/input/input.tsx +++ b/frontend/app/components/input/input.tsx @@ -1,456 +1,25 @@ /** @jsx createElement */ +import { createElement, JSX } from 'preact'; +import { forwardRef } from 'preact/compat'; +import b, { Mods, Mix } from 'bem-react-helper'; +import { Theme } from '@app/common/types'; -/* styles imports */ -import '@app/components/raw-content'; -import './styles'; - -import { createElement, Component, createRef } from 'preact'; -import b, { Mix } from 'bem-react-helper'; - -import { User, Theme, Image, ApiError } from '@app/common/types'; -import { BASE_URL, API_BASE } from '@app/common/constants'; -import { StaticStore } from '@app/common/static_store'; -import { siteId, url, pageTitle } from '@app/common/settings'; -import { extractErrorMessageFromResponse } from '@app/utils/errorUtils'; - -import MarkdownToolbar from './markdown-toolbar'; -import TextareaAutosize from './textarea-autosize'; -import { sleep } from '@app/utils/sleep'; -import { replaceSelection } from '@app/utils/replaceSelection'; - -const RSS_THREAD_URL = `${BASE_URL}${API_BASE}/rss/post?site=${siteId}&url=${url}`; -const RSS_SITE_URL = `${BASE_URL}${API_BASE}/rss/site?site=${siteId}`; -const RSS_REPLIES_URL = `${BASE_URL}${API_BASE}/rss/reply?site=${siteId}&user=`; - -let textareaId = 0; - -export interface Props { - /** user id for rss link generation */ - userId?: User['id']; - errorMessage?: string; - value?: string; +interface Props extends JSX.HTMLAttributes { + kind?: 'primary' | 'secondary'; + theme?: Theme; + mods?: Mods; mix?: Mix; - mode?: 'main' | 'edit' | 'reply'; - theme: Theme; - simpleView: boolean | undefined; - autofocus?: boolean; - - onSubmit(text: string, pageTitle: string): Promise; - getPreview(text: string): Promise; - /** action on cancel. optional as root input has no cancel option */ - onCancel?: () => void; - uploadImage?: (image: File) => Promise; + type?: string; } -interface State { - preview: string | null; - isErrorShown: boolean; - /** error message, if contains newlines, it will be splitted to multiple errors */ - errorMessage: string | null; - /** prevents error hiding on input event */ - errorLock: boolean; - isDisabled: boolean; - maxLength: number; - /** main input value */ - text: string; - /** override main button text */ - buttonText: null | string; -} - -const Labels = { - main: 'Send', - edit: 'Save', - reply: 'Reply', -}; - -const ImageMimeRegex = /image\//i; - -export class Input extends Component { - /** reference to textarea element */ - textAreaRef = createRef(); - textareaId: string; - - constructor(props: Props) { - super(props); - textareaId = textareaId + 1; - this.textareaId = `textarea_${textareaId}`; - this.state = { - preview: null, - isErrorShown: false, - errorMessage: null, - errorLock: false, - isDisabled: false, - maxLength: StaticStore.config.max_comment_size, - text: props.value || '', - buttonText: null, - }; - - this.send = this.send.bind(this); - this.getPreview = this.getPreview.bind(this); - this.onInput = this.onInput.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onDragOver = this.onDragOver.bind(this); - this.onDrop = this.onDrop.bind(this); - this.appendError = this.appendError.bind(this); - this.uploadImage = this.uploadImage.bind(this); - this.uploadImages = this.uploadImages.bind(this); - this.onPaste = this.onPaste.bind(this); - } - - componentWillReceiveProps(nextProps: Props) { - if (nextProps.value !== this.props.value) { - this.setState({ text: nextProps.value || '' }); - this.props.autofocus && this.textAreaRef.current && this.textAreaRef.current.focus(); - } - } - - shouldComponentUpdate(nextProps: Props, nextState: State) { - return ( - nextProps.mode !== this.props.mode || - nextProps.theme !== this.props.theme || - nextProps.userId !== this.props.userId || - nextProps.value !== this.props.value || - nextProps.errorMessage !== this.props.errorMessage || - nextState !== this.state - ); - } - - onKeyDown(e: KeyboardEvent) { - // send on cmd+enter / ctrl+enter - if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) { - this.send(e); - } - } - - onInput(e: Event) { - if (this.state.errorLock) { - this.setState({ - preview: null, - text: (e.target as HTMLInputElement).value, - }); - return; - } - this.setState({ - isErrorShown: false, - errorMessage: null, - preview: null, - text: (e.target as HTMLInputElement).value, - }); - } - - async onPaste(e: ClipboardEvent) { - if (!(e.clipboardData && e.clipboardData.files.length > 0)) { - return; - } - e.preventDefault(); - const files = Array.from(e.clipboardData.files); - await this.uploadImages(files); - } - - send(e: Event) { - const text = this.state.text; - const props = this.props; - - if (e) e.preventDefault(); - - if (!text || !text.trim()) return; - - if (text === this.props.value) { - this.props.onCancel && this.props.onCancel(); - this.setState({ preview: null, text: '' }); - } - - this.setState({ isDisabled: true, isErrorShown: false }); - - props - .onSubmit(text, pageTitle || document.title) - .then(() => { - this.setState({ preview: null, text: '' }); - }) - .catch(e => { - console.error(e); // eslint-disable-line no-console - const errorMessage = extractErrorMessageFromResponse(e); - this.setState({ isErrorShown: true, errorMessage }); - }) - .finally(() => this.setState({ isDisabled: false })); - } - - getPreview() { - const text = this.state.text; - - if (!text || !text.trim()) return; - - this.setState({ isErrorShown: false, errorMessage: null }); - - this.props - .getPreview(text) - .then(preview => this.setState({ preview })) - .catch(() => { - this.setState({ isErrorShown: true, errorMessage: null }); - }); - } - - /** appends error to input's error block */ - appendError(...errors: string[]) { - if (!this.state.errorMessage) { - this.setState({ - errorMessage: errors.join('\n'), - isErrorShown: true, - }); - return; - } - this.setState({ - errorMessage: this.state.errorMessage + '\n' + errors.join('\n'), - isErrorShown: true, - }); - } - - onDragOver(e: DragEvent) { - if (!this.props.uploadImage) return; - if (StaticStore.config.max_image_size === 0) return; - if (!this.textAreaRef) return; - if (!e.dataTransfer) return; - const items = Array.from(e.dataTransfer.items); - if (Array.from(items).filter(i => i.kind === 'file' && ImageMimeRegex.test(i.type)).length === 0) return; - e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - } - - onDrop(e: DragEvent) { - if (!this.props.uploadImage) return; - if (StaticStore.config.max_image_size === 0) return; - if (!e.dataTransfer) return; - - const data = Array.from(e.dataTransfer.files).filter(f => ImageMimeRegex.test(f.type)); - if (data.length === 0) return; - - e.preventDefault(); - - this.uploadImages(data); - } - - /** wrapper with error handling for props.uploadImage */ - uploadImage(file: File): Promise { - return this.props.uploadImage!(file).catch( - (e: ApiError | string) => - new Error( - typeof e === 'string' - ? `${file.name} upload failed with "${e}"` - : `${file.name} upload failed with "${e.error}"` - ) - ); - } - - /** performs upload process */ - async uploadImages(files: File[]) { - if (!this.props.uploadImage) return; - if (!this.textAreaRef.current) return; - - /** Human readable image size limit, i.e 5MB */ - const maxImageSizeString = (StaticStore.config.max_image_size / 1024 / 1024).toFixed(2) + 'MB'; - /** upload delay to avoid server rate limiter */ - const uploadDelay = 5000; - - const isSelectionSupported = this.textAreaRef.current.isSelectionSupported(); - - this.setState({ - errorLock: true, - errorMessage: null, - isErrorShown: false, - isDisabled: true, - buttonText: 'Uploading...', - }); - - // fallback for ie < 9 - if (!isSelectionSupported) { - for (let i = 0; i < files.length; i++) { - const file = files[i]; - const isFirst = i === 0; - const placeholderStart = this.state.text.length === 0 ? '' : '\n'; - - if (file.size > StaticStore.config.max_image_size) { - this.appendError(`${file.name} exceeds size limit of ${maxImageSizeString}`); - continue; - } - - !isFirst && (await sleep(uploadDelay)); - - const result = await this.uploadImage(file); - - if (result instanceof Error) { - this.appendError(result.message); - continue; - } - - const markdownString = `${placeholderStart}![${result.name}](${result.url})`; - this.setState({ - text: this.state.text + markdownString, - }); - } - - this.setState({ errorLock: false, isDisabled: false, buttonText: null }); - return; - } - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - const isFirst = i === 0; - const placeholderStart = this.state.text.length === 0 ? '' : '\n'; - - const uploadPlaceholder = `${placeholderStart}![uploading ${file.name}...]()`; - const uploadPlaceholderLength = uploadPlaceholder.length; - const selection = this.textAreaRef.current.getSelection(); - /** saved selection in case of error */ - const originalText = this.state.text; - const restoreSelection = async () => { - this.setState({ - text: originalText, - }); - /** sleeping awhile so textarea catch state change and its selection */ - await sleep(100); - this.textAreaRef.current!.setSelection(selection); - }; - - if (file.size > StaticStore.config.max_image_size) { - this.appendError(`${file.name} exceeds size limit of ${maxImageSizeString}`); - continue; - } - - this.setState({ - text: replaceSelection(this.state.text, selection, uploadPlaceholder), - }); - - !isFirst && (await sleep(uploadDelay)); - - const result = await this.uploadImage(file); - - if (result instanceof Error) { - this.appendError(result.message); - await restoreSelection(); - continue; - } - - const markdownString = `${placeholderStart}![${result.name}](${result.url})`; - this.setState({ - text: replaceSelection(this.state.text, [selection[0], selection[0] + uploadPlaceholderLength], markdownString), - }); - /** sleeping awhile so textarea catch state change and its selection */ - await sleep(100); - const selectionPointer = selection[0] + markdownString.length; - this.textAreaRef.current.setSelection([selectionPointer, selectionPointer]); - } - - this.setState({ errorLock: false, isDisabled: false, buttonText: null }); - } - - render(props: Props, { isDisabled, isErrorShown, errorMessage, preview, maxLength, text, buttonText }: State) { - const charactersLeft = maxLength - text.length; - errorMessage = props.errorMessage || errorMessage; - const label = buttonText || Labels[props.mode || 'main']; +export const Input = forwardRef( + ({ children, theme, mods, mix, type = 'text', ...props }, ref) => { + const className = b('input', { mix }, { theme, ...mods }); return ( -
- {!props.simpleView && ( -
- -
- )} -
- - - {charactersLeft < 100 && {charactersLeft}} -
- - {(isErrorShown || !!errorMessage) && - (errorMessage || 'Something went wrong. Please try again a bit later.').split('\n').map(e => ( -

- {e} -

- ))} - -
- {!props.simpleView && ( - - )} - - - - {!props.simpleView && props.mode === 'main' && ( -
-
- Styling with{' '} - - Markdown - {' '} - is supported -
- Subscribe to the{' '} - - Thread - - {', '} - - Site - {' '} - or  - - Replies - {' '} - by RSS -
- )} -
- - {// TODO: it can be more elegant; - // for example it can render full comment component here (or above textarea on mobile) - !!preview && ( -
-
-
- )} - + + {children} + ); } -} +); diff --git a/frontend/app/components/input/styles.ts b/frontend/app/components/input/styles.ts deleted file mode 100644 index 36df55d6..00000000 --- a/frontend/app/components/input/styles.ts +++ /dev/null @@ -1,26 +0,0 @@ -import '@app/components/raw-content'; - -import './input.scss'; -import './_simple/input_simple.scss'; - -import './__actions/input__actions.scss'; - -import './__button/input__button.scss'; -import './__button/_type/_preview/input__button_type_preview.scss'; -import './__button/_type/_send/input__button_type_send.scss'; - -import './__control-panel/input__control-panel.scss'; -import './__counter/input__counter.scss'; -import './__error/input__error.scss'; -import './__field/input__field.scss'; -import './__field-wrapper/input__field-wrapper.scss'; -import './__preview/input__preview.scss'; -import './__preview-wrapper/input__preview-wrapper.scss'; -import './__rss/input__rss.scss'; -import './__rss-link/input__rss-link.scss'; -import './__markdown/input__markdown.scss'; -import './__markdown-link/input__markdown-link.scss'; -import './__markdown-toolbar/input__markdown-toolbar.scss'; - -import './_theme/_dark/input_theme_dark.scss'; -import './_theme/_light/input_theme_light.scss'; diff --git a/frontend/app/components/root/__pinned-comments/root__pinned-comments.scss b/frontend/app/components/root/__pinned-comments/root__pinned-comments.scss index 43eb0ba6..f1b908b2 100644 --- a/frontend/app/components/root/__pinned-comments/root__pinned-comments.scss +++ b/frontend/app/components/root/__pinned-comments/root__pinned-comments.scss @@ -1,5 +1,5 @@ .root__pinned-comments { - margin-top: 24px; + margin-top: 20px; padding: 8px 12px; border-radius: 2px; } diff --git a/frontend/app/components/root/__show-more/root__show-more.scss b/frontend/app/components/root/__show-more/root__show-more.scss index 1b0fe800..0d309619 100644 --- a/frontend/app/components/root/__show-more/root__show-more.scss +++ b/frontend/app/components/root/__show-more/root__show-more.scss @@ -1,21 +1,5 @@ .root__show-more { - display: block; width: 300px; max-width: 100%; - height: 36px; margin: 20px auto; - padding: 0; - background: #259c9a; - border-radius: 4px; - border: 0; - font-size: 14px; - line-height: 36px; - box-shadow: none; - color: #fff; - cursor: pointer; - - &:hover, - &:focus { - background: #0aa; - } } diff --git a/frontend/app/components/root/root.tsx b/frontend/app/components/root/root.tsx index ea40d21b..783ef6e2 100644 --- a/frontend/app/components/root/root.tsx +++ b/frontend/app/components/root/root.tsx @@ -35,9 +35,10 @@ import { addComment, updateComment } from '@app/store/comments/actions'; import { AuthPanel } from '@app/components/auth-panel'; import Settings from '@app/components/settings'; import { ConnectedComment as Comment } from '@app/components/comment/connected-comment'; -import { Input } from '@app/components/input'; +import { CommentForm } from '@app/components/comment-form'; import Preloader from '@app/components/preloader'; import { Thread } from '@app/components/thread'; +import { Button } from '@app/components/button'; import { uploadImage, getPreview } from '@app/common/api'; import { isUserAnonymous } from '@app/utils/isUserAnonymous'; import { bindActions } from '@app/utils/actionBinder'; @@ -241,7 +242,7 @@ export class Root extends Component { {!this.props.isSettingsVisible && (
{!isGuest && !isCommentsDisabled && ( - { ))} {commentsShown < this.props.topComments.length && IS_MOBILE && ( - + )}
)} diff --git a/frontend/app/components/settings/settings.tsx b/frontend/app/components/settings/settings.tsx index 5770ded4..2f15563f 100644 --- a/frontend/app/components/settings/settings.tsx +++ b/frontend/app/components/settings/settings.tsx @@ -24,9 +24,9 @@ interface State { * reapply block on unblocked user */ blockedUsers: BlockedUser[]; - unblockedUsers: (User['id'])[]; + unblockedUsers: User['id'][]; hiddenUsers: { [id: string]: User }; - unhiddenUsers: (User['id'])[]; + unhiddenUsers: User['id'][]; } export default class Settings extends Component { diff --git a/frontend/app/components/thread/thread.tsx b/frontend/app/components/thread/thread.tsx index b3007d66..7b107544 100644 --- a/frontend/app/components/thread/thread.tsx +++ b/frontend/app/components/thread/thread.tsx @@ -22,7 +22,7 @@ const mapStateToProps = (state: StoreState, props: { id: CommentInterface['id'] interface OwnProps { id: CommentInterface['id']; - childs?: (CommentInterface['id'])[]; + childs?: CommentInterface['id'][]; level: number; mix?: string; diff --git a/frontend/app/hooks/useAction.ts b/frontend/app/hooks/useAction.ts index 698bb9e0..5a0e4feb 100644 --- a/frontend/app/hooks/useAction.ts +++ b/frontend/app/hooks/useAction.ts @@ -12,13 +12,10 @@ export const useActions = ( return useMemo( () => - Object.entries(actions).reduce>( - (result, [key, fn]) => { - (result as any)[key] = (...args: any[]) => dispatch(fn(...args)); - return result; - }, - {} as any - ), + Object.entries(actions).reduce>((result, [key, fn]) => { + (result as any)[key] = (...args: any[]) => dispatch(fn(...args)); + return result; + }, {} as any), [dispatch, ...Object.values(actions)] ) as any; }; diff --git a/frontend/app/store/comments/reducers.ts b/frontend/app/store/comments/reducers.ts index 36dd904e..9fb9bd1e 100644 --- a/frontend/app/store/comments/reducers.ts +++ b/frontend/app/store/comments/reducers.ts @@ -16,12 +16,15 @@ import { getPinnedComments } from './utils'; import { cmpRef } from '@app/utils/cmpRef'; export const topComments = ( - state: (Comment['id'])[] = [], + state: Comment['id'][] = [], action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION -): (Comment['id'])[] => { +): Comment['id'][] => { switch (action.type) { case COMMENTS_SET: { - return cmpRef(state, action.comments.map(x => x.comment.id)); + return cmpRef( + state, + action.comments.map(x => x.comment.id) + ); } case COMMENTS_APPEND: { if (action.comment.pid) return state; @@ -32,10 +35,7 @@ export const topComments = ( } }; -const reduceChildIds = ( - c: Record, - x: Node -): Record => { +const reduceChildIds = (c: Record, x: Node): Record => { if (!x.replies) return c; if (!c[x.comment.id]) { c[x.comment.id] = []; @@ -51,12 +51,12 @@ const reduceChildIds = ( }; export const childComments = ( - state: Record = {}, + state: Record = {}, action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION -): Record => { +): Record => { switch (action.type) { case COMMENTS_SET: { - return action.comments.reduce>(reduceChildIds, {}); + return action.comments.reduce>(reduceChildIds, {}); } case COMMENTS_APPEND: { if (!action.comment.pid) return state; @@ -134,9 +134,9 @@ export const activeComment = ( }; export const pinnedComments = ( - state: (Comment['id'])[] = [], + state: Comment['id'][] = [], action: COMMENTS_SET_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION -): (Comment['id'])[] => { +): Comment['id'][] => { switch (action.type) { case COMMENTS_SET: { return getPinnedComments(action.comments).map(x => x.id); @@ -157,7 +157,7 @@ export const pinnedComments = ( if (!action.patch.pin) { return state.filter(x => action.ids.indexOf(x) === -1); } - return [...state, ...action.ids].reduce<(Comment['id'])[]>((c, x) => { + return [...state, ...action.ids].reduce((c, x) => { if (c.indexOf(x) === -1) { c.push(x); } diff --git a/frontend/app/store/comments/types.ts b/frontend/app/store/comments/types.ts index c2bc79aa..fca87b12 100644 --- a/frontend/app/store/comments/types.ts +++ b/frontend/app/store/comments/types.ts @@ -26,7 +26,7 @@ export const COMMENTS_PATCH = 'COMMENTS/PATCH'; export interface COMMENTS_PATCH_ACTION { type: typeof COMMENTS_PATCH; - ids: (Comment['id'])[]; + ids: Comment['id'][]; patch: Partial; } diff --git a/frontend/app/store/thread/types.ts b/frontend/app/store/thread/types.ts index d4ca8424..c15799d8 100644 --- a/frontend/app/store/thread/types.ts +++ b/frontend/app/store/thread/types.ts @@ -3,7 +3,7 @@ import { Comment } from '@app/common/types'; export const THREAD_RESTORE_COLLAPSE = 'THREAD/COLLAPSE_RESTORE'; export interface THREAD_RESTORE_COLLAPSE_ACTION { type: typeof THREAD_RESTORE_COLLAPSE; - ids: (Comment['id'])[]; + ids: Comment['id'][]; } export const THREAD_SET_COLLAPSE = 'THREAD/COLLAPSE_SET'; diff --git a/frontend/app/store/thread/utils.ts b/frontend/app/store/thread/utils.ts index a4b43e53..b1734831 100644 --- a/frontend/app/store/thread/utils.ts +++ b/frontend/app/store/thread/utils.ts @@ -22,7 +22,7 @@ export const getCollapsedComments = (): string[] => /** * @param info list of string of type "site-id_url_comment-id */ -export const saveCollapsedComments = (siteId: string, url: string, info: (Comment['id'])[]): void => { +export const saveCollapsedComments = (siteId: string, url: string, info: Comment['id'][]): void => { const data = info.map(i => `${siteId}_${url}_${i}`); const notForThisPost = getFromLocalStorage().filter(entry => entry.indexOf(`${siteId}_${url}`) === -1); const all = new Set([...notForThisPost, ...data]); diff --git a/frontend/index.ejs b/frontend/index.ejs index 8fc69ac4..e961f13d 100644 --- a/frontend/index.ejs +++ b/frontend/index.ejs @@ -1,118 +1,121 @@ - - - - remark42 demo page + + + + remark42 demo page - - - - - -
-

Demo page

-

To install widgets on your website, follow the instructions.

-
- -
- Counter widget page
-
- Comments count: + + .container { + max-width: 800px; + padding: 40px; + } + + .document_theme_dark { + background: #22201c; + color: #ddd; + } + + .document_theme_dark a { + color: #5e5eff; + } + + .widgets { + display: flex; + flex-wrap: nowrap; + } + + .widget { + margin-top: 1rem; + } + + .widget__comments-frame { + border: 1px dashed #aaa; + width: 35rem; + max-width: 100%; + } + + .widgets__counter-widget { + margin-left: 1em; + } + + @media screen and (max-width: 800px) { + .container { + padding: 1em; + } + + .widgets { + flex-direction: column; + } + + .widgets__counter-widget { + margin-left: 0; + } + } + + + + + +
+

Demo page

+

+ To install widgets on your website, follow the + instructions. +

+
+ +
+ Counter widget page
+
Comments count:
+

+
-

-
-
- - - + (function(c) { + for (var i = 0; i < c.length; i++) { + var d = document, + s = d.createElement('script'); + s.src = remark_config.host + '/web/' + c[i] + '.js'; + (d.head || d.body).appendChild(s); + } + })(remark_config.components || ['embed']); + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4d8b340a..6b051651 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12016,9 +12016,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, "prettier-linter-helpers": { diff --git a/frontend/package.json b/frontend/package.json index 38d5d452..36d92342 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -80,7 +80,7 @@ "postcss-simple-vars": "^5.0.2", "postcss-url": "^8.0.0", "postcss-wrap": "0.0.4", - "prettier": "^1.18.2", + "prettier": "^1.19.1", "redux-mock-store": "^1.5.3", "style-loader": "^1.0.0", "ts-jest": "^24.1.0",