From 5825a55b6941da979c6b87bb2457792923b1a347 Mon Sep 17 00:00:00 2001 From: Pavel Mineev Date: Wed, 6 Jan 2021 03:34:42 +0300 Subject: [PATCH] Update frontend * change root dit and change way to import modules * update deps to latest versions * use latest tools for building bundles * rewrite webpack config * use nomodule technique for loading modern bundle * inject polyfills by babel usebuiltins * update eslint rules * rename all style files to CSS * use postcss preset env for building styles * proper typescript typing * put all html files to templates folder * etc --- README.md | 10 +- docker-init.sh | 4 +- frontend/.babelrc.js | 44 +- frontend/.browserslistrc | 1 - frontend/.eslintrc.js | 125 +- frontend/.gitignore | 6 +- frontend/.lintstagedrc.js | 4 +- frontend/.prettierignore | 2 + frontend/.prettierrc.js | 3 +- frontend/.size-limit.js | 32 +- frontend/.stylelintignore | 1 + frontend/.stylelintrc.js | 5 +- frontend/{Readme.md => README.md} | 3 +- .../app/@types/__webpack_public_path__.d.ts | 7 - frontend/app/@types/bem-react-helper.d.ts | 14 - frontend/app/@types/define-modules.d.ts | 2 - frontend/app/@types/global.d.ts | 28 - .../mockHeaders.ts => __mocks__/headers.ts} | 3 +- .../app/{testUtils/mocks => __stubs__}/jwt.ts | 0 .../index.ts => __stubs__/static-config.ts} | 13 +- .../mockStore.ts => __stubs__/store.ts} | 4 +- .../{testUtils/mocks => __stubs__}/user.ts | 2 +- frontend/app/common/__mocks__/constants.ts | 4 +- frontend/app/common/__mocks__/settings.ts | 4 +- frontend/app/common/api.test.ts | 12 +- frontend/app/common/api.ts | 6 +- frontend/app/common/closest-polyfill.ts | 17 - frontend/app/common/config-types.ts | 30 - frontend/app/common/copy.ts | 17 +- frontend/app/common/fetcher.test.ts | 16 +- frontend/app/common/fetcher.ts | 28 +- frontend/app/common/local-storage.test.ts | 2 +- frontend/app/common/local-storage.ts | 7 +- frontend/app/common/polyfills.ts | 42 - frontend/app/common/settings.ts | 9 +- .../{static_store.ts => static-store.ts} | 0 frontend/app/common/types.ts | 7 - frontend/app/common/user-info-settings.ts | 7 +- .../__column/auth-panel__column.css | 4 + .../__column/auth-panel__column.scss | 6 - ...el.scss => auth-panel__readonly-label.css} | 0 ...css => auth-panel__select-label-value.css} | 0 ...abel.scss => auth-panel__select-label.css} | 0 ...el__select.scss => auth-panel__select.css} | 0 ...-panel__sort.scss => auth-panel__sort.css} | 0 ...__user-id.scss => auth-panel__user-id.css} | 0 ...ogged-in.scss => auth-panel_logged-in.css} | 2 +- ...me_dark.scss => auth-panel_theme_dark.css} | 2 +- ..._light.scss => auth-panel_theme_light.css} | 2 +- .../{auth-panel.scss => auth-panel.css} | 0 .../components/auth-panel/auth-panel.test.tsx | 9 +- .../app/components/auth-panel/auth-panel.tsx | 28 +- frontend/app/components/auth-panel/index.ts | 34 +- ...rm.scss => auth__anonymous-login-form.css} | 0 .../auth__anonymous-login-form.tsx | 11 +- .../auth/__anonymous-login-form/index.ts | 4 +- ...n-form.scss => auth__email-login-form.css} | 0 .../auth__email-login-form.test.tsx | 20 +- .../auth__email-login-form.tsx | 28 +- .../auth/__email-login-form/index.ts | 5 +- .../{auth.module.pcss => auth.module.css} | 0 frontend/app/components/auth/auth.test.tsx | 13 +- frontend/app/components/auth/auth.tsx | 31 +- ...n_default.scss => avatar-icon_default.css} | 0 .../{avatar-icon.scss => avatar-icon.css} | 0 .../components/avatar-icon/avatar-icon.tsx | 5 +- frontend/app/components/avatar-icon/index.ts | 6 +- ...on_kind_link.scss => button_kind_link.css} | 0 ...d_primary.scss => button_kind_primary.css} | 0 ...condary.scss => button_kind_secondary.css} | 0 ..._size_large.scss => button_size_large.css} | 0 ...ize_middle.scss => button_size_middle.css} | 0 ..._theme_dark.scss => button_theme_dark.css} | 0 .../button/{button.scss => button.css} | 0 frontend/app/components/button/button.tsx | 26 +- frontend/app/components/button/index.ts | 22 +- ...actions.scss => comment-form__actions.css} | 0 ... => comment-form__button_type_preview.css} | 0 ...css => comment-form__button_type_send.css} | 0 ...__button.scss => comment-form__button.css} | 0 ...l.scss => comment-form__control-panel.css} | 0 ...counter.scss => comment-form__counter.css} | 0 ...rm__error.scss => comment-form__error.css} | 0 ...r.scss => comment-form__field-wrapper.css} | 0 ...rm__field.pcss => comment-form__field.css} | 17 +- ...k.scss => comment-form__markdown-link.css} | 0 ...css => comment-form__markdown-toolbar.css} | 0 ...rkdown.scss => comment-form__markdown.css} | 0 ...scss => comment-form__preview-wrapper.css} | 0 ...preview.scss => comment-form__preview.css} | 0 ...s-link.scss => comment-form__rss-link.css} | 0 ...t-form__rss.scss => comment-form__rss.css} | 0 ...s => comment-form__subscribe-by-email.css} | 0 .../comment-form__subscribe-by-email.test.tsx | 89 +- .../comment-form__subscribe-by-email.tsx | 81 +- ...css => comment-form__subscribe-by-rss.css} | 0 .../comment-form__subscribe-by-rss.test.tsx | 26 +- .../comment-form__subscribe-by-rss.tsx | 17 +- ...rm_simple.scss => comment-form_simple.css} | 0 ..._dark.scss => comment-form_theme_dark.css} | 18 +- ...ight.scss => comment-form_theme_light.css} | 14 +- .../{comment-form.scss => comment-form.css} | 2 +- .../comment-form/comment-form.test.tsx | 41 +- .../components/comment-form/comment-form.tsx | 46 +- frontend/app/components/comment-form/index.ts | 49 +- .../markdown-toolbar-icons/bold-icon.tsx | 3 +- .../markdown-toolbar-icons/code-icon.tsx | 3 +- .../markdown-toolbar-icons/header-icon.tsx | 3 +- .../markdown-toolbar-icons/image-icon.tsx | 3 +- .../markdown-toolbar-icons/italic-icon.tsx | 3 +- .../markdown-toolbar-icons/link-icon.tsx | 3 +- .../ordered-list-icon.tsx | 3 +- .../markdown-toolbar-icons/quote-icon.tsx | 3 +- .../unordered-list-icon.tsx | 3 +- .../comment-form/markdown-toolbar.tsx | 6 +- ...r.module.pcss => text-expander.module.css} | 0 .../components/comment-form/text-expander.tsx | 15 +- .../comment-form/textarea-autosize.tsx | 16 +- ...scss => comment__action_type_collapse.css} | 0 ...mment__action.scss => comment__action.css} | 4 +- .../{comment__body.scss => comment__body.css} | 0 ...scss => comment__control_select-label.css} | 0 ...elect.scss => comment__control_select.css} | 0 ...css => comment__control_view_inactive.css} | 0 ...ent__control.scss => comment__control.css} | 0 ...t__controls.scss => comment__controls.css} | 0 ...dit-timer.scss => comment__edit-timer.css} | 2 +- .../{comment__info.scss => comment__info.css} | 0 ...comment__input.scss => comment__input.css} | 0 ...arent.scss => comment__link-to-parent.css} | 0 ...re-value.scss => comment__score-value.css} | 0 ...comment__score.scss => comment__score.css} | 0 ...mment__status.scss => comment__status.css} | 0 .../{comment__text.scss => comment__text.css} | 0 .../{comment__time.scss => comment__time.css} | 0 ...ent__user-id.scss => comment__user-id.css} | 0 ...t__username.scss => comment__username.css} | 0 ....scss => comment__verification_active.css} | 0 ...ss => comment__verification_clickable.css} | 0 ...ication.scss => comment__verification.css} | 0 ...sabled.scss => comment__vote_disabled.css} | 0 ...lected.scss => comment__vote_selected.css} | 0 ..._down.scss => comment__vote_type_down.css} | 0 ...type_up.scss => comment__vote_type_up.css} | 0 .../{comment__vote.scss => comment__vote.css} | 0 ...t_collapsed.scss => comment_collapsed.css} | 16 +- ...mment_editing.scss => comment_editing.css} | 10 +- ...ent_replying.scss => comment_replying.css} | 10 +- ...theme_dark.scss => comment_theme_dark.css} | 36 +- ...eme_light.scss => comment_theme_light.css} | 36 +- ...mment_useless.scss => comment_useless.css} | 2 +- ...view_admin.scss => comment_view_admin.css} | 2 +- ..._preview.scss => comment_view_preview.css} | 10 +- ...t_view_user.scss => comment_view_user.css} | 10 +- .../comment/{comment.scss => comment.css} | 8 +- .../app/components/comment/comment.test.tsx | 66 +- frontend/app/components/comment/comment.tsx | 115 +- .../components/comment/connected-comment.tsx | 42 +- .../comment/getBlockingDurations.ts | 2 +- frontend/app/components/comment/index.ts | 2 +- frontend/app/components/comment/styles.ts | 76 +- frontend/app/components/countdown/index.tsx | 5 +- ...wn__content.scss => dropdown__content.css} | 0 ...dropdown__item.scss => dropdown__item.css} | 0 .../dropdown/__item/dropdown__item.tsx | 3 +- ...opdown__items.scss => dropdown__items.css} | 0 ...opdown__title.scss => dropdown__title.css} | 0 ...opdown_active.scss => dropdown_active.css} | 0 ...heme_dark.scss => dropdown_theme_dark.css} | 2 +- ...me_light.scss => dropdown_theme_light.css} | 2 +- .../dropdown/{dropdown.scss => dropdown.css} | 0 frontend/app/components/dropdown/dropdown.tsx | 15 +- frontend/app/components/dropdown/index.ts | 22 +- frontend/app/components/input/index.ts | 4 +- .../input/{input.scss => input.css} | 0 frontend/app/components/input/input.tsx | 26 +- ...nts__item.scss => list-comments__item.css} | 0 .../app/components/list-comments/index.ts | 7 +- .../{list-comments.scss => list-comments.css} | 0 .../list-comments/list-comments.tsx | 59 +- frontend/app/components/preloader/index.ts | 2 +- .../app/components/preloader/preloader.tsx | 7 +- ...e_dark.scss => raw-content_theme_dark.css} | 12 +- ...light.scss => raw-content_theme_light.css} | 10 +- .../components/raw-content/code-highlight.css | 106 + .../raw-content/code-highlight.scss | 106 - frontend/app/components/raw-content/index.ts | 8 +- .../{raw-content.scss => raw-content.css} | 52 +- ...ot__copyright.scss => root__copyright.css} | 0 .../{root__input.scss => root__input.css} | 0 ...-comment.scss => root__pinned-comment.css} | 0 ...omments.scss => root__pinned-comments.css} | 0 ...ot__preloader.scss => root__preloader.css} | 0 ...ot__show-more.scss => root__show-more.css} | 0 .../{root__thread.scss => root__thread.css} | 0 .../{root__threads.scss => root__threads.css} | 0 ...ot_theme_dark.scss => root_theme_dark.css} | 8 +- ..._theme_light.scss => root_theme_light.css} | 8 +- .../app/components/root/in-view/in-view.tsx | 38 +- frontend/app/components/root/index.ts | 28 +- .../components/root/{root.scss => root.css} | 2 +- frontend/app/components/root/root.tsx | 68 +- ...ings__action.scss => settings__action.css} | 0 ...ings__dimmed.scss => settings__dimmed.css} | 0 ...invisible.scss => settings__invisible.css} | 0 ...settings__list.scss => settings__list.css} | 4 +- ...gs__section.scss => settings__section.css} | 0 ...gs__user-id.scss => settings__user-id.css} | 0 ...__username.scss => settings__username.css} | 0 ...heme_dark.scss => settings_theme_dark.css} | 4 +- ...me_light.scss => settings_theme_light.css} | 4 +- frontend/app/components/settings/index.ts | 26 +- .../settings/{settings.scss => settings.css} | 0 frontend/app/components/settings/settings.tsx | 13 +- ...ad__collapse.scss => thread__collapse.css} | 0 ..._theme_dark.scss => thread_theme_dark.css} | 2 +- frontend/app/components/thread/index.ts | 10 +- .../thread/{thread.scss => thread.css} | 0 frontend/app/components/thread/thread.tsx | 31 +- ...nfo__avatar.scss => user-info__avatar.css} | 0 .../{user-info__id.scss => user-info__id.css} | 0 ...reloader.scss => user-info__preloader.css} | 0 ...-info__title.scss => user-info__title.css} | 0 frontend/app/components/user-info/index.ts | 14 +- .../user-info/last-comments-list.tsx | 18 +- .../{user-info.scss => user-info.css} | 0 .../app/components/user-info/user-info.tsx | 29 +- frontend/app/components/with-theme/index.tsx | 13 +- frontend/app/counter.ts | 26 +- frontend/app/deleteme.ts | 15 +- frontend/app/embed.ts | 85 +- frontend/app/hooks/useAction.ts | 10 +- frontend/app/hooks/useTheme.ts | 4 +- frontend/app/last-comments.tsx | 49 +- frontend/app/locales/en.json | 6 +- frontend/app/remark.tsx | 72 +- frontend/app/store/actions.ts | 2 +- frontend/app/store/comments/actions.test.ts | 6 +- frontend/app/store/comments/actions.ts | 10 +- frontend/app/store/comments/getters.ts | 2 +- frontend/app/store/comments/reducers.ts | 6 +- frontend/app/store/comments/types.ts | 2 +- frontend/app/store/comments/utils.test.ts | 2 +- frontend/app/store/comments/utils.ts | 6 +- frontend/app/store/index.ts | 3 +- .../store/{post_info => post-info}/actions.ts | 4 +- .../{post_info => post-info}/reducers.ts | 13 +- .../store/{post_info => post-info}/types.ts | 2 +- frontend/app/store/provider/actions.ts | 4 +- frontend/app/store/provider/reducers.test.ts | 4 +- frontend/app/store/provider/reducers.ts | 4 +- frontend/app/store/reducers.ts | 22 +- frontend/app/store/theme/actions.ts | 2 +- frontend/app/store/theme/reducers.ts | 10 +- frontend/app/store/theme/types.ts | 2 +- frontend/app/store/thread/actions.ts | 4 +- frontend/app/store/thread/getters.ts | 4 +- frontend/app/store/thread/reducers.test.ts | 2 +- frontend/app/store/thread/reducers.ts | 8 +- frontend/app/store/thread/types.ts | 2 +- frontend/app/store/thread/utils.ts | 8 +- frontend/app/store/user-info/actions.ts | 6 +- frontend/app/store/user-info/reducers.ts | 8 +- frontend/app/store/user-info/types.ts | 2 +- .../comments-store.json | 0 frontend/app/store/user/actions.test.ts | 24 +- frontend/app/store/user/actions.ts | 16 +- frontend/app/store/user/reducers.test.ts | 6 +- frontend/app/store/user/reducers.ts | 4 +- frontend/app/store/user/types.ts | 2 +- frontend/app/testUtils/mockApi.ts | 1 - frontend/app/testUtils/mockPostInfo.ts | 2 - frontend/app/testUtils/mockStyles.js | 1 - .../css-modules.d.ts} | 7 +- frontend/app/{@types => typings}/enzyme.d.ts | 0 frontend/app/typings/global.d.ts | 48 + frontend/app/{@types => typings}/preact.d.ts | 2 - frontend/app/utils/actionBinder.ts | 2 +- frontend/app/utils/bench.ts | 2 +- frontend/app/utils/debounce.ts | 11 +- frontend/app/utils/debug-node.ts | 2 +- frontend/app/utils/email.ts | 11 +- frontend/app/utils/errorUtils.ts | 12 + frontend/app/utils/get-hidden-users.test.ts | 2 +- frontend/app/utils/get-hidden-users.ts | 6 +- frontend/app/utils/getLocale.ts | 3 +- frontend/app/utils/isUserAnonymous.test.ts | 2 +- frontend/app/utils/isUserAnonymous.ts | 2 +- frontend/app/utils/jwt.test.ts | 10 +- frontend/app/utils/jwt.ts | 10 +- frontend/app/utils/loadLocale.ts | 44 +- frontend/app/utils/parseQuery.ts | 8 +- frontend/app/utils/postMessage.ts | 2 +- frontend/app/utils/shallowCompare.ts | 3 +- frontend/app/utils/sleep.ts | 2 +- frontend/app/utils/ttl-to-time.ts | 2 +- frontend/comments.ejs | 86 - frontend/counter.ejs | 59 - frontend/jest.config.js | 35 +- frontend/jest.setup.ts | 4 + frontend/last-comments.ejs | 45 - frontend/markdown-help.html | 415 - frontend/package-lock.json | 23824 +++++----------- frontend/package.json | 177 +- frontend/privacy.html | 91 - frontend/remark.image | Bin 9200 -> 0 bytes frontend/tasks/checkTranslation.js | 24 - frontend/tasks/checkTranslations.js | 35 + frontend/tasks/defaultMessages.js | 8 + frontend/tasks/generateDictionary.js | 40 - frontend/tasks/generateTranslations.js | 43 + frontend/tasks/getLocalePath.js | 11 +- frontend/tasks/getSupportedLocales.js | 6 - frontend/tasks/getTranslationKeys.js | 13 - frontend/tasks/localeLoadTemplate.js | 11 +- frontend/templates/counter.ejs | 66 + .../{deleteme.html => templates/deleteme.ejs} | 26 +- frontend/{index.ejs => templates/demo.ejs} | 34 +- .../{iframe.html => templates/iframe.ejs} | 29 +- frontend/templates/last-comments.ejs | 55 + frontend/templates/markdown-help.html | 400 + frontend/templates/privacy.html | 95 + frontend/tsconfig.json | 31 +- frontend/tsconfig.typecheck.json | 6 - frontend/webpack.config.js | 488 +- 325 files changed, 10488 insertions(+), 18795 deletions(-) delete mode 100644 frontend/.browserslistrc rename frontend/{Readme.md => README.md} (93%) delete mode 100644 frontend/app/@types/__webpack_public_path__.d.ts delete mode 100644 frontend/app/@types/bem-react-helper.d.ts delete mode 100644 frontend/app/@types/define-modules.d.ts delete mode 100644 frontend/app/@types/global.d.ts rename frontend/app/{testUtils/mockHeaders.ts => __mocks__/headers.ts} (82%) rename frontend/app/{testUtils/mocks => __stubs__}/jwt.ts (100%) rename frontend/app/{testUtils/index.ts => __stubs__/static-config.ts} (58%) rename frontend/app/{testUtils/mockStore.ts => __stubs__/store.ts} (64%) rename frontend/app/{testUtils/mocks => __stubs__}/user.ts (87%) delete mode 100644 frontend/app/common/closest-polyfill.ts delete mode 100644 frontend/app/common/config-types.ts delete mode 100644 frontend/app/common/polyfills.ts rename frontend/app/common/{static_store.ts => static-store.ts} (100%) create mode 100644 frontend/app/components/auth-panel/__column/auth-panel__column.css delete mode 100644 frontend/app/components/auth-panel/__column/auth-panel__column.scss rename frontend/app/components/auth-panel/__readonly-label/{auth-panel__readonly-label.scss => auth-panel__readonly-label.css} (100%) rename frontend/app/components/auth-panel/__select-label-value/{auth-panel__select-label-value.scss => auth-panel__select-label-value.css} (100%) rename frontend/app/components/auth-panel/__select-label/{auth-panel__select-label.scss => auth-panel__select-label.css} (100%) rename frontend/app/components/auth-panel/__select/{auth-panel__select.scss => auth-panel__select.css} (100%) rename frontend/app/components/auth-panel/__sort/{auth-panel__sort.scss => auth-panel__sort.css} (100%) rename frontend/app/components/auth-panel/__user-id/{auth-panel__user-id.scss => auth-panel__user-id.css} (100%) rename frontend/app/components/auth-panel/_logged-in/{auth-panel_logged-in.scss => auth-panel_logged-in.css} (79%) rename frontend/app/components/auth-panel/_theme/_dark/{auth-panel_theme_dark.scss => auth-panel_theme_dark.css} (67%) rename frontend/app/components/auth-panel/_theme/_light/{auth-panel_theme_light.scss => auth-panel_theme_light.css} (68%) rename frontend/app/components/auth-panel/{auth-panel.scss => auth-panel.css} (100%) rename frontend/app/components/auth/__anonymous-login-form/{auth__anonymous-login-form.scss => auth__anonymous-login-form.css} (100%) rename frontend/app/components/auth/__email-login-form/{auth__email-login-form.scss => auth__email-login-form.css} (100%) rename frontend/app/components/auth/{auth.module.pcss => auth.module.css} (100%) rename frontend/app/components/avatar-icon/_default/{avatar-icon_default.scss => avatar-icon_default.css} (100%) rename frontend/app/components/avatar-icon/{avatar-icon.scss => avatar-icon.css} (100%) rename frontend/app/components/button/_kind/_link/{button_kind_link.scss => button_kind_link.css} (100%) rename frontend/app/components/button/_kind/_primary/{button_kind_primary.scss => button_kind_primary.css} (100%) rename frontend/app/components/button/_kind/_secondary/{button_kind_secondary.scss => button_kind_secondary.css} (100%) rename frontend/app/components/button/_size/_large/{button_size_large.scss => button_size_large.css} (100%) rename frontend/app/components/button/_size/_middle/{button_size_middle.scss => button_size_middle.css} (100%) rename frontend/app/components/button/_theme/_dark/{button_theme_dark.scss => button_theme_dark.css} (100%) rename frontend/app/components/button/{button.scss => button.css} (100%) rename frontend/app/components/comment-form/__actions/{comment-form__actions.scss => comment-form__actions.css} (100%) rename frontend/app/components/comment-form/__button/_type/_preview/{comment-form__button_type_preview.scss => comment-form__button_type_preview.css} (100%) rename frontend/app/components/comment-form/__button/_type/_send/{comment-form__button_type_send.scss => comment-form__button_type_send.css} (100%) rename frontend/app/components/comment-form/__button/{comment-form__button.scss => comment-form__button.css} (100%) rename frontend/app/components/comment-form/__control-panel/{comment-form__control-panel.scss => comment-form__control-panel.css} (100%) rename frontend/app/components/comment-form/__counter/{comment-form__counter.scss => comment-form__counter.css} (100%) rename frontend/app/components/comment-form/__error/{comment-form__error.scss => comment-form__error.css} (100%) rename frontend/app/components/comment-form/__field-wrapper/{comment-form__field-wrapper.scss => comment-form__field-wrapper.css} (100%) rename frontend/app/components/comment-form/__field/{comment-form__field.pcss => comment-form__field.css} (64%) rename frontend/app/components/comment-form/__markdown-link/{comment-form__markdown-link.scss => comment-form__markdown-link.css} (100%) rename frontend/app/components/comment-form/__markdown-toolbar/{comment-form__markdown-toolbar.scss => comment-form__markdown-toolbar.css} (100%) rename frontend/app/components/comment-form/__markdown/{comment-form__markdown.scss => comment-form__markdown.css} (100%) rename frontend/app/components/comment-form/__preview-wrapper/{comment-form__preview-wrapper.scss => comment-form__preview-wrapper.css} (100%) rename frontend/app/components/comment-form/__preview/{comment-form__preview.scss => comment-form__preview.css} (100%) rename frontend/app/components/comment-form/__rss-link/{comment-form__rss-link.scss => comment-form__rss-link.css} (100%) rename frontend/app/components/comment-form/__rss/{comment-form__rss.scss => comment-form__rss.css} (100%) rename frontend/app/components/comment-form/__subscribe-by-email/{comment-form__subscribe-by-email.scss => comment-form__subscribe-by-email.css} (100%) rename frontend/app/components/comment-form/__subscribe-by-rss/{comment-form__subscribe-by-rss.scss => comment-form__subscribe-by-rss.css} (100%) rename frontend/app/components/comment-form/_simple/{comment-form_simple.scss => comment-form_simple.css} (100%) rename frontend/app/components/comment-form/_theme/_dark/{comment-form_theme_dark.scss => comment-form_theme_dark.css} (69%) rename frontend/app/components/comment-form/_theme/_light/{comment-form_theme_light.scss => comment-form_theme_light.css} (71%) rename frontend/app/components/comment-form/{comment-form.scss => comment-form.css} (89%) rename frontend/app/components/comment-form/{text-expander.module.pcss => text-expander.module.css} (100%) rename frontend/app/components/comment/__action/_type/_collapse/{comment__action_type_collapse.scss => comment__action_type_collapse.css} (100%) rename frontend/app/components/comment/__action/{comment__action.scss => comment__action.css} (80%) rename frontend/app/components/comment/__body/{comment__body.scss => comment__body.css} (100%) rename frontend/app/components/comment/__control/_select-label/{comment__control_select-label.scss => comment__control_select-label.css} (100%) rename frontend/app/components/comment/__control/_select/{comment__control_select.scss => comment__control_select.css} (100%) rename frontend/app/components/comment/__control/_view/_inactive/{comment__control_view_inactive.scss => comment__control_view_inactive.css} (100%) rename frontend/app/components/comment/__control/{comment__control.scss => comment__control.css} (100%) rename frontend/app/components/comment/__controls/{comment__controls.scss => comment__controls.css} (100%) rename frontend/app/components/comment/__edit-timer/{comment__edit-timer.scss => comment__edit-timer.css} (88%) rename frontend/app/components/comment/__info/{comment__info.scss => comment__info.css} (100%) rename frontend/app/components/comment/__input/{comment__input.scss => comment__input.css} (100%) rename frontend/app/components/comment/__link-to-parent/{comment__link-to-parent.scss => comment__link-to-parent.css} (100%) rename frontend/app/components/comment/__score-value/{comment__score-value.scss => comment__score-value.css} (100%) rename frontend/app/components/comment/__score/{comment__score.scss => comment__score.css} (100%) rename frontend/app/components/comment/__status/{comment__status.scss => comment__status.css} (100%) rename frontend/app/components/comment/__text/{comment__text.scss => comment__text.css} (100%) rename frontend/app/components/comment/__time/{comment__time.scss => comment__time.css} (100%) rename frontend/app/components/comment/__user-id/{comment__user-id.scss => comment__user-id.css} (100%) rename frontend/app/components/comment/__username/{comment__username.scss => comment__username.css} (100%) rename frontend/app/components/comment/__verification/_active/{comment__verification_active.scss => comment__verification_active.css} (100%) rename frontend/app/components/comment/__verification/_clickable/{comment__verification_clickable.scss => comment__verification_clickable.css} (100%) rename frontend/app/components/comment/__verification/{comment__verification.scss => comment__verification.css} (100%) rename frontend/app/components/comment/__vote/_disabled/{comment__vote_disabled.scss => comment__vote_disabled.css} (100%) rename frontend/app/components/comment/__vote/_selected/{comment__vote_selected.scss => comment__vote_selected.css} (100%) rename frontend/app/components/comment/__vote/_type/_down/{comment__vote_type_down.scss => comment__vote_type_down.css} (100%) rename frontend/app/components/comment/__vote/_type/_up/{comment__vote_type_up.scss => comment__vote_type_up.css} (100%) rename frontend/app/components/comment/__vote/{comment__vote.scss => comment__vote.css} (100%) rename frontend/app/components/comment/_collapsed/{comment_collapsed.scss => comment_collapsed.css} (58%) rename frontend/app/components/comment/_editing/{comment_editing.scss => comment_editing.css} (80%) rename frontend/app/components/comment/_replying/{comment_replying.scss => comment_replying.css} (81%) rename frontend/app/components/comment/_theme/_dark/{comment_theme_dark.scss => comment_theme_dark.css} (71%) rename frontend/app/components/comment/_theme/_light/{comment_theme_light.scss => comment_theme_light.css} (71%) rename frontend/app/components/comment/_useless/{comment_useless.scss => comment_useless.css} (79%) rename frontend/app/components/comment/_view/_admin/{comment_view_admin.scss => comment_view_admin.css} (81%) rename frontend/app/components/comment/_view/_preview/{comment_view_preview.scss => comment_view_preview.css} (82%) rename frontend/app/components/comment/_view/_user/{comment_view_user.scss => comment_view_user.css} (75%) rename frontend/app/components/comment/{comment.scss => comment.css} (75%) rename frontend/app/components/dropdown/__content/{dropdown__content.scss => dropdown__content.css} (100%) rename frontend/app/components/dropdown/__item/{dropdown__item.scss => dropdown__item.css} (100%) rename frontend/app/components/dropdown/__items/{dropdown__items.scss => dropdown__items.css} (100%) rename frontend/app/components/dropdown/__title/{dropdown__title.scss => dropdown__title.css} (100%) rename frontend/app/components/dropdown/_active/{dropdown_active.scss => dropdown_active.css} (100%) rename frontend/app/components/dropdown/_theme/_dark/{dropdown_theme_dark.scss => dropdown_theme_dark.css} (72%) rename frontend/app/components/dropdown/_theme/_light/{dropdown_theme_light.scss => dropdown_theme_light.css} (72%) rename frontend/app/components/dropdown/{dropdown.scss => dropdown.css} (100%) rename frontend/app/components/input/{input.scss => input.css} (100%) rename frontend/app/components/list-comments/__item/{list-comments__item.scss => list-comments__item.css} (100%) rename frontend/app/components/list-comments/{list-comments.scss => list-comments.css} (100%) rename frontend/app/components/raw-content/_theme/_dark/{raw-content_theme_dark.scss => raw-content_theme_dark.css} (80%) rename frontend/app/components/raw-content/_theme/_light/{raw-content_theme_light.scss => raw-content_theme_light.css} (84%) create mode 100644 frontend/app/components/raw-content/code-highlight.css delete mode 100644 frontend/app/components/raw-content/code-highlight.scss rename frontend/app/components/raw-content/{raw-content.scss => raw-content.css} (86%) rename frontend/app/components/root/__copyright/{root__copyright.scss => root__copyright.css} (100%) rename frontend/app/components/root/__input/{root__input.scss => root__input.css} (100%) rename frontend/app/components/root/__pinned-comment/{root__pinned-comment.scss => root__pinned-comment.css} (100%) rename frontend/app/components/root/__pinned-comments/{root__pinned-comments.scss => root__pinned-comments.css} (100%) rename frontend/app/components/root/__preloader/{root__preloader.scss => root__preloader.css} (100%) rename frontend/app/components/root/__show-more/{root__show-more.scss => root__show-more.css} (100%) rename frontend/app/components/root/__thread/{root__thread.scss => root__thread.css} (100%) rename frontend/app/components/root/__threads/{root__threads.scss => root__threads.css} (100%) rename frontend/app/components/root/_theme/_dark/{root_theme_dark.scss => root_theme_dark.css} (63%) rename frontend/app/components/root/_theme/_light/{root_theme_light.scss => root_theme_light.css} (63%) rename frontend/app/components/root/{root.scss => root.css} (90%) rename frontend/app/components/settings/__action/{settings__action.scss => settings__action.css} (100%) rename frontend/app/components/settings/__dimmed/{settings__dimmed.scss => settings__dimmed.css} (100%) rename frontend/app/components/settings/__invisible/{settings__invisible.scss => settings__invisible.css} (100%) rename frontend/app/components/settings/__list/{settings__list.scss => settings__list.css} (81%) rename frontend/app/components/settings/__section/{settings__section.scss => settings__section.css} (100%) rename frontend/app/components/settings/__user-id/{settings__user-id.scss => settings__user-id.css} (100%) rename frontend/app/components/settings/__username/{settings__username.scss => settings__username.css} (100%) rename frontend/app/components/settings/_theme/_dark/{settings_theme_dark.scss => settings_theme_dark.css} (63%) rename frontend/app/components/settings/_theme/_light/{settings_theme_light.scss => settings_theme_light.css} (63%) rename frontend/app/components/settings/{settings.scss => settings.css} (100%) rename frontend/app/components/thread/__collapse/{thread__collapse.scss => thread__collapse.css} (100%) rename frontend/app/components/thread/_theme_dark/{thread_theme_dark.scss => thread_theme_dark.css} (85%) rename frontend/app/components/thread/{thread.scss => thread.css} (100%) rename frontend/app/components/user-info/__avatar/{user-info__avatar.scss => user-info__avatar.css} (100%) rename frontend/app/components/user-info/__id/{user-info__id.scss => user-info__id.css} (100%) rename frontend/app/components/user-info/__preloader/{user-info__preloader.scss => user-info__preloader.css} (100%) rename frontend/app/components/user-info/__title/{user-info__title.scss => user-info__title.css} (100%) rename frontend/app/components/user-info/{user-info.scss => user-info.css} (100%) rename frontend/app/store/{post_info => post-info}/actions.ts (85%) rename frontend/app/store/{post_info => post-info}/reducers.ts (50%) rename frontend/app/store/{post_info => post-info}/types.ts (80%) rename frontend/app/store/user/{__mocks__ => __stubs__}/comments-store.json (100%) delete mode 100644 frontend/app/testUtils/mockApi.ts delete mode 100644 frontend/app/testUtils/mockPostInfo.ts delete mode 100644 frontend/app/testUtils/mockStyles.js rename frontend/app/{@types/module.css.d.ts => typings/css-modules.d.ts} (59%) rename frontend/app/{@types => typings}/enzyme.d.ts (100%) create mode 100644 frontend/app/typings/global.d.ts rename frontend/app/{@types => typings}/preact.d.ts (82%) delete mode 100644 frontend/comments.ejs delete mode 100644 frontend/counter.ejs create mode 100644 frontend/jest.setup.ts delete mode 100644 frontend/last-comments.ejs delete mode 100644 frontend/markdown-help.html delete mode 100644 frontend/privacy.html delete mode 100644 frontend/remark.image delete mode 100644 frontend/tasks/checkTranslation.js create mode 100644 frontend/tasks/checkTranslations.js create mode 100644 frontend/tasks/defaultMessages.js delete mode 100644 frontend/tasks/generateDictionary.js create mode 100644 frontend/tasks/generateTranslations.js delete mode 100644 frontend/tasks/getSupportedLocales.js delete mode 100644 frontend/tasks/getTranslationKeys.js create mode 100644 frontend/templates/counter.ejs rename frontend/{deleteme.html => templates/deleteme.ejs} (80%) rename frontend/{index.ejs => templates/demo.ejs} (82%) rename frontend/{iframe.html => templates/iframe.ejs} (86%) create mode 100644 frontend/templates/last-comments.ejs create mode 100644 frontend/templates/markdown-help.html create mode 100644 frontend/templates/privacy.html delete mode 100644 frontend/tsconfig.typecheck.json diff --git a/README.md b/README.md index 1375da71..067ecd91 100644 --- a/README.md +++ b/README.md @@ -462,16 +462,8 @@ Add this snippet to the bottom of web page: // if you set this param in `false` you will get notifications email notifications as admin // but your users won't have interface for subscription }; - - (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'; - s.defer = true; - (d.head || d.body).appendChild(s); - } - })(remark_config.components || ['embed']); + ``` And then add this node in the place where you want to see Remark42 widget: diff --git a/docker-init.sh b/docker-init.sh index cac2281e..a329c67f 100755 --- a/docker-init.sh +++ b/docker-init.sh @@ -1,8 +1,8 @@ #!/bin/sh echo "prepare environment" # replace BASE_URL constant by REMARK_URL -sed -i "s|https://demo.remark42.com|${REMARK_URL}|g" /srv/web/*.html -sed -i "s|https://demo.remark42.com|${REMARK_URL}|g" /srv/web/*.js +sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" /srv/web/*.html +sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" /srv/web/*.js if [ -n "${SITE_ID}" ]; then #replace "site_id: 'remark'" by SITE_ID diff --git a/frontend/.babelrc.js b/frontend/.babelrc.js index 76d6cd11..2104e039 100644 --- a/frontend/.babelrc.js +++ b/frontend/.babelrc.js @@ -1,18 +1,34 @@ +const getPresetEnv = options => ['@babel/preset-env', options]; +const preactPreset = [ + '@babel/preset-react', + { + pragma: 'h', + pragmaFrag: 'Fragment', + }, +]; + +const plugins = ['module:fast-async']; + module.exports = { presets: [ - [ - '@babel/preset-env', - { - bugfixes: true, - loose: true, - }, - ], - [ - '@babel/preset-react', - { - pragma: 'h', - pragmaFrag: 'Fragment', - }, - ], + getPresetEnv({ + targets: 'defaults, not IE 11, not samsung 12', + useBuiltIns: 'usage', + corejs: 3, + bugfixes: true, + loose: true, + }), + preactPreset, ], + plugins, + env: { + modern: { + presets: [getPresetEnv({ targets: { esmodules: true }, loose: true, bugfixes: true }), preactPreset], + plugins, + }, + test: { + presets: [getPresetEnv({ targets: { node: 'current' } }), preactPreset], + plugins, + }, + }, }; diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc deleted file mode 100644 index e94f8140..00000000 --- a/frontend/.browserslistrc +++ /dev/null @@ -1 +0,0 @@ -defaults diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 89e37fa0..a3076c9b 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -1,127 +1,40 @@ -/* eslint-disable @typescript-eslint/camelcase */ - module.exports = { - parser: 'babel-eslint', extends: [ - 'eslint:recommended', + 'react-app', + 'preact', 'plugin:jsx-a11y/recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', + 'prettier', + 'prettier/@typescript-eslint', + 'prettier/babel', + 'prettier/prettier', + 'prettier/react', ], - plugins: ['react', 'jsx-a11y', 'prettier'], + plugins: ['jsx-a11y', 'prettier'], + rules: { + 'prettier/prettier': 'error', + }, overrides: [ { - files: ['*.ts', '*.tsx'], - plugins: ['@typescript-eslint'], + files: ['*.ts?(x)'], parser: '@typescript-eslint/parser', - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - }, rules: { - // needs for using optional chaining - 'no-undef': 0, - '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 - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - // disabling because it's bad practice to mark accessibility in react classes - '@typescript-eslint/explicit-member-accessibility': 0, - // doesn't work in real world - '@typescript-eslint/no-non-null-assertion': 0, - // disabling because store actions use WATCH_ME_IM_SPECIAL case - '@typescript-eslint/class-name-casing': 0, - // disabling because server response contains snake case - '@typescript-eslint/camelcase': 0, - // disabling because it's standard behaviour that function is hoisted to top - '@typescript-eslint/no-use-before-define': 0, - // well - '@typescript-eslint/ban-ts-ignore': 0, - // better to be explicit here maybe? - '@typescript-eslint/no-inferrable-types': 0, + 'no-undef': 'off', + 'no-redeclare': 'off', + 'no-unused-vars': 'off', }, }, { files: ['*.d.ts'], rules: { - 'no-var': 0, + '@typescript-eslint/no-unused-vars': 'off', }, }, { - files: ['*.test.ts', '*.test.tsx'], + files: ['*.(spec|test).ts?(x)'], + extends: ['react-app/jest'], rules: { - '@typescript-eslint/no-explicit-any': 0, - '@typescript-eslint/no-object-literal-type-assertion': 0, - }, - globals: { - fail: true, - }, - }, - { - files: ['*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx'], - rules: { - 'max-nested-callbacks': ['warn', { max: 10 }], + 'import/first': 'off', }, }, ], - env: { - browser: true, - node: true, - es6: true, - jest: true, - }, - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - ecmaFeatures: { - modules: true, - jsx: true, - }, - }, - globals: { - remark_config: true, - __webpack_public_path__: true, - }, - rules: { - '@typescript-eslint/indent': 0, - 'react/jsx-uses-react': 2, - 'react/jsx-uses-vars': 2, - 'no-cond-assign': 1, - 'no-empty': ['error', { allowEmptyCatch: true }], - 'no-console': 1, - camelcase: 0, - 'comma-style': 2, - 'max-nested-callbacks': [2, 3], - 'no-eval': 2, - 'no-implied-eval': 2, - 'no-new-func': 2, - 'guard-for-in': 2, - eqeqeq: 2, - 'no-else-return': 2, - 'no-redeclare': 2, - 'no-dupe-keys': 2, - radix: 2, - strict: [2, 'never'], - 'no-shadow': 0, - 'callback-return': [1, ['callback', 'cb', 'next', 'done']], - 'no-delete-var': 2, - 'no-undef-init': 2, - 'no-shadow-restricted-names': 2, - 'handle-callback-err': 2, - 'no-lonely-if': 2, - 'constructor-super': 2, - 'no-this-before-super': 2, - 'no-dupe-class-members': 2, - 'no-const-assign': 2, - 'prefer-spread': 2, - 'prefer-const': 2, - 'no-useless-concat': 2, - 'no-var': 2, - 'object-shorthand': 2, - 'prefer-arrow-callback': 2, - 'prettier/prettier': 2, - '@typescript-eslint/no-var-requires': 0, - '@typescript-eslint/explicit-function-return-type': 0, - }, }; diff --git a/frontend/.gitignore b/frontend/.gitignore index 355e175c..55df0890 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,2 +1,6 @@ -.env +node_modules extracted-messages +/public +/*.log +.env +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/frontend/.lintstagedrc.js b/frontend/.lintstagedrc.js index 1622bd25..96730e4e 100644 --- a/frontend/.lintstagedrc.js +++ b/frontend/.lintstagedrc.js @@ -1,5 +1,5 @@ module.exports = { './**/*.{ts,tsx,js,jsx}': ['eslint --fix --max-warnings=0', 'prettier --write'], - './**/*.{scss,pcss,css}': ['prettier --write', 'stylelint'], - './iframe.html': ['prettier --write', 'stylelint'], + './**/*.css': ['prettier --write', 'stylelint'], + './templates/**.html': ['prettier --write', 'stylelint'], }; diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 36170a7e..79ee3f7b 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -1,2 +1,4 @@ node_modules public +package.json +package-lock.json \ No newline at end of file diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js index 7f94fc39..585d0f3f 100644 --- a/frontend/.prettierrc.js +++ b/frontend/.prettierrc.js @@ -1,6 +1,7 @@ module.exports = { printWidth: 120, useTabs: false, + tabWidth: 2, semi: true, singleQuote: true, trailingComma: 'es5', @@ -8,7 +9,7 @@ module.exports = { arrowParens: 'avoid', overrides: [ { - files: ['*.ejs', '*.html'], + files: ['*.html'], options: { trailingComma: 'none', }, diff --git a/frontend/.size-limit.js b/frontend/.size-limit.js index b9339f9e..8a2c337a 100644 --- a/frontend/.size-limit.js +++ b/frontend/.size-limit.js @@ -1,22 +1,42 @@ module.exports = [ { - path: 'public/embed.js', - limit: '2.7 KB', + path: 'public/embed.mjs', + limit: '2.5 KB', }, { - limit: '86 KB', + path: 'public/embed.js', + limit: '2.5 KB', + }, + { + limit: '70 KB', + path: 'public/remark.mjs', + }, + { + limit: '73.5 KB', path: 'public/remark.js', }, { - limit: '44 KB', + limit: '31 KB', + path: 'public/last-comments.mjs', + }, + { + limit: '38 KB', path: 'public/last-comments.js', }, + { + path: 'public/deleteme.mjs', + limit: '11 KB', + }, { path: 'public/deleteme.js', - limit: '35 KB', + limit: '18 KB', + }, + { + path: 'public/counter.mjs', + limit: '0.7 KB', }, { path: 'public/counter.js', - limit: '1.05 KB', + limit: '0.7 KB', }, ]; diff --git a/frontend/.stylelintignore b/frontend/.stylelintignore index 36170a7e..a197d7e6 100644 --- a/frontend/.stylelintignore +++ b/frontend/.stylelintignore @@ -1,2 +1,3 @@ node_modules public +extracted-messages \ No newline at end of file diff --git a/frontend/.stylelintrc.js b/frontend/.stylelintrc.js index 279d95e3..841565f7 100644 --- a/frontend/.stylelintrc.js +++ b/frontend/.stylelintrc.js @@ -1,4 +1,5 @@ -const path = require('path'); +const { CUSTOM_PROPERTIES_PATH } = require('./webpack.config'); + module.exports = { extends: ['stylelint-config-standard', 'stylelint-config-prettier'], plugins: ['stylelint-value-no-unknown-custom-properties', '@mavrin/stylelint-declaration-use-css-custom-properties'], @@ -19,7 +20,7 @@ module.exports = { 'csstools/value-no-unknown-custom-properties': [ true, { - importFrom: path.resolve(__dirname, './app/custom-properties.css'), + importFrom: CUSTOM_PROPERTIES_PATH, }, ], }, diff --git a/frontend/Readme.md b/frontend/README.md similarity index 93% rename from frontend/Readme.md rename to frontend/README.md index dd236123..98e81b74 100644 --- a/frontend/Readme.md +++ b/frontend/README.md @@ -8,6 +8,7 @@ - if you want IDE integration, you need `eslint` and `stylelint` plugin to be installed. ### CSS Styles + - although styles have `scss` extension, it is actually pack of post-css plugins, so syntax differs, for example in `calc` function. - now we are migrating to css-modules and this is recomended way to stylization. A file with styles should be named like `component.module.css` - old component styles use BEM notation (at least it should): `block__element_modifier`. Also there are `mix` classes: `block_modifier`. @@ -19,7 +20,7 @@ - imports for typescript, javascript files should be without extension: `./index`, not `./index.ts` - if file resides in the same directory or in subdirectory import should be relative: `./types/something` -- otherwise it should start from `@app` namespace: `@app/common/store` which mapped to `/app/common/store.ts` in webpack, tsconfig and jest +- otherwise it should start from ``namespace:`common/store`which mapped to`/appcommon/store.ts` in webpack, tsconfig and jest ### Testing diff --git a/frontend/app/@types/__webpack_public_path__.d.ts b/frontend/app/@types/__webpack_public_path__.d.ts deleted file mode 100644 index 15684cf4..00000000 --- a/frontend/app/@types/__webpack_public_path__.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Veriable responsive for dynamic setting public path for - * assets. Dynamic imports with relative url will be resolved over this path. - * - * https://webpack.js.org/guides/public-path/#on-the-fly - */ -declare var __webpack_public_path__: string; diff --git a/frontend/app/@types/bem-react-helper.d.ts b/frontend/app/@types/bem-react-helper.d.ts deleted file mode 100644 index 459fe670..00000000 --- a/frontend/app/@types/bem-react-helper.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module 'bem-react-helper' { - export interface Mods { - [key: string]: string | number | boolean | undefined | null; - } - export type Mix = Array | string; - export default function b( - classname: string, - props?: { - mods?: Mods; - mix?: Mix; - }, - override_props?: Mods - ): string; -} diff --git a/frontend/app/@types/define-modules.d.ts b/frontend/app/@types/define-modules.d.ts deleted file mode 100644 index 672a166a..00000000 --- a/frontend/app/@types/define-modules.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module 'intersection-observer'; -declare module 'whatwg-fetch'; diff --git a/frontend/app/@types/global.d.ts b/frontend/app/@types/global.d.ts deleted file mode 100644 index 0e417ea5..00000000 --- a/frontend/app/@types/global.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Theme } from '@app/common/types'; -import { CommentsConfig } from '@app/common/config-types'; - -declare global { - interface Window { - remark_config: CommentsConfig; - REMARK42: { - changeTheme?: (theme: Theme) => void; - destroy?: () => void; - createInstance: ( - remark_config: CommentsConfig - ) => - | { - changeTheme(theme: Theme): void; - destroy(): void; - } - | undefined; - }; - } - - namespace NodeJS { - interface Global { - Headers: typeof Headers; - localStorage: typeof Storage; - fetch: typeof fetch; - } - } -} diff --git a/frontend/app/testUtils/mockHeaders.ts b/frontend/app/__mocks__/headers.ts similarity index 82% rename from frontend/app/testUtils/mockHeaders.ts rename to frontend/app/__mocks__/headers.ts index 52a9873c..e89fbad5 100644 --- a/frontend/app/testUtils/mockHeaders.ts +++ b/frontend/app/__mocks__/headers.ts @@ -1,4 +1,4 @@ -global.Headers = class HeadersMock implements Headers { +global.Headers = class HeadersMock extends Headers implements Headers { private headers = new Map(); append(key: string, value: string) { @@ -16,7 +16,6 @@ global.Headers = class HeadersMock implements Headers { delete(key: string) { this.headers.delete(key); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: any) { this.headers.forEach((value, key) => { callbackfn.call(thisArg || this, value, key, this); diff --git a/frontend/app/testUtils/mocks/jwt.ts b/frontend/app/__stubs__/jwt.ts similarity index 100% rename from frontend/app/testUtils/mocks/jwt.ts rename to frontend/app/__stubs__/jwt.ts diff --git a/frontend/app/testUtils/index.ts b/frontend/app/__stubs__/static-config.ts similarity index 58% rename from frontend/app/testUtils/index.ts rename to frontend/app/__stubs__/static-config.ts index ba6d34bb..3dcb4d2b 100644 --- a/frontend/app/testUtils/index.ts +++ b/frontend/app/__stubs__/static-config.ts @@ -1,15 +1,4 @@ -import 'jest-extended'; -import 'jest-enzyme'; -import { configure } from 'enzyme'; -import PreactAdapter from 'enzyme-adapter-preact-pure'; - -import { StaticStore } from '@app/common/static_store'; - -import './mockHeaders'; - -configure({ adapter: new PreactAdapter() }); - -require('document-register-element/pony')(window); +import { StaticStore } from 'common/static-store'; beforeEach(() => { StaticStore.config = { diff --git a/frontend/app/testUtils/mockStore.ts b/frontend/app/__stubs__/store.ts similarity index 64% rename from frontend/app/testUtils/mockStore.ts rename to frontend/app/__stubs__/store.ts index ccb6da27..cd97280d 100644 --- a/frontend/app/testUtils/mockStore.ts +++ b/frontend/app/__stubs__/store.ts @@ -2,4 +2,6 @@ import createMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const mockStore = createMockStore([thunk]); +const mockStore = createMockStore([thunk]); + +export default mockStore; diff --git a/frontend/app/testUtils/mocks/user.ts b/frontend/app/__stubs__/user.ts similarity index 87% rename from frontend/app/testUtils/mocks/user.ts rename to frontend/app/__stubs__/user.ts index 1331f190..80035204 100644 --- a/frontend/app/testUtils/mocks/user.ts +++ b/frontend/app/__stubs__/user.ts @@ -1,4 +1,4 @@ -import { User } from '@app/common/types'; +import type { User } from 'common/types'; const user: Readonly = { id: 'email_1', diff --git a/frontend/app/common/__mocks__/constants.ts b/frontend/app/common/__mocks__/constants.ts index 3fb7aef8..44374a0a 100644 --- a/frontend/app/common/__mocks__/constants.ts +++ b/frontend/app/common/__mocks__/constants.ts @@ -1,6 +1,6 @@ // @ts-ignore -const mock: typeof import('@app/common/constants') = { - ...jest.requireActual('@app/common/constants'), +const mock: typeof import('common/constants') = { + ...jest.requireActual('common/constants'), BASE_URL: 'https://demo.remark42.com/', }; diff --git a/frontend/app/common/__mocks__/settings.ts b/frontend/app/common/__mocks__/settings.ts index adcb398c..6b0133c5 100644 --- a/frontend/app/common/__mocks__/settings.ts +++ b/frontend/app/common/__mocks__/settings.ts @@ -1,6 +1,6 @@ // @ts-ignore -const mock: typeof import('@app/common/settings') = { - ...jest.requireActual('@app/common/settings'), +const mock: typeof import('common/settings') = { + ...jest.requireActual('common/settings'), siteId: 'remark', pageTitle: 'remark test', url: 'https://remark42.com/test', diff --git a/frontend/app/common/api.test.ts b/frontend/app/common/api.test.ts index 4fdcb2b5..611c0c76 100644 --- a/frontend/app/common/api.test.ts +++ b/frontend/app/common/api.test.ts @@ -2,12 +2,12 @@ import jestFetchMock from 'jest-fetch-mock'; import { emailVerificationForSubscribe } from './api'; -jest.mock('@app/common/constants', () => ({ +jest.mock('common/constants', () => ({ BASE_URL: 'https://example.com', API_BASE: '/api', })); -jest.mock('@app/common/settings', () => ({ +jest.mock('common/settings', () => ({ siteId: 'remark42', })); @@ -29,11 +29,11 @@ describe('api', () => { expect(jestFetchMock.mock.calls.length).toEqual(1); const url = jestFetchMock.mock.calls[0][0] as string; - const match = url.match(/address=(\S+)$/); + const match = url.match(/address=(\S+)$/) as string[]; - expect(match).toBeArray(); - expect((match as string[]).length).toBeGreaterThan(1); - expect((match as string[])[1]).toBe( + expect(Array.isArray(match)).toBe(true); + expect(match.length).toBeGreaterThan(1); + expect(match[1]).toBe( "address.!%23%24%25%26'*%2B-%2F%3D%3F%5E_%60%7B%7C%7D~()%2C%3A%3B%3C%3E%5B%5C%5D%40example.com" ); }); diff --git a/frontend/app/common/api.ts b/frontend/app/common/api.ts index a618cab9..c469b31f 100644 --- a/frontend/app/common/api.ts +++ b/frontend/app/common/api.ts @@ -6,7 +6,7 @@ import fetcher from './fetcher'; /* common */ const __loginAnonymously = (username: string): Promise => { const url = `/auth/anonymous/login?user=${encodeURIComponent(username)}&aud=${siteId}&from=${encodeURIComponent( - location.origin + location.pathname + '?selfClose' + `${window.location.origin}${window.location.pathname}?selfClose` )}`; return fetcher.get({ url, withCredentials: true, overriddenApiBase: '' }); }; @@ -35,7 +35,7 @@ export const logIn = (provider: AuthProvider): Promise => { return new Promise((resolve, reject) => { const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent( - location.origin + location.pathname + '?selfClose' + `${window.location.origin}${window.location.pathname}?selfClose` )}&site=${siteId}`; const newWindow = window.open(url); @@ -267,7 +267,7 @@ export const uploadImage = (image: File): Promise => { name: image.name, size: image.size, type: image.type, - url: BASE_URL + API_BASE + '/picture/' + resp.id, + url: `${BASE_URL + API_BASE}/picture/${resp.id}`, })); }; diff --git a/frontend/app/common/closest-polyfill.ts b/frontend/app/common/closest-polyfill.ts deleted file mode 100644 index 5ed1eea7..00000000 --- a/frontend/app/common/closest-polyfill.ts +++ /dev/null @@ -1,17 +0,0 @@ -if (!Element.prototype.matches) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Element.prototype.matches = (Element.prototype as any).msMatchesSelector || Element.prototype.webkitMatchesSelector; -} - -if (!Element.prototype.closest) { - Element.prototype.closest = function (s: string) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-this-alias - let el: any = this; - - do { - if (el.matches(s)) return el; - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1); - return null; - }; -} diff --git a/frontend/app/common/config-types.ts b/frontend/app/common/config-types.ts deleted file mode 100644 index e2fdd913..00000000 --- a/frontend/app/common/config-types.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { UserInfo, Theme } from './types'; - -export interface CounterConfig { - host: string; - site_id: string; - url?: string; -} - -export type UserInfoConfig = UserInfo; - -export interface CommentsConfig { - host: string; - site_id: string; - url?: string; - max_shown_comments?: number; - theme?: Theme; - page_title?: string; - node?: string | HTMLElement; - locale?: string; - show_email_subscription?: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - __colors__?: any; -} - -export interface LastCommentsConfig { - host: string; - site_id: string; - max_last_comments: number; - locale?: string; -} diff --git a/frontend/app/common/copy.ts b/frontend/app/common/copy.ts index 9858b6fa..3733b62f 100644 --- a/frontend/app/common/copy.ts +++ b/frontend/app/common/copy.ts @@ -1,16 +1,15 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - // based on https://github.com/sindresorhus/copy-text-to-clipboard, but improved to copy text styles too -export default (input: string): boolean => { +export default function copy(input: string): boolean { const el = document.createElement('div'); el.innerHTML = input; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (el.style as any).contain = 'strict'; - el.style.position = 'absolute'; - el.style.left = '-9999px'; - el.style.fontSize = '12pt'; // Prevent zooming on iOS + Object.assign(el.style, { + contain: 'strict', + position: 'absolute', + left: '-9999px', + fontSize: '12pt', // Prevent zooming on iOS + }); document.body.appendChild(el); @@ -60,4 +59,4 @@ export default (input: string): boolean => { } return success; -}; +} diff --git a/frontend/app/common/fetcher.test.ts b/frontend/app/common/fetcher.test.ts index f433f27a..23ee9a7b 100644 --- a/frontend/app/common/fetcher.test.ts +++ b/frontend/app/common/fetcher.test.ts @@ -8,9 +8,9 @@ describe('fetcher', () => { error: 'you just cant', details: 'you just cant at all', }; - (window.fetch as any) = jest.fn().mockImplementation(async () => ({ + window.fetch = jest.fn().mockImplementation(async () => ({ status: 400, - headers: new (window as any).Headers(), + headers: new Headers(), json: async () => response, text: async () => JSON.stringify(response), })); @@ -18,7 +18,7 @@ describe('fetcher', () => { return fetcher .get('/api/some') .then(data => { - fail(data); + throw new Error('Request should be failed'); }) .catch(e => { expect(e.code).toBe(2); @@ -40,7 +40,7 @@ describe('fetcher', () => { return fetcher .get('/api/some') .then(data => { - fail(data); + throw new Error('Request should be failed'); }) .catch(e => { expect(e.code).toBe(401); @@ -51,16 +51,18 @@ describe('fetcher', () => { (jest.spyOn(window, 'fetch') as any).mockImplementation(async () => ({ status: 400, headers: new (window as any).Headers(), - json: async () => { + async json() { throw new Error('json parse error'); }, - text: async () => 'you given me something wrong', + async text() { + return 'you given me something wrong'; + }, })); return fetcher .get({ url: '/api/some', logError: false }) .then(data => { - fail(data); + throw new Error('Request should be failed'); }) .catch(e => { expect(e.code).toBe(0); diff --git a/frontend/app/common/fetcher.ts b/frontend/app/common/fetcher.ts index a70596fb..45a8194b 100644 --- a/frontend/app/common/fetcher.ts +++ b/frontend/app/common/fetcher.ts @@ -1,8 +1,9 @@ +import { httpErrorMap, isFailedFetch, httpMessages, RequestError } from 'utils/errorUtils'; + import { BASE_URL, API_BASE, HEADER_X_JWT } from './constants'; import { siteId } from './settings'; -import { StaticStore } from './static_store'; +import { StaticStore } from './static-store'; import { getCookie } from './cookies'; -import { httpErrorMap, isFailedFetch, httpMessages } from '@app/utils/errorUtils'; export type FetcherMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head'; const methods: FetcherMethod[] = ['get', 'post', 'put', 'patch', 'delete', 'head']; @@ -33,7 +34,7 @@ type FetcherObject = { [K in FetcherMethod]: (data: FetcherInit) => let activeJwtToken: string | undefined; const fetcher = methods.reduce>((acc, method) => { - acc[method] = (data: FetcherInit): Promise => { + acc[method] = async (data: FetcherInit): Promise => { const { url, body = undefined, @@ -77,7 +78,7 @@ const fetcher = methods.reduce>((acc, method) => { } if (siteId && method !== 'post' && !rurl.includes('?site=') && !rurl.includes('&site=')) { - rurl += (rurl.includes('?') ? '&' : '?') + `site=${siteId}`; + rurl += `${rurl.includes('?') ? '&' : '?'}site=${siteId}`; } return fetch(rurl, parameters) @@ -99,10 +100,8 @@ const fetcher = methods.reduce>((acc, method) => { if (res.status >= 400) { if (httpErrorMap.has(res.status)) { const descriptor = httpErrorMap.get(res.status) || httpMessages.unexpectedError; - throw { - code: res.status, - error: descriptor.defaultMessage, - }; + + throw new RequestError(descriptor.defaultMessage, res.status); } return res.text().then(text => { let err; @@ -110,13 +109,10 @@ const fetcher = methods.reduce>((acc, method) => { err = JSON.parse(text); } catch (e) { if (logError) { - // eslint-disable-next-line no-console console.error(err); } - throw { - code: 0, - error: httpMessages.unexpectedError.defaultMessage, - }; + + throw new RequestError(httpMessages.unexpectedError.defaultMessage, 0); } throw err; }); @@ -130,11 +126,9 @@ const fetcher = methods.reduce>((acc, method) => { }) .catch(e => { if (isFailedFetch(e)) { - throw { - code: -2, - error: e.message, - }; + throw new RequestError(e.message, -2); } + throw e; }); }; diff --git a/frontend/app/common/local-storage.test.ts b/frontend/app/common/local-storage.test.ts index b3ee40d9..68f73e15 100644 --- a/frontend/app/common/local-storage.test.ts +++ b/frontend/app/common/local-storage.test.ts @@ -67,7 +67,7 @@ describe('updateJsonItem', () => { }); it('should set data to empty localStorage', () => { - updateJsonItem>(LS_KEY, {}); + updateJsonItem(LS_KEY, {}); expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({})); }); diff --git a/frontend/app/common/local-storage.ts b/frontend/app/common/local-storage.ts index 18e39b28..3367bb02 100644 --- a/frontend/app/common/local-storage.ts +++ b/frontend/app/common/local-storage.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { IS_STORAGE_AVAILABLE } from './constants'; const failMessage = 'remark42: localStorage access denied, check browser preferences'; @@ -47,9 +46,9 @@ export function setJsonItem(key: string, data: T) { } } -export function updateJsonItem | any[]>(key: string, value: (data: T) => T): void; -export function updateJsonItem(key: string, value: T): void; -export function updateJsonItem>(key: string, value: T) { +export function updateJsonItem(key: string, value: (data: T) => T): void; +export function updateJsonItem(key: string, value: T): void; +export function updateJsonItem>(key: string, value: T) { const savedData = getJsonItem(key); if (Array.isArray(value)) { diff --git a/frontend/app/common/polyfills.ts b/frontend/app/common/polyfills.ts deleted file mode 100644 index f48f9c7c..00000000 --- a/frontend/app/common/polyfills.ts +++ /dev/null @@ -1,42 +0,0 @@ -import 'regenerator-runtime/runtime'; -import 'es6-promise/auto'; -import 'focus-visible'; -import '@webcomponents/custom-elements'; -import './closest-polyfill'; - -export default async function loadPolyfills() { - function fillCoreJs() { - if ( - 'startsWith' in String.prototype && - 'endsWith' in String.prototype && - 'includes' in Array.prototype && - 'assign' in Object && - 'keys' in Object - ) { - return; - } - - return import(/* webpackChunkName: "core-js" */ 'core-js'); - } - - function fillFetch() { - if ('fetch' in window) return; - - return import(/* webpackChunkName: "whatwg-fetch" */ 'whatwg-fetch'); - } - - function fillIntersectionObserver() { - if ( - 'IntersectionObserver' in window && - 'IntersectionObserverEntry' in window && - 'intersectionRatio' in window.IntersectionObserverEntry.prototype - ) { - return; - } - - return import(/* webpackChunkName: "intersection-observer" */ 'intersection-observer'); - } - - await Promise.all([fillCoreJs(), fillFetch(), fillIntersectionObserver()]); - return; -} diff --git a/frontend/app/common/settings.ts b/frontend/app/common/settings.ts index e36b3757..0b20e1b2 100644 --- a/frontend/app/common/settings.ts +++ b/frontend/app/common/settings.ts @@ -1,6 +1,7 @@ -import { Theme } from './types'; +import parseQuery from 'utils/parseQuery'; + +import type { Theme } from './types'; import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants'; -import parseQuery from '@app/utils/parseQuery'; export interface QuerySettingsType { site_id?: string; @@ -29,9 +30,9 @@ if (!querySettings.theme || THEMES.indexOf(querySettings.theme) === -1) { // eslint-disable-next-line @typescript-eslint/no-explicit-any querySettings.show_email_subscription = (querySettings.show_email_subscription as any) !== 'false'; -export const siteId = querySettings.site_id; +export const siteId = querySettings.site_id!; export const pageTitle = querySettings.page_title; export const url = querySettings.url; export const maxShownComments = querySettings.max_shown_comments; -export const token = querySettings.token; +export const token = querySettings.token!; export const theme = querySettings.theme; diff --git a/frontend/app/common/static_store.ts b/frontend/app/common/static-store.ts similarity index 100% rename from frontend/app/common/static_store.ts rename to frontend/app/common/static-store.ts diff --git a/frontend/app/common/types.ts b/frontend/app/common/types.ts index a159e7de..cedfd9d8 100644 --- a/frontend/app/common/types.ts +++ b/frontend/app/common/types.ts @@ -118,13 +118,6 @@ export interface Config { emoji_enabled: boolean; } -export interface RemarkConfig { - site_id: string; - url: string; - /** used in last comments widget */ - max_last_comments?: number; -} - export type Sorting = '-time' | '+time' | '-active' | '+active' | '-score' | '+score' | '-controversy' | '+controversy'; export type AuthProvider = diff --git a/frontend/app/common/user-info-settings.ts b/frontend/app/common/user-info-settings.ts index 5fee1f02..b352baac 100644 --- a/frontend/app/common/user-info-settings.ts +++ b/frontend/app/common/user-info-settings.ts @@ -1,7 +1,8 @@ -import { UserInfo } from './types'; -import parseQuery from '@app/utils/parseQuery'; +import parseQuery from 'utils/parseQuery'; -export const userInfo: Partial = parseQuery(); +import type { UserInfo } from './types'; + +export const userInfo: UserInfo = parseQuery(); // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isDefaultPicture = ((userInfo.isDefaultPicture as any) as string) !== '1'; diff --git a/frontend/app/components/auth-panel/__column/auth-panel__column.css b/frontend/app/components/auth-panel/__column/auth-panel__column.css new file mode 100644 index 00000000..350f95e4 --- /dev/null +++ b/frontend/app/components/auth-panel/__column/auth-panel__column.css @@ -0,0 +1,4 @@ +.auth-panel__column:last-child { + margin-left: 8px; + text-align: right; +} diff --git a/frontend/app/components/auth-panel/__column/auth-panel__column.scss b/frontend/app/components/auth-panel/__column/auth-panel__column.scss deleted file mode 100644 index 0a93f81c..00000000 --- a/frontend/app/components/auth-panel/__column/auth-panel__column.scss +++ /dev/null @@ -1,6 +0,0 @@ -.auth-panel__column { - &:last-child { - margin-left: 8px; - text-align: right; - } -} diff --git a/frontend/app/components/auth-panel/__readonly-label/auth-panel__readonly-label.scss b/frontend/app/components/auth-panel/__readonly-label/auth-panel__readonly-label.css similarity index 100% rename from frontend/app/components/auth-panel/__readonly-label/auth-panel__readonly-label.scss rename to frontend/app/components/auth-panel/__readonly-label/auth-panel__readonly-label.css diff --git a/frontend/app/components/auth-panel/__select-label-value/auth-panel__select-label-value.scss b/frontend/app/components/auth-panel/__select-label-value/auth-panel__select-label-value.css similarity index 100% rename from frontend/app/components/auth-panel/__select-label-value/auth-panel__select-label-value.scss rename to frontend/app/components/auth-panel/__select-label-value/auth-panel__select-label-value.css diff --git a/frontend/app/components/auth-panel/__select-label/auth-panel__select-label.scss b/frontend/app/components/auth-panel/__select-label/auth-panel__select-label.css similarity index 100% rename from frontend/app/components/auth-panel/__select-label/auth-panel__select-label.scss rename to frontend/app/components/auth-panel/__select-label/auth-panel__select-label.css diff --git a/frontend/app/components/auth-panel/__select/auth-panel__select.scss b/frontend/app/components/auth-panel/__select/auth-panel__select.css similarity index 100% rename from frontend/app/components/auth-panel/__select/auth-panel__select.scss rename to frontend/app/components/auth-panel/__select/auth-panel__select.css diff --git a/frontend/app/components/auth-panel/__sort/auth-panel__sort.scss b/frontend/app/components/auth-panel/__sort/auth-panel__sort.css similarity index 100% rename from frontend/app/components/auth-panel/__sort/auth-panel__sort.scss rename to frontend/app/components/auth-panel/__sort/auth-panel__sort.css 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.css similarity index 100% rename from frontend/app/components/auth-panel/__user-id/auth-panel__user-id.scss rename to frontend/app/components/auth-panel/__user-id/auth-panel__user-id.css diff --git a/frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.scss b/frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.css similarity index 79% rename from frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.scss rename to frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.css index 758c7d36..2d369149 100644 --- a/frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.scss +++ b/frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.css @@ -1,7 +1,7 @@ .auth-panel_logged-in { font-size: 12px; - .auth-panel__column { + & .auth-panel__column { &:first-child { font-weight: 400; } diff --git a/frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.scss b/frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.css similarity index 67% rename from frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.scss rename to frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.css index 54f008f1..ff243925 100644 --- a/frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.scss +++ b/frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.css @@ -1,5 +1,5 @@ .auth-panel_theme_dark { - .auth-panel__user-id { + & .auth-panel__user-id { color: var(--color5); } } diff --git a/frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.scss b/frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.css similarity index 68% rename from frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.scss rename to frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.css index 5258740b..c96302b7 100644 --- a/frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.scss +++ b/frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.css @@ -1,5 +1,5 @@ .auth-panel_theme_light { - .auth-panel__user-id { + & .auth-panel__user-id { color: var(--color13); } } diff --git a/frontend/app/components/auth-panel/auth-panel.scss b/frontend/app/components/auth-panel/auth-panel.css similarity index 100% rename from frontend/app/components/auth-panel/auth-panel.scss rename to frontend/app/components/auth-panel/auth-panel.css diff --git a/frontend/app/components/auth-panel/auth-panel.test.tsx b/frontend/app/components/auth-panel/auth-panel.test.tsx index 1a2951b3..32df5c62 100644 --- a/frontend/app/components/auth-panel/auth-panel.test.tsx +++ b/frontend/app/components/auth-panel/auth-panel.test.tsx @@ -1,16 +1,15 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; import { mount } from 'enzyme'; import createMockStore from 'redux-mock-store'; import { Middleware } from 'redux'; import { Provider } from 'react-redux'; import { IntlProvider } from 'react-intl'; -import enMessages from '@app/locales/en.json'; +import enMessages from 'locales/en.json'; import AuthPanel, { Props } from './auth-panel'; import { Button } from '../button'; -import { StaticStore } from '@app/common/static_store'; +import { StaticStore } from 'common/static-store'; const DefaultProps = { providers: ['google', 'github'], @@ -83,7 +82,7 @@ describe('', () => { const firstCol = element.find('.auth-panel__column').first(); const providerButtons = firstCol.find(Button); - expect(firstCol.text()).toStartWith('Login:'); + expect(firstCol.text().startsWith('Login:')).toBe(true); expect(providerButtons.at(0).text()).toBe('Google'); expect(providerButtons.at(1).text()).toBe('GitHub'); }); diff --git a/frontend/app/components/auth-panel/auth-panel.tsx b/frontend/app/components/auth-panel/auth-panel.tsx index f030de36..e2d8f5c0 100644 --- a/frontend/app/components/auth-panel/auth-panel.tsx +++ b/frontend/app/components/auth-panel/auth-panel.tsx @@ -1,22 +1,21 @@ -/** @jsx createElement */ -import { createElement, Component, Fragment } from 'preact'; +import { h, Component, Fragment } from 'preact'; import { useSelector } from 'react-redux'; import { FormattedMessage, defineMessages, IntlShape, useIntl } from 'react-intl'; import b from 'bem-react-helper'; -import { User, AuthProvider, Sorting, Theme, PostInfo } from '@app/common/types'; -import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from '@app/common/constants'; -import { requestDeletion } from '@app/utils/email'; -import postMessage from '@app/utils/postMessage'; -import { getHandleClickProps } from '@app/common/accessibility'; -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 Auth from '@app/components/auth'; +import { User, AuthProvider, Sorting, Theme, PostInfo } from 'common/types'; +import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants'; +import { requestDeletion } from 'utils/email'; +import postMessage from 'utils/postMessage'; +import { getHandleClickProps } from 'common/accessibility'; +import { StoreState } from 'store'; +import { ProviderState } from 'store/provider/reducers'; +import { Dropdown, DropdownItem } from 'components/dropdown'; +import { Button } from 'components/button'; +import Auth from 'components/auth'; -import useTheme from '@app/hooks/useTheme'; -import { StaticStore } from '@app/common/static_store'; +import useTheme from 'hooks/useTheme'; +import { StaticStore } from 'common/static-store'; export interface OwnProps { user: User | null; @@ -132,6 +131,7 @@ export class AuthPanel extends Component { className="auth-panel__pseudo-link" href={`${window.location.origin}/web/comments.html${window.location.search}`} target="_blank" + rel="noreferrer" > diff --git a/frontend/app/components/auth-panel/index.ts b/frontend/app/components/auth-panel/index.ts index f2c45fe4..f4ce12a6 100644 --- a/frontend/app/components/auth-panel/index.ts +++ b/frontend/app/components/auth-panel/index.ts @@ -1,18 +1,18 @@ +import './auth-panel.css'; + +import './__readonly-label/auth-panel__readonly-label.css'; + +import './__column/auth-panel__column.css'; +import './__select/auth-panel__select.css'; +import './__select-label/auth-panel__select-label.css'; +import './__select-label-value/auth-panel__select-label-value.css'; +import './__sort/auth-panel__sort.css'; + +import './__user-id/auth-panel__user-id.css'; + +import './_theme/_dark/auth-panel_theme_dark.css'; +import './_theme/_light/auth-panel_theme_light.css'; + +import './_logged-in/auth-panel_logged-in.css'; + export { default } from './auth-panel'; - -import './auth-panel.scss'; - -import './__readonly-label/auth-panel__readonly-label.scss'; - -import './__column/auth-panel__column.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 './_theme/_dark/auth-panel_theme_dark.scss'; -import './_theme/_light/auth-panel_theme_light.scss'; - -import './_logged-in/auth-panel_logged-in.scss'; diff --git a/frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.scss b/frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.css similarity index 100% rename from frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.scss rename to frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.css diff --git a/frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.tsx b/frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.tsx index 77ab38ba..3473985e 100644 --- a/frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.tsx +++ b/frontend/app/components/auth/__anonymous-login-form/auth__anonymous-login-form.tsx @@ -1,11 +1,10 @@ -/** @jsx createElement */ -import { createElement, Component, createRef } from 'preact'; +import { h, Component, createRef } from 'preact'; import b from 'bem-react-helper'; import { IntlShape, defineMessages, FormattedMessage } from 'react-intl'; -import { Theme } from '@app/common/types'; +import { Theme } from 'common/types'; -import { Input } from '@app/components/input'; -import { Button } from '@app/components/button'; +import { Input } from 'components/input'; +import { Button } from 'components/button'; import { validateUserName } from '../validateUserName'; @@ -89,7 +88,7 @@ export class AnonymousLoginForm extends Component { // TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname let className = b('auth-anonymous-login-form', {}, { theme: props.theme }); if (props.className) { - className += ' ' + b('auth-anonymous-login-form', {}, { theme: props.theme }); + className += ` ${b('auth-anonymous-login-form', {}, { theme: props.theme })}`; } const usernameInvalidReason = this.getUsernameInvalidReason(); diff --git a/frontend/app/components/auth/__anonymous-login-form/index.ts b/frontend/app/components/auth/__anonymous-login-form/index.ts index 34b6aa60..4b24d0b8 100644 --- a/frontend/app/components/auth/__anonymous-login-form/index.ts +++ b/frontend/app/components/auth/__anonymous-login-form/index.ts @@ -1,3 +1,3 @@ -export { AnonymousLoginForm } from './auth__anonymous-login-form'; +import './auth__anonymous-login-form.css'; -import './auth__anonymous-login-form.scss'; +export { AnonymousLoginForm } from './auth__anonymous-login-form'; diff --git a/frontend/app/components/auth/__email-login-form/auth__email-login-form.scss b/frontend/app/components/auth/__email-login-form/auth__email-login-form.css similarity index 100% rename from frontend/app/components/auth/__email-login-form/auth__email-login-form.scss rename to frontend/app/components/auth/__email-login-form/auth__email-login-form.css diff --git a/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx b/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx index 6b24b46b..8f7595b0 100644 --- a/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx +++ b/frontend/app/components/auth/__email-login-form/auth__email-login-form.test.tsx @@ -1,16 +1,16 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; import { mount, ReactWrapper } from 'enzyme'; import { EmailLoginFormConnected as EmailLoginForm, Props, State } from './auth__email-login-form'; -import { User } from '@app/common/types'; -import { sleep } from '@app/utils/sleep'; -import { validToken } from '@app/testUtils/mocks/jwt'; -import { sendEmailVerificationRequest } from '@app/common/api'; import { IntlProvider } from 'react-intl'; -import enMessages from '../../../locales/en.json'; -import { LS_EMAIL_KEY } from '@app/common/constants'; -jest.mock('@app/utils/jwt', () => ({ +import { validToken } from '__stubs__/jwt'; +import { LS_EMAIL_KEY } from 'common/constants'; +import { User } from 'common/types'; +import { sleep } from 'utils/sleep'; +import { sendEmailVerificationRequest } from 'common/api'; +import enMessages from 'locales/en.json'; + +jest.mock('utils/jwt', () => ({ isJwtExpired: jest .fn() .mockImplementationOnce(() => true) @@ -18,7 +18,7 @@ jest.mock('@app/utils/jwt', () => ({ .mockImplementationOnce(() => true), })); -jest.mock('@app/common/api'); +jest.mock('common/api'); function simulateInput(input: ReactWrapper, value: string) { input.getDOMNode().value = value; diff --git a/frontend/app/components/auth/__email-login-form/auth__email-login-form.tsx b/frontend/app/components/auth/__email-login-form/auth__email-login-form.tsx index 9d004b1f..d8c0c7f2 100644 --- a/frontend/app/components/auth/__email-login-form/auth__email-login-form.tsx +++ b/frontend/app/components/auth/__email-login-form/auth__email-login-form.tsx @@ -1,22 +1,20 @@ -/** @jsx createElement */ -import { createElement, Component, createRef } from 'preact'; +import { h, Component, createRef } from 'preact'; import { forwardRef } from 'preact/compat'; import b from 'bem-react-helper'; -import { Theme, User } from '@app/common/types'; -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/comment-form/textarea-autosize'; -import { Input } from '@app/components/input'; -import { Button } from '@app/components/button'; -import { isJwtExpired } from '@app/utils/jwt'; +import { Theme, User } from 'common/types'; +import { LS_EMAIL_KEY } from 'common/constants'; +import { sendEmailVerificationRequest } from 'common/api'; +import { extractErrorMessageFromResponse } from 'utils/errorUtils'; +import { getHandleClickProps } from 'common/accessibility'; +import { sleep } from 'utils/sleep'; +import TextareaAutosize from 'components/comment-form/textarea-autosize'; +import { Input } from 'components/input'; +import { Button } from 'components/button'; +import { isJwtExpired } from 'utils/jwt'; import { defineMessages, IntlShape, useIntl, FormattedMessage } from 'react-intl'; import { validateUserName } from '../validateUserName'; - import { messages as loginForm } from '../__anonymous-login-form/auth__anonymous-login-form'; -import { LS_EMAIL_KEY } from '@app/common/constants'; interface OwnProps { onSignIn(token: string): Promise; @@ -199,7 +197,7 @@ export class EmailLoginForm extends Component { // TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname let className = b('auth-email-login-form', {}, { theme: props.theme }); if (props.className) { - className += ' ' + b('auth-email-login-form', {}, { theme: props.theme }); + className += ` ${b('auth-email-login-form', {}, { theme: props.theme })}`; } const form1InvalidReason = this.getForm1InvalidReason(); @@ -208,7 +206,7 @@ export class EmailLoginForm extends Component { return (
', () => { const createWrapper = (store?: Partial) => mount( - + diff --git a/frontend/app/components/auth/auth.tsx b/frontend/app/components/auth/auth.tsx index deed7736..509407b0 100644 --- a/frontend/app/components/auth/auth.tsx +++ b/frontend/app/components/auth/auth.tsx @@ -1,26 +1,25 @@ -/** @jsx createElement */ -import { createElement, Component, createRef } from 'preact'; +import { h, Component, createRef } from 'preact'; import { useCallback } from 'preact/hooks'; import { IntlShape, FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { AuthProvider, Theme, User } from '@app/common/types'; -import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from '@app/common/constants'; -import { getHandleClickProps } from '@app/common/accessibility'; -import { Button } from '@app/components/button'; -import { Dropdown, DropdownItem } from '@app/components/dropdown'; +import { AuthProvider, Theme, User } from 'common/types'; +import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from 'common/constants'; +import { getHandleClickProps } from 'common/accessibility'; +import { Button } from 'components/button'; +import { Dropdown, DropdownItem } from 'components/dropdown'; -import debounce from '@app/utils/debounce'; -import { ProviderState } from '@app/store/provider/reducers'; -import { StaticStore } from '@app/common/static_store'; +import debounce from 'utils/debounce'; +import { ProviderState } from 'store/provider/reducers'; +import { StaticStore } from 'common/static-store'; import { useSelector, useDispatch } from 'react-redux'; -import { StoreState } from '@app/store'; -import useTheme from '@app/hooks/useTheme'; -import { logIn } from '@app/store/user/actions'; +import { StoreState } from 'store'; +import useTheme from 'hooks/useTheme'; +import { logIn } from 'store/user/actions'; import { AnonymousLoginForm } from './__anonymous-login-form'; import { EmailLoginFormConnected, EmailLoginFormRef } from './__email-login-form'; -import styles from './auth.module.pcss'; +import styles from './auth.module.css'; interface Props { intl: IntlShape; @@ -186,13 +185,13 @@ const authPanelMessages = defineMessages({ }, }); -export default function () { +export default function AuthWrapper() { const dispatch = useDispatch(); const provider = useSelector(store => store.provider); const user = useSelector(store => store.user); const theme = useTheme(); const intl = useIntl(); - const handleSignin = useCallback((provider: AuthProvider) => dispatch(logIn(provider)), []); + const handleSignin = useCallback((provider: AuthProvider) => dispatch(logIn(provider)), [dispatch]); return ; } diff --git a/frontend/app/components/avatar-icon/_default/avatar-icon_default.scss b/frontend/app/components/avatar-icon/_default/avatar-icon_default.css similarity index 100% rename from frontend/app/components/avatar-icon/_default/avatar-icon_default.scss rename to frontend/app/components/avatar-icon/_default/avatar-icon_default.css diff --git a/frontend/app/components/avatar-icon/avatar-icon.scss b/frontend/app/components/avatar-icon/avatar-icon.css similarity index 100% rename from frontend/app/components/avatar-icon/avatar-icon.scss rename to frontend/app/components/avatar-icon/avatar-icon.css diff --git a/frontend/app/components/avatar-icon/avatar-icon.tsx b/frontend/app/components/avatar-icon/avatar-icon.tsx index c11635b9..57c7e344 100644 --- a/frontend/app/components/avatar-icon/avatar-icon.tsx +++ b/frontend/app/components/avatar-icon/avatar-icon.tsx @@ -1,7 +1,6 @@ -/** @jsx createElement */ -import { createElement, JSX } from 'preact'; +import { h, JSX } from 'preact'; import b from 'bem-react-helper'; -import { Theme } from '@app/common/types'; +import { Theme } from 'common/types'; interface Props { picture?: string; diff --git a/frontend/app/components/avatar-icon/index.ts b/frontend/app/components/avatar-icon/index.ts index 572b27b9..7aaba217 100644 --- a/frontend/app/components/avatar-icon/index.ts +++ b/frontend/app/components/avatar-icon/index.ts @@ -1,4 +1,4 @@ -export { AvatarIcon } from './avatar-icon'; +import './avatar-icon.css'; +import './_default/avatar-icon_default.css'; -import './avatar-icon.scss'; -import './_default/avatar-icon_default.scss'; +export { AvatarIcon } from './avatar-icon'; diff --git a/frontend/app/components/button/_kind/_link/button_kind_link.scss b/frontend/app/components/button/_kind/_link/button_kind_link.css similarity index 100% rename from frontend/app/components/button/_kind/_link/button_kind_link.scss rename to frontend/app/components/button/_kind/_link/button_kind_link.css diff --git a/frontend/app/components/button/_kind/_primary/button_kind_primary.scss b/frontend/app/components/button/_kind/_primary/button_kind_primary.css similarity index 100% rename from frontend/app/components/button/_kind/_primary/button_kind_primary.scss rename to frontend/app/components/button/_kind/_primary/button_kind_primary.css diff --git a/frontend/app/components/button/_kind/_secondary/button_kind_secondary.scss b/frontend/app/components/button/_kind/_secondary/button_kind_secondary.css similarity index 100% rename from frontend/app/components/button/_kind/_secondary/button_kind_secondary.scss rename to frontend/app/components/button/_kind/_secondary/button_kind_secondary.css diff --git a/frontend/app/components/button/_size/_large/button_size_large.scss b/frontend/app/components/button/_size/_large/button_size_large.css similarity index 100% rename from frontend/app/components/button/_size/_large/button_size_large.scss rename to frontend/app/components/button/_size/_large/button_size_large.css diff --git a/frontend/app/components/button/_size/_middle/button_size_middle.scss b/frontend/app/components/button/_size/_middle/button_size_middle.css similarity index 100% rename from frontend/app/components/button/_size/_middle/button_size_middle.scss rename to frontend/app/components/button/_size/_middle/button_size_middle.css diff --git a/frontend/app/components/button/_theme/_dark/button_theme_dark.scss b/frontend/app/components/button/_theme/_dark/button_theme_dark.css similarity index 100% rename from frontend/app/components/button/_theme/_dark/button_theme_dark.scss rename to frontend/app/components/button/_theme/_dark/button_theme_dark.css diff --git a/frontend/app/components/button/button.scss b/frontend/app/components/button/button.css similarity index 100% rename from frontend/app/components/button/button.scss rename to frontend/app/components/button/button.css diff --git a/frontend/app/components/button/button.tsx b/frontend/app/components/button/button.tsx index 7b102f1a..b03ace54 100644 --- a/frontend/app/components/button/button.tsx +++ b/frontend/app/components/button/button.tsx @@ -1,26 +1,22 @@ -/** @jsx createElement */ -import { createElement, JSX } from 'preact'; +import { h, JSX } from 'preact'; import { forwardRef } from 'preact/compat'; import b, { Mods, Mix } from 'bem-react-helper'; -import { Theme } from '@app/common/types'; -interface Props extends Omit { +import type { Theme } from 'common/types'; + +export type ButtonProps = Omit & { kind?: 'primary' | 'secondary' | 'link'; size?: 'middle' | 'large'; theme?: Theme; mods?: Mods; mix?: Mix; type?: string; -} +}; -export const Button = forwardRef( - ({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => { - const className = b('button', { mods: { kind, size }, mix }, { theme, ...mods }); - - return ( - - ); - } +export const Button = forwardRef( + ({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => ( + + ) ); diff --git a/frontend/app/components/button/index.ts b/frontend/app/components/button/index.ts index 98ff80d3..17d7996d 100644 --- a/frontend/app/components/button/index.ts +++ b/frontend/app/components/button/index.ts @@ -1,12 +1,12 @@ +import './button.css'; + +import './_kind/_link/button_kind_link.css'; +import './_kind/_primary/button_kind_primary.css'; +import './_kind/_secondary/button_kind_secondary.css'; + +import './_size/_large/button_size_large.css'; +import './_size/_middle/button_size_middle.css'; + +import './_theme/_dark/button_theme_dark.css'; + export { Button } from './button'; - -import './button.scss'; - -import './_kind/_link/button_kind_link.scss'; -import './_kind/_primary/button_kind_primary.scss'; -import './_kind/_secondary/button_kind_secondary.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/comment-form/__actions/comment-form__actions.scss b/frontend/app/components/comment-form/__actions/comment-form__actions.css similarity index 100% rename from frontend/app/components/comment-form/__actions/comment-form__actions.scss rename to frontend/app/components/comment-form/__actions/comment-form__actions.css diff --git a/frontend/app/components/comment-form/__button/_type/_preview/comment-form__button_type_preview.scss b/frontend/app/components/comment-form/__button/_type/_preview/comment-form__button_type_preview.css similarity index 100% rename from frontend/app/components/comment-form/__button/_type/_preview/comment-form__button_type_preview.scss rename to frontend/app/components/comment-form/__button/_type/_preview/comment-form__button_type_preview.css diff --git a/frontend/app/components/comment-form/__button/_type/_send/comment-form__button_type_send.scss b/frontend/app/components/comment-form/__button/_type/_send/comment-form__button_type_send.css similarity index 100% rename from frontend/app/components/comment-form/__button/_type/_send/comment-form__button_type_send.scss rename to frontend/app/components/comment-form/__button/_type/_send/comment-form__button_type_send.css diff --git a/frontend/app/components/comment-form/__button/comment-form__button.scss b/frontend/app/components/comment-form/__button/comment-form__button.css similarity index 100% rename from frontend/app/components/comment-form/__button/comment-form__button.scss rename to frontend/app/components/comment-form/__button/comment-form__button.css diff --git a/frontend/app/components/comment-form/__control-panel/comment-form__control-panel.scss b/frontend/app/components/comment-form/__control-panel/comment-form__control-panel.css similarity index 100% rename from frontend/app/components/comment-form/__control-panel/comment-form__control-panel.scss rename to frontend/app/components/comment-form/__control-panel/comment-form__control-panel.css diff --git a/frontend/app/components/comment-form/__counter/comment-form__counter.scss b/frontend/app/components/comment-form/__counter/comment-form__counter.css similarity index 100% rename from frontend/app/components/comment-form/__counter/comment-form__counter.scss rename to frontend/app/components/comment-form/__counter/comment-form__counter.css diff --git a/frontend/app/components/comment-form/__error/comment-form__error.scss b/frontend/app/components/comment-form/__error/comment-form__error.css similarity index 100% rename from frontend/app/components/comment-form/__error/comment-form__error.scss rename to frontend/app/components/comment-form/__error/comment-form__error.css 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.css similarity index 100% rename from frontend/app/components/comment-form/__field-wrapper/comment-form__field-wrapper.scss rename to frontend/app/components/comment-form/__field-wrapper/comment-form__field-wrapper.css diff --git a/frontend/app/components/comment-form/__field/comment-form__field.pcss b/frontend/app/components/comment-form/__field/comment-form__field.css similarity index 64% rename from frontend/app/components/comment-form/__field/comment-form__field.pcss rename to frontend/app/components/comment-form/__field/comment-form__field.css index 417aa6d9..ee7165b2 100644 --- a/frontend/app/components/comment-form/__field/comment-form__field.pcss +++ b/frontend/app/components/comment-form/__field/comment-form__field.css @@ -1,20 +1,17 @@ .comment-form__field { - $lineHeight: 1.4; - $fontSize: 16px; - $paddingVrt: 10px; - $lines: 4; - $height: calc($fontSize * $lineHeight * $lines + $paddingVrt * 2); + /* font-size * line-height * lines * vertical-padding */ + --height: calc(16px * 1.4 * 4 + 10px * 2); display: block; box-sizing: border-box; width: 100%; - height: $height; - min-height: $height; - padding: $paddingVrt 12px; + height: var(--height); + min-height: var(--height); + padding: 10px 12px; margin: 0; font-family: 'PT Sans', Helvetica, Arial, sans-serif; - font-size: $fontSize; - line-height: $lineHeight; + font-size: 16px; + line-height: 1.4; border: 0; resize: none; backface-visibility: hidden; /* let's try to fix blinking in Safari */ diff --git a/frontend/app/components/comment-form/__markdown-link/comment-form__markdown-link.scss b/frontend/app/components/comment-form/__markdown-link/comment-form__markdown-link.css similarity index 100% rename from frontend/app/components/comment-form/__markdown-link/comment-form__markdown-link.scss rename to frontend/app/components/comment-form/__markdown-link/comment-form__markdown-link.css diff --git a/frontend/app/components/comment-form/__markdown-toolbar/comment-form__markdown-toolbar.scss b/frontend/app/components/comment-form/__markdown-toolbar/comment-form__markdown-toolbar.css similarity index 100% rename from frontend/app/components/comment-form/__markdown-toolbar/comment-form__markdown-toolbar.scss rename to frontend/app/components/comment-form/__markdown-toolbar/comment-form__markdown-toolbar.css diff --git a/frontend/app/components/comment-form/__markdown/comment-form__markdown.scss b/frontend/app/components/comment-form/__markdown/comment-form__markdown.css similarity index 100% rename from frontend/app/components/comment-form/__markdown/comment-form__markdown.scss rename to frontend/app/components/comment-form/__markdown/comment-form__markdown.css 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.css similarity index 100% rename from frontend/app/components/comment-form/__preview-wrapper/comment-form__preview-wrapper.scss rename to frontend/app/components/comment-form/__preview-wrapper/comment-form__preview-wrapper.css diff --git a/frontend/app/components/comment-form/__preview/comment-form__preview.scss b/frontend/app/components/comment-form/__preview/comment-form__preview.css similarity index 100% rename from frontend/app/components/comment-form/__preview/comment-form__preview.scss rename to frontend/app/components/comment-form/__preview/comment-form__preview.css diff --git a/frontend/app/components/comment-form/__rss-link/comment-form__rss-link.scss b/frontend/app/components/comment-form/__rss-link/comment-form__rss-link.css similarity index 100% rename from frontend/app/components/comment-form/__rss-link/comment-form__rss-link.scss rename to frontend/app/components/comment-form/__rss-link/comment-form__rss-link.css diff --git a/frontend/app/components/comment-form/__rss/comment-form__rss.scss b/frontend/app/components/comment-form/__rss/comment-form__rss.css similarity index 100% rename from frontend/app/components/comment-form/__rss/comment-form__rss.scss rename to frontend/app/components/comment-form/__rss/comment-form__rss.css diff --git a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.scss b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.css similarity index 100% rename from frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.scss rename to frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.css diff --git a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx index 3f46e196..26500709 100644 --- a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx +++ b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.test.tsx @@ -1,26 +1,35 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; import { mount } from 'enzyme'; import { act } from 'preact/test-utils'; import { Provider } from 'react-redux'; import { Middleware } from 'redux'; import createMockStore from 'redux-mock-store'; - -import '@app/testUtils/mockApi'; -import { user, anonymousUser } from '@app/testUtils/mocks/user'; -import { validToken } from '@app/testUtils/mocks/jwt'; - -import * as api from '@app/common/api'; -import { sleep } from '@app/utils/sleep'; -import { Input } from '@app/components/input'; -import { Button } from '@app/components/button'; -import { Dropdown } from '@app/components/dropdown'; -import TextareaAutosize from '@app/components/comment-form/textarea-autosize'; import { IntlProvider } from 'react-intl'; -import enMessages from '../../../locales/en.json'; -import { SubscribeByEmail, SubscribeByEmailForm } from './'; -import { LS_EMAIL_KEY } from '@app/common/constants'; +jest.mock('common/api'); + +import { user, anonymousUser } from '__stubs__/user'; +import { validToken } from '__stubs__/jwt'; +import { emailVerificationForSubscribe, emailConfirmationForSubscribe, unsubscribeFromEmailUpdates } from 'common/api'; +import { sleep } from 'utils/sleep'; +import { Input } from 'components/input'; +import { Button } from 'components/button'; +import { Dropdown } from 'components/dropdown'; +import TextareaAutosize from 'components/comment-form/textarea-autosize'; +import enMessages from 'locales/en.json'; +import { LS_EMAIL_KEY } from 'common/constants'; + +import { SubscribeByEmail, SubscribeByEmailForm } from '.'; + +const emailVerificationForSubscribeMock = (emailVerificationForSubscribe as unknown) as jest.Mock< + ReturnType +>; +const emailConfirmationForSubscribeMock = (emailConfirmationForSubscribe as unknown) as jest.Mock< + ReturnType +>; +const unsubscribeFromEmailUpdatesMock = (unsubscribeFromEmailUpdates as unknown) as jest.Mock< + ReturnType +>; const initialStore = { user, @@ -36,7 +45,7 @@ const makeInputEvent = (value: string) => ({ }, }); -jest.mock('@app/utils/jwt', () => ({ +jest.mock('utils/jwt', () => ({ isJwtExpired: jest.fn(() => false), })); @@ -94,49 +103,40 @@ describe('', () => { const wrapper = createWrapper(store); expect(wrapper.find('.comment-form__subscribe-by-email_subscribed')).toHaveLength(1); - expect(wrapper.text()).toStartWith('You are subscribed on updates by email'); + expect(wrapper.text().startsWith('You are subscribed on updates by email')).toBe(true); }); it('should pass throw subscribe process', async () => { const wrapper = createWrapper(); - const emailVerificationForSubscribe = jest.spyOn(api, 'emailVerificationForSubscribe'); - const emailConfirmationForSubscribe = jest.spyOn(api, 'emailConfirmationForSubscribe'); - const onInputEmail = wrapper.find(Input).prop('onInput'); + const input = wrapper.find('input'); const form = wrapper.find('form'); - expect(onInputEmail).toBeFunction(); - - act(() => onInputEmail(makeInputEvent('some@email.com'))); - - expect(form).toHaveLength(1); - + input.getDOMNode().value = 'some@email.com'; + input.simulate('input'); form.simulate('submit'); - expect(emailVerificationForSubscribe).toHaveBeenCalledWith('some@email.com'); + expect(emailVerificationForSubscribeMock).toHaveBeenCalledWith('some@email.com'); - await sleep(0); + await sleep(); wrapper.update(); - const textarea = wrapper.find(TextareaAutosize); - const onInputToken = textarea.prop('onInput') as (e: any) => void; - const button = wrapper.find(Button); + const textarea = wrapper.find('textarea'); + const button = wrapper.find('button'); - expect(textarea).toHaveLength(1); - expect(onInputToken).toBeFunction(); expect(button.at(0).text()).toEqual('Back'); expect(button.at(1).text()).toEqual('Subscribe'); - act(() => onInputToken(makeInputEvent('tokentokentoken'))); + textarea.getDOMNode().value = 'tokentokentoken'; + textarea.simulate('input'); + form.simulate('submit'); - wrapper.find('form').simulate('submit'); - - expect(emailConfirmationForSubscribe).toHaveBeenCalledWith('tokentokentoken'); + expect(emailConfirmationForSubscribeMock).toHaveBeenCalledWith('tokentokentoken'); await sleep(0); wrapper.update(); - expect(wrapper.text()).toStartWith('You have been subscribed on updates by email'); + expect(wrapper.text().startsWith('You have been subscribed on updates by email')).toBe(true); expect(wrapper.find(Button).text()).toEqual('Unsubscribe'); }); @@ -152,7 +152,7 @@ describe('', () => { const onInputEmail = wrapper.find(Input).prop('onInput'); const form = wrapper.find('form'); - expect(onInputEmail).toBeFunction(); + expect(typeof onInputEmail === 'function').toBe(true); act(() => onInputEmail(makeInputEvent('some@email.com'))); @@ -169,7 +169,7 @@ describe('', () => { await sleep(0); wrapper.update(); - expect(wrapper.text()).toStartWith('You have been subscribed on updates by email'); + expect(wrapper.text().startsWith('You have been subscribed on updates by email')).toBe(true); expect(wrapper.find(Button).text()).toEqual('Unsubscribe'); }); @@ -177,18 +177,17 @@ describe('', () => { const store = mockStore({ ...initialStore, user: { email_subscription: true } }); const wrapper = createWrapper(store); const onClick = wrapper.find(Button).prop('onClick'); - const unsubscribeFromEmailUpdates = jest.spyOn(api, 'unsubscribeFromEmailUpdates'); - expect(onClick).toBeFunction(); + expect(typeof onClick === 'function').toBe(true); act(() => onClick()); - expect(unsubscribeFromEmailUpdates).toHaveBeenCalled(); + expect(unsubscribeFromEmailUpdatesMock).toHaveBeenCalled(); await sleep(0); wrapper.update(); - expect(wrapper.text()).toStartWith('You have been unsubscribed by email to updates'); + expect(wrapper.text().startsWith('You have been unsubscribed by email to updates')).toBe(true); expect(wrapper.find(Button).text()).toEqual('Close'); }); }); diff --git a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx index 8e92fbfa..52f3e5f1 100644 --- a/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx +++ b/frontend/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.tsx @@ -1,30 +1,25 @@ -/** @jsx createElement */ -import { createElement, FunctionComponent, Fragment } from 'preact'; +import { h, FunctionComponent, Fragment } from 'preact'; import { useState, useCallback, useEffect, useRef, PropRef } from 'preact/hooks'; import { useSelector, useDispatch } from 'react-redux'; import b from 'bem-react-helper'; - -import { User } from '@app/common/types'; -import { StoreState } from '@app/store'; -import { setUserSubscribed } from '@app/store/user/actions'; -import { sleep } from '@app/utils/sleep'; -import { extractErrorMessageFromResponse } from '@app/utils/errorUtils'; -import useTheme from '@app/hooks/useTheme'; -import { getHandleClickProps } from '@app/common/accessibility'; -import { - emailVerificationForSubscribe, - emailConfirmationForSubscribe, - unsubscribeFromEmailUpdates, -} from '@app/common/api'; -import { Input } from '@app/components/input'; -import { Button } from '@app/components/button'; -import { Dropdown } from '@app/components/dropdown'; -import { Preloader } from '@app/components/preloader'; -import TextareaAutosize from '@app/components/comment-form/textarea-autosize'; -import { isUserAnonymous } from '@app/utils/isUserAnonymous'; -import { isJwtExpired } from '@app/utils/jwt'; import { useIntl, defineMessages, IntlShape, FormattedMessage } from 'react-intl'; -import { LS_EMAIL_KEY } from '@app/common/constants'; + +import { User } from 'common/types'; +import { LS_EMAIL_KEY } from 'common/constants'; +import { StoreState } from 'store'; +import { setUserSubscribed } from 'store/user/actions'; +import { sleep } from 'utils/sleep'; +import { extractErrorMessageFromResponse } from 'utils/errorUtils'; +import useTheme from 'hooks/useTheme'; +import { getHandleClickProps } from 'common/accessibility'; +import { emailVerificationForSubscribe, emailConfirmationForSubscribe, unsubscribeFromEmailUpdates } from 'common/api'; +import { Input } from 'components/input'; +import { Button } from 'components/button'; +import { Dropdown } from 'components/dropdown'; +import Preloader from 'components/preloader'; +import TextareaAutosize from 'components/comment-form/textarea-autosize'; +import { isUserAnonymous } from 'utils/isUserAnonymous'; +import { isJwtExpired } from 'utils/jwt'; const emailRegex = /[^@]+@[^.]+\..+/; @@ -105,7 +100,7 @@ const renderTokenPart = ( handleChangeToken: (e: Event) => void, setEmailStep: () => void ) => ( - + <> @@ -117,7 +112,7 @@ const renderTokenPart = ( disabled={loading} value={token} /> - + ); export const SubscribeByEmailForm: FunctionComponent = () => { @@ -165,7 +160,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => { setLoading(false); } }, - [setLoading, setError, setStep, step, emailAddress, token] + [setLoading, setError, setStep, step, emailAddress, token, dispatch, intl] ); const handleChangeEmail = useCallback((e: Event) => { @@ -193,7 +188,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => { setToken(value); }, - [sendForm, setError, setToken] + [sendForm, setError, setToken, intl] ); const handleSubmit = useCallback( @@ -212,6 +207,20 @@ export const SubscribeByEmailForm: FunctionComponent = () => { setStep(Step.Email); }, [setStep]); + const handleUnsubscribe = useCallback(async () => { + setLoading(true); + try { + await unsubscribeFromEmailUpdates(); + dispatch(setUserSubscribed(false)); + previousStep.current = Step.Subscribed; + setStep(Step.Unsubscribed); + } catch (e) { + setError(extractErrorMessageFromResponse(e, intl)); + } finally { + setLoading(false); + } + }, [setLoading, setStep, setError, dispatch, intl]); + useEffect(() => { if (emailAddressRef.current) { emailAddressRef.current.focus(); @@ -227,20 +236,6 @@ export const SubscribeByEmailForm: FunctionComponent = () => { } if (step === Step.Subscribed) { - const handleUnsubscribe = useCallback(async () => { - setLoading(true); - try { - await unsubscribeFromEmailUpdates(); - dispatch(setUserSubscribed(false)); - previousStep.current = Step.Subscribed; - setStep(Step.Unsubscribed); - } catch (e) { - setError(extractErrorMessageFromResponse(e, intl)); - } finally { - setLoading(false); - } - }, [setLoading, setStep, setError]); - const text = previousStep.current === Step.Token ? intl.formatMessage(messages.haveSubscribed) @@ -318,9 +313,7 @@ export const SubscribeByEmail: FunctionComponent = () => { const intl = useIntl(); const user = useSelector(({ user }) => user); const isAnonymous = isUserAnonymous(user); - const buttonTitle = isAnonymous - ? intl.formatMessage(messages.onlyRegisteredUsers) - : intl.formatMessage(messages.subscribeByEmail); + const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByEmail); return ( ({ useSelector: jest.fn(fn => fn({ theme: 'light' })), })); jest.mock('react-intl', () => { - // Require the original module to not be mocked... - const originalModule = jest.requireActual('react-intl'); + const reactIntl = jest.requireActual('react-intl'); + const messages = require('locales/en.json'); + const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {}); return { - ...originalModule, - useIntl: () => originalModule.createIntl({ locale: `en`, messages: enMessages }), + ...reactIntl, + useIntl: () => intlProvider.state.intl, }; }); describe('', () => { - let wrapper: ReturnType; - - beforeAll(() => { - wrapper = shallow(); - }); - it('should be render links in dropdown', () => { - wrapper.update(); + const wrapper = shallow(); + expect(wrapper.find('.comment-form__rss-dropdown__link')).toHaveLength(3); }); it('should have userId in replies link', () => { + const wrapper = shallow(); + expect(wrapper.find('.comment-form__rss-dropdown__link').at(2).prop('href')).toBe( createSubscribeUrl('reply', '&user=user-1') ); diff --git a/frontend/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.tsx b/frontend/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.tsx index 9ef68401..55e6de0e 100644 --- a/frontend/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.tsx +++ b/frontend/app/components/comment-form/__subscribe-by-rss/comment-form__subscribe-by-rss.tsx @@ -1,14 +1,13 @@ -/** @jsx createElement */ -import { createElement, FunctionComponent } from 'preact'; +import { h, FunctionComponent } from 'preact'; import { useMemo } from 'preact/hooks'; import { useIntl, defineMessages } from 'react-intl'; -import useTheme from '@app/hooks/useTheme'; -import { siteId, url } from '@app/common/settings'; -import { BASE_URL, API_BASE } from '@app/common/constants'; -import { Dropdown, DropdownItem } from '@app/components/dropdown'; +import useTheme from 'hooks/useTheme'; +import { siteId, url } from 'common/settings'; +import { BASE_URL, API_BASE } from 'common/constants'; +import { Dropdown, DropdownItem } from 'components/dropdown'; -export const createSubscribeUrl = (type: 'post' | 'site' | 'reply', urlParams: string = '') => +export const createSubscribeUrl = (type: 'post' | 'site' | 'reply', urlParams = '') => `${BASE_URL}${API_BASE}/rss/${type}?site=${siteId}${urlParams}`; const messages = defineMessages({ @@ -43,7 +42,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u [createSubscribeUrl('site'), intl.formatMessage(messages.site)], [createSubscribeUrl('reply', `&user=${userId}`), intl.formatMessage(messages.replies)], ], - [userId] + [userId, intl] ); return ( @@ -56,7 +55,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u > {items.map(([href, label]) => ( - + {label} diff --git a/frontend/app/components/comment-form/_simple/comment-form_simple.scss b/frontend/app/components/comment-form/_simple/comment-form_simple.css similarity index 100% rename from frontend/app/components/comment-form/_simple/comment-form_simple.scss rename to frontend/app/components/comment-form/_simple/comment-form_simple.css diff --git a/frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.scss b/frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.css similarity index 69% rename from frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.scss rename to frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.css index 317bf788..22ff7bc9 100644 --- a/frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.scss +++ b/frontend/app/components/comment-form/_theme/_dark/comment-form_theme_dark.css @@ -2,41 +2,41 @@ border-color: var(--color7); background: var(--color8); /* try to fix textarea blinking in Safari */ - .comment-form__actions { + & .comment-form__actions { background: var(--color7); } - .comment-form__button_type_preview { + & .comment-form__button_type_preview { background: var(--color8); color: var(--color20); } - .comment-form__button_type_send { + & .comment-form__button_type_send { color: var(--color20); } - .comment-form__error { + & .comment-form__error { border-top: 8px solid var(--color7); background: var(--color28); color: var(--color27); } - .comment-form__field { + & .comment-form__field { background: var(--color8); color: var(--color5); } - .comment-form__preview { + & .comment-form__preview { border-color: var(--color8); background: var(--color8); color: var(--color20); } - .comment-form__preview-wrapper { + & .comment-form__preview-wrapper { background: var(--color7); } - .comment-form__toolbar-item { + & .comment-form__toolbar-item { color: var(--color20); &:hover { @@ -44,7 +44,7 @@ } } - .comment-form__control-panel { + & .comment-form__control-panel { background-color: var(--color7); } } diff --git a/frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.scss b/frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.css similarity index 71% rename from frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.scss rename to frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.css index e5b896f5..6e373a66 100644 --- a/frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.scss +++ b/frontend/app/components/comment-form/_theme/_light/comment-form_theme_light.css @@ -2,37 +2,37 @@ border-color: var(--color5); background: var(--color6); /* try to fix textarea blinking in Safari */ - .comment-form__actions { + & .comment-form__actions { background: var(--color5); } - .comment-form__button_type_preview { + & .comment-form__button_type_preview { background: var(--color6); color: var(--color0); } - .comment-form__button_type_send { + & .comment-form__button_type_send { color: var(--color6); } - .comment-form__error { + & .comment-form__error { border-top: 8px solid var(--color5); background: var(--color26); color: var(--color25); } - .comment-form__field { + & .comment-form__field { background: var(--color6); color: var(--color0); } - .comment-form__preview { + & .comment-form__preview { border-color: var(--color5); background: var(--color6); color: var(--color7); } - .comment-form__preview-wrapper { + & .comment-form__preview-wrapper { background: var(--color5); } } diff --git a/frontend/app/components/comment-form/comment-form.scss b/frontend/app/components/comment-form/comment-form.css similarity index 89% rename from frontend/app/components/comment-form/comment-form.scss rename to frontend/app/components/comment-form/comment-form.css index b475a13e..13e2987c 100644 --- a/frontend/app/components/comment-form/comment-form.scss +++ b/frontend/app/components/comment-form/comment-form.css @@ -9,7 +9,7 @@ .comment-form__dropdown_rss { text-align: left; - .dropdown__content { + & .dropdown__content { width: 8em; } } diff --git a/frontend/app/components/comment-form/comment-form.test.tsx b/frontend/app/components/comment-form/comment-form.test.tsx index f798bd84..202d9365 100644 --- a/frontend/app/components/comment-form/comment-form.test.tsx +++ b/frontend/app/components/comment-form/comment-form.test.tsx @@ -1,13 +1,12 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; import { shallow } from 'enzyme'; -import { user, anonymousUser } from '@app/testUtils/mocks/user'; -import { StaticStore } from '@app/common/static_store'; -import { LS_SAVED_COMMENT_VALUE } from '@app/common/constants'; -import * as localStorageModule from '@app/common/local-storage'; +import { user, anonymousUser } from '__stubs__/user'; +import { StaticStore } from 'common/static-store'; +import { LS_SAVED_COMMENT_VALUE } from 'common/constants'; +import * as localStorageModule from 'common/local-storage'; -import { CommentForm, Props, messages, State } from './comment-form'; +import { CommentForm, CommentFormProps, messages } from './comment-form'; import { SubscribeByEmail } from './__subscribe-by-email'; import TextareaAutosize from './textarea-autosize'; @@ -19,7 +18,7 @@ function createEvent(type: string, value: T): E { return event as E; } -const DEFAULT_PROPS: Readonly> = { +const DEFAULT_PROPS: Readonly> = { mode: 'main', theme: 'light', onSubmit: () => Promise.resolve(), @@ -37,7 +36,7 @@ const intl = { describe('', () => { it('should shallow without control panel, preview button, and rss links in "simple view" mode', () => { const props = { ...DEFAULT_PROPS, simpleView: true, intl }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.exists('.comment-form__control-panel')).toEqual(false); expect(wrapper.exists('.comment-form__button_type_preview')).toEqual(false); @@ -48,7 +47,7 @@ describe('', () => { StaticStore.config.email_notifications = true; const props = { ...DEFAULT_PROPS, user, intl }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.exists(SubscribeByEmail)).toEqual(true); }); @@ -57,7 +56,7 @@ describe('', () => { StaticStore.config.email_notifications = false; const props = { ...DEFAULT_PROPS, user, intl }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.exists(SubscribeByEmail)).toEqual(false); }); @@ -70,7 +69,7 @@ describe('', () => { localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 2: 'text' })); const props = { ...DEFAULT_PROPS, user, intl }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('text')).toBe(''); expect(wrapper.find(TextareaAutosize).prop('value')).toBe(''); @@ -82,7 +81,7 @@ describe('', () => { localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: COMMENT_VALUE })); const props = { ...DEFAULT_PROPS, user, intl }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('text')).toBe(COMMENT_VALUE); expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE); @@ -94,7 +93,7 @@ describe('', () => { localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: 'text from localStorage' })); const props = { ...DEFAULT_PROPS, user, intl, value: COMMENT_VALUE }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('text')).toBe(COMMENT_VALUE); expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE); @@ -107,7 +106,8 @@ describe('', () => { }); it('should update value', () => { const props = { ...DEFAULT_PROPS, user, intl }; - const wrapper = shallow(); + + const wrapper = shallow(); const instance = wrapper.instance(); instance.onInput(createEvent('input', { value: '1' })); @@ -121,7 +121,8 @@ describe('', () => { localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ '1': 'asd' })); const updateJsonItemSpy = jest.spyOn(localStorageModule, 'updateJsonItem'); const props = { ...DEFAULT_PROPS, user, intl }; - const wrapper = shallow(); + + const wrapper = shallow(); const instance = wrapper.instance(); await instance.send(createEvent('send', { preventDefault: () => undefined })); @@ -132,7 +133,7 @@ describe('', () => { it('should show error message of image upload try by anonymous user', () => { const props = { ...DEFAULT_PROPS, user: anonymousUser, intl }; - const wrapper = shallow(); + const wrapper = shallow(); const instance = wrapper.instance(); instance.onDrop(new Event('drag') as DragEvent); @@ -142,7 +143,7 @@ describe('', () => { it('should show error message of image upload try by unauthorized user', () => { const props = { ...DEFAULT_PROPS, intl }; - const wrapper = shallow(); + const wrapper = shallow(); const instance = wrapper.instance(); instance.onDrop(new Event('drag') as DragEvent); @@ -156,7 +157,7 @@ describe('', () => { const originalConfig = { ...StaticStore.config }; StaticStore.config.max_comment_size = 2000; const props = { ...DEFAULT_PROPS, intl }; - const wrapper = shallow(); + const wrapper = shallow(); const instance = wrapper.instance(); const text = 'That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the chassis of a gutted game console. It was chambered for .22 long rifle, and Case would’ve preferred lead azide explosives to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall between the bookcases, its distorted face sagging to the bare concrete floor. Splayed in his elastic g-web, Case watched the other passengers as he made his way down Shiga from the sushi stall he cradled it in his jacket pocket. Images formed and reformed: a flickering montage of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the Japanese night like live wire voodoo and he’d cry for it, cry in his jacket pocket. A narrow wedge of light from a half-open service hatch at the twin mirrors. Still it was a square of faint light. The alarm still oscillated, louder here, the rear wall dulling the roar of the arcade showed him broken lengths of damp chipboard and the robot gardener. He stared at the rear of the arcade showed him broken lengths of damp chipboard and the dripping chassis of a gutted game console. That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the chassis of a gutted game console. It was chambered for .22 long rifle, and Case would’ve preferred lead azide explosives to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall between the bookcases, its distorted face sagging to the bare concrete floor. Splayed in his elastic g-web, Case watched the other passengers as he made his way down Shiga from the sushi stall he cradled it in his jacket pocket. Images formed and reformed: a flickering montage of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the Japanese night like live wire voodoo and he’d cry for it, cry in his jacket.'; @@ -177,7 +178,7 @@ describe('', () => { const originalConfig = { ...StaticStore.config }; StaticStore.config.max_comment_size = 2000; const props = { ...DEFAULT_PROPS, intl }; - const wrapper = shallow(); + const wrapper = shallow(); const instance = wrapper.instance(); const text = 'All the speed he took, all the turns he’d taken and the amplified breathing of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the dark. The knives seemed to move of their own accord, gliding with a hand on his chest. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the Flatline as a construct, a hardwired ROM cassette replicating a dead man’s skills, obsessions, kneejerk responses. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the bright void beyond the chain link. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Now this quiet courtyard, Sunday afternoon, this girl with a ritual lack of urgency through the arcs and passes of their dance, point passing point, as the men waited for an opening. They floated in the shade beneath a bridge or overpass. A graphic representation of data abstracted from the banks of every computer in the coffin for Armitage’s call. All the speed he took, all the turns he’d taken and the amplified breathing of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the dark. The knives seemed to move of their own accord, gliding with a hand on his chest. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the Flatline as a construct, a hardwired ROM cassette replicating a dead man’s skills, obsessions, kneejerk responses. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the bright void beyond the chain link. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Now this quiet courtyard, Sunday afternoon, this girl with a ritual lack of urgency through the arcs and passes of their dance, point passing point, as the men waited for an opening. They floated in the shade beneath a bridge or overpass. A graphic representation of data abstracted from the banks of every computer in the coffin for Armitage’s call.'; diff --git a/frontend/app/components/comment-form/comment-form.tsx b/frontend/app/components/comment-form/comment-form.tsx index 55756c9a..c3bc879f 100644 --- a/frontend/app/components/comment-form/comment-form.tsx +++ b/frontend/app/components/comment-form/comment-form.tsx @@ -1,19 +1,18 @@ -/** @jsx createElement */ -import { createElement, Component, createRef, Fragment } from 'preact'; +import { h, Component, createRef, Fragment } from 'preact'; import { FormattedMessage, IntlShape, defineMessages } from 'react-intl'; import b, { Mix } from 'bem-react-helper'; -import { User, Theme, Image, ApiError } from '@app/common/types'; -import { StaticStore } from '@app/common/static_store'; -import { pageTitle } from '@app/common/settings'; -import { extractErrorMessageFromResponse } from '@app/utils/errorUtils'; -import { isUserAnonymous } from '@app/utils/isUserAnonymous'; -import { sleep } from '@app/utils/sleep'; -import { replaceSelection } from '@app/utils/replaceSelection'; -import { Button } from '@app/components/button'; -import Auth from '@app/components/auth'; -import { getJsonItem, updateJsonItem } from '@app/common/local-storage'; -import { LS_SAVED_COMMENT_VALUE } from '@app/common/constants'; +import { User, Theme, Image, ApiError } from 'common/types'; +import { StaticStore } from 'common/static-store'; +import { pageTitle } from 'common/settings'; +import { extractErrorMessageFromResponse } from 'utils/errorUtils'; +import { isUserAnonymous } from 'utils/isUserAnonymous'; +import { sleep } from 'utils/sleep'; +import { replaceSelection } from 'utils/replaceSelection'; +import { Button } from 'components/button'; +import Auth from 'components/auth'; +import { getJsonItem, updateJsonItem } from 'common/local-storage'; +import { LS_SAVED_COMMENT_VALUE } from 'common/constants'; import { SubscribeByEmail } from './__subscribe-by-email'; import { SubscribeByRSS } from './__subscribe-by-rss'; @@ -24,7 +23,7 @@ import { TextExpander } from './text-expander'; let textareaId = 0; -export interface Props { +export type CommentFormProps = { id: string; user: User | null; errorMessage?: string; @@ -41,9 +40,9 @@ export interface Props { onCancel?: () => void; uploadImage?: (image: File) => Promise; intl: IntlShape; -} +}; -export interface State { +export type CommentFormState = { preview: string | null; isErrorShown: boolean; /** error message, if contains newlines, it will be splitted to multiple errors */ @@ -55,7 +54,7 @@ export interface State { text: string; /** override main button text */ buttonText: null | string; -} +}; const ImageMimeRegex = /image\//i; @@ -99,12 +98,12 @@ export const messages = defineMessages({ }, }); -export class CommentForm extends Component { +export class CommentForm extends Component { /** reference to textarea element */ textAreaRef = createRef(); textareaId: string; - constructor(props: Props) { + constructor(props: CommentFormProps) { super(props); textareaId = textareaId + 1; this.textareaId = `textarea_${textareaId}`; @@ -140,7 +139,7 @@ export class CommentForm extends Component { this.onPaste = this.onPaste.bind(this); } - componentWillReceiveProps(nextProps: Props) { + componentWillReceiveProps(nextProps: CommentFormProps) { if (nextProps.value !== this.props.value) { this.setState({ text: nextProps.value || '' }); this.props.autofocus && this.textAreaRef.current && this.textAreaRef.current.focus(); @@ -153,7 +152,7 @@ export class CommentForm extends Component { } } - shouldComponentUpdate(nextProps: Props, nextState: State) { + shouldComponentUpdate(nextProps: CommentFormProps, nextState: CommentFormState) { const userId = this.props.user !== null && this.props.user.id; const nextUserId = nextProps.user !== null && nextProps.user.id; @@ -260,7 +259,7 @@ export class CommentForm extends Component { return; } this.setState({ - errorMessage: this.state.errorMessage + '\n' + errors.join('\n'), + errorMessage: `${this.state.errorMessage}\n${errors.join('\n')}`, isErrorShown: true, }); } @@ -320,7 +319,7 @@ export class CommentForm extends Component { 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'; + const maxImageSizeString = `${(StaticStore.config.max_image_size / 1024 / 1024).toFixed(2)}MB`; /** upload delay to avoid server rate limiter */ const uploadDelay = 5000; @@ -560,6 +559,7 @@ export class CommentForm extends Component { className={b('comment-form__preview', { mix: b('raw-content', {}, { theme }), })} + // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: preview }} /> diff --git a/frontend/app/components/comment-form/index.ts b/frontend/app/components/comment-form/index.ts index 112de9b2..d6e47155 100644 --- a/frontend/app/components/comment-form/index.ts +++ b/frontend/app/components/comment-form/index.ts @@ -1,31 +1,32 @@ -export { CommentForm, Props } from './comment-form'; +import 'components/raw-content'; -import '@app/components/raw-content'; +import './comment-form.css'; -import './comment-form.scss'; +import './__actions/comment-form__actions.css'; -import './__actions/comment-form__actions.scss'; +import './__button/comment-form__button.css'; +import './__button/_type/_preview/comment-form__button_type_preview.css'; +import './__button/_type/_send/comment-form__button_type_send.css'; -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.css'; +import './__counter/comment-form__counter.css'; +import './__error/comment-form__error.css'; +import './__field/comment-form__field.css'; +import './__field-wrapper/comment-form__field-wrapper.css'; +import './__preview/comment-form__preview.css'; +import './__preview-wrapper/comment-form__preview-wrapper.css'; +import './__rss/comment-form__rss.css'; +import './__rss-link/comment-form__rss-link.css'; +import './__markdown/comment-form__markdown.css'; +import './__markdown-link/comment-form__markdown-link.css'; +import './__markdown-toolbar/comment-form__markdown-toolbar.css'; +import './__subscribe-by-email/comment-form__subscribe-by-email.css'; +import './__subscribe-by-rss/comment-form__subscribe-by-rss.css'; -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.pcss'; -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 './__subscribe-by-email/comment-form__subscribe-by-email.scss'; -import './__subscribe-by-rss/comment-form__subscribe-by-rss.scss'; +import './_theme/_dark/comment-form_theme_dark.css'; +import './_theme/_light/comment-form_theme_light.css'; -import './_theme/_dark/comment-form_theme_dark.scss'; -import './_theme/_light/comment-form_theme_light.scss'; +import './_simple/comment-form_simple.css'; -import './_simple/comment-form_simple.scss'; +export { CommentForm } from './comment-form'; +export type { CommentFormProps } from './comment-form'; diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/bold-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/bold-icon.tsx index 64be5802..c2137157 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/bold-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/bold-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function BoldIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/code-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/code-icon.tsx index b204de6d..d3aa35b7 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/code-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/code-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function CodeIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/header-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/header-icon.tsx index 8e097220..13e0ed7a 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/header-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/header-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function HeaderIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/image-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/image-icon.tsx index ce34b81b..fb9ce1dd 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/image-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/image-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function ImageIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/italic-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/italic-icon.tsx index 51c0acb1..986c3a1b 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/italic-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/italic-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function ItalicIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/link-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/link-icon.tsx index 9527e29c..dc1fa4bc 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/link-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/link-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function LinkIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/ordered-list-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/ordered-list-icon.tsx index 624be71e..f9428258 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/ordered-list-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/ordered-list-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function OrderedListIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/quote-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/quote-icon.tsx index b3070b70..9e2c4e6e 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/quote-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/quote-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function QuoteIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar-icons/unordered-list-icon.tsx b/frontend/app/components/comment-form/markdown-toolbar-icons/unordered-list-icon.tsx index e2706e45..01a51223 100644 --- a/frontend/app/components/comment-form/markdown-toolbar-icons/unordered-list-icon.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar-icons/unordered-list-icon.tsx @@ -1,5 +1,4 @@ -/** @jsx createElement */ -import { createElement } from 'preact'; +import { h } from 'preact'; export default function UnorderedListIcon() { return ( diff --git a/frontend/app/components/comment-form/markdown-toolbar.tsx b/frontend/app/components/comment-form/markdown-toolbar.tsx index fd96ac03..11103831 100644 --- a/frontend/app/components/comment-form/markdown-toolbar.tsx +++ b/frontend/app/components/comment-form/markdown-toolbar.tsx @@ -1,7 +1,9 @@ -/** @jsx createElement */ -import { createElement, Component } from 'preact'; +import '@ungap/custom-elements'; import '@github/markdown-toolbar-element'; +import { h, Component } from 'preact'; import { defineMessages, IntlShape } from 'react-intl'; + +// TODO: Use SVGR import BoldIcon from './markdown-toolbar-icons/bold-icon'; import HeaderIcon from './markdown-toolbar-icons/header-icon'; import ItalicIcon from './markdown-toolbar-icons/italic-icon'; diff --git a/frontend/app/components/comment-form/text-expander.module.pcss b/frontend/app/components/comment-form/text-expander.module.css similarity index 100% rename from frontend/app/components/comment-form/text-expander.module.pcss rename to frontend/app/components/comment-form/text-expander.module.css diff --git a/frontend/app/components/comment-form/text-expander.tsx b/frontend/app/components/comment-form/text-expander.tsx index faf90fc5..53bf4e96 100644 --- a/frontend/app/components/comment-form/text-expander.tsx +++ b/frontend/app/components/comment-form/text-expander.tsx @@ -1,12 +1,13 @@ -/** @jsx createElement */ -import { createElement, Fragment, render, FunctionalComponent } from 'preact'; -import { StaticStore } from '@app/common/static_store'; -import { useEffect, useRef } from 'preact/hooks'; import '@github/text-expander-element'; -import styles from './text-expander.module.pcss'; +import { h, Fragment, render, FunctionalComponent } from 'preact'; +import { useEffect, useRef } from 'preact/hooks'; import cx from 'classnames'; -import { Theme } from '@app/common/types'; -import useTheme from '@app/hooks/useTheme'; + +import { StaticStore } from 'common/static-store'; +import { Theme } from 'common/types'; +import useTheme from 'hooks/useTheme'; + +import styles from './text-expander.module.css'; type Emoji = { key: string; diff --git a/frontend/app/components/comment-form/textarea-autosize.tsx b/frontend/app/components/comment-form/textarea-autosize.tsx index 17449e26..ddcc0def 100644 --- a/frontend/app/components/comment-form/textarea-autosize.tsx +++ b/frontend/app/components/comment-form/textarea-autosize.tsx @@ -1,13 +1,11 @@ -/** @jsx createElement */ -import { createElement, JSX, Component, createRef, RefObject } from 'preact'; +import { h, JSX, Component, createRef, RefObject } from 'preact'; -export interface Props extends Omit { - autofocus: boolean; +export type TextareaAutosizeProps = { ref?: RefObject; -} +} & Omit, 'ref'>; // TODO: rewrite it to functional component and add ref forwarding -export default class TextareaAutosize extends Component { +export default class TextareaAutosize extends Component { textareaRef = createRef(); componentDidMount() { @@ -16,7 +14,7 @@ export default class TextareaAutosize extends Component { if (this.props.autofocus) this.focus(); } - componentDidUpdate(prevProps: Props) { + componentDidUpdate(prevProps: TextareaAutosizeProps) { if (prevProps.value !== this.props.value) { this.autoResize(); } @@ -84,7 +82,7 @@ export default class TextareaAutosize extends Component { } } - render(props: Props) { - return