@skroz/frontend 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENCE.md +21 -0
  2. package/dist/auth/Auth.d.ts +8 -0
  3. package/dist/auth/Auth.js +52 -0
  4. package/dist/auth/AuthFooterLinks.d.ts +4 -0
  5. package/dist/auth/AuthFooterLinks.js +26 -0
  6. package/dist/auth/Forgot.d.ts +7 -0
  7. package/dist/auth/Forgot.js +68 -0
  8. package/dist/auth/Login.d.ts +8 -0
  9. package/dist/auth/Login.js +65 -0
  10. package/dist/auth/LoginForm.d.ts +6 -0
  11. package/dist/auth/LoginForm.js +48 -0
  12. package/dist/auth/RecoverPassword.d.ts +2 -0
  13. package/dist/auth/RecoverPassword.js +96 -0
  14. package/dist/auth/Register.d.ts +8 -0
  15. package/dist/auth/Register.js +68 -0
  16. package/dist/auth/ResendLinkButton.d.ts +11 -0
  17. package/dist/auth/ResendLinkButton.js +50 -0
  18. package/dist/auth/index.d.ts +8 -0
  19. package/dist/auth/index.js +22 -0
  20. package/dist/graphql/ForgotPasswordMutation.graphql.d.ts +24 -0
  21. package/dist/graphql/ForgotPasswordMutation.graphql.js +76 -0
  22. package/dist/graphql/LoginMutation.graphql.d.ts +26 -0
  23. package/dist/graphql/LoginMutation.graphql.js +69 -0
  24. package/dist/graphql/RegisterMutation.graphql.d.ts +26 -0
  25. package/dist/graphql/RegisterMutation.graphql.js +69 -0
  26. package/dist/graphql/ResendLinkButtonMutation.graphql.d.ts +25 -0
  27. package/dist/graphql/ResendLinkButtonMutation.graphql.js +76 -0
  28. package/dist/graphql/index.d.ts +5 -0
  29. package/dist/graphql/index.js +16 -0
  30. package/dist/graphql/recoveryMutation.graphql.d.ts +19 -0
  31. package/dist/graphql/recoveryMutation.graphql.js +67 -0
  32. package/dist/index.d.ts +4 -0
  33. package/dist/index.js +20 -0
  34. package/dist/ui/AreYouSure.d.ts +10 -0
  35. package/dist/ui/AreYouSure.js +43 -0
  36. package/dist/ui/FormError.d.ts +3 -0
  37. package/dist/ui/FormError.js +15 -0
  38. package/dist/ui/FormItem.d.ts +12 -0
  39. package/dist/ui/FormItem.js +27 -0
  40. package/dist/ui/H.d.ts +16 -0
  41. package/dist/ui/H.js +39 -0
  42. package/dist/ui/Panel.d.ts +16 -0
  43. package/dist/ui/Panel.js +24 -0
  44. package/dist/ui/SeoHead.d.ts +13 -0
  45. package/dist/ui/SeoHead.js +14 -0
  46. package/dist/ui/index.d.ts +6 -0
  47. package/dist/ui/index.js +18 -0
  48. package/dist/utils/FrontendContext.d.ts +14 -0
  49. package/dist/utils/FrontendContext.js +30 -0
  50. package/dist/utils/getError.d.ts +11 -0
  51. package/dist/utils/getError.js +73 -0
  52. package/dist/utils/handleFormErrors.d.ts +15 -0
  53. package/dist/utils/handleFormErrors.js +62 -0
  54. package/dist/utils/index.d.ts +5 -0
  55. package/dist/utils/index.js +28 -0
  56. package/dist/utils/isObject.d.ts +2 -0
  57. package/dist/utils/isObject.js +6 -0
  58. package/dist/utils/limitExpiresAt.d.ts +3 -0
  59. package/dist/utils/limitExpiresAt.js +19 -0
  60. package/package.json +48 -0
  61. package/src/auth/Auth.tsx +76 -0
  62. package/src/auth/AuthFooterLinks.tsx +27 -0
  63. package/src/auth/Forgot.tsx +122 -0
  64. package/src/auth/Login.tsx +115 -0
  65. package/src/auth/LoginForm.tsx +74 -0
  66. package/src/auth/RecoverPassword.tsx +185 -0
  67. package/src/auth/Register.tsx +174 -0
  68. package/src/auth/ResendLinkButton.tsx +71 -0
  69. package/src/auth/index.ts +8 -0
  70. package/src/graphql/ForgotPasswordMutation.graphql.ts +100 -0
  71. package/src/graphql/LoginMutation.graphql.ts +95 -0
  72. package/src/graphql/RegisterMutation.graphql.ts +95 -0
  73. package/src/graphql/ResendLinkButtonMutation.graphql.ts +101 -0
  74. package/src/graphql/index.ts +5 -0
  75. package/src/graphql/recoveryMutation.graphql.ts +91 -0
  76. package/src/index.ts +4 -0
  77. package/src/locales/ru/common.json +271 -0
  78. package/src/styles/auth.less +142 -0
  79. package/src/styles/colors.less +55 -0
  80. package/src/styles/components.less +2 -0
  81. package/src/styles/panels.less +61 -0
  82. package/src/styles/sizes.less +92 -0
  83. package/src/ui/AreYouSure.tsx +55 -0
  84. package/src/ui/FormError.tsx +21 -0
  85. package/src/ui/FormItem.tsx +60 -0
  86. package/src/ui/H.tsx +76 -0
  87. package/src/ui/Panel.tsx +44 -0
  88. package/src/ui/SeoHead.tsx +69 -0
  89. package/src/ui/index.ts +6 -0
  90. package/src/utils/FrontendContext.tsx +30 -0
  91. package/src/utils/getError.ts +101 -0
  92. package/src/utils/handleFormErrors.ts +77 -0
  93. package/src/utils/index.ts +5 -0
  94. package/src/utils/isObject.ts +4 -0
  95. package/src/utils/limitExpiresAt.ts +14 -0
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { useExistingForm } from '@os-design/form';
3
+ import { Alert } from 'antd';
4
+ import FormItem from './FormItem';
5
+
6
+ const FormError: React.FC = () => {
7
+ const { useError } = useExistingForm();
8
+ const error = useError('_error'); // Use any name that is not used by any field
9
+ return error ? (
10
+ <FormItem>
11
+ <Alert
12
+ type='error'
13
+ message={error}
14
+ style={{ whiteSpace: 'break-spaces' }}
15
+ className='allow-text-selection'
16
+ />
17
+ </FormItem>
18
+ ) : null;
19
+ };
20
+
21
+ export default FormError;
@@ -0,0 +1,60 @@
1
+ import React, { CSSProperties } from 'react';
2
+ import { Form } from 'antd';
3
+ import { useTranslation } from 'next-i18next';
4
+
5
+ interface FormItemProps {
6
+ children?: React.ReactNode;
7
+ label?: React.ReactNode;
8
+ className?: string;
9
+ error?: string | undefined;
10
+ help?: React.ReactNode;
11
+ isRequired?: boolean;
12
+ style?: CSSProperties;
13
+ }
14
+
15
+ const FormItem: React.FC<FormItemProps> = ({
16
+ children,
17
+ label,
18
+ error,
19
+ help,
20
+ className,
21
+ isRequired = false,
22
+ style,
23
+ }) => {
24
+ const { t } = useTranslation('common');
25
+
26
+ return (
27
+ <Form.Item
28
+ style={style}
29
+ className={className}
30
+ label={
31
+ label ? (
32
+ <span
33
+ style={{
34
+ width: '100%',
35
+ display: 'flex',
36
+ justifyContent: 'space-between',
37
+ alignItems: 'center',
38
+ }}
39
+ >
40
+ <span>
41
+ {label}
42
+ {isRequired && (
43
+ <span style={{ color: 'red', marginLeft: 5 }} title={t('formItem.required')}>
44
+ *
45
+ </span>
46
+ )}
47
+ </span>
48
+ <span>{help}</span>
49
+ </span>
50
+ ) : null
51
+ }
52
+ help={error}
53
+ validateStatus={error !== undefined ? 'error' : undefined}
54
+ >
55
+ {children}
56
+ </Form.Item>
57
+ );
58
+ };
59
+
60
+ export default FormItem;
package/src/ui/H.tsx ADDED
@@ -0,0 +1,76 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Button } from 'antd';
3
+ import { LeftOutlined } from '@ant-design/icons';
4
+ import Link from 'next/link';
5
+ import classNames from 'classnames';
6
+
7
+ interface HProps {
8
+ type: 'h1' | 'h2' | 'h3' | 'h4';
9
+ onBack?: () => void; // если функция, например router.back()
10
+ backLink?: string; // если ссылкой
11
+ children: React.ReactNode;
12
+ extra?: React.ReactNode;
13
+ subHeader?: React.ReactNode;
14
+ preHeader?: React.ReactNode;
15
+ prefix?: React.ReactNode;
16
+ postfix?: React.ReactNode;
17
+ textAlign?: 'left' | 'center' | 'right';
18
+ className?: string; // Add className prop support
19
+ }
20
+
21
+ const H: React.FC<HProps> = ({
22
+ onBack,
23
+ children,
24
+ type,
25
+ extra,
26
+ subHeader,
27
+ backLink,
28
+ prefix,
29
+ postfix,
30
+ preHeader,
31
+ textAlign = 'left',
32
+ className,
33
+ }) => {
34
+ const header = useMemo(() => {
35
+ if (type === 'h1') return <h1>{children}</h1>;
36
+ if (type === 'h2') return <h2>{children}</h2>;
37
+ if (type === 'h3') return <h3>{children}</h3>;
38
+ if (type === 'h4') return <h4>{children}</h4>;
39
+ return null;
40
+ }, [children, type]);
41
+
42
+ const backButton = useMemo(
43
+ () => (
44
+ <Button
45
+ type='link'
46
+ icon={<LeftOutlined />}
47
+ size='large'
48
+ shape='circle'
49
+ onClick={onBack}
50
+ />
51
+ ),
52
+ [onBack]
53
+ );
54
+
55
+ return (
56
+ <div className={classNames('h', className)}>
57
+ {preHeader && <div className={`h-row ${textAlign}`}>{preHeader}</div>}
58
+ <div className={`h-row ${textAlign}`}>
59
+ {prefix && <div className='h-row-prefix'>{prefix}</div>}
60
+ {(onBack || backLink) && (
61
+ <div className='h-row-back'>
62
+ {backLink ? <Link href={backLink}>{backButton}</Link> : backButton}
63
+ </div>
64
+ )}
65
+ <div className='h-row-content'>
66
+ {header}
67
+ {postfix && <div className='h-row-content-postfix'>{postfix}</div>}
68
+ </div>
69
+ {extra && <div className='h-row-extra'>{extra}</div>}
70
+ </div>
71
+ {subHeader && <div className={`h-row ${textAlign}`}>{subHeader}</div>}
72
+ </div>
73
+ );
74
+ };
75
+
76
+ export default H;
@@ -0,0 +1,44 @@
1
+ import React, { CSSProperties } from 'react';
2
+ import Link from 'next/link';
3
+
4
+ interface PanelProps {
5
+ children: React.ReactNode;
6
+ bg: 'white' | 'primary' | 'secondary' | 'black' | 'blue' | 'venice' | 'grey';
7
+ link?: {
8
+ href: string;
9
+ title: string;
10
+ };
11
+ image?: React.ReactNode;
12
+ id?: string;
13
+ className?: string;
14
+ style?: CSSProperties;
15
+ marginBottom?: 'xs' | 's' | 'md';
16
+ }
17
+
18
+ const Panel: React.FC<PanelProps> = ({
19
+ children,
20
+ bg,
21
+ link,
22
+ image,
23
+ id,
24
+ className,
25
+ style,
26
+ marginBottom,
27
+ }) => {
28
+ const classNames = `panel ${bg}-bg ${link ? 'is-panel-link' : ''} ${className || ''
29
+ } ${marginBottom ? `mb-${marginBottom}` : ''}`;
30
+
31
+ return link ? (
32
+ <Link className={classNames} href={link.href} id={id} style={style}>
33
+ <div className='panel-image'>{image}</div>
34
+ <div className='panel-content'>{children}</div>
35
+ </Link>
36
+ ) : (
37
+ <div className={classNames} id={id} style={style}>
38
+ <div className='panel-image'>{image}</div>
39
+ <div className='panel-content'>{children}</div>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export default Panel;
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import Head from 'next/head';
3
+ import { useFrontendConfig } from '../utils/FrontendContext';
4
+
5
+ interface SeoHeadProps {
6
+ children?: React.ReactNode;
7
+ metaTitle: string;
8
+ metaDescription: string;
9
+ noindex?: boolean;
10
+ nofollow?: boolean;
11
+ smmType?: 'website' | 'profile';
12
+ smmImageUrl?: string;
13
+ smmPageUrl?: string;
14
+ }
15
+
16
+ const SeoHead: React.FC<SeoHeadProps> = ({
17
+ metaTitle,
18
+ metaDescription,
19
+ children,
20
+ noindex = false,
21
+ nofollow = false,
22
+ smmImageUrl,
23
+ smmPageUrl,
24
+ smmType = 'website',
25
+ }) => {
26
+ const { domain } = useFrontendConfig();
27
+
28
+ return (
29
+ <Head>
30
+ <title>{metaTitle}</title>
31
+ <meta name='description' content={metaDescription} />
32
+ <meta name='theme-color' content='#edf1f6' />
33
+ <meta charSet='UTF-8' />
34
+ <meta
35
+ name='viewport'
36
+ content='width=device-width, initial-scale=1, viewport-fit=cover'
37
+ />
38
+ <link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
39
+ <link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' />
40
+ <link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' />
41
+ <link rel='icon' type='image/x-icon' href='/favicon.ico' />
42
+ <link rel='manifest' href='/manifest.json' />
43
+
44
+ <meta
45
+ name='robots'
46
+ content={`${noindex ? 'noindex' : 'index'},${nofollow ? 'nofollow' : 'follow'
47
+ }`}
48
+ />
49
+
50
+ {/* для сниппета в социальных сетях */}
51
+ <meta property='og:type' content={smmType} />
52
+ {smmPageUrl && <meta property='og:url' content={smmPageUrl} />}
53
+ {smmImageUrl && <meta name='og:image' content={smmImageUrl} />}
54
+ <meta property='og:title' content={metaTitle} />
55
+ <meta property='og:description' content={metaDescription} />
56
+
57
+ <meta name='twitter:card' content='summary_large_image' />
58
+ {smmImageUrl && <meta name='twitter:image' content={smmImageUrl} />}
59
+ {domain && <meta property='twitter:domain' content={domain} />}
60
+ {smmPageUrl && <meta property='twitter:url' content={smmPageUrl} />}
61
+ <meta name='twitter:title' content={metaTitle} />
62
+ <meta name='twitter:description' content={metaDescription} />
63
+
64
+ {children}
65
+ </Head>
66
+ );
67
+ };
68
+
69
+ export default SeoHead;
@@ -0,0 +1,6 @@
1
+ export { default as H } from './H';
2
+ export { default as Panel } from './Panel';
3
+ export { default as FormItem } from './FormItem';
4
+ export { default as FormError } from './FormError';
5
+ export { default as AreYouSure } from './AreYouSure';
6
+ export { default as SeoHead } from './SeoHead';
@@ -0,0 +1,30 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ export interface FrontendConfig {
4
+ loginPath?: string;
5
+ registerPath?: string;
6
+ forgotPasswordPath?: string;
7
+ defaultPath?: string;
8
+ domain?: string;
9
+ websiteUrl?: string; // Used in SeoHead
10
+ }
11
+
12
+ const defaultConfig: FrontendConfig = {
13
+ loginPath: '/login',
14
+ registerPath: '/register',
15
+ forgotPasswordPath: '/forgot',
16
+ defaultPath: '/',
17
+ };
18
+
19
+ const FrontendContext = createContext<FrontendConfig>(defaultConfig);
20
+
21
+ export const useFrontendConfig = () => useContext(FrontendContext);
22
+
23
+ export const FrontendProvider: React.FC<{
24
+ config: FrontendConfig;
25
+ children: React.ReactNode;
26
+ }> = ({ config, children }) => (
27
+ <FrontendContext.Provider value={{ ...defaultConfig, ...config }}>
28
+ {children}
29
+ </FrontendContext.Provider>
30
+ );
@@ -0,0 +1,101 @@
1
+ interface Constraint {
2
+ code: string;
3
+ message: string;
4
+ }
5
+
6
+ interface ErrorObject {
7
+ code: string;
8
+ message: string;
9
+ data?: Record<string, Constraint>;
10
+ }
11
+
12
+ const DEFAULT_CODE = 'no_code';
13
+ const DEFAULT_MESSAGE = 'Error';
14
+
15
+ const isObject = (value: unknown): value is object =>
16
+ typeof value === 'object' && !Array.isArray(value) && value !== null;
17
+
18
+ const isData = (value: unknown): value is Record<string, Constraint> =>
19
+ isObject(value) &&
20
+ Object.values(value).every(
21
+ (constraint: unknown) =>
22
+ isObject(constraint) &&
23
+ 'code' in constraint &&
24
+ typeof constraint.code === 'string' &&
25
+ 'message' in constraint &&
26
+ typeof constraint.message === 'string'
27
+ );
28
+
29
+ const getError = (error: unknown): ErrorObject => {
30
+ // Trying to find the message of an error
31
+ if (
32
+ !isObject(error) ||
33
+ !('message' in error) ||
34
+ typeof (error as any).message !== 'string'
35
+ ) {
36
+ return {
37
+ code: DEFAULT_CODE,
38
+ message: DEFAULT_MESSAGE,
39
+ };
40
+ }
41
+
42
+ // Trying to find the first GraphQL error
43
+ if (
44
+ !('source' in error) ||
45
+ !isObject((error as any).source) ||
46
+ !('errors' in (error as any).source) ||
47
+ !Array.isArray((error as any).source.errors) ||
48
+ (error as any).source.errors.length === 0
49
+ ) {
50
+ return {
51
+ code: DEFAULT_CODE,
52
+ message: (error as any).message,
53
+ };
54
+ }
55
+
56
+ const firstError = (error as any).source.errors[0] as unknown;
57
+
58
+ // Trying to find the message of the GraphQL error
59
+ if (
60
+ !isObject(firstError) ||
61
+ !('message' in firstError) ||
62
+ typeof (firstError as any).message !== 'string'
63
+ ) {
64
+ return {
65
+ code: DEFAULT_CODE,
66
+ message: (error as any).message,
67
+ };
68
+ }
69
+
70
+ // Trying to find the code of the GraphQL message
71
+ if (
72
+ !('extensions' in firstError) ||
73
+ !isObject((firstError as any).extensions) ||
74
+ !('code' in (firstError as any).extensions) ||
75
+ typeof (firstError as any).extensions.code !== 'string'
76
+ ) {
77
+ return {
78
+ code: DEFAULT_CODE,
79
+ message: (firstError as any).message,
80
+ };
81
+ }
82
+
83
+ // Trying to find the data in the GraphQL message
84
+ if (
85
+ !('data' in (firstError as any).extensions) ||
86
+ !isData((firstError as any).extensions.data)
87
+ ) {
88
+ return {
89
+ code: (firstError as any).extensions.code,
90
+ message: (firstError as any).message,
91
+ };
92
+ }
93
+
94
+ return {
95
+ code: (firstError as any).extensions.code,
96
+ message: (firstError as any).message,
97
+ data: (firstError as any).extensions.data,
98
+ };
99
+ };
100
+
101
+ export default getError;
@@ -0,0 +1,77 @@
1
+ import { Form } from '@os-design/form';
2
+ import getError from './getError';
3
+ import isObject from './isObject';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ const handleFormErrors = (form: Form<any>, error: unknown): void => {
7
+ const { message, data } = getError(error);
8
+ if (data) {
9
+ Object.entries(data).forEach(([key, constraint]) => {
10
+ form.errors.set(key, constraint.message);
11
+ });
12
+ } else {
13
+ form.errors.set('_error', message);
14
+ }
15
+ };
16
+
17
+ /**
18
+ * @example { isExists: 'The error message' }
19
+ */
20
+ export type ConstraintMessageMap = Record<string, string>;
21
+
22
+ /**
23
+ * @example { name: { isExists: 'The error message' } }
24
+ */
25
+ export type FieldConstraintMessageMap = Record<string, ConstraintMessageMap>;
26
+
27
+ /**
28
+ * Formats the argument validation error to the field/message map.
29
+ */
30
+ export const formatArgumentValidationError = (
31
+ error: Error,
32
+ messages: FieldConstraintMessageMap = {}
33
+ ): Record<string, string> => {
34
+ // Check if validationErrors is exists
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const e = error as any;
37
+ if (
38
+ !isObject(e.source) ||
39
+ !Array.isArray(e.source.errors) ||
40
+ e.source.errors.length === 0 ||
41
+ !isObject(e.source.errors[0]) ||
42
+ e.source.errors[0].message !== 'Argument Validation Error' ||
43
+ !isObject(e.source.errors[0].extensions) ||
44
+ !isObject(e.source.errors[0].extensions.exception) ||
45
+ !Array.isArray(e.source.errors[0].extensions.exception.validationErrors)
46
+ )
47
+ return {};
48
+
49
+ const { validationErrors } = e.source.errors[0].extensions.exception;
50
+ const errors: Record<string, string> = {};
51
+
52
+ validationErrors.forEach((validationError: any) => {
53
+ // Check if validationError has property and constraints
54
+ if (
55
+ !isObject(validationError) ||
56
+ typeof validationError.property !== 'string' ||
57
+ !isObject(validationError.constraints)
58
+ )
59
+ return;
60
+
61
+ // Check if constraints has key/value
62
+ const constraints = Object.entries(validationError.constraints);
63
+ if (constraints.length === 0) return;
64
+
65
+ const [key, message] = constraints[0];
66
+ if (typeof message !== 'string') return;
67
+
68
+ const customMessage = messages[validationError.property]
69
+ ? messages[validationError.property][key]
70
+ : undefined;
71
+ errors[validationError.property] = customMessage || message;
72
+ });
73
+
74
+ return errors;
75
+ };
76
+
77
+ export default handleFormErrors;
@@ -0,0 +1,5 @@
1
+ export { default as getError } from './getError';
2
+ export { default as handleFormErrors } from './handleFormErrors';
3
+ export { default as isObject } from './isObject';
4
+ export * from './limitExpiresAt';
5
+ export * from './FrontendContext';
@@ -0,0 +1,4 @@
1
+ const isObject = (value: any): boolean =>
2
+ typeof value === 'object' && value !== null;
3
+
4
+ export default isObject;
@@ -0,0 +1,14 @@
1
+ const LIMIT_EXPIRES_AT_KEY = 'lea';
2
+
3
+ export const setLimitExpiresAt = (value: number) => {
4
+ if (typeof window === 'undefined') return;
5
+ window.sessionStorage.setItem(LIMIT_EXPIRES_AT_KEY, value.toString());
6
+ };
7
+
8
+ export const getLimitExpiresAt = () => {
9
+ if (typeof window === 'undefined') return 0;
10
+ const value = window.sessionStorage.getItem(LIMIT_EXPIRES_AT_KEY);
11
+ return Number(value) || 0;
12
+ };
13
+
14
+ export const getLimitExpiresIn = () => getLimitExpiresAt() - Date.now();