diff --git a/frontend/.stylelintrc.js b/frontend/.stylelintrc.js index ae2dc2daeb..c6a4b04722 100644 --- a/frontend/.stylelintrc.js +++ b/frontend/.stylelintrc.js @@ -12,6 +12,11 @@ module.exports = { ignore: ['after-comment'], }, ], + 'comment-empty-line-before': [ + 'always', + { except: ['first-nested'], ignore: ['after-comment', 'stylelint-commands'] }, + ], + 'value-keyword-case': ['lower', { ignoreProperties: ['composes'] }], 'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global'] }], 'property-no-unknown': [true, { ignoreProperties: ['composes'] }], 'mavrin/stylelint-declaration-use-css-custom-properties': { diff --git a/frontend/app/__stubs__/react-intl.ts b/frontend/app/__stubs__/react-intl.ts deleted file mode 100644 index ef68ebad72..0000000000 --- a/frontend/app/__stubs__/react-intl.ts +++ /dev/null @@ -1,10 +0,0 @@ -jest.mock('react-intl', () => { - const messages = require('locales/en.json'); - const reactIntl = jest.requireActual('react-intl'); - const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {}); - - return { - ...reactIntl, - useIntl: () => intlProvider.state.intl, - }; -}); diff --git a/frontend/app/common/api.ts b/frontend/app/common/api.ts index b0ae954ce5..558a93e938 100644 --- a/frontend/app/common/api.ts +++ b/frontend/app/common/api.ts @@ -11,13 +11,8 @@ export const getPostComments = (sort: Sorting) => apiFetcher.get('/find', export const getComment = (id: Comment['id']): Promise => apiFetcher.get(`/id/${id}`, { url }); -export const getUserComments = ( - userId: User['id'], - limit: number -): Promise<{ - comments: Comment[]; - count: number; -}> => apiFetcher.get('/comments', { user: userId, limit }); +export const getUserComments = (userId: User['id']): Promise<{ comments: Comment[] }> => + apiFetcher.get('/comments', { user: userId, limit: 10 }); export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise => apiFetcher.put(`/vote/${id}`, { url, vote: value }); diff --git a/frontend/app/common/settings.ts b/frontend/app/common/settings.ts index d7dfa51f91..3bceef89c3 100644 --- a/frontend/app/common/settings.ts +++ b/frontend/app/common/settings.ts @@ -1,4 +1,4 @@ -import { parseQuery } from 'utils/parseQuery'; +import { parseQuery } from 'utils/parse-query'; import type { Theme } from './types'; import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants'; diff --git a/frontend/app/common/types.ts b/frontend/app/common/types.ts index 1f9088ab5b..1cb4599be3 100644 --- a/frontend/app/common/types.ts +++ b/frontend/app/common/types.ts @@ -10,7 +10,7 @@ export type User = { }; /** data which is used on user-info page */ -export type UserInfo = Pick; +export type Profile = Pick & { current?: '1' }; export interface BlockedUser { id: string; @@ -46,7 +46,7 @@ export interface Comment { * if user hasn't voted delta will be 0, * -1/+1 for downvote/upvote */ - vote: number; + vote: 0 | 1 | -1; /** comment controversy, read only */ controversy?: number; /** pointer to have empty default in json response */ diff --git a/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.css b/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.css deleted file mode 100644 index 5ff2ec528e..0000000000 --- a/frontend/app/components/auth-panel/__user-id/auth-panel__user-id.css +++ /dev/null @@ -1,15 +0,0 @@ -.auth-panel__user-id { - overflow: hidden; - text-overflow: ellipsis; - padding: 5px; - cursor: pointer; -} - -.auth-panel__user-dropdown-title { - color: inherit; - - &:hover { - color: inherit; - opacity: 0.9; - } -} diff --git a/frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.css b/frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.css deleted file mode 100644 index 2d369149f7..0000000000 --- a/frontend/app/components/auth-panel/_logged-in/auth-panel_logged-in.css +++ /dev/null @@ -1,9 +0,0 @@ -.auth-panel_logged-in { - font-size: 12px; - - & .auth-panel__column { - &:first-child { - font-weight: 400; - } - } -} diff --git a/frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.css b/frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.css deleted file mode 100644 index ff243925b3..0000000000 --- a/frontend/app/components/auth-panel/_theme/_dark/auth-panel_theme_dark.css +++ /dev/null @@ -1,5 +0,0 @@ -.auth-panel_theme_dark { - & .auth-panel__user-id { - color: var(--color5); - } -} diff --git a/frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.css b/frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.css deleted file mode 100644 index c96302b73d..0000000000 --- a/frontend/app/components/auth-panel/_theme/_light/auth-panel_theme_light.css +++ /dev/null @@ -1,5 +0,0 @@ -.auth-panel_theme_light { - & .auth-panel__user-id { - color: var(--color13); - } -} diff --git a/frontend/app/components/auth-panel/auth-panel.module.css b/frontend/app/components/auth-panel/auth-panel.module.css new file mode 100644 index 0000000000..27f0a1e35d --- /dev/null +++ b/frontend/app/components/auth-panel/auth-panel.module.css @@ -0,0 +1,28 @@ +.user { + display: flex; + align-items: center; + font-size: 14px; +} + +.userButton { + display: flex; + align-items: center; + font-weight: bold; + color: rgb(var(--secondary-text-color)); + transition: color 0.15s; + + &:hover { + color: inherit; + } +} + +.userLogoutButton { + composes: userButton; + margin-left: 12px; +} + +.userAvatar { + width: 20px; + height: 20px; + margin-right: 8px; +} diff --git a/frontend/app/components/auth-panel/auth-panel.test.tsx b/frontend/app/components/auth-panel/auth-panel.test.tsx index 63d16544e0..15615c0a85 100644 --- a/frontend/app/components/auth-panel/auth-panel.test.tsx +++ b/frontend/app/components/auth-panel/auth-panel.test.tsx @@ -80,7 +80,7 @@ describe('', () => { const userInfo = authPanelColumn.first(); - expect(userInfo.text()).toEqual(expect.stringContaining('You logged in as John')); + expect(userInfo.text()).toEqual(expect.stringContaining('John')); }); }); describe('For admin user', () => { diff --git a/frontend/app/components/auth-panel/auth-panel.tsx b/frontend/app/components/auth-panel/auth-panel.tsx index 2a558045dd..34176c56f6 100644 --- a/frontend/app/components/auth-panel/auth-panel.tsx +++ b/frontend/app/components/auth-panel/auth-panel.tsx @@ -1,18 +1,23 @@ -import { h, Component, Fragment } from 'preact'; +import { h, Component } from 'preact'; import { useSelector } from 'react-redux'; import { FormattedMessage, defineMessages, IntlShape, useIntl } from 'react-intl'; import b from 'bem-react-helper'; +import clsx from 'clsx'; import { User, Sorting, Theme, PostInfo } from 'common/types'; import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants'; -import { requestDeletion } from 'utils/email'; import { postMessageToParent } from 'utils/postMessage'; import { getHandleClickProps } from 'common/accessibility'; import { StoreState } from 'store'; -import { Dropdown, DropdownItem } from 'components/dropdown'; +import { useTheme } from 'hooks/useTheme'; import { Button } from 'components/button'; import { Auth } from 'components/auth'; -import { useTheme } from 'hooks/useTheme'; +import { Avatar } from 'components/avatar'; +import { SignOutIcon } from 'components/icons/signout'; +import { IconButton } from 'components/icon-button/icon-button'; +import { messages } from 'components/auth/auth.messsages'; + +import styles from './auth-panel.module.css'; interface OwnProps { user: User | null; @@ -20,8 +25,8 @@ interface OwnProps { isCommentsDisabled: boolean; postInfo: PostInfo; + signout(): Promise; onSortChange(s: Sorting): Promise; - onSignOut(): Promise; onCommentsChangeReadOnlyMode(readOnly: boolean): Promise; onBlockedUsersShow(): void; onBlockedUsersHide(): void; @@ -72,46 +77,25 @@ class AuthPanelComponent extends Component { this.props.onCommentsChangeReadOnlyMode(!this.props.isCommentsDisabled); }; - toggleUserInfoVisibility = () => { - const { user } = this.props; - - if (!user) { - return; - } - - postMessageToParent({ profile: user }); - }; - renderAuthorized = (user: User) => { - const { onSignOut, theme } = this.props; - const isUserAnonymous = user && user.id.substr(0, 10) === 'anonymous_'; - return ( - <> - {' '} - - -
- {user.id} -
-
- - {!isUserAnonymous && ( - - - - )} -
{' '} - - +
+ {' '} +
+ + + +
+
); }; @@ -136,7 +120,9 @@ class AuthPanelComponent extends Component { }; renderCookiesWarning = () => { - if (IS_STORAGE_AVAILABLE || IS_THIRD_PARTY) return null; + if (IS_STORAGE_AVAILABLE || IS_THIRD_PARTY) { + return null; + } return (
diff --git a/frontend/app/components/auth-panel/index.ts b/frontend/app/components/auth-panel/index.ts index ed33afe274..30189cea7e 100644 --- a/frontend/app/components/auth-panel/index.ts +++ b/frontend/app/components/auth-panel/index.ts @@ -1,18 +1,10 @@ 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 * from './auth-panel'; diff --git a/frontend/app/components/auth/auth.messsages.ts b/frontend/app/components/auth/auth.messsages.ts index faa98cd519..de97e803cb 100644 --- a/frontend/app/components/auth/auth.messsages.ts +++ b/frontend/app/components/auth/auth.messsages.ts @@ -57,4 +57,12 @@ export const messages = defineMessages({ id: 'auth.submit', defaultMessage: 'Submit', }, + openProfile: { + id: 'auth.open-profile', + defaultMessage: 'Open My Profile', + }, + signout: { + id: 'auth.signout', + defaultMessage: 'Sign Out', + }, }); diff --git a/frontend/app/components/auth/auth.module.css b/frontend/app/components/auth/auth.module.css index 8e53d0efdf..73d4765242 100644 --- a/frontend/app/components/auth/auth.module.css +++ b/frontend/app/components/auth/auth.module.css @@ -25,15 +25,11 @@ left: 0; min-width: 240px; padding: 16px; - background-color: rgb(var(--white-color)); + background-color: rgb(var(--primary-background-color)); box-shadow: 0 10px 15px rgba(var(--black-color), 0.1), 0 -1px 6px rgba(var(--black-color), 0.05); border-radius: 6px; } -:global(.dark) .dropdown { - background-color: var(--color8); -} - .title { margin: 0 0 12px; font-size: 12px; @@ -148,16 +144,14 @@ .closeButton { position: relative; - display: inline-block; + display: inline-flex; width: 28px; height: 28px; - border: 0; - padding: 0; margin-left: auto; - background: unset; - cursor: pointer; border-radius: 2px; color: inherit; + justify-content: center; + align-items: center; &:hover { background-color: rgba(var(--primary-color), 0.1); @@ -194,15 +188,6 @@ font-size: 16px; } -.spinner { - width: 18px; - height: 18px; - border-radius: 50%; - border: 2px solid rgba(var(--white-color), 0.2); - border-right-color: rgb(var(--white-color)); - animation: spin 1s linear infinite; -} - .error { margin-bottom: 12px; padding: 6px 8px; diff --git a/frontend/app/components/auth/auth.spec.tsx b/frontend/app/components/auth/auth.spec.tsx index a4ce0bca3b..a7de420d36 100644 --- a/frontend/app/components/auth/auth.spec.tsx +++ b/frontend/app/components/auth/auth.spec.tsx @@ -1,6 +1,7 @@ import '@testing-library/jest-dom'; import { h } from 'preact'; -import { fireEvent, render, waitFor } from '@testing-library/preact'; +import { fireEvent, waitFor } from '@testing-library/preact'; +import { render } from 'tests/utils'; import { OAuthProvider, User } from 'common/types'; import { StaticStore } from 'common/static-store'; diff --git a/frontend/app/components/auth/auth.tsx b/frontend/app/components/auth/auth.tsx index 8ca79359e1..578aba5240 100644 --- a/frontend/app/components/auth/auth.tsx +++ b/frontend/app/components/auth/auth.tsx @@ -1,11 +1,12 @@ +import clsx from 'clsx'; import { h, Fragment } from 'preact'; import { useState } from 'preact/hooks'; import { useIntl } from 'react-intl'; -import clsx from 'clsx'; import { useDispatch } from 'react-redux'; import { setUser } from 'store/user/actions'; import { Input } from 'components/input'; +import { CrossIcon } from 'components/icons/cross'; import { TextareaAutosize } from 'components/textarea-autosize'; import { Button } from './components/button'; @@ -16,6 +17,7 @@ import { getProviders, getTokenInvalidReason } from './auth.utils'; import { emailSignin, verifyEmailSignin, anonymousSignin } from './auth.api'; import styles from './auth.module.css'; +import { Spinner } from 'components/spinner/spinner'; export function Auth() { const intl = useIntl(); @@ -107,15 +109,7 @@ export function Auth() { <> {errorMessage &&
{errorMessage}
} ); @@ -146,7 +140,7 @@ export function Auth() { <>
-
diff --git a/frontend/app/components/auth/components/button.module.css b/frontend/app/components/auth/components/button.module.css index c03c24fe3e..b660b5ec74 100644 --- a/frontend/app/components/auth/components/button.module.css +++ b/frontend/app/components/auth/components/button.module.css @@ -56,12 +56,16 @@ background-color: rgb(var(--primary-color)); } -.small { +.xs { height: 28px; font-size: 12px; text-transform: uppercase; } +.sm { + font-size: 14px; +} + .transparent { background-color: rgba(var(--primary-color), 0.1); color: rgb(var(--primary-color)); @@ -72,6 +76,17 @@ } } +.link { + color: rgb(var(--primary-color)); + background-color: unset; + text-transform: initial; + width: auto; + + &:hover { + background-color: rgba(var(--primary-color), 0.1); + } +} + :global(.dark) { & .button { border-color: rgba(var(--white-color), 0.1); diff --git a/frontend/app/components/auth/components/button.tsx b/frontend/app/components/auth/components/button.tsx index 00a6146936..d6fc36b332 100644 --- a/frontend/app/components/auth/components/button.tsx +++ b/frontend/app/components/auth/components/button.tsx @@ -4,8 +4,8 @@ import clsx from 'clsx'; import styles from './button.module.css'; type Props = Omit, 'size'> & { - size?: 'small'; - kind?: 'transparent'; + size?: 'xs' | 'sm'; + kind?: 'transparent' | 'link'; suffix?: VNode; loading?: boolean; selected?: boolean; diff --git a/frontend/app/components/auth/components/oauth.spec.tsx b/frontend/app/components/auth/components/oauth.spec.tsx index b54fd3b163..1622c3a16d 100644 --- a/frontend/app/components/auth/components/oauth.spec.tsx +++ b/frontend/app/components/auth/components/oauth.spec.tsx @@ -1,5 +1,6 @@ import { h } from 'preact'; -import { fireEvent, render, waitFor } from '@testing-library/preact'; +import { fireEvent, waitFor } from '@testing-library/preact'; +import { render } from 'tests/utils'; import type { User } from 'common/types'; import * as userActions from 'store/user/actions'; diff --git a/frontend/app/components/avatar/avatar.module.css b/frontend/app/components/avatar/avatar.module.css index 70dcf9c8d5..7d65ebe37e 100644 --- a/frontend/app/components/avatar/avatar.module.css +++ b/frontend/app/components/avatar/avatar.module.css @@ -1,9 +1,6 @@ .avatar { - display: inline-block; - width: 24px; - height: 24px; - margin-right: 8px; - vertical-align: middle; + display: block; + max-width: 100%; border-radius: 4px; } diff --git a/frontend/app/components/avatar/avatar.spec.tsx b/frontend/app/components/avatar/avatar.spec.tsx index 9ac51a524a..b64a75bf15 100644 --- a/frontend/app/components/avatar/avatar.spec.tsx +++ b/frontend/app/components/avatar/avatar.spec.tsx @@ -2,19 +2,18 @@ import '@testing-library/jest-dom'; import { h } from 'preact'; import { render } from '@testing-library/preact'; -import { BASE_URL } from 'common/constants.config'; - import { Avatar } from './avatar'; +import { BASE_URL } from 'common/constants.config'; describe('', () => { it('should have correct url', () => { - const { container } = render(); + const { container } = render(); expect(container.querySelector('img')).toHaveAttribute('src', `${BASE_URL}/image.svg`); }); it("shouldn't be accessible with screen reader", () => { - const { container } = render(); + const { container } = render(); expect(container.querySelector('img')).toHaveAttribute('aria-hidden', 'true'); }); diff --git a/frontend/app/components/avatar/avatar.tsx b/frontend/app/components/avatar/avatar.tsx index d3f8e7c4cf..c0224a5c60 100644 --- a/frontend/app/components/avatar/avatar.tsx +++ b/frontend/app/components/avatar/avatar.tsx @@ -8,19 +8,13 @@ import styles from './avatar.module.css'; type Props = { url?: string; - /** className should be used only in puprose of put permanent class on Avatar for user themization */ - className: string; }; -export function Avatar({ url, className }: Props) { +export function Avatar({ url }: Props) { const avatarUrl = url || `${BASE_URL}${ghostIconUrl}`; return ( // eslint-disable-next-line jsx-a11y/alt-text - + ); } 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 dd834d8bf7..78e803c0a5 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 @@ -294,7 +294,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => { type="submit" disabled={!isValidEmailAddress || loading} > - {loading ? : buttonLabel} + {loading ? : buttonLabel} ); diff --git a/frontend/app/components/comment/__username/comment__username.css b/frontend/app/components/comment/__username/comment__username.css index c07831e405..ee4a801b15 100644 --- a/frontend/app/components/comment/__username/comment__username.css +++ b/frontend/app/components/comment/__username/comment__username.css @@ -2,7 +2,6 @@ vertical-align: middle; font-weight: 700; text-decoration: none; - cursor: pointer; &:hover { opacity: 0.75; diff --git a/frontend/app/components/comment/__verification/_active/comment__verification_active.svg b/frontend/app/components/comment/__verification/_active/comment__verification_active.svg index 117e912e00..ec01998e09 100644 --- a/frontend/app/components/comment/__verification/_active/comment__verification_active.svg +++ b/frontend/app/components/comment/__verification/_active/comment__verification_active.svg @@ -1,6 +1,4 @@ - - - - + + diff --git a/frontend/app/components/comment/comment.css b/frontend/app/components/comment/comment.css index 399ee4ca04..e8448115f0 100644 --- a/frontend/app/components/comment/comment.css +++ b/frontend/app/components/comment/comment.css @@ -14,6 +14,12 @@ } } +.comment__avatar { + width: 24px; + height: 24px; + margin-right: 8px; +} + .comment_level_6 { & .comment__text, & .comment__actions { diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index d0a29b432f..98854f2cc0 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -24,7 +24,6 @@ function mountComment(props: CommentProps) { } const DefaultProps: Partial = { - CommentForm: null, post_info: { read_only: false, } as PostInfo, diff --git a/frontend/app/components/icon-button/icon-button.module.css b/frontend/app/components/icon-button/icon-button.module.css new file mode 100644 index 0000000000..f44a3a0fd5 --- /dev/null +++ b/frontend/app/components/icon-button/icon-button.module.css @@ -0,0 +1,23 @@ +.root { + display: inline-flex; + box-sizing: border-box; + border: 0; + margin: 0; + padding: 4px; + transition: transfrom 0.15s ease-out; + border-radius: 2px; + appearance: none; + + &:hover { + transform: scale(1.06); + transition: transfrom 0.15s ease-in; + } + + &:active { + transform: scale(1); + } + + &:focus { + box-shadow: inset 0 0 0 2px rgba(var(--primary-color), 0.5); + } +} diff --git a/frontend/app/components/icon-button/icon-button.tsx b/frontend/app/components/icon-button/icon-button.tsx new file mode 100644 index 0000000000..460af5ed76 --- /dev/null +++ b/frontend/app/components/icon-button/icon-button.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; +import { h, RenderableProps, JSX } from 'preact'; + +import styles from './icon-button.module.css'; + +type Props = JSX.HTMLAttributes & RenderableProps<{ className?: string }>; + +export function IconButton({ title, children, className, ...props }: Props) { + return ( + + ); +} diff --git a/frontend/app/components/icons/cross.tsx b/frontend/app/components/icons/cross.tsx new file mode 100644 index 0000000000..d31787f14a --- /dev/null +++ b/frontend/app/components/icons/cross.tsx @@ -0,0 +1,19 @@ +import { h, JSX } from 'preact'; + +type Props = Omit, 'size'> & { + size?: number | string; +}; + +export function CrossIcon({ size = 14, ...props }: Props) { + return ( + + + + ); +} diff --git a/frontend/app/components/icons/signout.tsx b/frontend/app/components/icons/signout.tsx new file mode 100644 index 0000000000..d28281dc0b --- /dev/null +++ b/frontend/app/components/icons/signout.tsx @@ -0,0 +1,19 @@ +import { h, JSX } from 'preact'; + +type Props = Omit, 'size'> & { + size?: number | string; +}; + +export function SignOutIcon({ size = 16, ...props }: Props) { + return ( + + + + ); +} diff --git a/frontend/app/components/list-comments/list-comments.tsx b/frontend/app/components/list-comments/list-comments.tsx index e57bfeada5..332e6c3e40 100644 --- a/frontend/app/components/list-comments/list-comments.tsx +++ b/frontend/app/components/list-comments/list-comments.tsx @@ -20,7 +20,6 @@ export function ListComments({ comments = [] }: Props) { ))}
diff --git a/frontend/app/components/preloader.tsx b/frontend/app/components/preloader.tsx new file mode 100644 index 0000000000..bbe34bed28 --- /dev/null +++ b/frontend/app/components/preloader.tsx @@ -0,0 +1,10 @@ +import { h } from 'preact'; +import clsx from 'clsx'; + +type Props = { + className?: string; +}; + +export function Preloader({ className }: Props) { + return
; +} diff --git a/frontend/app/components/preloader/index.ts b/frontend/app/components/preloader/index.ts deleted file mode 100644 index 6709a1ef77..0000000000 --- a/frontend/app/components/preloader/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './preloader'; - -// all styles were moved to iframe.html diff --git a/frontend/app/components/preloader/preloader.tsx b/frontend/app/components/preloader/preloader.tsx deleted file mode 100644 index 7d9a2966ce..0000000000 --- a/frontend/app/components/preloader/preloader.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { h } from 'preact'; -import b, { Mix } from 'bem-react-helper'; - -type Props = { - mix?: Mix; -}; - -export function Preloader({ mix }: Props) { - return
; -} diff --git a/frontend/app/components/profile/index.ts b/frontend/app/components/profile/index.ts new file mode 100644 index 0000000000..671f42568b --- /dev/null +++ b/frontend/app/components/profile/index.ts @@ -0,0 +1 @@ +export * from './profile'; diff --git a/frontend/app/components/profile/profile.module.css b/frontend/app/components/profile/profile.module.css new file mode 100644 index 0000000000..0fe1c0aa0d --- /dev/null +++ b/frontend/app/components/profile/profile.module.css @@ -0,0 +1,170 @@ +.root { + display: flex; + justify-content: flex-end; + height: 100%; + width: 100%; + transform: translateX(100%); + transition: transform 0.5s ease-out; + + @media (min-width: 448px) { + transform: translateX(448px); + } +} + +.rootAppear { + transform: translateX(0); +} + +.rootDisapear { + transition: transform 0.5s ease-in; +} + +.sidebar { + position: relative; + display: flex; + flex-direction: column; + flex-shrink: 0; + background: rgb(var(--primary-background-color)); + max-width: 100%; + width: 100%; + height: 100%; + + @media (min-width: 448px) { + width: 400px; + } +} + +.closeButtonWrapper { + position: absolute; + z-index: 1; + right: 10px; + top: 18px; + box-sizing: border-box; + color: rgb(var(--secondary-text-color)); + + @media (min-width: 448px) { + position: initial; + width: 100%; + padding: 4px; + box-sizing: border-box; + color: rgb(var(--white-color)); + text-align: right; /* Position close button at right */ + } +} + +.header { + display: flex; + padding: 16px; + flex-shrink: 0; + align-items: center; +} + +.content { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: auto; + padding: 0 16px 16px; +} + +.content::-webkit-scrollbar { + width: 10px; +} + +.content::-webkit-scrollbar-track { + background: unset; + border-radius: 3px; +} + +.content::-webkit-scrollbar-thumb { + background-color: rgb(var(--primary-background-color)); + border-radius: 5px; + border: 2px solid rgb(var(--white-color)); +} + +.footer { + position: sticky; + right: 0; + bottom: 0; + left: 0; + padding: 16px; + text-align: center; +} + +.avatar { + flex-shrink: 0; + height: 32px; + width: 32px; + margin-right: 8px; +} + +.info { + max-width: 100%; + margin: 0; + overflow: hidden; + line-height: 1; +} + +.name { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + font-size: 16px; + font-weight: 700; +} + +.id { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + font-size: 12px; + color: var(--color13); +} + +.signout { + margin-left: auto; + margin-right: 28px; + color: rgb(var(--secondary-text-color)); + + &:hover { + color: inherit; + } + + @media (min-width: 448px) { + margin-right: 0; + } +} + +.title { + position: sticky; + top: 0; + left: 0; + margin: 0 0 4px; + padding-top: 12px 0; + background-color: rgb(var(--primary-background-color)); + z-index: 1; + + &::after { + position: absolute; + top: 100%; + left: 0; + content: ''; + width: 100%; + height: 20px; + background-image: linear-gradient( + 0deg, + rgba(var(--primary-background-color), 0), + rgba(var(--primary-background-color), 1) + ); + } +} + +.preloader { + margin: 0 auto 18px; + color: var(--color13); +} + +.emptyState { + margin: auto; + color: var(--color13); +} diff --git a/frontend/app/components/profile/profile.spec.tsx b/frontend/app/components/profile/profile.spec.tsx new file mode 100644 index 0000000000..3f38c20bc2 --- /dev/null +++ b/frontend/app/components/profile/profile.spec.tsx @@ -0,0 +1,98 @@ +import { h } from 'preact'; +import '@testing-library/jest-dom'; +import { waitFor } from '@testing-library/preact'; + +import { render } from 'tests/utils'; +import * as api from 'common/api'; +import * as pq from 'utils/parse-query'; +import type { Comment, User } from 'common/types'; + +import { Profile } from './profile'; + +const userParamsStub = { + id: '1', + name: 'username', + picture: '/avatar.png', +}; + +const userStub: User = { + ...userParamsStub, + ip: '', + picture: '/avatar.png', + admin: false, + block: false, + verified: false, +}; + +const commentStub: Comment = { + id: '1', + pid: '2', + text: 'comment content', + locator: { + site: '', + url: '', + }, + score: 0, + vote: 0, + voted_ips: [], + time: '2021-04-02T14:52:39.985281605-05:00', + user: userStub, +}; +const commentsStub = [commentStub, commentStub, commentStub]; + +describe('', () => { + it('should render preloader', () => { + jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub })); + const { container } = render(); + + expect(container.querySelector('[aria-label="Loading..."]')).toBeInTheDocument(); + }); + + it('should render without comments', async () => { + jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub })); + const getUserComments = jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: [] })); + + const { getByText } = render(); + + await waitFor(() => expect(getUserComments).toHaveBeenCalledWith('1')); + await waitFor(() => expect(getByText("Don't have comments yet")).toBeInTheDocument()); + }); + + it('should render user with comments', async () => { + jest.spyOn(pq, 'parseQuery').mockImplementation(() => userParamsStub); + jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: commentsStub })); + + const { getByText } = render(); + + await waitFor(() => expect(getByText('Recent comments')).toBeInTheDocument()); + }); + + it('shoud render current user without comments', async () => { + jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub, current: '1' })); + + const { getByText, getByTitle } = render(); + + expect(getByTitle('Sign Out')).toBeInTheDocument(); + expect(getByText('Request my data removal')).toBeInTheDocument(); + }); + + it('shoud render current user with comments', async () => { + jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub, current: '1' })); + jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: commentsStub })); + + const { getByText, getByTitle } = render(); + + expect(getByTitle('Sign Out')).toBeInTheDocument(); + expect(getByText('Request my data removal')).toBeInTheDocument(); + await waitFor(() => expect(getByText('My recent comments')).toBeInTheDocument()); + }); + + it('should render user without footer', async () => { + jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub })); + jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: commentsStub })); + + const { container } = render(); + + expect(container.querySelector('profile-footer')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/app/components/profile/profile.tsx b/frontend/app/components/profile/profile.tsx new file mode 100644 index 0000000000..dfccb58b91 --- /dev/null +++ b/frontend/app/components/profile/profile.tsx @@ -0,0 +1,174 @@ +import clsx from 'clsx'; +import { h, Fragment } from 'preact'; +import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useIntl, FormattedMessage } from 'react-intl'; + +import { getUserComments } from 'common/api'; +import { parseQuery } from 'utils/parse-query'; +import { requestDeletion } from 'utils/email'; +import { setStyles } from 'utils/set-dom-props'; +import { postMessageToParent } from 'utils/postMessage'; +import { Avatar } from 'components/avatar'; +import { Comment, messages } from 'components/comment'; +import { Preloader } from 'components/preloader'; +import { SignOutIcon } from 'components/icons/signout'; +import { logout } from 'components/auth/auth.api'; +import { Spinner } from 'components/spinner/spinner'; +import { CrossIcon } from 'components/icons/cross'; +import { IconButton } from 'components/icon-button/icon-button'; +import { Button } from 'components/auth/components/button'; +import { messages as authMessages } from 'components/auth/auth.messsages'; +import type { Comment as CommentType } from 'common/types'; + +import styles from './profile.module.css'; + +async function signout() { + postMessageToParent({ profile: null, signout: true }); + await logout(); +} + +// TODO: rewrite hide user logic and bring button to user profile +export function Profile() { + const intl = useIntl(); + const rootRef = useRef(null); + const user = useMemo(() => parseQuery(), []); + const [error, setError] = useState(false); + const [comments, setComments] = useState(null); + const [isSigningOut, setSigningOut] = useState(false); + + function handleClickClose() { + const rootElement = rootRef.current; + + rootElement.classList.remove(styles.rootAppear); + rootElement.classList.add(styles.rootDisapear); + // No need to unsubscribe because iframe will be destroyed + rootElement.addEventListener('transitionend', () => { + postMessageToParent({ profile: null }); + }); + } + + async function handleClickLogout() { + setSigningOut(true); + await signout(); + setSigningOut?.(false); + } + + async function handleClickRequestRemoveData() { + await requestDeletion(); + await signout(); + } + + useEffect(() => { + getUserComments(user.id) + .then(({ comments }) => setComments(comments)) + .catch(() => setError(true)); + }, [user.id]); + + useEffect(() => { + const styles = { height: '100%', padding: 0 }; + + setStyles(document.documentElement, styles); + setStyles(document.body, styles); + + function handleKeydown(evt: KeyboardEvent): void { + if (evt.code !== 'Escape') { + return; + } + + postMessageToParent({ profile: null }); + } + + document.addEventListener('keydown', handleKeydown); + + return () => { + document.removeEventListener('keydown', handleKeydown); + }; + }, []); + + useEffect(() => { + rootRef.current.classList.add(styles.rootAppear); + }, []); + + if (!user.id) { + return null; + } + + const isCurrent = user.current === '1'; + const commentsJSX = comments?.length ? ( + <> +

+ {isCurrent ? ( + + ) : ( + + )} +

+ {comments.map((comment) => ( + + ))} + + ) : ( +

+ +

+ ); + + return ( +
+ {/* disable jsx-a11y/no-static-element-interactions and jsx-a11y/click-events-have-key-events */} + {/* that's fine because inside of the element we have button that will throw all events and provide all of the interactions */} + {/* eslint-disable-next-line */} +
+ + + +
+ +
+ ); +} diff --git a/frontend/app/components/root/root.css b/frontend/app/components/root/root.css index d8fd2e452f..e87306f976 100644 --- a/frontend/app/components/root/root.css +++ b/frontend/app/components/root/root.css @@ -1,6 +1,5 @@ .root { position: relative; - font-family: 'PT Sans', Helvetica, Arial, sans-serif; &::selection { background: var(--color42); diff --git a/frontend/app/components/root/root.tsx b/frontend/app/components/root/root.tsx index 5aaba8a107..28308e2258 100644 --- a/frontend/app/components/root/root.tsx +++ b/frontend/app/components/root/root.tsx @@ -1,19 +1,13 @@ -import { h, Component, FunctionComponent, Fragment } from 'preact'; +import { h, Component, Fragment } from 'preact'; import { useEffect, useRef } from 'preact/hooks'; import { useSelector } from 'react-redux'; import b from 'bem-react-helper'; import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl'; import clsx from 'clsx'; -import type { Sorting } from 'common/types'; +import 'styles/global.css'; import type { StoreState } from 'store'; -import { - COMMENT_NODE_CLASSNAME_PREFIX, - MAX_SHOWN_ROOT_COMMENTS, - THEMES, - IS_MOBILE, - LS_EMAIL_KEY, -} from 'common/constants'; +import { COMMENT_NODE_CLASSNAME_PREFIX, MAX_SHOWN_ROOT_COMMENTS, THEMES, IS_MOBILE } from 'common/constants'; import { maxShownComments, url } from 'common/settings'; import { StaticStore } from 'common/static-store'; @@ -25,11 +19,14 @@ import { fetchBlockedUsers, hideUser, unhideUser, + signout, } from 'store/user/actions'; import { fetchComments, updateSorting, addComment, updateComment, unsetCommentMode } from 'store/comments/actions'; import { setCommentsReadOnlyState } from 'store/post-info/actions'; import { setTheme } from 'store/theme/actions'; +// TODO: make this button as default for all cases and replace current `components/Button` +import { Button } from 'components/auth/components/button'; import { Preloader } from 'components/preloader'; import { Settings } from 'components/settings'; import { AuthPanel } from 'components/auth-panel'; @@ -42,8 +39,7 @@ import { bindActions } from 'utils/actionBinder'; import { postMessageToParent, parseMessage } from 'utils/postMessage'; import { useActions } from 'hooks/useAction'; import { setCollapse } from 'store/thread/actions'; -import { logout } from 'components/auth/auth.api'; -import { Button } from 'components/auth/components/button'; +import { Sorting } from 'common/types'; import styles from './root.module.css'; @@ -85,6 +81,7 @@ const boundActions = bindActions({ updateComment, setCollapse, unsetCommentMode, + signout, }); type Props = ReturnType & typeof boundActions & { intl: IntlShape }; @@ -98,7 +95,7 @@ interface State { const messages = defineMessages({ pinnedComments: { - id: `root.pinned-comments`, + id: 'root.pinned-comments', defaultMessage: 'Pinned comments', }, }); @@ -125,12 +122,12 @@ export class Root extends Component { const userloading = this.props.fetchUser().finally(() => this.setState({ isUserLoading: false })); Promise.all([userloading, this.props.fetchComments()]).finally(() => { - postMessageToParent({ height: document.body.offsetHeight }); setTimeout(this.checkUrlHash); window.addEventListener('hashchange', this.checkUrlHash); + postMessageToParent({ height: document.body.offsetHeight }); }); - window.addEventListener('message', this.onMessage.bind(this)); + window.addEventListener('message', this.onMessage); } changeSort = async (sort: Sorting) => { @@ -139,14 +136,6 @@ export class Root extends Component { await this.props.updateSorting(sort); }; - logout = async () => { - await logout(); - this.props.setUser(); - this.props.unsetCommentMode(); - localStorage.removeItem(LS_EMAIL_KEY); - await this.props.fetchComments(); - }; - checkUrlHash = (e: Event & { newURL: string }) => { const hash = e ? `#${e.newURL.split('#')[1]}` : window.location.hash; @@ -180,15 +169,19 @@ export class Root extends Component { } }; - onMessage(event: MessageEvent) { + onMessage = (event: MessageEvent) => { const data = parseMessage(event); + if (data.signout === true) { + this.props.signout(false); + } + if (!data.theme || !THEMES.includes(data.theme)) { return; } this.props.setTheme(data.theme); - } + }; onBlockedUsersShow = async () => { if (this.props.user && this.props.user.admin) { @@ -220,7 +213,7 @@ export class Root extends Component { render(props: Props, { isUserLoading, commentsShown, isSettingsVisible }: State) { if (isUserLoading) { - return ; + return ; } const isCommentsDisabled = props.info.read_only!; @@ -234,7 +227,7 @@ export class Root extends Component { onSortChange={this.changeSort} isCommentsDisabled={isCommentsDisabled} postInfo={this.props.info} - onSignOut={this.logout} + signout={this.props.signout} onBlockedUsersShow={this.onBlockedUsersShow} onBlockedUsersHide={this.onBlockedUsersHide} onCommentsChangeReadOnlyMode={this.props.setCommentsReadOnlyState} @@ -315,7 +308,7 @@ export class Root extends Component { {props.isCommentsLoading && (
- +
)} @@ -333,10 +326,10 @@ const CopyrightLink = (title: string) => ( ); /** Root component connected to redux */ -export const ConnectedRoot: FunctionComponent = () => { +export function ConnectedRoot() { + const intl = useIntl(); const props = useSelector(mapStateToProps); const actions = useActions(boundActions); - const intl = useIntl(); const rootRef = useRef(null); useEffect(() => { @@ -362,4 +355,4 @@ export const ConnectedRoot: FunctionComponent = () => {

); -}; +} diff --git a/frontend/app/components/spinner/spinner.module.css b/frontend/app/components/spinner/spinner.module.css new file mode 100644 index 0000000000..f3bcdfa453 --- /dev/null +++ b/frontend/app/components/spinner/spinner.module.css @@ -0,0 +1,8 @@ +.root { + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid rgba(var(--white-color), 0.2); + border-right-color: rgb(var(--white-color)); + animation: spin 1s linear infinite; +} diff --git a/frontend/app/components/spinner/spinner.tsx b/frontend/app/components/spinner/spinner.tsx new file mode 100644 index 0000000000..7f3c6c520d --- /dev/null +++ b/frontend/app/components/spinner/spinner.tsx @@ -0,0 +1,18 @@ +import clsx from 'clsx'; +import { h } from 'preact'; +import { messages } from 'components/auth/auth.messsages'; +import { useIntl } from 'react-intl'; + +import styles from './Spinner.module.css'; + +export function Spinner() { + const intl = useIntl(); + + return ( +
+ ); +} diff --git a/frontend/app/components/user-info/__avatar/user-info__avatar.css b/frontend/app/components/user-info/__avatar/user-info__avatar.css deleted file mode 100644 index 1cb6cf290a..0000000000 --- a/frontend/app/components/user-info/__avatar/user-info__avatar.css +++ /dev/null @@ -1,6 +0,0 @@ -.user-info__avatar { - position: absolute; - top: 17px; - height: 30px; - width: 30px; -} diff --git a/frontend/app/components/user-info/__id/user-info__id.css b/frontend/app/components/user-info/__id/user-info__id.css deleted file mode 100644 index ce057b95cc..0000000000 --- a/frontend/app/components/user-info/__id/user-info__id.css +++ /dev/null @@ -1,8 +0,0 @@ -.user-info__id { - margin: 0 0 18px; - padding-left: 40px; - font-size: 14px; - font-weight: 400; - line-height: 18px; - color: var(--color13); -} diff --git a/frontend/app/components/user-info/__preloader/user-info__preloader.css b/frontend/app/components/user-info/__preloader/user-info__preloader.css deleted file mode 100644 index f1b7ccb1c8..0000000000 --- a/frontend/app/components/user-info/__preloader/user-info__preloader.css +++ /dev/null @@ -1,4 +0,0 @@ -.user-info__preloader { - margin: 0 auto 18px; - color: var(--color13); -} diff --git a/frontend/app/components/user-info/__title/user-info__title.css b/frontend/app/components/user-info/__title/user-info__title.css deleted file mode 100644 index 24fa566653..0000000000 --- a/frontend/app/components/user-info/__title/user-info__title.css +++ /dev/null @@ -1,7 +0,0 @@ -.user-info__title { - margin: 6px 0 0; - font-size: 18px; - font-weight: 700; - line-height: 20px; - padding-left: 40px; -} diff --git a/frontend/app/components/user-info/index.ts b/frontend/app/components/user-info/index.ts deleted file mode 100644 index f1751673d2..0000000000 --- a/frontend/app/components/user-info/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import './user-info.css'; - -import './__avatar/user-info__avatar.css'; -import './__id/user-info__id.css'; -import './__preloader/user-info__preloader.css'; -import './__title/user-info__title.css'; - -export { ConnectedUserInfo as UserInfo } from './user-info'; diff --git a/frontend/app/components/user-info/last-comments-list.tsx b/frontend/app/components/user-info/last-comments-list.tsx deleted file mode 100644 index 5ed743a11b..0000000000 --- a/frontend/app/components/user-info/last-comments-list.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { h, Fragment } from 'preact'; -import { useIntl } from 'react-intl'; - -import { Comment as CommentType } from 'common/types'; - -import { Comment } from 'components/comment'; -import { Preloader } from 'components/preloader'; - -type Props = { - comments: CommentType[]; - isLoading: boolean; -}; - -export function LastCommentsList({ comments, isLoading }: Props) { - const intl = useIntl(); - - if (isLoading) { - return ; - } - - return ( - <> - {comments.map((comment) => ( - - ))} - - ); -} diff --git a/frontend/app/components/user-info/user-info.css b/frontend/app/components/user-info/user-info.css deleted file mode 100644 index eabb8b0f9b..0000000000 --- a/frontend/app/components/user-info/user-info.css +++ /dev/null @@ -1,15 +0,0 @@ -.user-info { - position: absolute; - top: 0; - right: 0; - bottom: 0; - overflow: inherit; - overflow-x: hidden; - overflow-y: auto; - box-sizing: border-box; - width: 100%; - padding: 10px; - border: 1px solid var(--color41); - border-radius: 2px; - background: var(--color6); -} diff --git a/frontend/app/components/user-info/user-info.tsx b/frontend/app/components/user-info/user-info.tsx deleted file mode 100644 index 7dd3f62bfc..0000000000 --- a/frontend/app/components/user-info/user-info.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { h, JSX, Component, FunctionComponent } from 'preact'; -import b from 'bem-react-helper'; -import { useSelector } from 'react-redux'; -import { useIntl, defineMessages, FormattedMessage, IntlShape } from 'react-intl'; - -import { StoreState } from 'store'; -import { Comment } from 'common/types'; -import { fetchInfo } from 'store/user-info/actions'; -import { parseQuery } from 'utils/parseQuery'; -import { bindActions } from 'utils/actionBinder'; -import { postMessageToParent } from 'utils/postMessage'; -import { useActions } from 'hooks/useAction'; -import { Avatar } from 'components/avatar'; - -import { LastCommentsList } from './last-comments-list'; - -const boundActions = bindActions({ fetchInfo }); - -const messages = defineMessages({ - unexpectedError: { - id: 'user-info.unexpected-error', - defaultMessage: 'Something went wrong', - }, -}); - -const user = parseQuery(); - -type Props = { - comments: Comment[] | null; -} & typeof boundActions & { intl: IntlShape }; - -interface State { - isLoading: boolean; - error: string | null; -} - -class UserInfo extends Component { - state = { isLoading: true, error: null }; - - componentWillMount(): void { - if (!this.props.comments && this.state.isLoading) { - this.props - .fetchInfo(user.id) - .then(() => { - this.setState({ isLoading: false }); - }) - .catch(() => { - this.setState({ isLoading: false, error: this.props.intl.formatMessage(messages.unexpectedError) }); - }); - } - - document.addEventListener('keydown', UserInfo.onKeyDown); - } - - componentWillUnmount(): void { - document.removeEventListener('keydown', UserInfo.onKeyDown); - } - - render(): JSX.Element | null { - const { comments = [] } = this.props; - const { isLoading } = this.state; - - // TODO: handle - if (!user) { - return null; - } - - return ( -
- -

- -

-

{user.id}

- {!!comments && } -
- ); - } - - /** - * Global on `keydown` handler which is set on component mount. - * Listens for user's `esc` key press - */ - static onKeyDown(e: KeyboardEvent): void { - // ESCAPE key pressed - if (e.keyCode === 27) { - postMessageToParent({ profile: null }); - } - } -} - -const commentsSelector = (state: StoreState) => state.userComments[user.id]; - -export const ConnectedUserInfo: FunctionComponent = () => { - const comments = useSelector(commentsSelector); - const actions = useActions(boundActions); - const intl = useIntl(); - - return ; -}; diff --git a/frontend/app/components/verified.tsx b/frontend/app/components/verified.tsx new file mode 100644 index 0000000000..7c9d265ad2 --- /dev/null +++ b/frontend/app/components/verified.tsx @@ -0,0 +1,17 @@ +export function Verified() { + return ( + + + + + ); +} diff --git a/frontend/app/custom-properties.css b/frontend/app/custom-properties.css deleted file mode 100644 index 3e98fa40db..0000000000 --- a/frontend/app/custom-properties.css +++ /dev/null @@ -1,80 +0,0 @@ -:root { - --color0: #0f172a; - --color8: #262626; - --color22: #2d2d2c; - --color24: #313133; - --color7: #333; - --color23: #393734; - --color18: #383838; - --color19: #404040; - --color36: #575757; - --color34: #555; - --color37: #586069; - --color14: #6a6a6a; - --color10: #777; - --color13: #888; - --color11: #969696; - --color32: #a6a6a6; - --color1: #aaa; - --color35: #d1d5db; - --color20: #ddd; - --color46: rgba(27, 31, 35, 0.15); - --color45: rgba(0, 0, 0, 0.1); - --color41: #efefef; - --color5: #eee; - --color44: rgba(255, 255, 255, 0.3); - --color6: #fff; - --color3: #e2efef; - --color4: #edf6f7; - --color16: #e2e8f0; - --color31: #cbd5e1; - --color21: #f1f5f9; - --color29: #0e7e9d; - --color9: #0aa; - --color15: #099; - --color33: #06c5c5; - --color40: #9cdddb; - --color43: #b7dddd; - --color42: #c6efef; - --color48: rgba(37, 156, 154, 0.6); - --color47: rgba(37, 156, 154, 0.4); - --color12: #259e06; - --color28: #672323; - --color25: #9a0000; - --color30: #cc0606; - --color38: #ef0000; - --color27: #f98989; - --color26: #ffd7d7; - - /* code-highlight */ - --chroma-bg: rgba(0, 0, 0, 0.05); - --chroma-base: #586e75; - --chroma-c: #7d7d7d; - --chroma-01: #589000; - --chroma-02: #cb4b16; - --chroma-03: #268bd2; - --chroma-04: #2aa198; - --chroma-05: #859900; - --chroma-06: #d33682; - --chroma-07: #00aee2; - - /* Named variables */ - --primary-color: 0, 170, 170; - --primary-brighter-color: 0, 153, 153; - --secondary-text-color: 100, 116, 139; - --black-color: 0, 0, 0; - --white-color: 255, 255, 255; - --error-color: #b91c1c; - --error-background: #ff466f2b; - --line-color: var(--color16); - --line-brighter-color: var(--color31); -} - -:root .dark { - --line-color: var(--color36); - --primary-color: 0, 153, 153; - --primary-brighter-color: 0, 170, 170; - --secondary-text-color: 209, 213, 219; - --error-color: #ffa0a0; - --line-brighter-color: var(--color11); -} diff --git a/frontend/app/embed.ts b/frontend/app/embed.ts index 1ea47532b8..16ce813ed4 100644 --- a/frontend/app/embed.ts +++ b/frontend/app/embed.ts @@ -1,6 +1,8 @@ -import type { UserInfo, Theme } from 'common/types'; -import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants.config'; +import { NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants.config'; import { parseMessage, postMessageToIframe } from 'utils/postMessage'; +import { createIframe } from 'utils/create-iframe'; +import type { Theme } from 'common/types'; +import { closeProfile, openProfile } from 'profile'; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); @@ -8,49 +10,6 @@ if (document.readyState === 'loading') { init(); } -function removeDomNode(node: HTMLElement | null) { - if (node && node.parentNode) { - node.parentNode.removeChild(node); - } -} - -function createFrame({ - host, - query, - height, - margin = '-6px', - __colors__ = {}, -}: { - host: string; - query: string; - height?: string; - margin?: string; - __colors__?: Record; -}) { - const iframe = document.createElement('iframe'); - - iframe.src = `${host}/web/iframe.html?${query}`; - iframe.name = JSON.stringify({ __colors__ }); - iframe.setAttribute('width', '100%'); - iframe.setAttribute('frameborder', '0'); - iframe.setAttribute('allowtransparency', 'true'); - iframe.setAttribute('scrolling', 'no'); - iframe.setAttribute('tabindex', '0'); - iframe.setAttribute('title', 'Comments | Remark42'); - iframe.setAttribute('horizontalscrolling', 'no'); - iframe.setAttribute('verticalscrolling', 'no'); - iframe.setAttribute( - 'style', - `width: 1px !important; min-width: 100% !important; border: none !important; overflow: hidden !important; margin: ${margin};` - ); - - if (height) { - iframe.setAttribute('height', height); - } - - return iframe; -} - function init() { window.REMARK42 = window.REMARK42 || {}; window.REMARK42.createInstance = createInstance; @@ -73,32 +32,20 @@ function createInstance(config: typeof window.remark_config) { throw new Error('Remark42: Site ID is undefined.'); } - let initDataAnimationTimeout: number | null = null; let titleObserver: MutationObserver | null = null; config.url = (config.url || `${window.location.origin}${window.location.pathname}`).split('#')[0]; - const query = Object.keys(config) - .filter((key) => key !== '__colors__') - .map( - (key) => - `${encodeURIComponent(key)}=${encodeURIComponent( - config[key as keyof Omit] as string | number | boolean - )}` - ) - .join('&'); - - const iframe = - (root.firstElementChild as HTMLIFrameElement) || - createFrame({ host: BASE_URL, query, __colors__: config.__colors__ }); + const iframe = (root.firstElementChild as HTMLIFrameElement) || createIframe(config); root.appendChild(iframe); - window.addEventListener('message', receiveMessages); - window.addEventListener('hashchange', postHashToIframe); + window.addEventListener('message', handleReceiveMessage); + window.addEventListener('hashchange', handleHashChange); document.addEventListener('click', postClickOutsideToIframe); const titleElement = document.querySelector('title'); + if (titleElement) { titleObserver = new MutationObserver((mutations) => postTitleToIframe(mutations[0].target.textContent!)); titleObserver.observe(titleElement, { @@ -108,198 +55,49 @@ function createInstance(config: typeof window.remark_config) { }); } - const remarkRootId = 'remark-km423lmfdslkm34'; - const userInfo: { - node: HTMLElement | null; - back: HTMLElement | null; - closeEl: HTMLElement | null; - iframe: HTMLIFrameElement | null; - style: HTMLStyleElement | null; - init: (user: UserInfo) => void; - close: () => void; - delay: number | null; - events: string[]; - onAnimationClose: () => void; - onKeyDown: (e: KeyboardEvent) => void; - animationStop: () => void; - remove: () => void; - } = { - node: null, - back: null, - closeEl: null, - iframe: null, - style: null, - init(user) { - this.animationStop(); - if (!this.style) { - this.style = document.createElement('style'); - this.style.setAttribute('rel', 'stylesheet'); - this.style.setAttribute('type', 'text/css'); - this.style.innerHTML = ` - #${remarkRootId}-node { - position: fixed; - top: 0; - right: 0; - bottom: 0; - width: 400px; - transition: transform 0.4s ease-out; - max-width: 100%; - transform: translate(400px, 0); - } - #${remarkRootId}-node[data-animation] { - transform: translate(0, 0); - } - #${remarkRootId}-back { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.7); - opacity: 0; - transition: opacity 0.4s ease-out; - } - #${remarkRootId}-back[data-animation] { - opacity: 1; - } - #${remarkRootId}-close { - top: 0px; - right: 400px; - position: absolute; - text-align: center; - font-size: 25px; - cursor: pointer; - color: white; - border-color: transparent; - border-width: 0; - padding: 0; - margin-right: 4px; - background-color: transparent; - } - @media all and (max-width: 430px) { - #${remarkRootId}-close { - right: 0px; - font-size: 20px; - color: black; - } - } - `; - } - if (!this.node) { - this.node = document.createElement('div'); - this.node.id = `${remarkRootId}-node`; - } - if (!this.back) { - this.back = document.createElement('div'); - this.back.id = `${remarkRootId}-back`; - this.back.onclick = () => this.close(); - } - if (!this.closeEl) { - this.closeEl = document.createElement('button'); - this.closeEl.id = `${remarkRootId}-close`; - this.closeEl.innerHTML = '✖'; - this.closeEl.onclick = () => this.close(); - } - const queryUserInfo = `${query}&page=user-info&&id=${user.id}&name=${user.name}&picture=${user.picture || ''}`; - const iframe = createFrame({ host: BASE_URL, query: queryUserInfo, height: '100%', margin: '0' }); - this.node.appendChild(iframe); - this.iframe = iframe; - this.node.appendChild(this.closeEl); - document.body.appendChild(this.style); - document.body.appendChild(this.back); - document.body.appendChild(this.node); - document.addEventListener('keydown', this.onKeyDown); - initDataAnimationTimeout = window.setTimeout(() => { - this.back!.setAttribute('data-animation', ''); - this.node!.setAttribute('data-animation', ''); - iframe.focus(); - }, 400); - }, - close() { - if (this.node) { - if (this.iframe) { - this.node.removeChild(this.iframe); - } - this.onAnimationClose(); - this.node.removeAttribute('data-animation'); - } - if (this.back) { - this.back.removeAttribute('data-animation'); - } - document.removeEventListener('keydown', this.onKeyDown); - }, - delay: null, - events: ['', 'webkit', 'moz', 'MS', 'o'].map((prefix) => (prefix ? `${prefix}TransitionEnd` : 'transitionend')), - onAnimationClose() { - const el = this.node!; - if (!this.node) { - return; - } - this.delay = window.setTimeout(this.animationStop, 1000); - this.events.forEach((event) => el.addEventListener(event, this.animationStop, false)); - }, - onKeyDown(e) { - // ESCAPE key pressed - if (e.keyCode === 27) { - userInfo.close(); - } - }, - animationStop() { - const t = userInfo; - if (!t.node) { - return; - } - if (t.delay) { - clearTimeout(t.delay); - t.delay = null; - } - t.events.forEach((event) => t.node!.removeEventListener(event, t.animationStop, false)); - return t.remove(); - }, - remove() { - const t = userInfo; - removeDomNode(t.node); - removeDomNode(t.back); - removeDomNode(t.style); - }, - }; - - function receiveMessages(event: MessageEvent): void { + function handleReceiveMessage(event: MessageEvent): void { const data = parseMessage(event); - if (data.height) { + if (typeof data.height === 'number') { iframe.style.height = `${data.height}px`; } - if (data.scrollTo) { + if (typeof data.scrollTo === 'number') { window.scrollTo(window.pageXOffset, data.scrollTo + iframe.getBoundingClientRect().top + window.pageYOffset); } if (typeof data.profile === 'object') { if (data.profile === null) { - userInfo.close(); + closeProfile(); } else { - userInfo.init(data.profile); + openProfile({ ...config, ...data.profile }); } } - if (data.inited) { + if (data.signout === true) { + postMessageToIframe(iframe, { signout: true }); + } + + if (data.inited === true) { postHashToIframe(); postTitleToIframe(document.title); } } - function postHashToIframe(evt?: Event & { newURL: string }) { - const hash = evt ? `#${evt.newURL.split('#')[1]}` : window.location.hash; - + function postHashToIframe(hash = window.location.hash) { if (!hash.startsWith(`#${COMMENT_NODE_CLASSNAME_PREFIX}`)) { return; } - evt?.preventDefault(); postMessageToIframe(iframe, { hash }); } + function handleHashChange(evt: HashChangeEvent) { + const url = new URL(evt.newURL); + + postHashToIframe(url.hash); + } + function postTitleToIframe(title: string) { postMessageToIframe(iframe, { title }); } @@ -312,16 +110,13 @@ function createInstance(config: typeof window.remark_config) { } function changeTheme(theme: Theme) { + window.remark_config.theme = theme; postMessageToIframe(iframe, { theme }); } function destroy() { - if (initDataAnimationTimeout) { - clearTimeout(initDataAnimationTimeout); - } - - window.removeEventListener('message', receiveMessages); - window.removeEventListener('hashchange', postHashToIframe); + window.removeEventListener('message', handleReceiveMessage); + window.removeEventListener('hashchange', handleHashChange); document.removeEventListener('click', postClickOutsideToIframe); if (titleObserver) { diff --git a/frontend/app/locales/be.json b/frontend/app/locales/be.json index f643f31c9e..5e6f47d269 100644 --- a/frontend/app/locales/be.json +++ b/frontend/app/locales/be.json @@ -4,8 +4,10 @@ "auth.loading": "Загрузка...", "auth.oauth-button": "Увайсці праз {provider}", "auth.oauth-source": "Скарыстацца сацыяльнай сеткай", + "auth.open-profile": "Open My Profile", "auth.or": "або", "auth.signin": "Увайсці", + "auth.signout": "Выйсці?", "auth.submit": "Адправіць", "auth.symbols-restriction": "імя карыстальніка мусіць пачынацца з літары і ўтрымоўваць толькі лацінскія літары, лічбы, знакі падкрэслівання і прабелы(?)", "auth.user-not-found": "Карыстальнік не знойдзены", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Уключыць каментары", "authPanel.enable-cookies": "Дазвольце кукі, каб увайсці і каментаваць", "authPanel.hide-settings": "Схаваць налады", - "authPanel.logged-as": "Вы ўвайшлі як", - "authPanel.logout": "Выйсці?", "authPanel.new-page": "новая старонка", "authPanel.read-only": "Толькі для чытання", - "authPanel.request-to-delete-data": "Запытаць выдаленне маіх даных", "authPanel.show-settings": "Паказаць налады", "blockingDuration.day": "На дзень", "blockingDuration.month": "На месяц", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Старыя", "commentsSort.recently-updated": "Нядаўна абноўленыя", "commentsSort.worst": "Горшыя", + "empty-state": "Don't have comments yet", "errors.0": "Нешта пайшло не так. Калі ласка, паспрабуйце яшчэ раз крыху пазней.", "errors.1": "Каментар не знойдзены. Калі ласка, абнавіце старонку і паспрабуйце зноў.", "errors.10": "Час рэдагавання каментара сышоў.", @@ -94,6 +94,7 @@ "errors.18": "Запытаны файл не знойдзены.", "errors.19": "Comment contains restricted words.", "errors.2": "Не атрымалася апрацаваць адказ сервера.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Вы не маеце дазволу для гэтага дзеяння.", "errors.4": "Няправільна адфарматаваны каментар.", "errors.5": "Каментар не знойдзены. Калі ласка, абнавіце старонку і паспрабуйце яшчэ раз.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Нумараваны спіс", "toolbar.quote": "Цытата", "toolbar.unordered-list": "Ненумараваны спіс", - "user-info.last-comments": "Апошнія каментары {userName}", - "user-info.unexpected-error": "Нешта пайшло не так", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Ананімныя карыстальнікі не могуць галасаваць", "vote.deleted": "Нельга галасаваць за выдалены каментар", "vote.guest": "Увайдзіце ў сістэму, каб галасаваць", diff --git a/frontend/app/locales/bg.json b/frontend/app/locales/bg.json index 781d5333ca..b566c557fa 100644 --- a/frontend/app/locales/bg.json +++ b/frontend/app/locales/bg.json @@ -4,8 +4,10 @@ "auth.loading": "Зареждане...", "auth.oauth-button": "Sign In with {provider}", "auth.oauth-source": "Use Social Network", + "auth.open-profile": "Open My Profile", "auth.or": "или", "auth.signin": "Вход", + "auth.signout": "Изход", "auth.submit": "Изпрати", "auth.symbols-restriction": "Потребителското име трябва да започва с буква и да бъде само от латински букви, цифри, подчертавки или разтояния", "auth.user-not-found": "Не бе намерен потребител", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Разреши коментарите", "authPanel.enable-cookies": "Разреши бисквитките за вход и коментар", "authPanel.hide-settings": "Скрий настройките", - "authPanel.logged-as": "Вие влязохте като", - "authPanel.logout": "Изход?", "authPanel.new-page": "нова страница", "authPanel.read-only": "Само за четене", - "authPanel.request-to-delete-data": "Заявка за премахване на моите данни", "authPanel.show-settings": "Покажи настройките", "blockingDuration.day": "За един ден", "blockingDuration.month": "За един месец", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Най-стар", "commentsSort.recently-updated": "Най-скоро променен", "commentsSort.worst": "Най-лошия", + "empty-state": "Don't have comments yet", "errors.0": "Има някакъв проблем. Моля, опитайте отново по-късно.", "errors.1": "Коментара не бе намерен. Моля презаредете страницата и опитайте пак.", "errors.10": "Твърде късно е за редактиране на коментара.", @@ -94,6 +94,7 @@ "errors.18": "Файла не бе намерен.", "errors.19": "Comment contains restricted words.", "errors.2": "Неуспешно премахване на входящата заявка.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Нямате привилегия за тази операция.", "errors.4": "Невалидни данни на коментара.", "errors.5": "Коментара не бе намерен. Моля презаредете странцата и опитайте пак.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Добави номериран списък", "toolbar.quote": "Добави цитат", "toolbar.unordered-list": "Добави обозначен списък", - "user-info.last-comments": "Последни коментари от {userName}", - "user-info.unexpected-error": "Има някакъв проблем", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Анонимни потребители не могат да гласуват", "vote.deleted": "Не може да гласъвате за изтрит коментарт", "vote.guest": "Влезте за да гласувате", diff --git a/frontend/app/locales/bp.json b/frontend/app/locales/bp.json index fbcbdedbd5..b6fcc8b856 100644 --- a/frontend/app/locales/bp.json +++ b/frontend/app/locales/bp.json @@ -4,8 +4,10 @@ "auth.loading": "Carregando...", "auth.oauth-button": "Entrar com {provider}", "auth.oauth-source": "Use redes sociais", + "auth.open-profile": "Open My Profile", "auth.or": "ou", "auth.signin": "Entrar", + "auth.signout": "Fazer logout", "auth.submit": "Enviar", "auth.symbols-restriction": "O nome de usuário deve conter apenas letras, números, sublinhados ou espaços", "auth.user-not-found": "Nenhum usuário encontrado", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Habilitar comentários", "authPanel.enable-cookies": "Permita cookies para fazer login e comentar", "authPanel.hide-settings": "Ocultar configurações", - "authPanel.logged-as": "Você fez login como", - "authPanel.logout": "Fazer logout?", "authPanel.new-page": "nova página", "authPanel.read-only": "Somente leitura", - "authPanel.request-to-delete-data": "Solicitar remoção dos meus dados", "authPanel.show-settings": "Mostrar configurações", "blockingDuration.day": "Por um dia", "blockingDuration.month": "Por um mês", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Mais antigo", "commentsSort.recently-updated": "Atualizado recentemente", "commentsSort.worst": "Pior", + "empty-state": "Don't have comments yet", "errors.0": "Ocorreu um erro. Tente novamente mais tarde.", "errors.1": "O comentário não pode ser encontrado. Atualize a página e tente novamente.", "errors.10": "É muito tarde para editar o comentário.", @@ -94,6 +94,7 @@ "errors.18": "O arquivo solicitado não foi encontrado.", "errors.19": "Comment contains restricted words.", "errors.2": "Falha ao fazer unmarshalling da solicitação de entrada.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Você não tem permissão para esta operação.", "errors.4": "Dados de comentário inválidos.", "errors.5": "O comentário não pode ser encontrado. Atualize a página e tente novamente.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Adicionar uma lista numerada", "toolbar.quote": "Inserir citação", "toolbar.unordered-list": "Adicionar lista com marcadores", - "user-info.last-comments": "Últimos comentários de {userName}", - "user-info.unexpected-error": "Algo deu errado", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Usuários anônimos não podem votar", "vote.deleted": "Não posso votar em um comentário excluído", "vote.guest": "Faça login para votar", diff --git a/frontend/app/locales/de.json b/frontend/app/locales/de.json index 6a084a68df..9522e88d6a 100644 --- a/frontend/app/locales/de.json +++ b/frontend/app/locales/de.json @@ -4,8 +4,10 @@ "auth.loading": "Wird geladen ...", "auth.oauth-button": "Mit {provider} anmelden", "auth.oauth-source": "Soziales Netzwerk verwenden", + "auth.open-profile": "Open My Profile", "auth.or": "oder", "auth.signin": "Einloggen", + "auth.signout": "Abmelden", "auth.submit": "Absenden", "auth.symbols-restriction": "Der Benutzername darf nur Buchstaben, Zahlen, Unterstriche oder Leerzeichen enthalten", "auth.user-not-found": "Benutzer konnte nicht gefunden werden", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Kommentarfunktion aktivieren", "authPanel.enable-cookies": "Cookies für die Anmeldung und das Kommentieren zulassen", "authPanel.hide-settings": "Einstellungen ausblenden", - "authPanel.logged-as": "Sie haben sich angemeldet als", - "authPanel.logout": "Abmelden?", "authPanel.new-page": "neue Seite", "authPanel.read-only": "Schreibgeschützt", - "authPanel.request-to-delete-data": "Löschung meiner Daten anfordern", "authPanel.show-settings": "Einstellungen anzeigen", "blockingDuration.day": "Für einen Tag", "blockingDuration.month": "Für einen Monat", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Älteste", "commentsSort.recently-updated": "Kürzlich aktualisierte", "commentsSort.worst": "Schlechteste", + "empty-state": "Don't have comments yet", "errors.0": "Leider ist etwas schiefgegangen. Bitte versuchen Sie es später erneut.", "errors.1": "Kommentar nicht gefunden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.", "errors.10": "Das Zeitfenster für das Bearbeiten des Kommentars ist verstrichen.", @@ -94,6 +94,7 @@ "errors.18": "Die angeforderte Datei wurde nicht gefunden.", "errors.19": "Comment contains restricted words.", "errors.2": "Die eingehende Anfrage konnte nicht verarbeitet werden.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Für diesen Vorgang haben Sie keine ausreichende Berechtigung.", "errors.4": "Ungültige Kommentardaten.", "errors.5": "Kommentar nicht gefunden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Nummerierte Liste einfügen", "toolbar.quote": "Zitat einfügen", "toolbar.unordered-list": "Aufzählungsliste einfügen", - "user-info.last-comments": "Kommentare von Benutzer {userName}", - "user-info.unexpected-error": "Leider ist etwas schiefgegangen.", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Anonyme Benutzer können leider nicht abstimmen", "vote.deleted": "Sie können nicht für einen gelöschten Kommentar abstimmen", "vote.guest": "Melden Sie sich an, um abzustimmen", diff --git a/frontend/app/locales/en.json b/frontend/app/locales/en.json index 232ee3f7a9..a24ecb5690 100644 --- a/frontend/app/locales/en.json +++ b/frontend/app/locales/en.json @@ -4,8 +4,10 @@ "auth.loading": "Loading...", "auth.oauth-button": "Sign In with {provider}", "auth.oauth-source": "Use Social Network", + "auth.open-profile": "Open My Profile", "auth.or": "or", "auth.signin": "Sign In", + "auth.signout": "Sign Out", "auth.submit": "Submit", "auth.symbols-restriction": "Username must contain only letters, numbers, underscores or spaces", "auth.user-not-found": "No user was found", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Enable comments", "authPanel.enable-cookies": "Allow cookies to login and comment", "authPanel.hide-settings": "Hide settings", - "authPanel.logged-as": "You logged in as", - "authPanel.logout": "Logout?", "authPanel.new-page": "new page", "authPanel.read-only": "Read-only", - "authPanel.request-to-delete-data": "Request my data removal", "authPanel.show-settings": "Show settings", "blockingDuration.day": "For a day", "blockingDuration.month": "For a month", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Oldest", "commentsSort.recently-updated": "Recently updated", "commentsSort.worst": "Worst", + "empty-state": "Don't have comments yet", "errors.0": "Something went wrong. Please try again a bit later.", "errors.1": "Comment cannot be found. Please refresh the page and try again.", "errors.10": "It is too late to edit the comment.", @@ -94,12 +94,13 @@ "errors.18": "Requested file cannot be found.", "errors.19": "Comment contains restricted words.", "errors.2": "Failed to unmarshal incoming request.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "You don't have permission for this operation.", "errors.4": "Invalid comment data.", "errors.5": "Comment cannot be found. Please refresh the page and try again.", "errors.6": "Site cannot be found. Please refresh the page and try again.", "errors.7": "User has been blocked.", - "errors.8": "User has been blocked.", + "errors.8": "Can't post comments on this page. Comments are read only.", "errors.9": "Comment changing failed. Please try again a bit later.", "errors.failed-fetch": "Failed to fetch. Please check your internet connection or try again a bit later", "errors.forbidden": "Forbidden.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Add a numbered list", "toolbar.quote": "Insert a quote", "toolbar.unordered-list": "Add a bulleted list", - "user-info.last-comments": "Last comments by {userName}", - "user-info.unexpected-error": "Something went wrong", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Anonymous users can't vote", "vote.deleted": "Can't vote for deleted comment", "vote.guest": "Sign in to vote", diff --git a/frontend/app/locales/es.json b/frontend/app/locales/es.json index af44002fba..428f65cbd2 100644 --- a/frontend/app/locales/es.json +++ b/frontend/app/locales/es.json @@ -4,8 +4,10 @@ "auth.loading": "Cargando...", "auth.oauth-button": "Inicie sesión con {provider}", "auth.oauth-source": "Acceda a través de sus redes sociales", + "auth.open-profile": "Open My Profile", "auth.or": "o", "auth.signin": "Acceder", + "auth.signout": "¿Salir?", "auth.submit": "Enviar", "auth.symbols-restriction": "El nombre de usuario debe comenzar con una letra y contener solamente letras latinas, números, guión bajo o espacio", "auth.user-not-found": "No se encontró el usuario", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Habilitar comentarios", "authPanel.enable-cookies": "Habilitar cookies para acceder y comentar", "authPanel.hide-settings": "Ocultar opciones", - "authPanel.logged-as": "Accediste como", - "authPanel.logout": "¿Salir?", "authPanel.new-page": "nueva página", "authPanel.read-only": "Solo lectura", - "authPanel.request-to-delete-data": "Solicitar la eliminación de mis datos", "authPanel.show-settings": "Mostrar opciones", "blockingDuration.day": "Por un día", "blockingDuration.month": "Por un mes", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Más antiguo", "commentsSort.recently-updated": "Actualizado más recientemente", "commentsSort.worst": "Peor", + "empty-state": "Don't have comments yet", "errors.0": "Algo salió mal. Por favor vuelve a intentar más tarde.", "errors.1": "No se ha encontrado el comentario. Por favor refresca la página y vuelve a intentar.", "errors.10": "Es muy tarde para editar el comentario.", @@ -94,6 +94,7 @@ "errors.18": "No se ha encontrado el archivo solicitado.", "errors.19": "Comment contains restricted words.", "errors.2": "No se ha podido deserializar la petición entrante.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "No tienes permisos para esta operación.", "errors.4": "Datos de comentario inválidos.", "errors.5": "El comentario no se ha encontrado. Por favor refresca la página y vuelve a intentar.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Agrega una lista numerada", "toolbar.quote": "Inserta una cita", "toolbar.unordered-list": "Agrega una lista sin numerar", - "user-info.last-comments": "Últimos comentarios de {userName}", - "user-info.unexpected-error": "Algo salió mal", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Los usuarios anónimos no puede votar", "vote.deleted": "No se puede votar un comentario eliminado", "vote.guest": "Accede para votar", diff --git a/frontend/app/locales/fi.json b/frontend/app/locales/fi.json index 63d0950ed4..1c8fb789ee 100644 --- a/frontend/app/locales/fi.json +++ b/frontend/app/locales/fi.json @@ -4,8 +4,10 @@ "auth.loading": "Ladataan...", "auth.oauth-button": "Sign In with {provider}", "auth.oauth-source": "Use Social Network", + "auth.open-profile": "Open My Profile", "auth.or": "tai", "auth.signin": "Kirjaudu sisään", + "auth.signout": "Kirjaudu ulos", "auth.submit": "Lähetä", "auth.symbols-restriction": "Käyttäjätunnuksen tulee alkaa kirjaimella ja sisältää vain latinalaisia kirjaimia, numeroita, alaviivoja ja välilyöntejä", "auth.user-not-found": "Käyttäjää ei löytynyt", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Ota kommentit käyttöön", "authPanel.enable-cookies": "Salli evästeet kirjautuaksesi sisään ja kommentoidaksesi", "authPanel.hide-settings": "Piilota asetukset", - "authPanel.logged-as": "Olet kirjautunut sisään nimellä", - "authPanel.logout": "Kirjaudu ulos?", "authPanel.new-page": "uusi sivu", "authPanel.read-only": "Vain luku", - "authPanel.request-to-delete-data": "Pyydä tietojen poistamista", "authPanel.show-settings": "Näytä asetukset", "blockingDuration.day": "Päiväksi", "blockingDuration.month": "Kuukaudeksi", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Vanhin", "commentsSort.recently-updated": "Äskettäin päivitetty", "commentsSort.worst": "Huonoin", + "empty-state": "Don't have comments yet", "errors.0": "Jotain meni pieleen. Yritä uudelleen myöhemmin.", "errors.1": "Kommenttia ei löydy. Päivitä sivu ja yritä uudelleen.", "errors.10": "Aikaikkuna kommentin muokkaamiseen on ohitettu.", @@ -94,6 +94,7 @@ "errors.18": "Pyydettyä tiedostoa ei löydy.", "errors.19": "Comment contains restricted words.", "errors.2": "Failed to unmarshal incoming request.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Sinulla ei ole lupaa tähän operaatioon.", "errors.4": "Virheellinen kommentti.", "errors.5": "Kommenttia ei löydy. Päivitä sivu ja yritä uudelleen.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Järjestetty luettelo", "toolbar.quote": "Lainaus", "toolbar.unordered-list": "Järjestämätön luettelo", - "user-info.last-comments": "Uusimmat kommentit käyttäjältä {userName}", - "user-info.unexpected-error": "Jotain meni pieleen", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Anonyymit käyttäjät eivät voi äänestää", "vote.deleted": "Poistettua kommenttia ei voi äänestää", "vote.guest": "Kirjaudu sisään äänestääksesi", diff --git a/frontend/app/locales/fr.json b/frontend/app/locales/fr.json index 0b4d95a967..288466c00a 100644 --- a/frontend/app/locales/fr.json +++ b/frontend/app/locales/fr.json @@ -4,8 +4,10 @@ "auth.loading": "Chargement...", "auth.oauth-button": "Se connecter avec {provider}", "auth.oauth-source": "Utiliser les réseaux sociaux", + "auth.open-profile": "Open My Profile", "auth.or": "ou", "auth.signin": "Se connecter", + "auth.signout": "Se déconnecter", "auth.submit": "Valider", "auth.symbols-restriction": "Le nom d'utilisateur ne doit contenir que des lettres, nombres, tirets bas et espaces.", "auth.user-not-found": "Aucun utilisateur n'a été trouvé", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Permettre les commentaires", "authPanel.enable-cookies": "Activez les cookies pour vous connecter et commenter", "authPanel.hide-settings": "Cacher les paramètres", - "authPanel.logged-as": "Vous êtes connecté en tant que", - "authPanel.logout": "Se déconnecter ?", "authPanel.new-page": "nouvelle page", "authPanel.read-only": "Lecture seule", - "authPanel.request-to-delete-data": "Demander la suppression de mes données", "authPanel.show-settings": "Afficher les paramètres", "blockingDuration.day": "Pour un jour", "blockingDuration.month": "Pour un mois", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Le plus ancien", "commentsSort.recently-updated": "Mis à jour récemment", "commentsSort.worst": "Le pire", + "empty-state": "Don't have comments yet", "errors.0": "Une erreur s'est produite. Veuillez réessayer un peu plus tard.", "errors.1": "Commentaire introuvable. Rafraichissez la page et réessayez.", "errors.10": "Il n'est plus possible de modifier ce commentaire.", @@ -94,6 +94,7 @@ "errors.18": "Le fichier demandé est introuvable.", "errors.19": "Comment contains restricted words.", "errors.2": "Échec du traitement de la requête entrante.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Vous n'avez pas l'autorisation d'effectuer cette opération.", "errors.4": "Données de commentaire non valides.", "errors.5": "Commentaire introuvable. Rafraichissez la page et réessayez.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Ajouter une liste numérotée", "toolbar.quote": "Insérer une citation", "toolbar.unordered-list": "Ajouter une liste à puces", - "user-info.last-comments": "Derniers commentaires par {userName}", - "user-info.unexpected-error": "Un problème est survenu", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Les utilisateurs anonymes ne peuvent pas voter", "vote.deleted": "Vous ne pouvez pas voter pour un commentaire supprimé", "vote.guest": "Connectez-vous pour voter", diff --git a/frontend/app/locales/ja.json b/frontend/app/locales/ja.json index e6e643e0bd..332ba4aadb 100644 --- a/frontend/app/locales/ja.json +++ b/frontend/app/locales/ja.json @@ -4,8 +4,10 @@ "auth.loading": "読み込んでいます…", "auth.oauth-button": "{provider} でサインイン", "auth.oauth-source": "ソーシャルネットワークを使用", + "auth.open-profile": "Open My Profile", "auth.or": "または", "auth.signin": "サインイン", + "auth.signout": "ログアウトしますか", "auth.submit": "送信", "auth.symbols-restriction": "ユーザー名には英数字、アンダースコア、またはスペースのみを指定してください", "auth.user-not-found": "ユーザーが見つかりませんでした", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "コメントの有効化", "authPanel.enable-cookies": "Cookie を許可してログインとコメントを有効にしてください", "authPanel.hide-settings": "設定を非表示", - "authPanel.logged-as": "次でログイン中:", - "authPanel.logout": "ログアウトしますか?", "authPanel.new-page": "新規ページ", "authPanel.read-only": "読み取り専用", - "authPanel.request-to-delete-data": "データ除去の依頼", "authPanel.show-settings": "設定を表示", "blockingDuration.day": "1 日", "blockingDuration.month": "1 カ月", @@ -81,6 +80,7 @@ "commentsSort.oldest": "古い順", "commentsSort.recently-updated": "更新が最も新しい順", "commentsSort.worst": "最悪", + "empty-state": "Don't have comments yet", "errors.0": "不明なエラーが発生しました。しばらくしてからもう一度お試しください。", "errors.1": "コメントが見つかりません。ページを再読み込みしてからもう一度お試しください。", "errors.10": "コメントを編集するには遅すぎます。", @@ -94,6 +94,7 @@ "errors.18": "リクエストされたファイルが見つかりません。", "errors.19": "Comment contains restricted words.", "errors.2": "受信したリクエストを処理できません", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "この操作を実行する権限がありません。", "errors.4": "コメントデータが無効です。", "errors.5": "コメントが見つかりません。ページを再読み込みしてからもう一度お試しください。", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "番号付きリストの追加", "toolbar.quote": "引用符の挿入", "toolbar.unordered-list": "記号付きリストの追加", - "user-info.last-comments": "{userName}の最新コメント", - "user-info.unexpected-error": "不明なエラーが発生しました", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "匿名ユーザーは投稿できません", "vote.deleted": "削除済みコメントには投票できません", "vote.guest": "投票するにはサインインしてください", diff --git a/frontend/app/locales/ko.json b/frontend/app/locales/ko.json index c898059e78..612aeef270 100644 --- a/frontend/app/locales/ko.json +++ b/frontend/app/locales/ko.json @@ -4,8 +4,10 @@ "auth.loading": "로드 중…", "auth.oauth-button": "{provider}(으)로 로그인", "auth.oauth-source": "소셜 네트워크 사용", + "auth.open-profile": "Open My Profile", "auth.or": "또는", "auth.signin": "로그인", + "auth.signout": "로그아웃하시겠어요", "auth.submit": "확인", "auth.symbols-restriction": "사용자 이름에는 문자, 숫자, 밑줄 또는 공백만 포함되어야 합니다", "auth.user-not-found": "사용자를 찾을 수 없습니다", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "댓글 활성화", "authPanel.enable-cookies": "로그인 및 댓글 작업을 할 수 있도록 쿠키 허용", "authPanel.hide-settings": "설정 숨기기", - "authPanel.logged-as": "다음으로 로그인되어 있습니다", - "authPanel.logout": "로그아웃하시겠어요?", "authPanel.new-page": "새 페이지", "authPanel.read-only": "읽기 전용", - "authPanel.request-to-delete-data": "내 데이터 제거 요청", "authPanel.show-settings": "설정 표시", "blockingDuration.day": "1일", "blockingDuration.month": "1개월", @@ -81,6 +80,7 @@ "commentsSort.oldest": "오래된 순", "commentsSort.recently-updated": "최신 업데이트 순", "commentsSort.worst": "최저", + "empty-state": "Don't have comments yet", "errors.0": "문제가 발생했습니다. 잠시 후 다시 시도해 주세요.", "errors.1": "댓글을 찾을 수 없습니다. 페이지를 새로고침하여 다시 시도하세요.", "errors.10": "댓글을 추가하기에 너무 늦었습니다.", @@ -94,6 +94,7 @@ "errors.18": "요청한 파일을 찾을 수 없습니다.", "errors.19": "Comment contains restricted words.", "errors.2": "수신 요청 처리가 실패했습니다.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "이 작업을 수행할 수 있는 권한이 없습니다.", "errors.4": "댓글 데이터가 유효하지 않습니다.", "errors.5": "댓글을 찾을 수 없습니다. 페이지를 새로고침하여 다시 시도하세요.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "숫자 지정된 목록 추가", "toolbar.quote": "인용구 삽입", "toolbar.unordered-list": "글머리 기호가 지정된 목록 추가", - "user-info.last-comments": "{userName} 님의 마지막 댓글", - "user-info.unexpected-error": "문제가 발생했습니다", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "익명의 사용자는 투표할 수 없습니다", "vote.deleted": "삭제된 댓글에는 투표할 수 없습니다", "vote.guest": "투표하려면 로그인하세요", diff --git a/frontend/app/locales/pl.json b/frontend/app/locales/pl.json index 6b18f4bec7..60e7488d50 100644 --- a/frontend/app/locales/pl.json +++ b/frontend/app/locales/pl.json @@ -4,8 +4,10 @@ "auth.loading": "Ładuję...", "auth.oauth-button": "Sign In with {provider}", "auth.oauth-source": "Use Social Network", + "auth.open-profile": "Open My Profile", "auth.or": "lub", "auth.signin": "Zaloguj się", + "auth.signout": "Wyloguj", "auth.submit": "Potwierdź", "auth.symbols-restriction": "Nazwa użytkownika powinna składać się z liter, numerów, podkreślinków lub spacji", "auth.user-not-found": "Żaden użytkownik nie został znaleziony", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Włącz komentarze", "authPanel.enable-cookies": "Zezwalaj na cookies w celu logowania i komentowania", "authPanel.hide-settings": "Ukryj ustawienia", - "authPanel.logged-as": "Jesteś zalogowany jako", - "authPanel.logout": "Wyloguj?", "authPanel.new-page": "nowa strona", "authPanel.read-only": "Tylko do odczytu", - "authPanel.request-to-delete-data": "Zażądaj usunięcia swoich danych", "authPanel.show-settings": "Pokaż ustawienia", "blockingDuration.day": "Na dzień", "blockingDuration.month": "Na miesiąc", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Najstarsze", "commentsSort.recently-updated": "Ostatnio aktualizowane", "commentsSort.worst": "Najgorsze", + "empty-state": "Don't have comments yet", "errors.0": "Coś poszło nie tak. Spróbuj ponownie później.", "errors.1": "Komentarz nie został znaleziony. Odśwież stronę i spróbuj ponownie.", "errors.10": "Jest już za późno żeby edytować ten komentarz.", @@ -94,6 +94,7 @@ "errors.18": "Żądany plik nie został znaleziony.", "errors.19": "Comment contains restricted words.", "errors.2": "Nie udało sie sparsować przychodzącego zapytania do struktury danych.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Nie masz wystarczających uprawnień by wykonać te operację.", "errors.4": "Niepoprawne dane komentarza.", "errors.5": "Komentarz nie może zostać odnaleziony. Odśwież stronę i spróbuj ponownie.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Dodaj numerowaną listę", "toolbar.quote": "Dodaj cytat", "toolbar.unordered-list": "Dodaj listę punktowaną", - "user-info.last-comments": "Ostatnie komentarze {userName}", - "user-info.unexpected-error": "Coś poszło nie tak", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Anonimowi użytkownicy nie mają prawa głosu", "vote.deleted": "Nie można głosować na usunięte komentarze", "vote.guest": "Zaloguj się żeby głosować", diff --git a/frontend/app/locales/ru.json b/frontend/app/locales/ru.json index 85d853f9a9..e6d833fa25 100644 --- a/frontend/app/locales/ru.json +++ b/frontend/app/locales/ru.json @@ -4,8 +4,10 @@ "auth.loading": "Загрузка...", "auth.oauth-button": "Войти через {provider}", "auth.oauth-source": "Использовать социальные сети", + "auth.open-profile": "Open My Profile", "auth.or": "или", "auth.signin": "Войти", + "auth.signout": "Выйти", "auth.submit": "Отправить", "auth.symbols-restriction": "Имя пользователя должно начинаться с буквы и содержать только латинские буквы, цифры, знаки подчеркивания и пробелы", "auth.user-not-found": "Пользователь не найден", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Включить комментарии", "authPanel.enable-cookies": "Разрешите использование файлов cookie, чтобы авторизоваться и комментировать", "authPanel.hide-settings": "Скрыть настройки", - "authPanel.logged-as": "Вы вошли как", - "authPanel.logout": "Выйти?", "authPanel.new-page": "новой странице", "authPanel.read-only": "Только для чтения", - "authPanel.request-to-delete-data": "Запросить удаление своих данных", "authPanel.show-settings": "Показать настройки", "blockingDuration.day": "На день", "blockingDuration.month": "На месяц", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Самые старые", "commentsSort.recently-updated": "Обновленные недавно", "commentsSort.worst": "Худшие", + "empty-state": "Don't have comments yet", "errors.0": "Что-то пошло не так. Попробуйте еще раз позже.", "errors.1": "Комментарий не найден. Обновите страницу и попробуйте еще раз.", "errors.10": "Время редактирования комментария истекло.", @@ -94,6 +94,7 @@ "errors.18": "Не удалось найти запрошенный файл.", "errors.19": "Comment contains restricted words.", "errors.2": "Не удалось обработать ответ от сервера.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "У вас недостаточно прав для выполнения этого действия.", "errors.4": "Комментарий содержит недопустимые данные.", "errors.5": "Комментарий не найден. Обновите страницу и попробуйте еще раз.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Добавить нумерованный список", "toolbar.quote": "Вставить цитату", "toolbar.unordered-list": "Добавить маркированный список", - "user-info.last-comments": "Последние комментарии {userName}", - "user-info.unexpected-error": "Что-то пошло не так", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Анонимные пользователи не могут голосовать", "vote.deleted": "Нельзя голосовать за удаленный комментарий", "vote.guest": "Войдите, чтобы проголосовать", diff --git a/frontend/app/locales/tr.json b/frontend/app/locales/tr.json index fb15f3fb1c..b21ae478db 100644 --- a/frontend/app/locales/tr.json +++ b/frontend/app/locales/tr.json @@ -4,8 +4,10 @@ "auth.loading": "Yükleniyor...", "auth.oauth-button": "{provider} ile Oturum Aç", "auth.oauth-source": "Sosyal Ağ Kullan", + "auth.open-profile": "Open My Profile", "auth.or": "veya", "auth.signin": "Giriş yap", + "auth.signout": "Çıkış yap", "auth.submit": "Gönder", "auth.symbols-restriction": "Kullanıcı adı harf ile başlayıp; yalnızca harf, rakam, alt çizgi veya boşluk içerebilir", "auth.user-not-found": "Kullanıcı bulunamadı", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Yorumları etkinleştir", "authPanel.enable-cookies": "Giriş yapmak ve yorum yazmak için çerezleri etkinleştir", "authPanel.hide-settings": "Ayarları gizle", - "authPanel.logged-as": "Kullanıcı adınız", - "authPanel.logout": "Çıkış yap?", "authPanel.new-page": "yeni sayfa", "authPanel.read-only": "Yazmaya kapalı", - "authPanel.request-to-delete-data": "Bilgilerimi sil", "authPanel.show-settings": "Ayarları göster", "blockingDuration.day": "bir günlük", "blockingDuration.month": "bir aylık", @@ -81,6 +80,7 @@ "commentsSort.oldest": "En eskiler", "commentsSort.recently-updated": "En son güncellenen", "commentsSort.worst": "En kötüler", + "empty-state": "Don't have comments yet", "errors.0": "Bir hata oluştu. Lütfen daha sonra tekrar deneyin.", "errors.1": "Yorum bulunamadı. Lütfen sayfayı yenileyip tekrar deneyin.", "errors.10": "Yorumu düzenlemek için artık çok geç.", @@ -94,6 +94,7 @@ "errors.18": "İstenilen dosya bulunamadı.", "errors.19": "Comment contains restricted words.", "errors.2": "Gelen talep işlenemedi.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Bu işlemi yapmak için yetkiniz yok.", "errors.4": "Yorum verisi geçersiz.", "errors.5": "Yorum bulunamadı. Lütfen sayfayı yenileyip tekrar deneyin.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Sıralı liste ekle", "toolbar.quote": "Alıntı ekle", "toolbar.unordered-list": "Liste ekle", - "user-info.last-comments": "{userName} tarafından son yorumlar", - "user-info.unexpected-error": "Bir hata oluştu", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Anonim kullanıcılar oy kullanamaz", "vote.deleted": "Silinmiş yorum oylanamaz", "vote.guest": "Oy vermek için giriş yapın", diff --git a/frontend/app/locales/ua.json b/frontend/app/locales/ua.json index a94554dce7..0f09f453c9 100644 --- a/frontend/app/locales/ua.json +++ b/frontend/app/locales/ua.json @@ -4,8 +4,10 @@ "auth.loading": "Завантаження...", "auth.oauth-button": "Sign In with {provider}", "auth.oauth-source": "Use Social Network", + "auth.open-profile": "Open My Profile", "auth.or": "або", "auth.signin": "Увійти", + "auth.signout": "Вийти", "auth.submit": "Відправити", "auth.symbols-restriction": "Ім’я користувача повинно починатися з літери і містити тільки латинські букви,цифри,знаки підкреслення і прогалини", "auth.user-not-found": "Користувач не знайдений", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Увімкнути коментари", "authPanel.enable-cookies": "Дозвольте Cookies", "authPanel.hide-settings": "Заховати налаштування", - "authPanel.logged-as": "Ви увійшли як", - "authPanel.logout": "Вийти?", "authPanel.new-page": "нова сторінка", "authPanel.read-only": "Тільки для читання", - "authPanel.request-to-delete-data": "Запросити видалення моїх даних", "authPanel.show-settings": "Показати налаштування", "blockingDuration.day": "На день", "blockingDuration.month": "На місяць", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Старі", "commentsSort.recently-updated": "Нещодавно оновлені", "commentsSort.worst": "Гірші", + "empty-state": "Don't have comments yet", "errors.0": "Щось пішло не так,спробуйте ще раз пізніше.", "errors.1": "Коментар не знайдений. Перезавантажте сторінку і спробуйте ще раз.", "errors.10": "Час редагування коментаря минув.", @@ -94,6 +94,7 @@ "errors.18": "Запрашиваемый файл не найден.", "errors.19": "Comment contains restricted words.", "errors.2": "Не вдалося обробити відповідь від сервера.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Недостатньо прав на здійснення цієї дії.", "errors.4": "Неправильно відформатований коментар.", "errors.5": "Коментар не знайдений. Перезавантажте сторінку і спробуйте ще раз.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Упорядкованний список", "toolbar.quote": "Цитата", "toolbar.unordered-list": "Неупорядкованний список", - "user-info.last-comments": "Останні коментарі {userName}", - "user-info.unexpected-error": "Щось пішло не так", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Не можна голосувати анонімному користувачу", "vote.deleted": "Не можна голосувати за видалений коментар", "vote.guest": "Увійдіть в систему для голосування", diff --git a/frontend/app/locales/vi.json b/frontend/app/locales/vi.json index 4498a87e33..5e32df6190 100644 --- a/frontend/app/locales/vi.json +++ b/frontend/app/locales/vi.json @@ -2,10 +2,12 @@ "auth.back": "Quay lại", "auth.email-address": "Địa chỉ Email", "auth.loading": "Đang tải...", - "auth.oauth-button": "Sign In with {provider}", + "auth.oauth-button": "Đăng nhập with {provider}", "auth.oauth-source": "Use Social Network", + "auth.open-profile": "Open My Profile", "auth.or": "hoặc", - "auth.signin": "Sign In", + "auth.signin": "Đăng nhập", + "auth.signout": "Thoát", "auth.submit": "Gửi đi", "auth.symbols-restriction": "Tên người dùng chỉ được chứa các chữ cái, số, dấu gạch dưới hoặc dấu cách", "auth.user-not-found": "Không tìm thấy người dùng nào", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "Mở bình luận", "authPanel.enable-cookies": "Cho phép cookies đăng nhập và bình luận", "authPanel.hide-settings": "Ẩn cài đặt", - "authPanel.logged-as": "Đã đăng nhập bằng", - "authPanel.logout": "Thoát?", "authPanel.new-page": "trang mới", "authPanel.read-only": "Chỉ đọc", - "authPanel.request-to-delete-data": "Yêu cầu xóa dữ liệu của tôi", "authPanel.show-settings": "Hiện cài đặt", "blockingDuration.day": "Trong một ngày", "blockingDuration.month": "Trong một tháng", @@ -81,6 +80,7 @@ "commentsSort.oldest": "Cũ nhất", "commentsSort.recently-updated": "Mới được cập nhật", "commentsSort.worst": "Tệ nhất", + "empty-state": "Don't have comments yet", "errors.0": "Có gì đó sai sai. Vui lòng thử lại sau.", "errors.1": "Bình luận không được tìm thấy, xin hãy làm mới trang và thử lại.", "errors.10": "Đã quá muộn để sửa bình luận.", @@ -94,6 +94,7 @@ "errors.18": "Không tìm thấy file được yêu cầu.", "errors.19": "Comment contains restricted words.", "errors.2": "Yêu cầu đến không quản lý được.", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "Bạn không có quyền thực hiện thao tác này.", "errors.4": "Dữ liệu bình luận không hợp lệ.", "errors.5": "Bình luận không được tìm thấy, xin hãy làm mới trang và thử lại.", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "Thêm danh sách số", "toolbar.quote": "Thêm trích dẫn", "toolbar.unordered-list": "Thêm danh sách", - "user-info.last-comments": "Bình luận cuối cùng của {userName}", - "user-info.unexpected-error": "Có gì đó sai sai", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "Người dùng ẩn danh không thể vote", "vote.deleted": "Không thể vote bình luận đã xoá", "vote.guest": "Đăng nhập để vote", diff --git a/frontend/app/locales/zh.json b/frontend/app/locales/zh.json index ed28906637..afc4897926 100644 --- a/frontend/app/locales/zh.json +++ b/frontend/app/locales/zh.json @@ -4,8 +4,10 @@ "auth.loading": "加载中...", "auth.oauth-button": "使用 {provider} 登录", "auth.oauth-source": "使用社交网络", + "auth.open-profile": "Open My Profile", "auth.or": "或", "auth.signin": "登录", + "auth.signout": "登出", "auth.submit": "提交", "auth.symbols-restriction": "用户名只能包含字母、数字、下划线或空格", "auth.user-not-found": "未找到用户", @@ -15,11 +17,8 @@ "authPanel.enable-comments": "启用评论", "authPanel.enable-cookies": "允许使用 Cookie 登录并发表评论", "authPanel.hide-settings": "隐藏设置", - "authPanel.logged-as": "您以以下身份登录", - "authPanel.logout": "登出?", "authPanel.new-page": "新页面", "authPanel.read-only": "只读", - "authPanel.request-to-delete-data": "要求删除我的数据", "authPanel.show-settings": "显示设置", "blockingDuration.day": "一天", "blockingDuration.month": "一个月", @@ -81,6 +80,7 @@ "commentsSort.oldest": "最旧", "commentsSort.recently-updated": "最近更新", "commentsSort.worst": "最差", + "empty-state": "Don't have comments yet", "errors.0": "出了些问题,请稍后再试。", "errors.1": "找不到评论。请刷新页面,然后重试。", "errors.10": "现在编辑评论已经太晚了。", @@ -94,6 +94,7 @@ "errors.18": "找不到请求的文件。", "errors.19": "Comment contains restricted words.", "errors.2": "处理传入请求失败。", + "errors.20": "Posted image not found. Please try to upload it again.", "errors.3": "您无权执行此操作。", "errors.4": "评论数据无效。", "errors.5": "找不到评论。请刷新页面,然后重试。", @@ -153,8 +154,8 @@ "toolbar.ordered-list": "添加编号列表", "toolbar.quote": "插入引用文本", "toolbar.unordered-list": "添加项目符号列表", - "user-info.last-comments": "最近由 {userName} 发表的评论", - "user-info.unexpected-error": "出了些问题", + "user.my-comments": "My recent comments", + "user.recent-comments": "Recent comments", "vote.anonymous": "匿名用户无法投票", "vote.deleted": "无法为已删除的评论投票", "vote.guest": "登录以投票", diff --git a/frontend/app/profile.ts b/frontend/app/profile.ts new file mode 100644 index 0000000000..0fe401916c --- /dev/null +++ b/frontend/app/profile.ts @@ -0,0 +1,124 @@ +import { setAttributes, setStyles, StylesDeclaration } from 'utils/set-dom-props'; +import { createIframe } from 'utils/create-iframe'; +import type { Profile } from 'common/types'; + +let root: HTMLDivElement | null = null; +let iframe: HTMLIFrameElement | null = null; + +function removeIframe() { + iframe?.parentNode?.removeChild(iframe); +} + +function createElement( + tagName: K, + styles: StylesDeclaration, + attrs?: Record +) { + const element = document.createElement(tagName); + setStyles(element, styles); + setAttributes(element, attrs); + return element; +} + +function createFragment(params: Profile & Record) { + removeIframe(); + iframe = createIframe({ ...params, page: 'profile', styles: styles.iframe }); + + if (!root) { + root = createElement('div', styles.root); + document.body.appendChild(root); + } + + root.appendChild(iframe); + setStyles(root, styles.rootShowed); + setTimeout(() => iframe?.focus()); +} + +function animateAppear(): void { + window.requestAnimationFrame(() => { + if (!root || !iframe) { + return; + } + setStyles(root, styles.rootAppear); + }); +} + +function animateDisappear(): Promise { + return new Promise((resolve) => { + function handleTransitionEnd() { + resolve(); + + if (!root) { + return; + } + setStyles(root, styles.rootHidden); + root.removeEventListener('transitionend', handleTransitionEnd); + } + window.requestAnimationFrame(() => { + if (!root || !iframe) { + return; + } + setStyles(root, styles.rootDissapear); + root.addEventListener('transitionend', handleTransitionEnd); + }); + }); +} + +function handleKeydown(evt: KeyboardEvent) { + console.log(evt.code); + if (evt.code !== 'Escape') { + return; + } + closeProfile(); +} + +export function openProfile(params: Profile & Record) { + setStyles(document.body, { overflow: 'hidden' }); + createFragment(params); + animateAppear(); + window.addEventListener('keydown', handleKeydown); +} + +export function closeProfile() { + window.removeEventListener('keydown', handleKeydown); + animateDisappear().then(() => { + removeIframe(); + document.body.style.removeProperty('overflow'); + }); +} + +const styles = { + root: { + display: 'none', + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0, + width: '100%', + height: '100%', + transition: 'opacity 0.5s ease-in', + background: 'rgba(0, 0, 0, .4)', + opacity: 0, + zIndex: 99999999, + }, + rootShowed: { + display: 'block', + }, + rootHidden: { + display: 'none', + }, + rootAppear: { + opacity: '1', + }, + rootDissapear: { + opacity: '0', + transition: 'opacity 0.3s ease-out', + }, + iframe: { + position: 'absolute', + right: 0, + width: '100%', + height: '100%', + }, +}; diff --git a/frontend/app/remark.tsx b/frontend/app/remark.tsx index b7b7783884..f0e9b0f69a 100644 --- a/frontend/app/remark.tsx +++ b/frontend/app/remark.tsx @@ -6,15 +6,16 @@ import { IntlProvider } from 'react-intl'; import { loadLocale } from 'utils/loadLocale'; import { getLocale } from 'utils/getLocale'; import { ConnectedRoot } from 'components/root'; -import { UserInfo } from 'components/user-info'; +import { Profile } from 'components/profile'; import { store } from 'store'; import { NODE_ID, BASE_URL } from 'common/constants'; import { StaticStore } from 'common/static-store'; import { getConfig } from 'common/api'; import { fetchHiddenUsers } from 'store/user/actions'; import { restoreCollapsedThreads } from 'store/thread/actions'; -import { parseQuery } from 'utils/parseQuery'; +import { parseQuery } from 'utils/parse-query'; import { parseBooleansFromDictionary } from 'utils/parse-booleans-from-dictionary'; +import { parseMessage } from 'utils/postMessage'; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); @@ -36,6 +37,24 @@ async function init(): Promise { const messages = await loadLocale(locale).catch(() => ({})); const boundActions = bindActionCreators({ fetchHiddenUsers, restoreCollapsedThreads }, store.dispatch); + node.innerHTML = ''; + + window.addEventListener('message', (evt) => { + const data = parseMessage(evt); + + if (data.theme === 'light') { + document.body.classList.remove('dark'); + } + + if (data.theme === 'dark') { + document.body.classList.add('dark'); + } + }); + + if (params.theme === 'dark') { + document.body.classList.add('dark'); + } + boundActions.fetchHiddenUsers(); boundActions.restoreCollapsedThreads(); @@ -45,7 +64,7 @@ async function init(): Promise { render( - {params.page === 'user-info' ? : } + {params.page === 'profile' ? : } , node ); diff --git a/frontend/app/store/actions.ts b/frontend/app/store/actions.ts index e5d779f7f5..9ef6003331 100644 --- a/frontend/app/store/actions.ts +++ b/frontend/app/store/actions.ts @@ -3,13 +3,6 @@ import { POST_INFO_ACTIONS } from './post-info/types'; import { THEME_ACTIONS } from './theme/types'; import { THREAD_ACTIONS } from './thread/types'; import { USER_ACTIONS } from './user/types'; -import { USER_INFO_ACTIONS } from './user-info/types'; /** Merged store actions */ -export type ACTIONS = - | COMMENTS_ACTIONS - | POST_INFO_ACTIONS - | THEME_ACTIONS - | THREAD_ACTIONS - | USER_ACTIONS - | USER_INFO_ACTIONS; +export type ACTIONS = COMMENTS_ACTIONS | POST_INFO_ACTIONS | THEME_ACTIONS | THREAD_ACTIONS | USER_ACTIONS; diff --git a/frontend/app/store/reducers.ts b/frontend/app/store/reducers.ts index 36d5ad15a2..8057bad086 100644 --- a/frontend/app/store/reducers.ts +++ b/frontend/app/store/reducers.ts @@ -2,7 +2,6 @@ import * as comments from './comments/reducers'; import * as postInfo from './post-info/reducers'; import * as theme from './theme/reducers'; import * as user from './user/reducers'; -import * as userInfo from './user-info/reducers'; import * as thread from './thread/reducers'; /** Merged store reducers */ @@ -10,7 +9,6 @@ export const rootProvider = { ...comments, ...theme, ...postInfo, - ...userInfo, ...thread, ...user, }; diff --git a/frontend/app/store/user-info/actions.ts b/frontend/app/store/user-info/actions.ts deleted file mode 100644 index 77c35640e3..0000000000 --- a/frontend/app/store/user-info/actions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getUserComments } from 'common/api'; -import { Comment } from 'common/types'; - -import { StoreAction } from '../index'; -import { USER_INFO_SET } from './types'; - -export const fetchInfo = (id: string): StoreAction> => async (dispatch) => { - // TODO: limit - const info = await getUserComments(id, 10); - - dispatch({ - type: USER_INFO_SET, - id, - comments: info.comments, - }); - return info.comments; -}; diff --git a/frontend/app/store/user-info/reducers.ts b/frontend/app/store/user-info/reducers.ts deleted file mode 100644 index 47381cd615..0000000000 --- a/frontend/app/store/user-info/reducers.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Comment } from 'common/types'; - -import { USER_INFO_SET, USER_INFO_ACTIONS } from './types'; - -export interface UserCommentsState { - [key: string]: Comment[]; -} - -export function userComments(state: UserCommentsState = {}, action: USER_INFO_ACTIONS): UserCommentsState { - switch (action.type) { - case USER_INFO_SET: { - return { - ...state, - [action.id]: action.comments, - }; - } - default: - return state; - } -} diff --git a/frontend/app/store/user-info/types.ts b/frontend/app/store/user-info/types.ts deleted file mode 100644 index 3fe659c783..0000000000 --- a/frontend/app/store/user-info/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Comment, User } from 'common/types'; - -export const USER_INFO_SET = 'USER-INFO/SET'; - -export interface USER_INFO_SET_ACTION { - type: typeof USER_INFO_SET; - id: User['id']; - comments: Comment[]; -} - -export type USER_INFO_ACTIONS = USER_INFO_SET_ACTION; diff --git a/frontend/app/store/user/actions.ts b/frontend/app/store/user/actions.ts index d4cdec4523..d2b41daea8 100644 --- a/frontend/app/store/user/actions.ts +++ b/frontend/app/store/user/actions.ts @@ -1,8 +1,9 @@ import * as api from 'common/api'; +import { logout } from 'components/auth/auth.api'; import { User, BlockedUser, BlockTTL } from 'common/types'; import { ttlToTime } from 'utils/ttl-to-time'; import { getHiddenUsers } from 'utils/get-hidden-users'; -import { LS_HIDDEN_USERS_KEY } from 'common/constants'; +import { LS_EMAIL_KEY, LS_HIDDEN_USERS_KEY } from 'common/constants'; import { setItem } from 'common/local-storage'; import { StoreAction } from '../index'; @@ -17,7 +18,7 @@ import { USER_SUBSCRIPTION_SET, USER_SET_ACTION, } from './types'; -import { fetchComments } from '../comments/actions'; +import { fetchComments, unsetCommentMode } from '../comments/actions'; import { COMMENTS_PATCH } from '../comments/types'; export function setUser(user: User | null = null): USER_SET_ACTION { @@ -27,6 +28,18 @@ export function setUser(user: User | null = null): USER_SET_ACTION { }; } +export function signout(cleanSession = true): StoreAction> { + return async (dispatch) => { + if (cleanSession) { + await logout(); + } + localStorage.removeItem(LS_EMAIL_KEY); + dispatch(setUser()); + dispatch(unsetCommentMode()); + dispatch(fetchComments()); + }; +} + export const fetchUser = (): StoreAction> => async (dispatch) => { const user = await api.getUser(); dispatch(setUser(user)); diff --git a/frontend/app/styles/custom-properties.css b/frontend/app/styles/custom-properties.css index 3e98fa40db..9c3734fa36 100644 --- a/frontend/app/styles/custom-properties.css +++ b/frontend/app/styles/custom-properties.css @@ -77,4 +77,6 @@ --secondary-text-color: 209, 213, 219; --error-color: #ffa0a0; --line-brighter-color: var(--color11); + + color-scheme: dark; } diff --git a/frontend/app/styles/global.css b/frontend/app/styles/global.css index 888f3e5212..9e1551bc5c 100644 --- a/frontend/app/styles/global.css +++ b/frontend/app/styles/global.css @@ -4,10 +4,33 @@ body { margin: 0; padding: 6px; font-family: PT Sans, Helvetica, Arial, sans-serif; + color: rgb(var(--primary-text-color)); + box-sizing: border-box; } -/* Preloader */ +input, +button { + font-family: inherit; + color: inherit; +} + +button { + padding: 0; + margin: 0; + border: 0; + background: unset; + font-size: inherit; + transition-property: color, background, border-color; + transition-timing-function: linear; + transition-duration: 150ms; + appearance: none; +} + +#remark42 { + height: 100%; +} +/* Preloader */ .preloader, .preloader::before, .preloader::after { diff --git a/frontend/app/tests/utils.tsx b/frontend/app/tests/utils.tsx new file mode 100644 index 0000000000..9d1d59925f --- /dev/null +++ b/frontend/app/tests/utils.tsx @@ -0,0 +1,13 @@ +import { h, ComponentChild } from 'preact'; +import { IntlProvider } from 'react-intl'; +import { render as originalRender } from '@testing-library/preact'; + +import en from 'locales/en.json'; + +export function render(children: ComponentChild) { + return originalRender( + + {children} + + ); +} diff --git a/frontend/app/utils/create-iframe.ts b/frontend/app/utils/create-iframe.ts new file mode 100644 index 0000000000..3779ea39c4 --- /dev/null +++ b/frontend/app/utils/create-iframe.ts @@ -0,0 +1,32 @@ +import { BASE_URL } from 'common/constants.config'; +import { setStyles, setAttributes, StylesDeclaration } from 'utils/set-dom-props'; + +type Params = { [key: string]: unknown; __colors__?: Record; styles?: StylesDeclaration }; + +export function createIframe({ __colors__, styles, ...params }: Params) { + const iframe = document.createElement('iframe'); + const query = new URLSearchParams(params as Record).toString(); + + setAttributes(iframe, { + src: `${BASE_URL}/web/iframe.html?${query}`, + name: JSON.stringify({ __colors__ }), + frameborder: '0', + allowtransparency: 'true', + scrolling: 'no', + tabindex: '0', + title: 'Comments | Remark42', + horizontalscrolling: 'no', + verticalscrolling: 'no', + }); + setStyles(iframe, { + height: '100%', + width: '100%', + border: 'none', + padding: 0, + margin: 0, + overflow: 'hidden', + ...styles, + }); + + return iframe; +} diff --git a/frontend/app/utils/parseQuery.test.ts b/frontend/app/utils/parse-query.test.ts similarity index 94% rename from frontend/app/utils/parseQuery.test.ts rename to frontend/app/utils/parse-query.test.ts index 5286ff775b..d222df4dbe 100644 --- a/frontend/app/utils/parseQuery.test.ts +++ b/frontend/app/utils/parse-query.test.ts @@ -1,4 +1,4 @@ -import { parseQuery } from './parseQuery'; +import { parseQuery } from './parse-query'; describe('parseQuery', () => { it('should return empty object', () => { diff --git a/frontend/app/utils/parseQuery.ts b/frontend/app/utils/parse-query.ts similarity index 100% rename from frontend/app/utils/parseQuery.ts rename to frontend/app/utils/parse-query.ts diff --git a/frontend/app/utils/postMessage.ts b/frontend/app/utils/postMessage.ts index 776b487c5c..a3f6a7eb58 100644 --- a/frontend/app/utils/postMessage.ts +++ b/frontend/app/utils/postMessage.ts @@ -1,10 +1,11 @@ -import type { Theme, UserInfo } from 'common/types'; +import type { Theme, Profile } from 'common/types'; type ParentMessage = { inited?: true; scrollTo?: number; height?: number; - profile?: UserInfo | null; + signout?: true; + profile?: Profile | null; }; type ChildMessage = { @@ -12,6 +13,7 @@ type ChildMessage = { hash?: string; title?: string; theme?: Theme; + signout?: true; }; type AllMessages = ChildMessage & ParentMessage; diff --git a/frontend/app/utils/set-dom-props.ts b/frontend/app/utils/set-dom-props.ts new file mode 100644 index 0000000000..c01f0079a1 --- /dev/null +++ b/frontend/app/utils/set-dom-props.ts @@ -0,0 +1,23 @@ +export type StylesDeclaration = Partial< + Record< + keyof Omit< + CSSStyleDeclaration, + 'length' | 'parentRule' | 'setProperty' | 'removeProperty' | 'item' | 'getPropertyValue' | 'getPropertyPriority' + >, + string | number + > +>; + +export function setStyles(element: HTMLElement, styles: StylesDeclaration = {}) { + const entr = Object.entries(styles); + + entr.forEach(([p, v]) => { + element.style[p as keyof StylesDeclaration] = `${v}`; + }); +} + +export function setAttributes(element: HTMLElement, attrs: Record = {}) { + Object.entries(attrs).forEach(([p, v]) => { + element.setAttribute(p, `${v}`); + }); +} diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 4a154ba06f..9cbc8868a6 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -20,7 +20,6 @@ module.exports = { setupFilesAfterEnv: [ '/app/__mocks__/fetch.ts', '/app/__mocks__/localstorage.ts', - '/app/__stubs__/react-intl.ts', '/app/__stubs__/remark-config.ts', '/app/__stubs__/static-config.ts', '/app/__stubs__/settings.ts', @@ -31,6 +30,7 @@ module.exports = { '!**/__stubs__/**', '!app/locales/**', '!app/utils/loadLocale.ts', + '!app/tests', ], globals: { 'ts-jest': { diff --git a/frontend/templates/iframe.ejs b/frontend/templates/iframe.ejs index 84df30082c..e8a2b33fd8 100644 --- a/frontend/templates/iframe.ejs +++ b/frontend/templates/iframe.ejs @@ -33,82 +33,6 @@ } })(); - - <% if (htmlWebpackPlugin.options.env === 'production') { %> <% } %> diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 42c8166be5..5d4b112dba 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,7 +3,7 @@ "incremental": true, "target": "es6", "module": "esnext", - "lib": ["es2018", "dom"], + "lib": ["es2019", "dom"], "jsx": "preserve", "jsxFactory": "h", "jsxFragmentFactory": "Fragment", @@ -12,7 +12,7 @@ "allowJs": true, "moduleResolution": "node", "resolveJsonModule": true, - "baseUrl": "app" , + "baseUrl": "app", "esModuleInterop": true, "skipLibCheck": true, "sourceMap": true diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index b4b99576da..9dfb8101a4 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -19,7 +19,7 @@ const PORT = process.env.PORT || 9000; const REMARK_API_BASE_URL = process.env.REMARK_API_BASE_URL || 'http://127.0.0.1:8080'; const DEVSERVER_BASE_PATH = process.env.DEVSERVER_BASE_PATH || 'http://127.0.0.1:9000'; const PUBLIC_FOLDER_PATH = path.resolve(__dirname, 'public'); -const CUSTOM_PROPERTIES_PATH = path.resolve(__dirname, './app/custom-properties.css'); +const CUSTOM_PROPERTIES_PATH = path.resolve(__dirname, './app/styles/custom-properties.css'); const genId = incstr.idGenerator(); const modulesMap = {}; @@ -138,8 +138,16 @@ module.exports = (_, { mode, analyze }) => { sourceMap: isDev, postcssOptions: { plugins: [ - ['postcss-preset-env', { stage: 0 }], - ['postcss-custom-properties', { importFrom: CUSTOM_PROPERTIES_PATH }], + [ + 'postcss-preset-env', + { + browsers: 'defaults, not IE 11, not samsung 12', + stage: 0, + features: { + 'custom-properties': CUSTOM_PROPERTIES_PATH, + }, + }, + ], 'cssnano', ], },