@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.
Files changed (141) hide show
  1. package/lib/components/settings/company-details/company-details-form.stories.d.ts +9 -0
  2. package/lib/components/settings/company-details/company-details-form.stories.d.ts.map +1 -0
  3. package/lib/components/settings/company-details/company-details-form.stories.js +45 -0
  4. package/lib/components/settings/company-details/company-details-form.stories.js.map +1 -0
  5. package/lib/components/settings/company-details/index.d.ts +20 -0
  6. package/lib/components/settings/company-details/index.d.ts.map +1 -0
  7. package/lib/components/settings/company-details/index.js +11 -0
  8. package/lib/components/settings/company-details/index.js.map +1 -0
  9. package/lib/components/settings/company-email-footer/company-email-footer.stories.d.ts +9 -0
  10. package/lib/components/settings/company-email-footer/company-email-footer.stories.d.ts.map +1 -0
  11. package/lib/components/settings/company-email-footer/company-email-footer.stories.js +38 -0
  12. package/lib/components/settings/company-email-footer/company-email-footer.stories.js.map +1 -0
  13. package/lib/components/settings/company-email-footer/index.d.ts +11 -0
  14. package/lib/components/settings/company-email-footer/index.d.ts.map +1 -0
  15. package/lib/components/settings/company-email-footer/index.js +10 -0
  16. package/lib/components/settings/company-email-footer/index.js.map +1 -0
  17. package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.d.ts +9 -0
  18. package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.d.ts.map +1 -0
  19. package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.js +36 -0
  20. package/lib/components/settings/company-email-reply-to/company-email-reply-to.stories.js.map +1 -0
  21. package/lib/components/settings/company-email-reply-to/index.d.ts +7 -0
  22. package/lib/components/settings/company-email-reply-to/index.d.ts.map +1 -0
  23. package/lib/components/settings/company-email-reply-to/index.js +12 -0
  24. package/lib/components/settings/company-email-reply-to/index.js.map +1 -0
  25. package/lib/components/settings/company-email-sender/company-email-sender.module.less +7 -0
  26. package/lib/components/settings/company-email-sender/company-email-sender.stories.d.ts +9 -0
  27. package/lib/components/settings/company-email-sender/company-email-sender.stories.d.ts.map +1 -0
  28. package/lib/components/settings/company-email-sender/company-email-sender.stories.js +38 -0
  29. package/lib/components/settings/company-email-sender/company-email-sender.stories.js.map +1 -0
  30. package/lib/components/settings/company-email-sender/index.d.ts +12 -0
  31. package/lib/components/settings/company-email-sender/index.d.ts.map +1 -0
  32. package/lib/components/settings/company-email-sender/index.js +29 -0
  33. package/lib/components/settings/company-email-sender/index.js.map +1 -0
  34. package/lib/components/settings/company-trade-checkbox/company-trade-checkbox.module.less +13 -0
  35. package/lib/components/settings/company-trade-checkbox/index.d.ts +10 -0
  36. package/lib/components/settings/company-trade-checkbox/index.d.ts.map +1 -0
  37. package/lib/components/settings/company-trade-checkbox/index.js +14 -0
  38. package/lib/components/settings/company-trade-checkbox/index.js.map +1 -0
  39. package/lib/components/settings/company-trades-picker/company-trades-picker.stories.d.ts +10 -0
  40. package/lib/components/settings/company-trades-picker/company-trades-picker.stories.d.ts.map +1 -0
  41. package/lib/components/settings/company-trades-picker/company-trades-picker.stories.js +68 -0
  42. package/lib/components/settings/company-trades-picker/company-trades-picker.stories.js.map +1 -0
  43. package/lib/components/settings/company-trades-picker/index.d.ts +19 -0
  44. package/lib/components/settings/company-trades-picker/index.d.ts.map +1 -0
  45. package/lib/components/settings/company-trades-picker/index.js +16 -0
  46. package/lib/components/settings/company-trades-picker/index.js.map +1 -0
  47. package/lib/components/settings/double-opt-in/double-opt-in.module.less +3 -0
  48. package/lib/components/settings/double-opt-in/double-opt-in.stories.d.ts +9 -0
  49. package/lib/components/settings/double-opt-in/double-opt-in.stories.d.ts.map +1 -0
  50. package/lib/components/settings/double-opt-in/double-opt-in.stories.js +42 -0
  51. package/lib/components/settings/double-opt-in/double-opt-in.stories.js.map +1 -0
  52. package/lib/components/settings/double-opt-in/index.d.ts +16 -0
  53. package/lib/components/settings/double-opt-in/index.d.ts.map +1 -0
  54. package/lib/components/settings/double-opt-in/index.js +23 -0
  55. package/lib/components/settings/double-opt-in/index.js.map +1 -0
  56. package/lib/components/settings/email-preview/email-preview.d.ts +13 -0
  57. package/lib/components/settings/email-preview/email-preview.d.ts.map +1 -0
  58. package/lib/components/settings/email-preview/email-preview.js +9 -0
  59. package/lib/components/settings/email-preview/email-preview.js.map +1 -0
  60. package/lib/components/settings/email-preview/email-preview.module.less +62 -0
  61. package/lib/components/settings/email-preview/opt-in-email-preview.d.ts +10 -0
  62. package/lib/components/settings/email-preview/opt-in-email-preview.d.ts.map +1 -0
  63. package/lib/components/settings/email-preview/opt-in-email-preview.js +10 -0
  64. package/lib/components/settings/email-preview/opt-in-email-preview.js.map +1 -0
  65. package/lib/components/settings/email-preview/opt-out-email-preview.d.ts +9 -0
  66. package/lib/components/settings/email-preview/opt-out-email-preview.d.ts.map +1 -0
  67. package/lib/components/settings/email-preview/opt-out-email-preview.js +9 -0
  68. package/lib/components/settings/email-preview/opt-out-email-preview.js.map +1 -0
  69. package/lib/components/settings/index.d.ts +10 -0
  70. package/lib/components/settings/index.d.ts.map +1 -0
  71. package/lib/components/settings/index.js +10 -0
  72. package/lib/components/settings/index.js.map +1 -0
  73. package/lib/components/settings/logo-picker/index.d.ts +32 -0
  74. package/lib/components/settings/logo-picker/index.d.ts.map +1 -0
  75. package/lib/components/settings/logo-picker/index.js +88 -0
  76. package/lib/components/settings/logo-picker/index.js.map +1 -0
  77. package/lib/components/settings/logo-picker/logo-picker.module.less +21 -0
  78. package/lib/components/settings/logo-picker/logo-picker.stories.d.ts +9 -0
  79. package/lib/components/settings/logo-picker/logo-picker.stories.d.ts.map +1 -0
  80. package/lib/components/settings/logo-picker/logo-picker.stories.js +13 -0
  81. package/lib/components/settings/logo-picker/logo-picker.stories.js.map +1 -0
  82. package/lib/components/settings/opt-out-message/index.d.ts +17 -0
  83. package/lib/components/settings/opt-out-message/index.d.ts.map +1 -0
  84. package/lib/components/settings/opt-out-message/index.js +22 -0
  85. package/lib/components/settings/opt-out-message/index.js.map +1 -0
  86. package/lib/components/settings/opt-out-message/opt-out-message.module.less +20 -0
  87. package/lib/components/settings/opt-out-message/opt-out-message.stories.d.ts +9 -0
  88. package/lib/components/settings/opt-out-message/opt-out-message.stories.d.ts.map +1 -0
  89. package/lib/components/settings/opt-out-message/opt-out-message.stories.js +44 -0
  90. package/lib/components/settings/opt-out-message/opt-out-message.stories.js.map +1 -0
  91. package/lib/components/settings/settings-section/index.d.ts +11 -0
  92. package/lib/components/settings/settings-section/index.d.ts.map +1 -0
  93. package/lib/components/settings/settings-section/index.js +6 -0
  94. package/lib/components/settings/settings-section/index.js.map +1 -0
  95. package/lib/index.d.ts +3 -0
  96. package/lib/index.d.ts.map +1 -0
  97. package/lib/index.js +3 -0
  98. package/lib/index.js.map +1 -0
  99. package/lib/utils/helpers.d.ts +7 -0
  100. package/lib/utils/helpers.d.ts.map +1 -0
  101. package/lib/utils/helpers.js +24 -0
  102. package/lib/utils/helpers.js.map +1 -0
  103. package/package.json +36 -0
  104. package/src/components/settings/company-details/company-details-form.stories.tsx +39 -0
  105. package/src/components/settings/company-details/index.tsx +157 -0
  106. package/src/components/settings/company-email-footer/company-email-footer.stories.tsx +26 -0
  107. package/src/components/settings/company-email-footer/index.tsx +79 -0
  108. package/src/components/settings/company-email-reply-to/company-email-reply-to.stories.tsx +23 -0
  109. package/src/components/settings/company-email-reply-to/index.tsx +38 -0
  110. package/src/components/settings/company-email-sender/company-email-sender.module.less +7 -0
  111. package/src/components/settings/company-email-sender/company-email-sender.module.less.d.ts +3 -0
  112. package/src/components/settings/company-email-sender/company-email-sender.stories.tsx +26 -0
  113. package/src/components/settings/company-email-sender/index.tsx +129 -0
  114. package/src/components/settings/company-trade-checkbox/company-trade-checkbox.module.less +13 -0
  115. package/src/components/settings/company-trade-checkbox/company-trade-checkbox.module.less.d.ts +4 -0
  116. package/src/components/settings/company-trade-checkbox/index.tsx +43 -0
  117. package/src/components/settings/company-trades-picker/company-trades-picker.stories.tsx +78 -0
  118. package/src/components/settings/company-trades-picker/index.tsx +78 -0
  119. package/src/components/settings/double-opt-in/double-opt-in.module.less +3 -0
  120. package/src/components/settings/double-opt-in/double-opt-in.module.less.d.ts +3 -0
  121. package/src/components/settings/double-opt-in/double-opt-in.stories.tsx +28 -0
  122. package/src/components/settings/double-opt-in/index.tsx +143 -0
  123. package/src/components/settings/email-preview/email-preview.module.less +62 -0
  124. package/src/components/settings/email-preview/email-preview.module.less.d.ts +8 -0
  125. package/src/components/settings/email-preview/email-preview.tsx +69 -0
  126. package/src/components/settings/email-preview/opt-in-email-preview.tsx +30 -0
  127. package/src/components/settings/email-preview/opt-out-email-preview.tsx +31 -0
  128. package/src/components/settings/index.ts +9 -0
  129. package/src/components/settings/logo-picker/index.tsx +256 -0
  130. package/src/components/settings/logo-picker/logo-picker.module.less +21 -0
  131. package/src/components/settings/logo-picker/logo-picker.module.less.d.ts +4 -0
  132. package/src/components/settings/logo-picker/logo-picker.stories.tsx +21 -0
  133. package/src/components/settings/opt-out-message/index.tsx +154 -0
  134. package/src/components/settings/opt-out-message/opt-out-message.module.less +20 -0
  135. package/src/components/settings/opt-out-message/opt-out-message.module.less.d.ts +5 -0
  136. package/src/components/settings/opt-out-message/opt-out-message.stories.tsx +31 -0
  137. package/src/components/settings/settings-section/index.tsx +35 -0
  138. package/src/index.ts +3 -0
  139. package/src/utils/helpers.ts +31 -0
  140. package/tsconfig.json +11 -0
  141. 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,4 @@
1
+ export const __esModule: true;
2
+ export const logoCard: string;
3
+ export const logoAction: string;
4
+
@@ -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,5 @@
1
+ export const __esModule: true;
2
+ export const outOutMessage: string;
3
+ export const buttonInput: string;
4
+ export const input: string;
5
+
@@ -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,3 @@
1
+ export * from './components/settings';
2
+
3
+ export * from './utils/helpers';
@@ -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
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@servicetitan/startup/tsconfig/base",
3
+ "compilerOptions": {
4
+ "outDir": "lib",
5
+ "rootDir": "src",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["src/**/__tests__/**/*"],
10
+ "references": [{ "path": "../utils" }]
11
+ }