Update frontend
* change root dit and change way to import modules * update deps to latest versions * use latest tools for building bundles * rewrite webpack config * use nomodule technique for loading modern bundle * inject polyfills by babel usebuiltins * update eslint rules * rename all style files to CSS * use postcss preset env for building styles * proper typescript typing * put all html files to templates folder * etc
This commit is contained in:
@@ -462,16 +462,8 @@ Add this snippet to the bottom of web page:
|
||||
// if you set this param in `false` you will get notifications email notifications as admin
|
||||
// but your users won't have interface for subscription
|
||||
};
|
||||
|
||||
(function(c) {
|
||||
for(var i = 0; i < c.length; i++){
|
||||
var d = document, s = d.createElement('script');
|
||||
s.src = remark_config.host + '/web/' +c[i] +'.js';
|
||||
s.defer = true;
|
||||
(d.head || d.body).appendChild(s);
|
||||
}
|
||||
})(remark_config.components || ['embed']);
|
||||
</script>
|
||||
<script>!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);</script>
|
||||
```
|
||||
|
||||
And then add this node in the place where you want to see Remark42 widget:
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
echo "prepare environment"
|
||||
# replace BASE_URL constant by REMARK_URL
|
||||
sed -i "s|https://demo.remark42.com|${REMARK_URL}|g" /srv/web/*.html
|
||||
sed -i "s|https://demo.remark42.com|${REMARK_URL}|g" /srv/web/*.js
|
||||
sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" /srv/web/*.html
|
||||
sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" /srv/web/*.js
|
||||
|
||||
if [ -n "${SITE_ID}" ]; then
|
||||
#replace "site_id: 'remark'" by SITE_ID
|
||||
|
||||
+30
-14
@@ -1,18 +1,34 @@
|
||||
const getPresetEnv = options => ['@babel/preset-env', options];
|
||||
const preactPreset = [
|
||||
'@babel/preset-react',
|
||||
{
|
||||
pragma: 'h',
|
||||
pragmaFrag: 'Fragment',
|
||||
},
|
||||
];
|
||||
|
||||
const plugins = ['module:fast-async'];
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
bugfixes: true,
|
||||
loose: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
pragma: 'h',
|
||||
pragmaFrag: 'Fragment',
|
||||
},
|
||||
],
|
||||
getPresetEnv({
|
||||
targets: 'defaults, not IE 11, not samsung 12',
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3,
|
||||
bugfixes: true,
|
||||
loose: true,
|
||||
}),
|
||||
preactPreset,
|
||||
],
|
||||
plugins,
|
||||
env: {
|
||||
modern: {
|
||||
presets: [getPresetEnv({ targets: { esmodules: true }, loose: true, bugfixes: true }), preactPreset],
|
||||
plugins,
|
||||
},
|
||||
test: {
|
||||
presets: [getPresetEnv({ targets: { node: 'current' } }), preactPreset],
|
||||
plugins,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
defaults
|
||||
+19
-106
@@ -1,127 +1,40 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
module.exports = {
|
||||
parser: 'babel-eslint',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'react-app',
|
||||
'preact',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'prettier/@typescript-eslint',
|
||||
'prettier/babel',
|
||||
'prettier/prettier',
|
||||
'prettier/react',
|
||||
],
|
||||
plugins: ['react', 'jsx-a11y', 'prettier'],
|
||||
plugins: ['jsx-a11y', 'prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
files: ['*.ts?(x)'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
// needs for using optional chaining
|
||||
'no-undef': 0,
|
||||
'jsx-a11y/no-autofocus': 0,
|
||||
// disabling because typescipt uses it's own lint (see next rule)
|
||||
'no-unused-vars': 0,
|
||||
// allow Rust-like var starting with _underscore
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
// disabling because it's bad practice to mark accessibility in react classes
|
||||
'@typescript-eslint/explicit-member-accessibility': 0,
|
||||
// doesn't work in real world
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
// disabling because store actions use WATCH_ME_IM_SPECIAL case
|
||||
'@typescript-eslint/class-name-casing': 0,
|
||||
// disabling because server response contains snake case
|
||||
'@typescript-eslint/camelcase': 0,
|
||||
// disabling because it's standard behaviour that function is hoisted to top
|
||||
'@typescript-eslint/no-use-before-define': 0,
|
||||
// well
|
||||
'@typescript-eslint/ban-ts-ignore': 0,
|
||||
// better to be explicit here maybe?
|
||||
'@typescript-eslint/no-inferrable-types': 0,
|
||||
'no-undef': 'off',
|
||||
'no-redeclare': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.d.ts'],
|
||||
rules: {
|
||||
'no-var': 0,
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.test.ts', '*.test.tsx'],
|
||||
files: ['*.(spec|test).ts?(x)'],
|
||||
extends: ['react-app/jest'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/no-object-literal-type-assertion': 0,
|
||||
},
|
||||
globals: {
|
||||
fail: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx'],
|
||||
rules: {
|
||||
'max-nested-callbacks': ['warn', { max: 10 }],
|
||||
'import/first': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
remark_config: true,
|
||||
__webpack_public_path__: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/indent': 0,
|
||||
'react/jsx-uses-react': 2,
|
||||
'react/jsx-uses-vars': 2,
|
||||
'no-cond-assign': 1,
|
||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||
'no-console': 1,
|
||||
camelcase: 0,
|
||||
'comma-style': 2,
|
||||
'max-nested-callbacks': [2, 3],
|
||||
'no-eval': 2,
|
||||
'no-implied-eval': 2,
|
||||
'no-new-func': 2,
|
||||
'guard-for-in': 2,
|
||||
eqeqeq: 2,
|
||||
'no-else-return': 2,
|
||||
'no-redeclare': 2,
|
||||
'no-dupe-keys': 2,
|
||||
radix: 2,
|
||||
strict: [2, 'never'],
|
||||
'no-shadow': 0,
|
||||
'callback-return': [1, ['callback', 'cb', 'next', 'done']],
|
||||
'no-delete-var': 2,
|
||||
'no-undef-init': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'handle-callback-err': 2,
|
||||
'no-lonely-if': 2,
|
||||
'constructor-super': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-dupe-class-members': 2,
|
||||
'no-const-assign': 2,
|
||||
'prefer-spread': 2,
|
||||
'prefer-const': 2,
|
||||
'no-useless-concat': 2,
|
||||
'no-var': 2,
|
||||
'object-shorthand': 2,
|
||||
'prefer-arrow-callback': 2,
|
||||
'prettier/prettier': 2,
|
||||
'@typescript-eslint/no-var-requires': 0,
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
},
|
||||
};
|
||||
|
||||
+5
-1
@@ -1,2 +1,6 @@
|
||||
.env
|
||||
node_modules
|
||||
extracted-messages
|
||||
/public
|
||||
/*.log
|
||||
.env
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
'./**/*.{ts,tsx,js,jsx}': ['eslint --fix --max-warnings=0', 'prettier --write'],
|
||||
'./**/*.{scss,pcss,css}': ['prettier --write', 'stylelint'],
|
||||
'./iframe.html': ['prettier --write', 'stylelint'],
|
||||
'./**/*.css': ['prettier --write', 'stylelint'],
|
||||
'./templates/**.html': ['prettier --write', 'stylelint'],
|
||||
};
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
node_modules
|
||||
public
|
||||
package.json
|
||||
package-lock.json
|
||||
@@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
@@ -8,7 +9,7 @@ module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ejs', '*.html'],
|
||||
files: ['*.html'],
|
||||
options: {
|
||||
trailingComma: 'none',
|
||||
},
|
||||
|
||||
+26
-6
@@ -1,22 +1,42 @@
|
||||
module.exports = [
|
||||
{
|
||||
path: 'public/embed.js',
|
||||
limit: '2.7 KB',
|
||||
path: 'public/embed.mjs',
|
||||
limit: '2.5 KB',
|
||||
},
|
||||
{
|
||||
limit: '86 KB',
|
||||
path: 'public/embed.js',
|
||||
limit: '2.5 KB',
|
||||
},
|
||||
{
|
||||
limit: '70 KB',
|
||||
path: 'public/remark.mjs',
|
||||
},
|
||||
{
|
||||
limit: '73.5 KB',
|
||||
path: 'public/remark.js',
|
||||
},
|
||||
{
|
||||
limit: '44 KB',
|
||||
limit: '31 KB',
|
||||
path: 'public/last-comments.mjs',
|
||||
},
|
||||
{
|
||||
limit: '38 KB',
|
||||
path: 'public/last-comments.js',
|
||||
},
|
||||
{
|
||||
path: 'public/deleteme.mjs',
|
||||
limit: '11 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/deleteme.js',
|
||||
limit: '35 KB',
|
||||
limit: '18 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/counter.mjs',
|
||||
limit: '0.7 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/counter.js',
|
||||
limit: '1.05 KB',
|
||||
limit: '0.7 KB',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
public
|
||||
extracted-messages
|
||||
@@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const { CUSTOM_PROPERTIES_PATH } = require('./webpack.config');
|
||||
|
||||
module.exports = {
|
||||
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
|
||||
plugins: ['stylelint-value-no-unknown-custom-properties', '@mavrin/stylelint-declaration-use-css-custom-properties'],
|
||||
@@ -19,7 +20,7 @@ module.exports = {
|
||||
'csstools/value-no-unknown-custom-properties': [
|
||||
true,
|
||||
{
|
||||
importFrom: path.resolve(__dirname, './app/custom-properties.css'),
|
||||
importFrom: CUSTOM_PROPERTIES_PATH,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- if you want IDE integration, you need `eslint` and `stylelint` plugin to be installed.
|
||||
|
||||
### CSS Styles
|
||||
|
||||
- although styles have `scss` extension, it is actually pack of post-css plugins, so syntax differs, for example in `calc` function.
|
||||
- now we are migrating to css-modules and this is recomended way to stylization. A file with styles should be named like `component.module.css`
|
||||
- old component styles use BEM notation (at least it should): `block__element_modifier`. Also there are `mix` classes: `block_modifier`.
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
- imports for typescript, javascript files should be without extension: `./index`, not `./index.ts`
|
||||
- if file resides in the same directory or in subdirectory import should be relative: `./types/something`
|
||||
- otherwise it should start from `@app` namespace: `@app/common/store` which mapped to `/app/common/store.ts` in webpack, tsconfig and jest
|
||||
- otherwise it should start from ``namespace:`common/store`which mapped to`/appcommon/store.ts` in webpack, tsconfig and jest
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Veriable responsive for dynamic setting public path for
|
||||
* assets. Dynamic imports with relative url will be resolved over this path.
|
||||
*
|
||||
* https://webpack.js.org/guides/public-path/#on-the-fly
|
||||
*/
|
||||
declare var __webpack_public_path__: string;
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
declare module 'bem-react-helper' {
|
||||
export interface Mods {
|
||||
[key: string]: string | number | boolean | undefined | null;
|
||||
}
|
||||
export type Mix = Array<string | undefined> | string;
|
||||
export default function b(
|
||||
classname: string,
|
||||
props?: {
|
||||
mods?: Mods;
|
||||
mix?: Mix;
|
||||
},
|
||||
override_props?: Mods
|
||||
): string;
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
declare module 'intersection-observer';
|
||||
declare module 'whatwg-fetch';
|
||||
Vendored
-28
@@ -1,28 +0,0 @@
|
||||
import { Theme } from '@app/common/types';
|
||||
import { CommentsConfig } from '@app/common/config-types';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
remark_config: CommentsConfig;
|
||||
REMARK42: {
|
||||
changeTheme?: (theme: Theme) => void;
|
||||
destroy?: () => void;
|
||||
createInstance: (
|
||||
remark_config: CommentsConfig
|
||||
) =>
|
||||
| {
|
||||
changeTheme(theme: Theme): void;
|
||||
destroy(): void;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
}
|
||||
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
Headers: typeof Headers;
|
||||
localStorage: typeof Storage;
|
||||
fetch: typeof fetch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
global.Headers = class HeadersMock implements Headers {
|
||||
global.Headers = class HeadersMock extends Headers implements Headers {
|
||||
private headers = new Map();
|
||||
|
||||
append(key: string, value: string) {
|
||||
@@ -16,7 +16,6 @@ global.Headers = class HeadersMock implements Headers {
|
||||
delete(key: string) {
|
||||
this.headers.delete(key);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
forEach(callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: any) {
|
||||
this.headers.forEach((value, key) => {
|
||||
callbackfn.call(thisArg || this, value, key, this);
|
||||
@@ -1,15 +1,4 @@
|
||||
import 'jest-extended';
|
||||
import 'jest-enzyme';
|
||||
import { configure } from 'enzyme';
|
||||
import PreactAdapter from 'enzyme-adapter-preact-pure';
|
||||
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
|
||||
import './mockHeaders';
|
||||
|
||||
configure({ adapter: new PreactAdapter() });
|
||||
|
||||
require('document-register-element/pony')(window);
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
beforeEach(() => {
|
||||
StaticStore.config = {
|
||||
@@ -2,4 +2,6 @@ import createMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const mockStore = createMockStore<any, any>([thunk]);
|
||||
const mockStore = createMockStore<any, any>([thunk]);
|
||||
|
||||
export default mockStore;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User } from '@app/common/types';
|
||||
import type { User } from 'common/types';
|
||||
|
||||
const user: Readonly<User> = {
|
||||
id: 'email_1',
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-ignore
|
||||
const mock: typeof import('@app/common/constants') = {
|
||||
...jest.requireActual('@app/common/constants'),
|
||||
const mock: typeof import('common/constants') = {
|
||||
...jest.requireActual('common/constants'),
|
||||
BASE_URL: 'https://demo.remark42.com/',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-ignore
|
||||
const mock: typeof import('@app/common/settings') = {
|
||||
...jest.requireActual('@app/common/settings'),
|
||||
const mock: typeof import('common/settings') = {
|
||||
...jest.requireActual('common/settings'),
|
||||
siteId: 'remark',
|
||||
pageTitle: 'remark test',
|
||||
url: 'https://remark42.com/test',
|
||||
|
||||
@@ -2,12 +2,12 @@ import jestFetchMock from 'jest-fetch-mock';
|
||||
|
||||
import { emailVerificationForSubscribe } from './api';
|
||||
|
||||
jest.mock('@app/common/constants', () => ({
|
||||
jest.mock('common/constants', () => ({
|
||||
BASE_URL: 'https://example.com',
|
||||
API_BASE: '/api',
|
||||
}));
|
||||
|
||||
jest.mock('@app/common/settings', () => ({
|
||||
jest.mock('common/settings', () => ({
|
||||
siteId: 'remark42',
|
||||
}));
|
||||
|
||||
@@ -29,11 +29,11 @@ describe('api', () => {
|
||||
expect(jestFetchMock.mock.calls.length).toEqual(1);
|
||||
|
||||
const url = jestFetchMock.mock.calls[0][0] as string;
|
||||
const match = url.match(/address=(\S+)$/);
|
||||
const match = url.match(/address=(\S+)$/) as string[];
|
||||
|
||||
expect(match).toBeArray();
|
||||
expect((match as string[]).length).toBeGreaterThan(1);
|
||||
expect((match as string[])[1]).toBe(
|
||||
expect(Array.isArray(match)).toBe(true);
|
||||
expect(match.length).toBeGreaterThan(1);
|
||||
expect(match[1]).toBe(
|
||||
"address.!%23%24%25%26'*%2B-%2F%3D%3F%5E_%60%7B%7C%7D~()%2C%3A%3B%3C%3E%5B%5C%5D%40example.com"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import fetcher from './fetcher';
|
||||
/* common */
|
||||
const __loginAnonymously = (username: string): Promise<User | null> => {
|
||||
const url = `/auth/anonymous/login?user=${encodeURIComponent(username)}&aud=${siteId}&from=${encodeURIComponent(
|
||||
location.origin + location.pathname + '?selfClose'
|
||||
`${window.location.origin}${window.location.pathname}?selfClose`
|
||||
)}`;
|
||||
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
|
||||
};
|
||||
@@ -35,7 +35,7 @@ export const logIn = (provider: AuthProvider): Promise<User | null> => {
|
||||
|
||||
return new Promise<User | null>((resolve, reject) => {
|
||||
const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent(
|
||||
location.origin + location.pathname + '?selfClose'
|
||||
`${window.location.origin}${window.location.pathname}?selfClose`
|
||||
)}&site=${siteId}`;
|
||||
|
||||
const newWindow = window.open(url);
|
||||
@@ -267,7 +267,7 @@ export const uploadImage = (image: File): Promise<Image> => {
|
||||
name: image.name,
|
||||
size: image.size,
|
||||
type: image.type,
|
||||
url: BASE_URL + API_BASE + '/picture/' + resp.id,
|
||||
url: `${BASE_URL + API_BASE}/picture/${resp.id}`,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
if (!Element.prototype.matches) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Element.prototype.matches = (Element.prototype as any).msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
||||
}
|
||||
|
||||
if (!Element.prototype.closest) {
|
||||
Element.prototype.closest = function (s: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-this-alias
|
||||
let el: any = this;
|
||||
|
||||
do {
|
||||
if (el.matches(s)) return el;
|
||||
el = el.parentElement || el.parentNode;
|
||||
} while (el !== null && el.nodeType === 1);
|
||||
return null;
|
||||
};
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { UserInfo, Theme } from './types';
|
||||
|
||||
export interface CounterConfig {
|
||||
host: string;
|
||||
site_id: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export type UserInfoConfig = UserInfo;
|
||||
|
||||
export interface CommentsConfig {
|
||||
host: string;
|
||||
site_id: string;
|
||||
url?: string;
|
||||
max_shown_comments?: number;
|
||||
theme?: Theme;
|
||||
page_title?: string;
|
||||
node?: string | HTMLElement;
|
||||
locale?: string;
|
||||
show_email_subscription?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
__colors__?: any;
|
||||
}
|
||||
|
||||
export interface LastCommentsConfig {
|
||||
host: string;
|
||||
site_id: string;
|
||||
max_last_comments: number;
|
||||
locale?: string;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
// based on https://github.com/sindresorhus/copy-text-to-clipboard, but improved to copy text styles too
|
||||
export default (input: string): boolean => {
|
||||
export default function copy(input: string): boolean {
|
||||
const el = document.createElement('div');
|
||||
|
||||
el.innerHTML = input;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(el.style as any).contain = 'strict';
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
el.style.fontSize = '12pt'; // Prevent zooming on iOS
|
||||
Object.assign(el.style, {
|
||||
contain: 'strict',
|
||||
position: 'absolute',
|
||||
left: '-9999px',
|
||||
fontSize: '12pt', // Prevent zooming on iOS
|
||||
});
|
||||
|
||||
document.body.appendChild(el);
|
||||
|
||||
@@ -60,4 +59,4 @@ export default (input: string): boolean => {
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ describe('fetcher', () => {
|
||||
error: 'you just cant',
|
||||
details: 'you just cant at all',
|
||||
};
|
||||
(window.fetch as any) = jest.fn().mockImplementation(async () => ({
|
||||
window.fetch = jest.fn().mockImplementation(async () => ({
|
||||
status: 400,
|
||||
headers: new (window as any).Headers(),
|
||||
headers: new Headers(),
|
||||
json: async () => response,
|
||||
text: async () => JSON.stringify(response),
|
||||
}));
|
||||
@@ -18,7 +18,7 @@ describe('fetcher', () => {
|
||||
return fetcher
|
||||
.get('/api/some')
|
||||
.then(data => {
|
||||
fail(data);
|
||||
throw new Error('Request should be failed');
|
||||
})
|
||||
.catch(e => {
|
||||
expect(e.code).toBe(2);
|
||||
@@ -40,7 +40,7 @@ describe('fetcher', () => {
|
||||
return fetcher
|
||||
.get('/api/some')
|
||||
.then(data => {
|
||||
fail(data);
|
||||
throw new Error('Request should be failed');
|
||||
})
|
||||
.catch(e => {
|
||||
expect(e.code).toBe(401);
|
||||
@@ -51,16 +51,18 @@ describe('fetcher', () => {
|
||||
(jest.spyOn(window, 'fetch') as any).mockImplementation(async () => ({
|
||||
status: 400,
|
||||
headers: new (window as any).Headers(),
|
||||
json: async () => {
|
||||
async json() {
|
||||
throw new Error('json parse error');
|
||||
},
|
||||
text: async () => 'you given me something wrong',
|
||||
async text() {
|
||||
return 'you given me something wrong';
|
||||
},
|
||||
}));
|
||||
|
||||
return fetcher
|
||||
.get({ url: '/api/some', logError: false })
|
||||
.then(data => {
|
||||
fail(data);
|
||||
throw new Error('Request should be failed');
|
||||
})
|
||||
.catch(e => {
|
||||
expect(e.code).toBe(0);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { httpErrorMap, isFailedFetch, httpMessages, RequestError } from 'utils/errorUtils';
|
||||
|
||||
import { BASE_URL, API_BASE, HEADER_X_JWT } from './constants';
|
||||
import { siteId } from './settings';
|
||||
import { StaticStore } from './static_store';
|
||||
import { StaticStore } from './static-store';
|
||||
import { getCookie } from './cookies';
|
||||
import { httpErrorMap, isFailedFetch, httpMessages } from '@app/utils/errorUtils';
|
||||
|
||||
export type FetcherMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
|
||||
const methods: FetcherMethod[] = ['get', 'post', 'put', 'patch', 'delete', 'head'];
|
||||
@@ -33,7 +34,7 @@ type FetcherObject = { [K in FetcherMethod]: <T = unknown>(data: FetcherInit) =>
|
||||
let activeJwtToken: string | undefined;
|
||||
|
||||
const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
acc[method] = <T = unknown>(data: FetcherInit): Promise<T> => {
|
||||
acc[method] = async <T = unknown>(data: FetcherInit): Promise<T> => {
|
||||
const {
|
||||
url,
|
||||
body = undefined,
|
||||
@@ -77,7 +78,7 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
}
|
||||
|
||||
if (siteId && method !== 'post' && !rurl.includes('?site=') && !rurl.includes('&site=')) {
|
||||
rurl += (rurl.includes('?') ? '&' : '?') + `site=${siteId}`;
|
||||
rurl += `${rurl.includes('?') ? '&' : '?'}site=${siteId}`;
|
||||
}
|
||||
|
||||
return fetch(rurl, parameters)
|
||||
@@ -99,10 +100,8 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
if (res.status >= 400) {
|
||||
if (httpErrorMap.has(res.status)) {
|
||||
const descriptor = httpErrorMap.get(res.status) || httpMessages.unexpectedError;
|
||||
throw {
|
||||
code: res.status,
|
||||
error: descriptor.defaultMessage,
|
||||
};
|
||||
|
||||
throw new RequestError(descriptor.defaultMessage, res.status);
|
||||
}
|
||||
return res.text().then(text => {
|
||||
let err;
|
||||
@@ -110,13 +109,10 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
err = JSON.parse(text);
|
||||
} catch (e) {
|
||||
if (logError) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
throw {
|
||||
code: 0,
|
||||
error: httpMessages.unexpectedError.defaultMessage,
|
||||
};
|
||||
|
||||
throw new RequestError(httpMessages.unexpectedError.defaultMessage, 0);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
@@ -130,11 +126,9 @@ const fetcher = methods.reduce<Partial<FetcherObject>>((acc, method) => {
|
||||
})
|
||||
.catch(e => {
|
||||
if (isFailedFetch(e)) {
|
||||
throw {
|
||||
code: -2,
|
||||
error: e.message,
|
||||
};
|
||||
throw new RequestError(e.message, -2);
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('updateJsonItem', () => {
|
||||
});
|
||||
|
||||
it('should set data to empty localStorage', () => {
|
||||
updateJsonItem<Record<string, string>>(LS_KEY, {});
|
||||
updateJsonItem(LS_KEY, {});
|
||||
|
||||
expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({}));
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { IS_STORAGE_AVAILABLE } from './constants';
|
||||
|
||||
const failMessage = 'remark42: localStorage access denied, check browser preferences';
|
||||
@@ -47,9 +46,9 @@ export function setJsonItem<T = any>(key: string, data: T) {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateJsonItem<T = Record<string, any> | any[]>(key: string, value: (data: T) => T): void;
|
||||
export function updateJsonItem<T = any[]>(key: string, value: T): void;
|
||||
export function updateJsonItem<T = Record<string, any>>(key: string, value: T) {
|
||||
export function updateJsonItem<T extends {}>(key: string, value: (data: T) => T): void;
|
||||
export function updateJsonItem<T extends {}>(key: string, value: T): void;
|
||||
export function updateJsonItem<T = Record<string, unknown>>(key: string, value: T) {
|
||||
const savedData = getJsonItem<any>(key);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import 'regenerator-runtime/runtime';
|
||||
import 'es6-promise/auto';
|
||||
import 'focus-visible';
|
||||
import '@webcomponents/custom-elements';
|
||||
import './closest-polyfill';
|
||||
|
||||
export default async function loadPolyfills() {
|
||||
function fillCoreJs() {
|
||||
if (
|
||||
'startsWith' in String.prototype &&
|
||||
'endsWith' in String.prototype &&
|
||||
'includes' in Array.prototype &&
|
||||
'assign' in Object &&
|
||||
'keys' in Object
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return import(/* webpackChunkName: "core-js" */ 'core-js');
|
||||
}
|
||||
|
||||
function fillFetch() {
|
||||
if ('fetch' in window) return;
|
||||
|
||||
return import(/* webpackChunkName: "whatwg-fetch" */ 'whatwg-fetch');
|
||||
}
|
||||
|
||||
function fillIntersectionObserver() {
|
||||
if (
|
||||
'IntersectionObserver' in window &&
|
||||
'IntersectionObserverEntry' in window &&
|
||||
'intersectionRatio' in window.IntersectionObserverEntry.prototype
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return import(/* webpackChunkName: "intersection-observer" */ 'intersection-observer');
|
||||
}
|
||||
|
||||
await Promise.all([fillCoreJs(), fillFetch(), fillIntersectionObserver()]);
|
||||
return;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Theme } from './types';
|
||||
import parseQuery from 'utils/parseQuery';
|
||||
|
||||
import type { Theme } from './types';
|
||||
import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants';
|
||||
import parseQuery from '@app/utils/parseQuery';
|
||||
|
||||
export interface QuerySettingsType {
|
||||
site_id?: string;
|
||||
@@ -29,9 +30,9 @@ if (!querySettings.theme || THEMES.indexOf(querySettings.theme) === -1) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
querySettings.show_email_subscription = (querySettings.show_email_subscription as any) !== 'false';
|
||||
|
||||
export const siteId = querySettings.site_id;
|
||||
export const siteId = querySettings.site_id!;
|
||||
export const pageTitle = querySettings.page_title;
|
||||
export const url = querySettings.url;
|
||||
export const maxShownComments = querySettings.max_shown_comments;
|
||||
export const token = querySettings.token;
|
||||
export const token = querySettings.token!;
|
||||
export const theme = querySettings.theme;
|
||||
|
||||
@@ -118,13 +118,6 @@ export interface Config {
|
||||
emoji_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface RemarkConfig {
|
||||
site_id: string;
|
||||
url: string;
|
||||
/** used in last comments widget */
|
||||
max_last_comments?: number;
|
||||
}
|
||||
|
||||
export type Sorting = '-time' | '+time' | '-active' | '+active' | '-score' | '+score' | '-controversy' | '+controversy';
|
||||
|
||||
export type AuthProvider =
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { UserInfo } from './types';
|
||||
import parseQuery from '@app/utils/parseQuery';
|
||||
import parseQuery from 'utils/parseQuery';
|
||||
|
||||
export const userInfo: Partial<UserInfo> = parseQuery();
|
||||
import type { UserInfo } from './types';
|
||||
|
||||
export const userInfo: UserInfo = parseQuery();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isDefaultPicture = ((userInfo.isDefaultPicture as any) as string) !== '1';
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.auth-panel__column:last-child {
|
||||
margin-left: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
.auth-panel__column {
|
||||
&:last-child {
|
||||
margin-left: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
.auth-panel_logged-in {
|
||||
font-size: 12px;
|
||||
|
||||
.auth-panel__column {
|
||||
& .auth-panel__column {
|
||||
&:first-child {
|
||||
font-weight: 400;
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.auth-panel_theme_dark {
|
||||
.auth-panel__user-id {
|
||||
& .auth-panel__user-id {
|
||||
color: var(--color5);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.auth-panel_theme_light {
|
||||
.auth-panel__user-id {
|
||||
& .auth-panel__user-id {
|
||||
color: var(--color13);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import createMockStore from 'redux-mock-store';
|
||||
import { Middleware } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import enMessages from '@app/locales/en.json';
|
||||
import enMessages from 'locales/en.json';
|
||||
|
||||
import AuthPanel, { Props } from './auth-panel';
|
||||
import { Button } from '../button';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
const DefaultProps = {
|
||||
providers: ['google', 'github'],
|
||||
@@ -83,7 +82,7 @@ describe('<AuthPanel />', () => {
|
||||
const firstCol = element.find('.auth-panel__column').first();
|
||||
const providerButtons = firstCol.find(Button);
|
||||
|
||||
expect(firstCol.text()).toStartWith('Login:');
|
||||
expect(firstCol.text().startsWith('Login:')).toBe(true);
|
||||
expect(providerButtons.at(0).text()).toBe('Google');
|
||||
expect(providerButtons.at(1).text()).toBe('GitHub');
|
||||
});
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, Fragment } from 'preact';
|
||||
import { h, Component, Fragment } from 'preact';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FormattedMessage, defineMessages, IntlShape, useIntl } from 'react-intl';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { User, AuthProvider, Sorting, Theme, PostInfo } from '@app/common/types';
|
||||
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from '@app/common/constants';
|
||||
import { requestDeletion } from '@app/utils/email';
|
||||
import postMessage from '@app/utils/postMessage';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { StoreState } from '@app/store';
|
||||
import { ProviderState } from '@app/store/provider/reducers';
|
||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
||||
import { Button } from '@app/components/button';
|
||||
import Auth from '@app/components/auth';
|
||||
import { User, AuthProvider, Sorting, Theme, PostInfo } from 'common/types';
|
||||
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants';
|
||||
import { requestDeletion } from 'utils/email';
|
||||
import postMessage from 'utils/postMessage';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { StoreState } from 'store';
|
||||
import { ProviderState } from 'store/provider/reducers';
|
||||
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||
import { Button } from 'components/button';
|
||||
import Auth from 'components/auth';
|
||||
|
||||
import useTheme from '@app/hooks/useTheme';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
export interface OwnProps {
|
||||
user: User | null;
|
||||
@@ -132,6 +131,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
className="auth-panel__pseudo-link"
|
||||
href={`${window.location.origin}/web/comments.html${window.location.search}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<FormattedMessage id="authPanel.new-page" defaultMessage="new page" />
|
||||
</a>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import './auth-panel.css';
|
||||
|
||||
import './__readonly-label/auth-panel__readonly-label.css';
|
||||
|
||||
import './__column/auth-panel__column.css';
|
||||
import './__select/auth-panel__select.css';
|
||||
import './__select-label/auth-panel__select-label.css';
|
||||
import './__select-label-value/auth-panel__select-label-value.css';
|
||||
import './__sort/auth-panel__sort.css';
|
||||
|
||||
import './__user-id/auth-panel__user-id.css';
|
||||
|
||||
import './_theme/_dark/auth-panel_theme_dark.css';
|
||||
import './_theme/_light/auth-panel_theme_light.css';
|
||||
|
||||
import './_logged-in/auth-panel_logged-in.css';
|
||||
|
||||
export { default } from './auth-panel';
|
||||
|
||||
import './auth-panel.scss';
|
||||
|
||||
import './__readonly-label/auth-panel__readonly-label.scss';
|
||||
|
||||
import './__column/auth-panel__column.scss';
|
||||
import './__select/auth-panel__select.scss';
|
||||
import './__select-label/auth-panel__select-label.scss';
|
||||
import './__select-label-value/auth-panel__select-label-value.scss';
|
||||
import './__sort/auth-panel__sort.scss';
|
||||
|
||||
import './__user-id/auth-panel__user-id.scss';
|
||||
|
||||
import './_theme/_dark/auth-panel_theme_dark.scss';
|
||||
import './_theme/_light/auth-panel_theme_light.scss';
|
||||
|
||||
import './_logged-in/auth-panel_logged-in.scss';
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { IntlShape, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { Theme } from '@app/common/types';
|
||||
import { Theme } from 'common/types';
|
||||
|
||||
import { Input } from '@app/components/input';
|
||||
import { Button } from '@app/components/button';
|
||||
import { Input } from 'components/input';
|
||||
import { Button } from 'components/button';
|
||||
|
||||
import { validateUserName } from '../validateUserName';
|
||||
|
||||
@@ -89,7 +88,7 @@ export class AnonymousLoginForm extends Component<Props, State> {
|
||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||
let className = b('auth-anonymous-login-form', {}, { theme: props.theme });
|
||||
if (props.className) {
|
||||
className += ' ' + b('auth-anonymous-login-form', {}, { theme: props.theme });
|
||||
className += ` ${b('auth-anonymous-login-form', {}, { theme: props.theme })}`;
|
||||
}
|
||||
|
||||
const usernameInvalidReason = this.getUsernameInvalidReason();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { AnonymousLoginForm } from './auth__anonymous-login-form';
|
||||
import './auth__anonymous-login-form.css';
|
||||
|
||||
import './auth__anonymous-login-form.scss';
|
||||
export { AnonymousLoginForm } from './auth__anonymous-login-form';
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { EmailLoginFormConnected as EmailLoginForm, Props, State } from './auth__email-login-form';
|
||||
import { User } from '@app/common/types';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
import { validToken } from '@app/testUtils/mocks/jwt';
|
||||
import { sendEmailVerificationRequest } from '@app/common/api';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import enMessages from '../../../locales/en.json';
|
||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
||||
|
||||
jest.mock('@app/utils/jwt', () => ({
|
||||
import { validToken } from '__stubs__/jwt';
|
||||
import { LS_EMAIL_KEY } from 'common/constants';
|
||||
import { User } from 'common/types';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import { sendEmailVerificationRequest } from 'common/api';
|
||||
import enMessages from 'locales/en.json';
|
||||
|
||||
jest.mock('utils/jwt', () => ({
|
||||
isJwtExpired: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => true)
|
||||
@@ -18,7 +18,7 @@ jest.mock('@app/utils/jwt', () => ({
|
||||
.mockImplementationOnce(() => true),
|
||||
}));
|
||||
|
||||
jest.mock('@app/common/api');
|
||||
jest.mock('common/api');
|
||||
|
||||
function simulateInput(input: ReactWrapper, value: string) {
|
||||
input.getDOMNode<HTMLTextAreaElement>().value = value;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme, User } from '@app/common/types';
|
||||
import { sendEmailVerificationRequest } from '@app/common/api';
|
||||
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
|
||||
import { Input } from '@app/components/input';
|
||||
import { Button } from '@app/components/button';
|
||||
import { isJwtExpired } from '@app/utils/jwt';
|
||||
import { Theme, User } from 'common/types';
|
||||
import { LS_EMAIL_KEY } from 'common/constants';
|
||||
import { sendEmailVerificationRequest } from 'common/api';
|
||||
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
||||
import { Input } from 'components/input';
|
||||
import { Button } from 'components/button';
|
||||
import { isJwtExpired } from 'utils/jwt';
|
||||
import { defineMessages, IntlShape, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { validateUserName } from '../validateUserName';
|
||||
|
||||
import { messages as loginForm } from '../__anonymous-login-form/auth__anonymous-login-form';
|
||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
||||
|
||||
interface OwnProps {
|
||||
onSignIn(token: string): Promise<User | null>;
|
||||
@@ -199,7 +197,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||
let className = b('auth-email-login-form', {}, { theme: props.theme });
|
||||
if (props.className) {
|
||||
className += ' ' + b('auth-email-login-form', {}, { theme: props.theme });
|
||||
className += ` ${b('auth-email-login-form', {}, { theme: props.theme })}`;
|
||||
}
|
||||
|
||||
const form1InvalidReason = this.getForm1InvalidReason();
|
||||
@@ -208,7 +206,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
return (
|
||||
<form className={className} onSubmit={this.onVerificationSubmit}>
|
||||
<Input
|
||||
autoFocus
|
||||
autofocus
|
||||
name="username"
|
||||
mix="auth-email-login-form__input"
|
||||
ref={this.usernameInputRef}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import './auth__email-login-form.scss';
|
||||
import './auth__email-login-form.css';
|
||||
|
||||
export { EmailLoginForm, EmailLoginFormConnected, EmailLoginFormRef } from './auth__email-login-form';
|
||||
export { EmailLoginForm, EmailLoginFormConnected } from './auth__email-login-form';
|
||||
export type { EmailLoginFormRef } from './auth__email-login-form';
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import enMessages from '@app/locales/en.json';
|
||||
import { mockStore } from '@app/testUtils/mockStore';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import enMessages from 'locales/en.json';
|
||||
import stubStore from '__stubs__/store';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
import Auth from './auth';
|
||||
import { mount } from 'enzyme';
|
||||
import { Button } from '../button';
|
||||
import { StoreState } from '@app/store';
|
||||
import { StoreState } from 'store';
|
||||
|
||||
const initialStore = {
|
||||
provider: { name: 'google' },
|
||||
@@ -24,7 +23,7 @@ describe('<Auth/>', () => {
|
||||
const createWrapper = (store?: Partial<StoreState>) =>
|
||||
mount(
|
||||
<IntlProvider locale="en" messages={enMessages}>
|
||||
<Provider store={mockStore(store || initialStore)}>
|
||||
<Provider store={stubStore(store || initialStore)}>
|
||||
<Auth />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import { h, Component, createRef } from 'preact';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
import { IntlShape, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { AuthProvider, Theme, User } from '@app/common/types';
|
||||
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from '@app/common/constants';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { Button } from '@app/components/button';
|
||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
||||
import { AuthProvider, Theme, User } from 'common/types';
|
||||
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from 'common/constants';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { Button } from 'components/button';
|
||||
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||
|
||||
import debounce from '@app/utils/debounce';
|
||||
import { ProviderState } from '@app/store/provider/reducers';
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import debounce from 'utils/debounce';
|
||||
import { ProviderState } from 'store/provider/reducers';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { StoreState } from '@app/store';
|
||||
import useTheme from '@app/hooks/useTheme';
|
||||
import { logIn } from '@app/store/user/actions';
|
||||
import { StoreState } from 'store';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { logIn } from 'store/user/actions';
|
||||
|
||||
import { AnonymousLoginForm } from './__anonymous-login-form';
|
||||
import { EmailLoginFormConnected, EmailLoginFormRef } from './__email-login-form';
|
||||
|
||||
import styles from './auth.module.pcss';
|
||||
import styles from './auth.module.css';
|
||||
|
||||
interface Props {
|
||||
intl: IntlShape;
|
||||
@@ -186,13 +185,13 @@ const authPanelMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
export default function () {
|
||||
export default function AuthWrapper() {
|
||||
const dispatch = useDispatch();
|
||||
const provider = useSelector<StoreState, ProviderState>(store => store.provider);
|
||||
const user = useSelector<StoreState, User | null>(store => store.user);
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const handleSignin = useCallback((provider: AuthProvider) => dispatch(logIn(provider)), []);
|
||||
const handleSignin = useCallback((provider: AuthProvider) => dispatch(logIn(provider)), [dispatch]);
|
||||
|
||||
return <Auth provider={provider} theme={theme} onSignIn={handleSignin} intl={intl} user={user} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX } from 'preact';
|
||||
import { h, JSX } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
import { Theme } from 'common/types';
|
||||
|
||||
interface Props {
|
||||
picture?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { AvatarIcon } from './avatar-icon';
|
||||
import './avatar-icon.css';
|
||||
import './_default/avatar-icon_default.css';
|
||||
|
||||
import './avatar-icon.scss';
|
||||
import './_default/avatar-icon_default.scss';
|
||||
export { AvatarIcon } from './avatar-icon';
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX } from 'preact';
|
||||
import { h, JSX } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import b, { Mods, Mix } from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
|
||||
interface Props extends Omit<JSX.HTMLAttributes, 'size' | 'className'> {
|
||||
import type { Theme } from 'common/types';
|
||||
|
||||
export type ButtonProps = Omit<JSX.HTMLAttributes, 'size' | 'className'> & {
|
||||
kind?: 'primary' | 'secondary' | 'link';
|
||||
size?: 'middle' | 'large';
|
||||
theme?: Theme;
|
||||
mods?: Mods;
|
||||
mix?: Mix;
|
||||
type?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||
({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => {
|
||||
const className = b('button', { mods: { kind, size }, mix }, { theme, ...mods });
|
||||
|
||||
return (
|
||||
<button className={className} type={type} {...props} ref={ref}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => (
|
||||
<button className={b('button', { mods: { kind, size, theme }, mix }, { ...mods })} type={type} {...props} ref={ref}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import './button.css';
|
||||
|
||||
import './_kind/_link/button_kind_link.css';
|
||||
import './_kind/_primary/button_kind_primary.css';
|
||||
import './_kind/_secondary/button_kind_secondary.css';
|
||||
|
||||
import './_size/_large/button_size_large.css';
|
||||
import './_size/_middle/button_size_middle.css';
|
||||
|
||||
import './_theme/_dark/button_theme_dark.css';
|
||||
|
||||
export { Button } from './button';
|
||||
|
||||
import './button.scss';
|
||||
|
||||
import './_kind/_link/button_kind_link.scss';
|
||||
import './_kind/_primary/button_kind_primary.scss';
|
||||
import './_kind/_secondary/button_kind_secondary.scss';
|
||||
|
||||
import './_size/_large/button_size_large.scss';
|
||||
import './_size/_middle/button_size_middle.scss';
|
||||
|
||||
import './_theme/_dark/button_theme_dark.scss';
|
||||
|
||||
+7
-10
@@ -1,20 +1,17 @@
|
||||
.comment-form__field {
|
||||
$lineHeight: 1.4;
|
||||
$fontSize: 16px;
|
||||
$paddingVrt: 10px;
|
||||
$lines: 4;
|
||||
$height: calc($fontSize * $lineHeight * $lines + $paddingVrt * 2);
|
||||
/* font-size * line-height * lines * vertical-padding */
|
||||
--height: calc(16px * 1.4 * 4 + 10px * 2);
|
||||
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: $height;
|
||||
min-height: $height;
|
||||
padding: $paddingVrt 12px;
|
||||
height: var(--height);
|
||||
min-height: var(--height);
|
||||
padding: 10px 12px;
|
||||
margin: 0;
|
||||
font-family: 'PT Sans', Helvetica, Arial, sans-serif;
|
||||
font-size: $fontSize;
|
||||
line-height: $lineHeight;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
border: 0;
|
||||
resize: none;
|
||||
backface-visibility: hidden; /* let's try to fix blinking in Safari */
|
||||
+44
-45
@@ -1,26 +1,35 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'preact/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Middleware } from 'redux';
|
||||
import createMockStore from 'redux-mock-store';
|
||||
|
||||
import '@app/testUtils/mockApi';
|
||||
import { user, anonymousUser } from '@app/testUtils/mocks/user';
|
||||
import { validToken } from '@app/testUtils/mocks/jwt';
|
||||
|
||||
import * as api from '@app/common/api';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
import { Input } from '@app/components/input';
|
||||
import { Button } from '@app/components/button';
|
||||
import { Dropdown } from '@app/components/dropdown';
|
||||
import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import enMessages from '../../../locales/en.json';
|
||||
|
||||
import { SubscribeByEmail, SubscribeByEmailForm } from './';
|
||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
||||
jest.mock('common/api');
|
||||
|
||||
import { user, anonymousUser } from '__stubs__/user';
|
||||
import { validToken } from '__stubs__/jwt';
|
||||
import { emailVerificationForSubscribe, emailConfirmationForSubscribe, unsubscribeFromEmailUpdates } from 'common/api';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import { Input } from 'components/input';
|
||||
import { Button } from 'components/button';
|
||||
import { Dropdown } from 'components/dropdown';
|
||||
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
||||
import enMessages from 'locales/en.json';
|
||||
import { LS_EMAIL_KEY } from 'common/constants';
|
||||
|
||||
import { SubscribeByEmail, SubscribeByEmailForm } from '.';
|
||||
|
||||
const emailVerificationForSubscribeMock = (emailVerificationForSubscribe as unknown) as jest.Mock<
|
||||
ReturnType<typeof emailVerificationForSubscribe>
|
||||
>;
|
||||
const emailConfirmationForSubscribeMock = (emailConfirmationForSubscribe as unknown) as jest.Mock<
|
||||
ReturnType<typeof emailConfirmationForSubscribe>
|
||||
>;
|
||||
const unsubscribeFromEmailUpdatesMock = (unsubscribeFromEmailUpdates as unknown) as jest.Mock<
|
||||
ReturnType<typeof unsubscribeFromEmailUpdates>
|
||||
>;
|
||||
|
||||
const initialStore = {
|
||||
user,
|
||||
@@ -36,7 +45,7 @@ const makeInputEvent = (value: string) => ({
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('@app/utils/jwt', () => ({
|
||||
jest.mock('utils/jwt', () => ({
|
||||
isJwtExpired: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
@@ -94,49 +103,40 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
const wrapper = createWrapper(store);
|
||||
|
||||
expect(wrapper.find('.comment-form__subscribe-by-email_subscribed')).toHaveLength(1);
|
||||
expect(wrapper.text()).toStartWith('You are subscribed on updates by email');
|
||||
expect(wrapper.text().startsWith('You are subscribed on updates by email')).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass throw subscribe process', async () => {
|
||||
const wrapper = createWrapper();
|
||||
|
||||
const emailVerificationForSubscribe = jest.spyOn(api, 'emailVerificationForSubscribe');
|
||||
const emailConfirmationForSubscribe = jest.spyOn(api, 'emailConfirmationForSubscribe');
|
||||
const onInputEmail = wrapper.find(Input).prop('onInput');
|
||||
const input = wrapper.find('input');
|
||||
const form = wrapper.find('form');
|
||||
|
||||
expect(onInputEmail).toBeFunction();
|
||||
|
||||
act(() => onInputEmail(makeInputEvent('some@email.com')));
|
||||
|
||||
expect(form).toHaveLength(1);
|
||||
|
||||
input.getDOMNode<HTMLInputElement>().value = 'some@email.com';
|
||||
input.simulate('input');
|
||||
form.simulate('submit');
|
||||
|
||||
expect(emailVerificationForSubscribe).toHaveBeenCalledWith('some@email.com');
|
||||
expect(emailVerificationForSubscribeMock).toHaveBeenCalledWith('some@email.com');
|
||||
|
||||
await sleep(0);
|
||||
await sleep();
|
||||
wrapper.update();
|
||||
|
||||
const textarea = wrapper.find(TextareaAutosize);
|
||||
const onInputToken = textarea.prop('onInput') as (e: any) => void;
|
||||
const button = wrapper.find(Button);
|
||||
const textarea = wrapper.find('textarea');
|
||||
const button = wrapper.find('button');
|
||||
|
||||
expect(textarea).toHaveLength(1);
|
||||
expect(onInputToken).toBeFunction();
|
||||
expect(button.at(0).text()).toEqual('Back');
|
||||
expect(button.at(1).text()).toEqual('Subscribe');
|
||||
|
||||
act(() => onInputToken(makeInputEvent('tokentokentoken')));
|
||||
textarea.getDOMNode<HTMLTextAreaElement>().value = 'tokentokentoken';
|
||||
textarea.simulate('input');
|
||||
form.simulate('submit');
|
||||
|
||||
wrapper.find('form').simulate('submit');
|
||||
|
||||
expect(emailConfirmationForSubscribe).toHaveBeenCalledWith('tokentokentoken');
|
||||
expect(emailConfirmationForSubscribeMock).toHaveBeenCalledWith('tokentokentoken');
|
||||
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.text()).toStartWith('You have been subscribed on updates by email');
|
||||
expect(wrapper.text().startsWith('You have been subscribed on updates by email')).toBe(true);
|
||||
expect(wrapper.find(Button).text()).toEqual('Unsubscribe');
|
||||
});
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
const onInputEmail = wrapper.find(Input).prop('onInput');
|
||||
const form = wrapper.find('form');
|
||||
|
||||
expect(onInputEmail).toBeFunction();
|
||||
expect(typeof onInputEmail === 'function').toBe(true);
|
||||
|
||||
act(() => onInputEmail(makeInputEvent('some@email.com')));
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.text()).toStartWith('You have been subscribed on updates by email');
|
||||
expect(wrapper.text().startsWith('You have been subscribed on updates by email')).toBe(true);
|
||||
expect(wrapper.find(Button).text()).toEqual('Unsubscribe');
|
||||
});
|
||||
|
||||
@@ -177,18 +177,17 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
const store = mockStore({ ...initialStore, user: { email_subscription: true } });
|
||||
const wrapper = createWrapper(store);
|
||||
const onClick = wrapper.find(Button).prop('onClick');
|
||||
const unsubscribeFromEmailUpdates = jest.spyOn(api, 'unsubscribeFromEmailUpdates');
|
||||
|
||||
expect(onClick).toBeFunction();
|
||||
expect(typeof onClick === 'function').toBe(true);
|
||||
|
||||
act(() => onClick());
|
||||
|
||||
expect(unsubscribeFromEmailUpdates).toHaveBeenCalled();
|
||||
expect(unsubscribeFromEmailUpdatesMock).toHaveBeenCalled();
|
||||
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.text()).toStartWith('You have been unsubscribed by email to updates');
|
||||
expect(wrapper.text().startsWith('You have been unsubscribed by email to updates')).toBe(true);
|
||||
expect(wrapper.find(Button).text()).toEqual('Close');
|
||||
});
|
||||
});
|
||||
|
||||
+37
-44
@@ -1,30 +1,25 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, FunctionComponent, Fragment } from 'preact';
|
||||
import { h, FunctionComponent, Fragment } from 'preact';
|
||||
import { useState, useCallback, useEffect, useRef, PropRef } from 'preact/hooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { User } from '@app/common/types';
|
||||
import { StoreState } from '@app/store';
|
||||
import { setUserSubscribed } from '@app/store/user/actions';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
|
||||
import useTheme from '@app/hooks/useTheme';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import {
|
||||
emailVerificationForSubscribe,
|
||||
emailConfirmationForSubscribe,
|
||||
unsubscribeFromEmailUpdates,
|
||||
} from '@app/common/api';
|
||||
import { Input } from '@app/components/input';
|
||||
import { Button } from '@app/components/button';
|
||||
import { Dropdown } from '@app/components/dropdown';
|
||||
import { Preloader } from '@app/components/preloader';
|
||||
import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
|
||||
import { isUserAnonymous } from '@app/utils/isUserAnonymous';
|
||||
import { isJwtExpired } from '@app/utils/jwt';
|
||||
import { useIntl, defineMessages, IntlShape, FormattedMessage } from 'react-intl';
|
||||
import { LS_EMAIL_KEY } from '@app/common/constants';
|
||||
|
||||
import { User } from 'common/types';
|
||||
import { LS_EMAIL_KEY } from 'common/constants';
|
||||
import { StoreState } from 'store';
|
||||
import { setUserSubscribed } from 'store/user/actions';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { emailVerificationForSubscribe, emailConfirmationForSubscribe, unsubscribeFromEmailUpdates } from 'common/api';
|
||||
import { Input } from 'components/input';
|
||||
import { Button } from 'components/button';
|
||||
import { Dropdown } from 'components/dropdown';
|
||||
import Preloader from 'components/preloader';
|
||||
import TextareaAutosize from 'components/comment-form/textarea-autosize';
|
||||
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
||||
import { isJwtExpired } from 'utils/jwt';
|
||||
|
||||
const emailRegex = /[^@]+@[^.]+\..+/;
|
||||
|
||||
@@ -105,7 +100,7 @@ const renderTokenPart = (
|
||||
handleChangeToken: (e: Event) => void,
|
||||
setEmailStep: () => void
|
||||
) => (
|
||||
<Fragment>
|
||||
<>
|
||||
<Button kind="link" mix="auth-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||
<FormattedMessage id="subscribeByEmail.back" defaultMessage="Back" />
|
||||
</Button>
|
||||
@@ -117,7 +112,7 @@ const renderTokenPart = (
|
||||
disabled={loading}
|
||||
value={token}
|
||||
/>
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
|
||||
export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
@@ -165,7 +160,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[setLoading, setError, setStep, step, emailAddress, token]
|
||||
[setLoading, setError, setStep, step, emailAddress, token, dispatch, intl]
|
||||
);
|
||||
|
||||
const handleChangeEmail = useCallback((e: Event) => {
|
||||
@@ -193,7 +188,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
|
||||
setToken(value);
|
||||
},
|
||||
[sendForm, setError, setToken]
|
||||
[sendForm, setError, setToken, intl]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
@@ -212,6 +207,20 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
setStep(Step.Email);
|
||||
}, [setStep]);
|
||||
|
||||
const handleUnsubscribe = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await unsubscribeFromEmailUpdates();
|
||||
dispatch(setUserSubscribed(false));
|
||||
previousStep.current = Step.Subscribed;
|
||||
setStep(Step.Unsubscribed);
|
||||
} catch (e) {
|
||||
setError(extractErrorMessageFromResponse(e, intl));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [setLoading, setStep, setError, dispatch, intl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (emailAddressRef.current) {
|
||||
emailAddressRef.current.focus();
|
||||
@@ -227,20 +236,6 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
}
|
||||
|
||||
if (step === Step.Subscribed) {
|
||||
const handleUnsubscribe = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await unsubscribeFromEmailUpdates();
|
||||
dispatch(setUserSubscribed(false));
|
||||
previousStep.current = Step.Subscribed;
|
||||
setStep(Step.Unsubscribed);
|
||||
} catch (e) {
|
||||
setError(extractErrorMessageFromResponse(e, intl));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [setLoading, setStep, setError]);
|
||||
|
||||
const text =
|
||||
previousStep.current === Step.Token
|
||||
? intl.formatMessage(messages.haveSubscribed)
|
||||
@@ -318,9 +313,7 @@ export const SubscribeByEmail: FunctionComponent = () => {
|
||||
const intl = useIntl();
|
||||
const user = useSelector<StoreState, User | null>(({ user }) => user);
|
||||
const isAnonymous = isUserAnonymous(user);
|
||||
const buttonTitle = isAnonymous
|
||||
? intl.formatMessage(messages.onlyRegisteredUsers)
|
||||
: intl.formatMessage(messages.subscribeByEmail);
|
||||
const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByEmail);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
|
||||
+11
-15
@@ -1,37 +1,33 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { shallow } from 'enzyme';
|
||||
import enMessages from '../../../locales/en.json';
|
||||
|
||||
import { SubscribeByRSS, createSubscribeUrl } from './';
|
||||
import { SubscribeByRSS, createSubscribeUrl } from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn(fn => fn({ theme: 'light' })),
|
||||
}));
|
||||
|
||||
jest.mock('react-intl', () => {
|
||||
// Require the original module to not be mocked...
|
||||
const originalModule = jest.requireActual('react-intl');
|
||||
const reactIntl = jest.requireActual('react-intl');
|
||||
const messages = require('locales/en.json');
|
||||
const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {});
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useIntl: () => originalModule.createIntl({ locale: `en`, messages: enMessages }),
|
||||
...reactIntl,
|
||||
useIntl: () => intlProvider.state.intl,
|
||||
};
|
||||
});
|
||||
|
||||
describe('<SubscribeByRSS/>', () => {
|
||||
let wrapper: ReturnType<typeof shallow>;
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||
});
|
||||
|
||||
it('should be render links in dropdown', () => {
|
||||
wrapper.update();
|
||||
const wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||
|
||||
expect(wrapper.find('.comment-form__rss-dropdown__link')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should have userId in replies link', () => {
|
||||
const wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||
|
||||
expect(wrapper.find('.comment-form__rss-dropdown__link').at(2).prop('href')).toBe(
|
||||
createSubscribeUrl('reply', '&user=user-1')
|
||||
);
|
||||
|
||||
+8
-9
@@ -1,14 +1,13 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, FunctionComponent } from 'preact';
|
||||
import { h, FunctionComponent } from 'preact';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import useTheme from '@app/hooks/useTheme';
|
||||
import { siteId, url } from '@app/common/settings';
|
||||
import { BASE_URL, API_BASE } from '@app/common/constants';
|
||||
import { Dropdown, DropdownItem } from '@app/components/dropdown';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { siteId, url } from 'common/settings';
|
||||
import { BASE_URL, API_BASE } from 'common/constants';
|
||||
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||
|
||||
export const createSubscribeUrl = (type: 'post' | 'site' | 'reply', urlParams: string = '') =>
|
||||
export const createSubscribeUrl = (type: 'post' | 'site' | 'reply', urlParams = '') =>
|
||||
`${BASE_URL}${API_BASE}/rss/${type}?site=${siteId}${urlParams}`;
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -43,7 +42,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u
|
||||
[createSubscribeUrl('site'), intl.formatMessage(messages.site)],
|
||||
[createSubscribeUrl('reply', `&user=${userId}`), intl.formatMessage(messages.replies)],
|
||||
],
|
||||
[userId]
|
||||
[userId, intl]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -56,7 +55,7 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u
|
||||
>
|
||||
{items.map(([href, label]) => (
|
||||
<DropdownItem>
|
||||
<a href={href} className="comment-form__rss-dropdown__link" target="_blank">
|
||||
<a href={href} className="comment-form__rss-dropdown__link" target="_blank" rel="noreferrer">
|
||||
{label}
|
||||
</a>
|
||||
</DropdownItem>
|
||||
|
||||
+9
-9
@@ -2,41 +2,41 @@
|
||||
border-color: var(--color7);
|
||||
background: var(--color8); /* try to fix textarea blinking in Safari */
|
||||
|
||||
.comment-form__actions {
|
||||
& .comment-form__actions {
|
||||
background: var(--color7);
|
||||
}
|
||||
|
||||
.comment-form__button_type_preview {
|
||||
& .comment-form__button_type_preview {
|
||||
background: var(--color8);
|
||||
color: var(--color20);
|
||||
}
|
||||
|
||||
.comment-form__button_type_send {
|
||||
& .comment-form__button_type_send {
|
||||
color: var(--color20);
|
||||
}
|
||||
|
||||
.comment-form__error {
|
||||
& .comment-form__error {
|
||||
border-top: 8px solid var(--color7);
|
||||
background: var(--color28);
|
||||
color: var(--color27);
|
||||
}
|
||||
|
||||
.comment-form__field {
|
||||
& .comment-form__field {
|
||||
background: var(--color8);
|
||||
color: var(--color5);
|
||||
}
|
||||
|
||||
.comment-form__preview {
|
||||
& .comment-form__preview {
|
||||
border-color: var(--color8);
|
||||
background: var(--color8);
|
||||
color: var(--color20);
|
||||
}
|
||||
|
||||
.comment-form__preview-wrapper {
|
||||
& .comment-form__preview-wrapper {
|
||||
background: var(--color7);
|
||||
}
|
||||
|
||||
.comment-form__toolbar-item {
|
||||
& .comment-form__toolbar-item {
|
||||
color: var(--color20);
|
||||
|
||||
&:hover {
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form__control-panel {
|
||||
& .comment-form__control-panel {
|
||||
background-color: var(--color7);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user