@servicetitan/mpa-components 0.1.0
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.
- package/lib/components/settings/company-details/company-details-form.stories.d.ts +9 -0
- package/lib/components/settings/company-details/company-details-form.stories.d.ts.map +1 -0
- package/lib/components/settings/company-details/company-details-form.stories.js +45 -0
- package/lib/components/settings/company-details/company-details-form.stories.js.map +1 -0
- package/lib/components/settings/company-details/index.d.ts +20 -0
- package/lib/components/settings/company-details/index.d.ts.map +1 -0
- package/lib/components/settings/company-details/index.js +11 -0
- package/lib/components/settings/company-details/index.js.map +1 -0
- package/lib/components/settings/company-email-footer/company-email-footer.stories.d.ts +9 -0
- package/lib/components/settings/company-email-footer/company-email-footer.stories.d.ts.map +1 -0
- package/lib/components/settings/company-email-footer/company-email-footer.stories.js +38 -0
- package/lib/components/settings/company-email-footer/company-email-footer.stories.js.map +1 -0
- package/lib/components/settings/company-email-footer/index.d.ts +11 -0
- package/lib/components/settings/company-email-footer/index.d.ts.map +1 -0
- package/lib/components/settings/company-email-footer/index.js +10 -0
- package/lib/components/settings/company-email-footer/index.js.map +1 -0
- package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.d.ts +9 -0
- package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.d.ts.map +1 -0
- package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.js +36 -0
- package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.js.map +1 -0
- package/lib/components/settings/company-email-reply-to/index.d.ts +7 -0
- package/lib/components/settings/company-email-reply-to/index.d.ts.map +1 -0
- package/lib/components/settings/company-email-reply-to/index.js +12 -0
- package/lib/components/settings/company-email-reply-to/index.js.map +1 -0
- package/lib/components/settings/company-email-sender/company-email-sender.module.less +7 -0
- package/lib/components/settings/company-email-sender/company-email-sender.stories.d.ts +9 -0
- package/lib/components/settings/company-email-sender/company-email-sender.stories.d.ts.map +1 -0
- package/lib/components/settings/company-email-sender/company-email-sender.stories.js +38 -0
- package/lib/components/settings/company-email-sender/company-email-sender.stories.js.map +1 -0
- package/lib/components/settings/company-email-sender/index.d.ts +12 -0
- package/lib/components/settings/company-email-sender/index.d.ts.map +1 -0
- package/lib/components/settings/company-email-sender/index.js +29 -0
- package/lib/components/settings/company-email-sender/index.js.map +1 -0
- package/lib/components/settings/company-trade-checkbox/company-trade-checkbox.module.less +13 -0
- package/lib/components/settings/company-trade-checkbox/index.d.ts +10 -0
- package/lib/components/settings/company-trade-checkbox/index.d.ts.map +1 -0
- package/lib/components/settings/company-trade-checkbox/index.js +14 -0
- package/lib/components/settings/company-trade-checkbox/index.js.map +1 -0
- package/lib/components/settings/company-trades-picker/company-trades-picker.stories.d.ts +10 -0
- package/lib/components/settings/company-trades-picker/company-trades-picker.stories.d.ts.map +1 -0
- package/lib/components/settings/company-trades-picker/company-trades-picker.stories.js +68 -0
- package/lib/components/settings/company-trades-picker/company-trades-picker.stories.js.map +1 -0
- package/lib/components/settings/company-trades-picker/index.d.ts +19 -0
- package/lib/components/settings/company-trades-picker/index.d.ts.map +1 -0
- package/lib/components/settings/company-trades-picker/index.js +16 -0
- package/lib/components/settings/company-trades-picker/index.js.map +1 -0
- package/lib/components/settings/double-opt-in/double-opt-in.module.less +3 -0
- package/lib/components/settings/double-opt-in/double-opt-in.stories.d.ts +9 -0
- package/lib/components/settings/double-opt-in/double-opt-in.stories.d.ts.map +1 -0
- package/lib/components/settings/double-opt-in/double-opt-in.stories.js +42 -0
- package/lib/components/settings/double-opt-in/double-opt-in.stories.js.map +1 -0
- package/lib/components/settings/double-opt-in/index.d.ts +16 -0
- package/lib/components/settings/double-opt-in/index.d.ts.map +1 -0
- package/lib/components/settings/double-opt-in/index.js +23 -0
- package/lib/components/settings/double-opt-in/index.js.map +1 -0
- package/lib/components/settings/email-preview/email-preview.d.ts +13 -0
- package/lib/components/settings/email-preview/email-preview.d.ts.map +1 -0
- package/lib/components/settings/email-preview/email-preview.js +9 -0
- package/lib/components/settings/email-preview/email-preview.js.map +1 -0
- package/lib/components/settings/email-preview/email-preview.module.less +62 -0
- package/lib/components/settings/email-preview/opt-in-email-preview.d.ts +10 -0
- package/lib/components/settings/email-preview/opt-in-email-preview.d.ts.map +1 -0
- package/lib/components/settings/email-preview/opt-in-email-preview.js +10 -0
- package/lib/components/settings/email-preview/opt-in-email-preview.js.map +1 -0
- package/lib/components/settings/email-preview/opt-out-email-preview.d.ts +9 -0
- package/lib/components/settings/email-preview/opt-out-email-preview.d.ts.map +1 -0
- package/lib/components/settings/email-preview/opt-out-email-preview.js +9 -0
- package/lib/components/settings/email-preview/opt-out-email-preview.js.map +1 -0
- package/lib/components/settings/index.d.ts +10 -0
- package/lib/components/settings/index.d.ts.map +1 -0
- package/lib/components/settings/index.js +10 -0
- package/lib/components/settings/index.js.map +1 -0
- package/lib/components/settings/logo-picker/index.d.ts +32 -0
- package/lib/components/settings/logo-picker/index.d.ts.map +1 -0
- package/lib/components/settings/logo-picker/index.js +88 -0
- package/lib/components/settings/logo-picker/index.js.map +1 -0
- package/lib/components/settings/logo-picker/logo-picker.module.less +21 -0
- package/lib/components/settings/logo-picker/logo-picker.stories.d.ts +9 -0
- package/lib/components/settings/logo-picker/logo-picker.stories.d.ts.map +1 -0
- package/lib/components/settings/logo-picker/logo-picker.stories.js +13 -0
- package/lib/components/settings/logo-picker/logo-picker.stories.js.map +1 -0
- package/lib/components/settings/opt-out-message/index.d.ts +17 -0
- package/lib/components/settings/opt-out-message/index.d.ts.map +1 -0
- package/lib/components/settings/opt-out-message/index.js +22 -0
- package/lib/components/settings/opt-out-message/index.js.map +1 -0
- package/lib/components/settings/opt-out-message/opt-out-message.module.less +20 -0
- package/lib/components/settings/opt-out-message/opt-out-message.stories.d.ts +9 -0
- package/lib/components/settings/opt-out-message/opt-out-message.stories.d.ts.map +1 -0
- package/lib/components/settings/opt-out-message/opt-out-message.stories.js +44 -0
- package/lib/components/settings/opt-out-message/opt-out-message.stories.js.map +1 -0
- package/lib/components/settings/settings-section/index.d.ts +11 -0
- package/lib/components/settings/settings-section/index.d.ts.map +1 -0
- package/lib/components/settings/settings-section/index.js +6 -0
- package/lib/components/settings/settings-section/index.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/utils/helpers.d.ts +7 -0
- package/lib/utils/helpers.d.ts.map +1 -0
- package/lib/utils/helpers.js +24 -0
- package/lib/utils/helpers.js.map +1 -0
- package/package.json +36 -0
- package/src/components/settings/company-details/company-details-form.stories.tsx +39 -0
- package/src/components/settings/company-details/index.tsx +157 -0
- package/src/components/settings/company-email-footer/company-email-footer.stories.tsx +26 -0
- package/src/components/settings/company-email-footer/index.tsx +79 -0
- package/src/components/settings/company-email-reply-to/company-email-reply-to.stories.tsx +23 -0
- package/src/components/settings/company-email-reply-to/index.tsx +38 -0
- package/src/components/settings/company-email-sender/company-email-sender.module.less +7 -0
- package/src/components/settings/company-email-sender/company-email-sender.module.less.d.ts +3 -0
- package/src/components/settings/company-email-sender/company-email-sender.stories.tsx +26 -0
- package/src/components/settings/company-email-sender/index.tsx +129 -0
- package/src/components/settings/company-trade-checkbox/company-trade-checkbox.module.less +13 -0
- package/src/components/settings/company-trade-checkbox/company-trade-checkbox.module.less.d.ts +4 -0
- package/src/components/settings/company-trade-checkbox/index.tsx +43 -0
- package/src/components/settings/company-trades-picker/company-trades-picker.stories.tsx +78 -0
- package/src/components/settings/company-trades-picker/index.tsx +78 -0
- package/src/components/settings/double-opt-in/double-opt-in.module.less +3 -0
- package/src/components/settings/double-opt-in/double-opt-in.module.less.d.ts +3 -0
- package/src/components/settings/double-opt-in/double-opt-in.stories.tsx +28 -0
- package/src/components/settings/double-opt-in/index.tsx +143 -0
- package/src/components/settings/email-preview/email-preview.module.less +62 -0
- package/src/components/settings/email-preview/email-preview.module.less.d.ts +8 -0
- package/src/components/settings/email-preview/email-preview.tsx +69 -0
- package/src/components/settings/email-preview/opt-in-email-preview.tsx +30 -0
- package/src/components/settings/email-preview/opt-out-email-preview.tsx +31 -0
- package/src/components/settings/index.ts +9 -0
- package/src/components/settings/logo-picker/index.tsx +256 -0
- package/src/components/settings/logo-picker/logo-picker.module.less +21 -0
- package/src/components/settings/logo-picker/logo-picker.module.less.d.ts +4 -0
- package/src/components/settings/logo-picker/logo-picker.stories.tsx +21 -0
- package/src/components/settings/opt-out-message/index.tsx +154 -0
- package/src/components/settings/opt-out-message/opt-out-message.module.less +20 -0
- package/src/components/settings/opt-out-message/opt-out-message.module.less.d.ts +5 -0
- package/src/components/settings/opt-out-message/opt-out-message.stories.tsx +31 -0
- package/src/components/settings/settings-section/index.tsx +35 -0
- package/src/index.ts +3 -0
- package/src/utils/helpers.ts +31 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react';
|
|
3
|
+
|
|
4
|
+
import { EmailPreview } from './email-preview';
|
|
5
|
+
import type { DoubleOptInPropsFormState } from '../double-opt-in';
|
|
6
|
+
|
|
7
|
+
interface OptInEmailPreviewProps extends DoubleOptInPropsFormState {
|
|
8
|
+
open: boolean;
|
|
9
|
+
footerText: string;
|
|
10
|
+
onClose(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const OptInEmailPreview: FC<OptInEmailPreviewProps> = observer(
|
|
14
|
+
({ open, footerText, onClose, emailHeader, emailBody, emailButtonText }) => {
|
|
15
|
+
if (!open) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<EmailPreview
|
|
21
|
+
title="Double Opt-In Email Preview"
|
|
22
|
+
onClose={onClose}
|
|
23
|
+
header={emailHeader.value}
|
|
24
|
+
body={emailBody.value}
|
|
25
|
+
buttonText={emailButtonText.value}
|
|
26
|
+
footer={footerText}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { EmailPreview } from './email-preview';
|
|
4
|
+
import { OptOutMessageState } from '../opt-out-message';
|
|
5
|
+
|
|
6
|
+
interface OptOutMessagePreviewProps extends OptOutMessageState {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onClose(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const OptOutEmailPreview: FC<OptOutMessagePreviewProps> = ({
|
|
12
|
+
open,
|
|
13
|
+
onClose,
|
|
14
|
+
header,
|
|
15
|
+
body,
|
|
16
|
+
buttonText,
|
|
17
|
+
}) => {
|
|
18
|
+
if (!open) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<EmailPreview
|
|
24
|
+
title="Opt-Out Email Preview"
|
|
25
|
+
onClose={onClose}
|
|
26
|
+
header={header.value}
|
|
27
|
+
body={body.value}
|
|
28
|
+
buttonText={buttonText.value}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './company-details';
|
|
2
|
+
export * from './company-email-footer';
|
|
3
|
+
export * from './company-email-reply-to';
|
|
4
|
+
export * from './company-email-sender';
|
|
5
|
+
export * from './company-trades-picker';
|
|
6
|
+
export * from './double-opt-in';
|
|
7
|
+
export * from './logo-picker';
|
|
8
|
+
export * from './opt-out-message';
|
|
9
|
+
export * from './settings-section';
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useEffect, useState, ReactNode, FC, useRef } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FilePicker,
|
|
6
|
+
ButtonGroup,
|
|
7
|
+
Tooltip,
|
|
8
|
+
Button,
|
|
9
|
+
Card,
|
|
10
|
+
Banner,
|
|
11
|
+
Grid,
|
|
12
|
+
} from '@servicetitan/design-system';
|
|
13
|
+
import { Label } from '@servicetitan/form';
|
|
14
|
+
|
|
15
|
+
import { getImageSize, formatBytes } from '../../../utils/helpers';
|
|
16
|
+
|
|
17
|
+
export const LOGO_MIN_SIZE = 180;
|
|
18
|
+
export const DEFAULT_MIN_DIMENSIONS = { width: LOGO_MIN_SIZE, height: LOGO_MIN_SIZE };
|
|
19
|
+
export const DEFAULT_LOGO_TIPS = (
|
|
20
|
+
<ul>
|
|
21
|
+
<li>PNGs with a transparent background work best, but aren't required.</li>
|
|
22
|
+
<li>
|
|
23
|
+
Don't know where your logo is? Your social media profile would be a good place to look.
|
|
24
|
+
</li>
|
|
25
|
+
<li>Use a logo that is at least 180x180px in size.</li>
|
|
26
|
+
<li>
|
|
27
|
+
Avoid using a screenshot of your logo, right click and “save as” if you're pulling it
|
|
28
|
+
from somewhere else.
|
|
29
|
+
</li>
|
|
30
|
+
</ul>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
import * as Styles from './logo-picker.module.less';
|
|
34
|
+
|
|
35
|
+
export interface LogoPickerProps {
|
|
36
|
+
error?: string;
|
|
37
|
+
image?: File;
|
|
38
|
+
imageUrl?: string;
|
|
39
|
+
loaded?: boolean;
|
|
40
|
+
maxSize?: number;
|
|
41
|
+
minDimensions?: { width: number; height: number };
|
|
42
|
+
tips?: ReactNode;
|
|
43
|
+
|
|
44
|
+
deleteImage(): void;
|
|
45
|
+
downloadImage?(): void;
|
|
46
|
+
onBadImage?(opts: { category: string; error: string; data: { imageUrl: string } }): void;
|
|
47
|
+
onFileChange(files: FileList | null): void;
|
|
48
|
+
setError?(error: string): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const LogoPicker: FC<LogoPickerProps> = observer(
|
|
52
|
+
({
|
|
53
|
+
deleteImage,
|
|
54
|
+
downloadImage,
|
|
55
|
+
error,
|
|
56
|
+
image,
|
|
57
|
+
imageUrl,
|
|
58
|
+
loaded = true,
|
|
59
|
+
maxSize,
|
|
60
|
+
minDimensions = DEFAULT_MIN_DIMENSIONS,
|
|
61
|
+
onBadImage,
|
|
62
|
+
onFileChange,
|
|
63
|
+
setError,
|
|
64
|
+
tips = DEFAULT_LOGO_TIPS,
|
|
65
|
+
}) => {
|
|
66
|
+
const [recommendLargerImage, setRecommendLargerImage] = useState(false);
|
|
67
|
+
const [localError, setLocalError] = useState('');
|
|
68
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
69
|
+
const [sizeError, setSizeError] = useState('');
|
|
70
|
+
|
|
71
|
+
const fileRef = useRef<HTMLInputElement>(null);
|
|
72
|
+
const formattedMaxSize = useRef(formatBytes(maxSize, 2, false));
|
|
73
|
+
|
|
74
|
+
const handleClick = () => fileRef.current?.click();
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const handleImageUrlChange = async () => {
|
|
78
|
+
if (!imageUrl || !minDimensions) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setIsLoading(true);
|
|
83
|
+
try {
|
|
84
|
+
const size = await getImageSize(imageUrl);
|
|
85
|
+
setRecommendLargerImage(
|
|
86
|
+
size.height < minDimensions.height || size.width < minDimensions.width
|
|
87
|
+
);
|
|
88
|
+
setLocalError('');
|
|
89
|
+
|
|
90
|
+
if (setError) {
|
|
91
|
+
setError('');
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
onBadImage?.({
|
|
95
|
+
category: 'LogoPicker',
|
|
96
|
+
error: 'Got a bad image',
|
|
97
|
+
data: { imageUrl },
|
|
98
|
+
});
|
|
99
|
+
deleteImage();
|
|
100
|
+
|
|
101
|
+
setLocalError('File is not a valid image');
|
|
102
|
+
setRecommendLargerImage(false);
|
|
103
|
+
|
|
104
|
+
if (setError) {
|
|
105
|
+
setError('File is not a valid image');
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
setIsLoading(false);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
handleImageUrlChange();
|
|
112
|
+
}, [imageUrl, minDimensions, setError, deleteImage, onBadImage]);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!image?.size || !maxSize) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setIsLoading(true);
|
|
120
|
+
|
|
121
|
+
if (image && image.size > maxSize) {
|
|
122
|
+
deleteImage();
|
|
123
|
+
|
|
124
|
+
setSizeError(`File size should be less than ${formattedMaxSize.current}.`);
|
|
125
|
+
|
|
126
|
+
if (setError) {
|
|
127
|
+
setError(`File size should be less than ${formattedMaxSize.current}.`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
setSizeError('');
|
|
131
|
+
|
|
132
|
+
if (setError) {
|
|
133
|
+
setError('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setIsLoading(false);
|
|
138
|
+
}, [image, maxSize, setError, deleteImage]);
|
|
139
|
+
|
|
140
|
+
if (!loaded || isLoading) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="field">
|
|
146
|
+
<Label label="Logo" hasError={!!error || !!localError || !!sizeError} />
|
|
147
|
+
{(error || localError) && (
|
|
148
|
+
<Banner
|
|
149
|
+
className="m-b-2 qa-settings-logo-error"
|
|
150
|
+
title={error ?? localError}
|
|
151
|
+
status="critical"
|
|
152
|
+
icon
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
{sizeError && (
|
|
156
|
+
<Banner
|
|
157
|
+
className="m-b-2 qa-settings-logo-error"
|
|
158
|
+
title={sizeError}
|
|
159
|
+
status="critical"
|
|
160
|
+
icon
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
{imageUrl === undefined ? (
|
|
164
|
+
<FilePicker
|
|
165
|
+
typesNote="Allowed file types: jpeg, png, jpg"
|
|
166
|
+
className="qa-settings-logo-file-picker"
|
|
167
|
+
accept="image/png, image/jpeg"
|
|
168
|
+
onSelected={onFileChange}
|
|
169
|
+
/>
|
|
170
|
+
) : (
|
|
171
|
+
<Grid>
|
|
172
|
+
<Grid.Column width={4}>
|
|
173
|
+
<Card raised padding="none" className={Styles.logoCard}>
|
|
174
|
+
<Card.Section>
|
|
175
|
+
<img
|
|
176
|
+
className="qa-settings-logo-image"
|
|
177
|
+
src={imageUrl}
|
|
178
|
+
alt="logo"
|
|
179
|
+
/>
|
|
180
|
+
<div className={Styles.logoAction}>
|
|
181
|
+
<ButtonGroup>
|
|
182
|
+
<Tooltip el="div" text="Replace">
|
|
183
|
+
<Button
|
|
184
|
+
outline
|
|
185
|
+
iconName="sync"
|
|
186
|
+
onClick={handleClick}
|
|
187
|
+
className="qa-settings-logo-replace shadow-1-i bg-white-i"
|
|
188
|
+
/>
|
|
189
|
+
<input
|
|
190
|
+
hidden
|
|
191
|
+
type="file"
|
|
192
|
+
accept="image/png, image/jpeg"
|
|
193
|
+
ref={fileRef}
|
|
194
|
+
onChange={({ currentTarget: { files } }) =>
|
|
195
|
+
onFileChange(files)
|
|
196
|
+
}
|
|
197
|
+
/>
|
|
198
|
+
</Tooltip>
|
|
199
|
+
<Tooltip el="div" text="Download">
|
|
200
|
+
{downloadImage || image !== undefined ? (
|
|
201
|
+
<Button
|
|
202
|
+
className="qa-settings-logo-download shadow-1-i bg-white-i"
|
|
203
|
+
disabled={image !== undefined}
|
|
204
|
+
onClick={downloadImage}
|
|
205
|
+
iconName="file_download"
|
|
206
|
+
outline
|
|
207
|
+
/>
|
|
208
|
+
) : (
|
|
209
|
+
<a
|
|
210
|
+
download={`logo.${imageUrl
|
|
211
|
+
.split('.')
|
|
212
|
+
.pop()}`}
|
|
213
|
+
rel="noreferrer"
|
|
214
|
+
target="_blank"
|
|
215
|
+
href={imageUrl}
|
|
216
|
+
>
|
|
217
|
+
<Button
|
|
218
|
+
className="qa-settings-logo-download shadow-1-i bg-white-i"
|
|
219
|
+
iconName="file_download"
|
|
220
|
+
outline
|
|
221
|
+
/>
|
|
222
|
+
</a>
|
|
223
|
+
)}
|
|
224
|
+
</Tooltip>
|
|
225
|
+
<Tooltip el="div" text="Delete">
|
|
226
|
+
<Button
|
|
227
|
+
className="qa-settings-logo-delete shadow-1-i bg-white-i"
|
|
228
|
+
onClick={deleteImage}
|
|
229
|
+
iconName="delete"
|
|
230
|
+
outline
|
|
231
|
+
/>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
</ButtonGroup>
|
|
234
|
+
</div>
|
|
235
|
+
</Card.Section>
|
|
236
|
+
</Card>
|
|
237
|
+
</Grid.Column>
|
|
238
|
+
</Grid>
|
|
239
|
+
)}
|
|
240
|
+
{recommendLargerImage && minDimensions && (imageUrl || image) && (
|
|
241
|
+
<Banner
|
|
242
|
+
title={`We strongly recommend using a logo that is at least ${minDimensions.width}x${minDimensions.height}px in size. Using a lower-resolution image may result in logo pixelation in your templates.`}
|
|
243
|
+
className="m-t-2"
|
|
244
|
+
status="warning"
|
|
245
|
+
icon
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
{(imageUrl === undefined || recommendLargerImage || localError || sizeError) && (
|
|
249
|
+
<Banner icon title="Tips on Uploading Your Logo:" className="m-t-2">
|
|
250
|
+
{tips}
|
|
251
|
+
</Banner>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@import (reference) '~@servicetitan/tokens/dist/tokens.less';
|
|
2
|
+
|
|
3
|
+
.logo-card {
|
|
4
|
+
position: relative;
|
|
5
|
+
|
|
6
|
+
.logo-action {
|
|
7
|
+
position: absolute;
|
|
8
|
+
right: 0;
|
|
9
|
+
top: 0;
|
|
10
|
+
padding: @spacing-2;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&:not(:hover) .logo-action {
|
|
14
|
+
display: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
img {
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: auto;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { LogoPicker as Component } from '.';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'MPA Components/settings/LogoPicker',
|
|
6
|
+
component: LogoPicker,
|
|
7
|
+
parameters: {},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function LogoPicker() {
|
|
11
|
+
const [url, setUrl] = useState<string | undefined>();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Component
|
|
15
|
+
imageUrl={url}
|
|
16
|
+
minDimensions={{ height: 300, width: 300 }}
|
|
17
|
+
onFileChange={() => setUrl('https://place-hold.it/300x300')}
|
|
18
|
+
deleteImage={() => alert('image deleted')}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { observer } from 'mobx-react';
|
|
2
|
+
import { FC, Fragment, useCallback, useState } from 'react';
|
|
3
|
+
import classnames from 'classnames';
|
|
4
|
+
|
|
5
|
+
import { CheckboxFieldState, InputFieldState } from '@servicetitan/form';
|
|
6
|
+
import { Button, Form, Stack, Text, ToggleSwitch } from '@servicetitan/design-system';
|
|
7
|
+
import { useConfirm } from '@servicetitan/confirm';
|
|
8
|
+
|
|
9
|
+
import { SettingsSection } from '../settings-section';
|
|
10
|
+
import { OptOutEmailPreview } from '../email-preview/opt-out-email-preview';
|
|
11
|
+
|
|
12
|
+
import * as Styles from './opt-out-message.module.less';
|
|
13
|
+
|
|
14
|
+
export interface OptOutMessageState {
|
|
15
|
+
subjectLine: InputFieldState<string>;
|
|
16
|
+
header: InputFieldState<string>;
|
|
17
|
+
body: InputFieldState<string>;
|
|
18
|
+
buttonText: InputFieldState<string>;
|
|
19
|
+
monthsLimit: InputFieldState<number>;
|
|
20
|
+
enabled: CheckboxFieldState;
|
|
21
|
+
autoSuppressionEnabled: CheckboxFieldState;
|
|
22
|
+
}
|
|
23
|
+
export interface OptOutMessageProps {
|
|
24
|
+
formState: OptOutMessageState;
|
|
25
|
+
onHandleClickEnable?(checked: boolean): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const OptOutMessage: FC<OptOutMessageProps> = observer(
|
|
29
|
+
({ onHandleClickEnable, formState }) => {
|
|
30
|
+
const {
|
|
31
|
+
subjectLine,
|
|
32
|
+
header,
|
|
33
|
+
body,
|
|
34
|
+
buttonText,
|
|
35
|
+
monthsLimit,
|
|
36
|
+
enabled,
|
|
37
|
+
autoSuppressionEnabled,
|
|
38
|
+
} = formState;
|
|
39
|
+
|
|
40
|
+
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
41
|
+
const openPreview = useCallback(() => setIsPreviewOpen(true), [setIsPreviewOpen]);
|
|
42
|
+
const closePreview = useCallback(() => setIsPreviewOpen(false), [setIsPreviewOpen]);
|
|
43
|
+
|
|
44
|
+
const handleClickEnable = (_0: any, checked: boolean) => {
|
|
45
|
+
onHandleClickEnable?.(checked);
|
|
46
|
+
enabled.onChange(checked);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const [Confirm, handleConfirmed] = useConfirm(handleClickEnable);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<SettingsSection
|
|
53
|
+
className={Styles.outOutMessage}
|
|
54
|
+
qaPrefix="qa-opt-out-message"
|
|
55
|
+
title="Opt-Out Message"
|
|
56
|
+
text={
|
|
57
|
+
<Fragment>
|
|
58
|
+
<div>
|
|
59
|
+
If customers haven’t <b>opened</b> one of your emails in a set time
|
|
60
|
+
frame, you can send an opt-out message to allow them to set their email
|
|
61
|
+
preferences.
|
|
62
|
+
</div>
|
|
63
|
+
<ToggleSwitch
|
|
64
|
+
label="Enable"
|
|
65
|
+
onChange={handleConfirmed}
|
|
66
|
+
checked={enabled.value}
|
|
67
|
+
className="m-t-2 qa-opt-out-message-enable"
|
|
68
|
+
/>
|
|
69
|
+
</Fragment>
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
<Text bold className="m-b-2">
|
|
73
|
+
Opt-Out Email Content
|
|
74
|
+
</Text>
|
|
75
|
+
<Form className="m-b-0-i">
|
|
76
|
+
<Form.Input
|
|
77
|
+
className="m-b-2-i qa-opt-out-message-subject-line"
|
|
78
|
+
value={subjectLine.value}
|
|
79
|
+
onChange={subjectLine.onChangeHandler}
|
|
80
|
+
error={subjectLine.hasError}
|
|
81
|
+
label="Subject Line"
|
|
82
|
+
fluid
|
|
83
|
+
disabled={!enabled.value}
|
|
84
|
+
/>
|
|
85
|
+
<Form.Input
|
|
86
|
+
className="m-b-2-i qa-opt-out-message-header"
|
|
87
|
+
value={header.value}
|
|
88
|
+
onChange={header.onChangeHandler}
|
|
89
|
+
error={header.hasError}
|
|
90
|
+
label="Header"
|
|
91
|
+
fluid
|
|
92
|
+
disabled={!enabled.value}
|
|
93
|
+
/>
|
|
94
|
+
<Form.TextArea
|
|
95
|
+
className="m-b-2-i qa-opt-out-message-body-copy"
|
|
96
|
+
label="Body Copy"
|
|
97
|
+
value={body.value}
|
|
98
|
+
onChange={body.onChangeHandler}
|
|
99
|
+
error={body.hasError}
|
|
100
|
+
rows={2}
|
|
101
|
+
disabled={!enabled.value}
|
|
102
|
+
/>
|
|
103
|
+
<Form.Input
|
|
104
|
+
className={classnames(
|
|
105
|
+
Styles.buttonInput,
|
|
106
|
+
'm-b-3-i qa-opt-out-message-button-text'
|
|
107
|
+
)}
|
|
108
|
+
value={buttonText.value}
|
|
109
|
+
onChange={buttonText.onChangeHandler}
|
|
110
|
+
error={buttonText.hasError}
|
|
111
|
+
label="Button Text"
|
|
112
|
+
disabled={!enabled.value}
|
|
113
|
+
/>
|
|
114
|
+
<Button
|
|
115
|
+
className="qa-opt-out-message-preview-email"
|
|
116
|
+
primary
|
|
117
|
+
onClick={openPreview}
|
|
118
|
+
outline
|
|
119
|
+
small
|
|
120
|
+
disabled={!enabled.value}
|
|
121
|
+
>
|
|
122
|
+
Preview Email
|
|
123
|
+
</Button>
|
|
124
|
+
<hr />
|
|
125
|
+
</Form>
|
|
126
|
+
<Stack alignItems="center" wrap="wrap">
|
|
127
|
+
<Text className="m-b-1">Send when a recipient hasn’t opened an email in</Text>
|
|
128
|
+
<Form.Input
|
|
129
|
+
className={classnames(Styles.input, 'qa-opt-out-message-months', 'm-b-0')}
|
|
130
|
+
value={monthsLimit.value}
|
|
131
|
+
onChange={monthsLimit.onChangeHandler}
|
|
132
|
+
error={monthsLimit.hasError}
|
|
133
|
+
disabled={!enabled.value}
|
|
134
|
+
/>
|
|
135
|
+
<Text className="m-b-1">months.</Text>
|
|
136
|
+
</Stack>
|
|
137
|
+
<Form.Checkbox
|
|
138
|
+
className={classnames('m-t-2-i', 'qa-opt-out-message-auto-suppress')}
|
|
139
|
+
checked={autoSuppressionEnabled.value}
|
|
140
|
+
onChange={autoSuppressionEnabled.onChangeHandler}
|
|
141
|
+
disabled={!enabled.value}
|
|
142
|
+
label="Auto-suppress dormant emails"
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<Confirm
|
|
146
|
+
title="Disable Opt-Out Messaging"
|
|
147
|
+
message="Opt-Out messaging helps you keep dormant emails out of your audiences. Disabling it could potentially lead to worse email performance. Are you sure?"
|
|
148
|
+
when={enabled.value}
|
|
149
|
+
/>
|
|
150
|
+
<OptOutEmailPreview {...formState} open={isPreviewOpen} onClose={closePreview} />
|
|
151
|
+
</SettingsSection>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
@import (reference) '~@servicetitan/tokens/dist/tokens.less';
|
|
2
|
+
|
|
3
|
+
.out-out-message {
|
|
4
|
+
hr {
|
|
5
|
+
margin: @spacing-2 @spacing-0;
|
|
6
|
+
border: 0;
|
|
7
|
+
border-bottom: 1px solid @color-neutral-50;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.button-input {
|
|
12
|
+
width: 160px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.input {
|
|
16
|
+
width: @spacing-5;
|
|
17
|
+
margin-left: @spacing-1;
|
|
18
|
+
margin-right: @spacing-1;
|
|
19
|
+
margin-bottom: @spacing-0;
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CheckboxFieldState, InputFieldState } from '@servicetitan/form';
|
|
2
|
+
import { injectable, provide, useDependencies } from '@servicetitan/react-ioc';
|
|
3
|
+
import { FormState } from 'formstate';
|
|
4
|
+
import { OptOutMessage as Component } from '.';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'MPA Components/settings/OptOutMessage',
|
|
8
|
+
component: Component,
|
|
9
|
+
parameters: {},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
@injectable()
|
|
13
|
+
class OptOutStore {
|
|
14
|
+
form = new FormState({
|
|
15
|
+
subjectLine: new InputFieldState('Good bye'),
|
|
16
|
+
header: new InputFieldState('Opt out of emails'),
|
|
17
|
+
body: new InputFieldState('Sorry to see you go!'),
|
|
18
|
+
buttonText: new InputFieldState('Bye'),
|
|
19
|
+
monthsLimit: new InputFieldState(6),
|
|
20
|
+
enabled: new CheckboxFieldState(true),
|
|
21
|
+
autoSuppressionEnabled: new CheckboxFieldState(true),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const OptOutMessage = provide({
|
|
26
|
+
singletons: [OptOutStore],
|
|
27
|
+
})(() => {
|
|
28
|
+
const [store] = useDependencies(OptOutStore);
|
|
29
|
+
|
|
30
|
+
return <Component formState={store.form.$} />;
|
|
31
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { ReactNode, FC } from 'react';
|
|
3
|
+
import { observer } from 'mobx-react';
|
|
4
|
+
|
|
5
|
+
import { Text, Card, Layout } from '@servicetitan/design-system';
|
|
6
|
+
|
|
7
|
+
interface SectionProps {
|
|
8
|
+
title: string;
|
|
9
|
+
text?: JSX.Element | string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
qaPrefix?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SettingsSection: FC<SectionProps> = observer(
|
|
16
|
+
({ title, text, children, qaPrefix, className = '' }: SectionProps) => (
|
|
17
|
+
<Layout type="support" direction="left" className={classNames(qaPrefix, className)}>
|
|
18
|
+
<Layout.Section>
|
|
19
|
+
<Text size={3} bold className={`${qaPrefix}-title`}>
|
|
20
|
+
{title}
|
|
21
|
+
</Text>
|
|
22
|
+
{text && (
|
|
23
|
+
<Text size={2} subdued className={`${qaPrefix}-text`}>
|
|
24
|
+
{text}
|
|
25
|
+
</Text>
|
|
26
|
+
)}
|
|
27
|
+
</Layout.Section>
|
|
28
|
+
<Layout.Section>
|
|
29
|
+
<Card raised>
|
|
30
|
+
<Card.Section className={`${qaPrefix}-content`}>{children}</Card.Section>
|
|
31
|
+
</Card>
|
|
32
|
+
</Layout.Section>
|
|
33
|
+
</Layout>
|
|
34
|
+
)
|
|
35
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const formatBytes = (bytes: number | undefined, decimals = 2, isBinary = true) => {
|
|
2
|
+
if (!bytes) {
|
|
3
|
+
return '0 Bytes';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const k = isBinary ? 1024 : 1000;
|
|
7
|
+
const dm = Math.max(0, decimals);
|
|
8
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
9
|
+
|
|
10
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
11
|
+
|
|
12
|
+
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getImageSize = (url: string) => {
|
|
16
|
+
return new Promise<{ width: number; height: number }>((resolve, reject) => {
|
|
17
|
+
const image = document.createElement('img');
|
|
18
|
+
|
|
19
|
+
image.onload = () => {
|
|
20
|
+
resolve({ width: image.width, height: image.height });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
image.onerror = () => {
|
|
24
|
+
reject(undefined);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
image.src = url;
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const convertDomainName = (value: string) => value.toLowerCase().replace(/ /g, '');
|
package/tsconfig.json
ADDED