@servicetitan/marketing-ui 0.5.0 → 0.8.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 (106) hide show
  1. package/dist/components/image-cropper/image-cropper.d.ts +23 -0
  2. package/dist/components/image-cropper/image-cropper.d.ts.map +1 -0
  3. package/dist/components/image-cropper/image-cropper.js +146 -0
  4. package/dist/components/image-cropper/image-cropper.js.map +1 -0
  5. package/dist/components/image-cropper/image-cropper.stories.d.ts +10 -0
  6. package/dist/components/image-cropper/image-cropper.stories.d.ts.map +1 -0
  7. package/dist/components/image-cropper/image-cropper.stories.js +55 -0
  8. package/dist/components/image-cropper/image-cropper.stories.js.map +1 -0
  9. package/dist/components/ui/date-range-picker/date-range-picker.d.ts +10 -0
  10. package/dist/components/ui/date-range-picker/date-range-picker.d.ts.map +1 -0
  11. package/dist/components/ui/date-range-picker/date-range-picker.js +77 -0
  12. package/dist/components/ui/date-range-picker/date-range-picker.js.map +1 -0
  13. package/dist/components/ui/date-range-picker/date-range-picker.module.less +42 -0
  14. package/dist/components/ui/date-range-picker/date-range-picker.stories.d.ts +10 -0
  15. package/dist/components/ui/date-range-picker/date-range-picker.stories.d.ts.map +1 -0
  16. package/dist/components/ui/date-range-picker/date-range-picker.stories.js +17 -0
  17. package/dist/components/ui/date-range-picker/date-range-picker.stories.js.map +1 -0
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/utils/date/__mocks__/date-mock.d.ts +5 -0
  23. package/dist/utils/date/__mocks__/date-mock.d.ts.map +1 -0
  24. package/dist/utils/date/__mocks__/date-mock.js +23 -0
  25. package/dist/utils/date/__mocks__/date-mock.js.map +1 -0
  26. package/dist/utils/date/date-range-picker-options.d.ts +45 -0
  27. package/dist/utils/date/date-range-picker-options.d.ts.map +1 -0
  28. package/dist/utils/date/date-range-picker-options.js +138 -0
  29. package/dist/utils/date/date-range-picker-options.js.map +1 -0
  30. package/dist/utils/date/date-range-picker-state.d.ts +24 -0
  31. package/dist/utils/date/date-range-picker-state.d.ts.map +1 -0
  32. package/dist/utils/date/date-range-picker-state.js +73 -0
  33. package/dist/utils/date/date-range-picker-state.js.map +1 -0
  34. package/dist/utils/date/date-tenant.d.ts +17 -0
  35. package/dist/utils/date/date-tenant.d.ts.map +1 -0
  36. package/dist/utils/date/date-tenant.js +51 -0
  37. package/dist/utils/date/date-tenant.js.map +1 -0
  38. package/dist/utils/date/date.d.ts +11 -0
  39. package/dist/utils/date/date.d.ts.map +1 -0
  40. package/dist/utils/date/date.js +22 -0
  41. package/dist/utils/date/date.js.map +1 -0
  42. package/dist/utils/date/index.d.ts +5 -0
  43. package/dist/utils/date/index.d.ts.map +1 -0
  44. package/dist/utils/date/index.js +17 -0
  45. package/dist/utils/date/index.js.map +1 -0
  46. package/dist/utils/history/history.d.ts +9 -3
  47. package/dist/utils/history/history.d.ts.map +1 -1
  48. package/dist/utils/history/history.js +9 -7
  49. package/dist/utils/history/history.js.map +1 -1
  50. package/dist/utils/history/index.d.ts +1 -0
  51. package/dist/utils/history/index.d.ts.map +1 -1
  52. package/dist/utils/history/index.js +1 -0
  53. package/dist/utils/history/index.js.map +1 -1
  54. package/dist/utils/history/query-params-handler.d.ts +1 -0
  55. package/dist/utils/history/query-params-handler.d.ts.map +1 -1
  56. package/dist/utils/history/query-params-handler.js +3 -0
  57. package/dist/utils/history/query-params-handler.js.map +1 -1
  58. package/dist/utils/history/url-params-handler.d.ts +18 -0
  59. package/dist/utils/history/url-params-handler.d.ts.map +1 -0
  60. package/dist/utils/history/url-params-handler.js +64 -0
  61. package/dist/utils/history/url-params-handler.js.map +1 -0
  62. package/dist/utils/history/use-query-params.d.ts.map +1 -1
  63. package/dist/utils/history/use-query-params.js +5 -3
  64. package/dist/utils/history/use-query-params.js.map +1 -1
  65. package/dist/utils/history/use-url-params.d.ts +5 -5
  66. package/dist/utils/history/use-url-params.d.ts.map +1 -1
  67. package/dist/utils/history/use-url-params.js +4 -19
  68. package/dist/utils/history/use-url-params.js.map +1 -1
  69. package/dist/utils/{use-init-effect.d.ts → invariable-hooks.d.ts} +6 -1
  70. package/dist/utils/invariable-hooks.d.ts.map +1 -0
  71. package/dist/utils/{use-init-effect.js → invariable-hooks.js} +11 -2
  72. package/dist/utils/invariable-hooks.js.map +1 -0
  73. package/dist/utils/param-parsers.d.ts +2 -1
  74. package/dist/utils/param-parsers.d.ts.map +1 -1
  75. package/dist/utils/param-parsers.js +21 -1
  76. package/dist/utils/param-parsers.js.map +1 -1
  77. package/package.json +5 -3
  78. package/src/components/image-cropper/image-cropper.stories.tsx +69 -0
  79. package/src/components/image-cropper/image-cropper.tsx +108 -0
  80. package/src/components/ui/date-range-picker/date-range-picker.module.less +42 -0
  81. package/src/components/ui/date-range-picker/date-range-picker.module.less.d.ts +4 -0
  82. package/src/components/ui/date-range-picker/date-range-picker.stories.tsx +22 -0
  83. package/src/components/ui/date-range-picker/date-range-picker.tsx +118 -0
  84. package/src/index.ts +2 -1
  85. package/src/utils/__tests__/param-parsers.test.ts +11 -2
  86. package/src/utils/date/__mocks__/date-mock.ts +23 -0
  87. package/src/utils/date/__tests__/date-range-picker.test.ts +139 -0
  88. package/src/utils/date/__tests__/date-tenant.test.ts +38 -0
  89. package/src/utils/date/date-range-picker-options.ts +167 -0
  90. package/src/utils/date/date-range-picker-state.ts +62 -0
  91. package/src/utils/date/date-tenant.ts +49 -0
  92. package/src/utils/date/date.ts +29 -0
  93. package/src/utils/date/index.ts +4 -0
  94. package/src/utils/history/__tests__/history.test.ts +9 -2
  95. package/src/utils/history/__tests__/url-params-handler.test.ts +32 -0
  96. package/src/utils/history/__tests__/use-url-params.test.ts +2 -1
  97. package/src/utils/history/history.ts +27 -10
  98. package/src/utils/history/index.ts +1 -0
  99. package/src/utils/history/query-params-handler.ts +4 -0
  100. package/src/utils/history/url-params-handler.ts +65 -0
  101. package/src/utils/history/use-query-params.ts +5 -3
  102. package/src/utils/history/use-url-params.ts +7 -32
  103. package/src/utils/{use-init-effect.ts → invariable-hooks.ts} +10 -1
  104. package/src/utils/param-parsers.ts +26 -1
  105. package/dist/utils/use-init-effect.d.ts.map +0 -1
  106. package/dist/utils/use-init-effect.js.map +0 -1
@@ -0,0 +1,118 @@
1
+ import React, { useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import { observer } from 'mobx-react';
4
+ import { BodyText, Form, Stack } from '@servicetitan/design-system';
5
+ import { DateRange, DateRangePickerOptionsStateType } from '../../../utils/date';
6
+ import * as Styles from './date-range-picker.module.less';
7
+
8
+ export interface DateRangePickerProps<OptionKeys extends string> {
9
+ state: DateRangePickerOptionsStateType<OptionKeys>;
10
+ onChange?: DateRangePickerOptionsStateType<OptionKeys>['onChange'];
11
+ compare?: boolean;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ const convertDateRange = (val?: DateRange, backward?: boolean): DateRange | undefined =>
16
+ val
17
+ ? {
18
+ start: val.start,
19
+ end: new Date(
20
+ val.end.getFullYear(),
21
+ val.end.getMonth(),
22
+ val.end.getDate() + (backward ? 1 : -1)
23
+ ),
24
+ }
25
+ : undefined;
26
+
27
+ const formatDate = (dt: Date) =>
28
+ dt.toLocaleString('en-US', {
29
+ day: 'numeric',
30
+ year: 'numeric',
31
+ month: 'short',
32
+ });
33
+
34
+ const formatDateRange = (range: DateRange) =>
35
+ `${formatDate(range.start)} - ${formatDate(range.end)}`;
36
+
37
+ export const DateRangePicker = observer(
38
+ <OptionKeys extends string>({
39
+ compare,
40
+ disabled,
41
+ state,
42
+ onChange,
43
+ }: DateRangePickerProps<OptionKeys>) => {
44
+ const selectedOption = state.selectedOption;
45
+
46
+ /*
47
+ * DateRangePickerOptions uses exclusive date ranges for end date (start >= dt > end)
48
+ * but anvil's DateRangePicker uses inclusive dates (with end set to endOfDay),
49
+ * so we performing some transformations here
50
+ * convertDateRange (predefined options, value and onChange)
51
+ */
52
+ const onPickerChange: DateRangePickerOptionsStateType<OptionKeys>['onChange'] = val => {
53
+ const newVal = convertDateRange(val, true);
54
+
55
+ if (onChange) {
56
+ onChange(newVal);
57
+ } else {
58
+ state.onChange(newVal);
59
+ }
60
+ };
61
+ const options = useMemo(
62
+ () =>
63
+ state.options.options.map(o => ({
64
+ text: o.text,
65
+ value: convertDateRange(o.value),
66
+ selected: o.key === selectedOption?.key,
67
+ })),
68
+ [selectedOption, state.options.options]
69
+ );
70
+ const value = useMemo(() => convertDateRange(state.value), [state.value]);
71
+ const compareValue = useMemo(
72
+ () =>
73
+ state.value && compare
74
+ ? convertDateRange(state.options.getComparisonDateRange(state.value))
75
+ : undefined,
76
+ [compare, state.value, state.options]
77
+ );
78
+ const textValue = selectedOption
79
+ ? selectedOption.text
80
+ : compare || !value
81
+ ? 'Custom Date Range'
82
+ : formatDateRange(value);
83
+
84
+ return (
85
+ <Stack>
86
+ <Stack.Item className={Styles.pickerContainer}>
87
+ <Form.DateRangePicker
88
+ onChange={onPickerChange}
89
+ options={options}
90
+ value={value}
91
+ required
92
+ disabled={disabled}
93
+ label={
94
+ <div
95
+ className={classNames(
96
+ Styles.pickerValue,
97
+ 'd-f flex-column justify-content-center t-truncate cursor-pointer'
98
+ )}
99
+ >
100
+ {textValue}
101
+ </div>
102
+ }
103
+ />
104
+ </Stack.Item>
105
+ {!!compare && (
106
+ <Stack direction="column" justifyContent="center" className="m-l-2">
107
+ <BodyText>{value ? formatDateRange(value) : ''}</BodyText>
108
+ {!!compareValue && (
109
+ <BodyText size="xsmall" subdued>
110
+ vs. {formatDateRange(compareValue)}
111
+ </BodyText>
112
+ )}
113
+ </Stack>
114
+ )}
115
+ </Stack>
116
+ );
117
+ }
118
+ );
package/src/index.ts CHANGED
@@ -2,12 +2,13 @@ export * from './components/stat/stat-card';
2
2
  export * from './components/ads/ads-stat';
3
3
  export * from './components/ui/centered-spinner';
4
4
  export * from './components/ui/line-text';
5
+ export * from './components/image-cropper/image-cropper';
5
6
 
6
7
  export * from './utils/ads-texts';
7
8
  export * from './utils/formatters';
8
9
  export * from './utils/string-case';
9
10
  export * from './utils/use-client-rect';
10
- export * from './utils/use-init-effect';
11
+ export * from './utils/invariable-hooks';
11
12
  export * from './utils/history';
12
13
  export * from './utils/helpers';
13
14
  export * from './utils/param-parsers';
@@ -6,14 +6,23 @@ describe('paramParsers', () => {
6
6
  expect(paramParsers.string.stringify('qq')).toEqual('qq');
7
7
  });
8
8
 
9
- it('should parse strings enum', () => {
10
- const parser = paramParsers.stringEnum(['Search', 'Display']);
9
+ it('should parse strings set', () => {
10
+ const parser = paramParsers.stringSet(['Search', 'Display']);
11
11
 
12
12
  expect(parser.parse('qq')).toEqual(undefined);
13
13
  expect(parser.parse('Search')).toEqual('Search');
14
14
  expect(parser.stringify('Search')).toEqual('Search');
15
15
  });
16
16
 
17
+ it('should parse strings enums', () => {
18
+ const parser = paramParsers.stringEnum({ Search: 1, Display: 0 });
19
+
20
+ expect(parser.parse('qq')).toEqual(undefined);
21
+ expect(parser.parse('Search')).toEqual(1);
22
+ expect(parser.parse('Display')).toEqual(0);
23
+ expect(parser.stringify(0)).toEqual('Display');
24
+ });
25
+
17
26
  it('should parse uri', () => {
18
27
  expect(paramParsers.uri.parse('qq%26qq')).toEqual('qq&qq');
19
28
  expect(paramParsers.uri.stringify('qq&qq')).toEqual('qq&qq');
@@ -0,0 +1,23 @@
1
+ export const dateMockSpy = () => {
2
+ let dateSpy: jest.SpyInstance;
3
+
4
+ return {
5
+ mock: (dateStr: string, localOffset = -240) => {
6
+ const OriginalDate = Date;
7
+ const dt = new OriginalDate(dateStr);
8
+
9
+ dt.getTimezoneOffset = () => localOffset;
10
+
11
+ dateSpy = jest
12
+ .spyOn(global, 'Date')
13
+ .mockImplementation((...args) =>
14
+ args.length ? new OriginalDate(...args) : (dt as any)
15
+ );
16
+ },
17
+ clear: () => {
18
+ if (dateSpy) {
19
+ dateSpy.mockRestore();
20
+ }
21
+ },
22
+ };
23
+ };
@@ -0,0 +1,139 @@
1
+ import {
2
+ DateRangePickerOptionsTenantAds,
3
+ DateRangePickerOptionsTenantAdsTypes,
4
+ } from '../date-range-picker-options';
5
+ import { dateMockSpy } from '../__mocks__/date-mock';
6
+
7
+ const newYorkOffset = -240;
8
+
9
+ type OptType = [
10
+ [number, number, number],
11
+ [number, number, number],
12
+ string,
13
+ DateRangePickerOptionsTenantAdsTypes
14
+ ];
15
+ const opt = ([sy, sm, sd]: [number, number, number], [ey, em, ed]: [number, number, number]) => ({
16
+ start: new Date(sy, sm - 1, sd),
17
+ end: new Date(ey, em - 1, ed),
18
+ });
19
+
20
+ const opts = (options: OptType[]) =>
21
+ options.map(([[sy, sm, sd], [ey, em, ed], text, key]) => ({
22
+ key,
23
+ value: opt([sy, sm, sd], [ey, em, ed]),
24
+ text,
25
+ }));
26
+
27
+ describe('DateRangePickerOptionsTenantAds', () => {
28
+ const dateSpy = dateMockSpy();
29
+
30
+ const options1: OptType[] = [
31
+ [[2021, 8, 20], [2021, 8, 21], 'Today', 'today'],
32
+ [[2021, 8, 19], [2021, 8, 20], 'Yesterday', 'yesterday'],
33
+ [[2021, 8, 14], [2021, 8, 21], 'Last 7 Days', 'last7'],
34
+ [[2021, 7, 22], [2021, 8, 21], 'Last 30 Days', 'last30'],
35
+ [[2021, 8, 1], [2021, 8, 21], 'Current Month', 'currentMonth'],
36
+ [[2021, 7, 1], [2021, 8, 1], 'Last Month', 'lastMonth'],
37
+ [[2021, 5, 23], [2021, 8, 21], 'Last 90 Days', 'last90'],
38
+ [[2021, 1, 1], [2021, 8, 21], 'Current Year', 'currentYear'],
39
+ ];
40
+
41
+ afterEach(() => {
42
+ dateSpy.clear();
43
+ });
44
+
45
+ it('works correct when local tz and tenant tz has same dates', () => {
46
+ dateSpy.mock('2021-08-20T12:24:00');
47
+
48
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
49
+
50
+ expect(store.options).toEqual(opts(options1));
51
+ });
52
+
53
+ it('works correct when local tz and tenant tz has different dates', () => {
54
+ dateSpy.mock('2021-08-03T04:24:00');
55
+
56
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
57
+
58
+ expect(store.options).toEqual(
59
+ opts([
60
+ [[2021, 8, 2], [2021, 8, 3], 'Today', 'today'],
61
+ [[2021, 8, 1], [2021, 8, 2], 'Yesterday', 'yesterday'],
62
+ [[2021, 7, 27], [2021, 8, 3], 'Last 7 Days', 'last7'],
63
+ [[2021, 7, 4], [2021, 8, 3], 'Last 30 Days', 'last30'],
64
+ [[2021, 8, 1], [2021, 8, 3], 'Current Month', 'currentMonth'],
65
+ [[2021, 7, 1], [2021, 8, 1], 'Last Month', 'lastMonth'],
66
+ [[2021, 5, 5], [2021, 8, 3], 'Last 90 Days', 'last90'],
67
+ [[2021, 1, 1], [2021, 8, 3], 'Current Year', 'currentYear'],
68
+ ])
69
+ );
70
+ });
71
+
72
+ it('set correct `today` for last day of month', () => {
73
+ dateSpy.mock('2021-08-31T12:24:00');
74
+
75
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
76
+
77
+ expect(store.findOption('today')?.value).toEqual(opt([2021, 8, 31], [2021, 9, 1]));
78
+ });
79
+
80
+ it('set correct `yesterday` for first day of month', () => {
81
+ dateSpy.mock('2021-08-01T12:24:00');
82
+
83
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
84
+
85
+ expect(store.findOption('yesterday')?.value).toEqual(opt([2021, 7, 31], [2021, 8, 1]));
86
+ });
87
+
88
+ it('set correct `lastMonth` for first month of year', () => {
89
+ dateSpy.mock('2021-01-02T12:24:00');
90
+
91
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
92
+
93
+ expect(store.findOption('lastMonth')?.value).toEqual(opt([2020, 12, 1], [2021, 1, 1]));
94
+ });
95
+
96
+ describe('findOption', () => {
97
+ options1.forEach(op => {
98
+ it(`'${op[2]}' option should be found`, () => {
99
+ dateSpy.mock('2021-08-20T12:24:00');
100
+
101
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
102
+
103
+ expect(store.findOption(opt(op[0], op[1]))?.key).toEqual(op[3]);
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('getComparisonDateRange', () => {
109
+ it(`should generate comparison for current month option`, () => {
110
+ dateSpy.mock('2021-08-20T12:24:00');
111
+
112
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
113
+
114
+ expect(store.getComparisonDateRange(store.findOption('currentMonth').value)).toEqual(
115
+ opt([2021, 7, 1], [2021, 8, 1])
116
+ );
117
+ });
118
+
119
+ it(`should generate comparison for prev month option`, () => {
120
+ dateSpy.mock('2021-01-20T12:24:00');
121
+
122
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
123
+
124
+ expect(store.getComparisonDateRange(store.findOption('currentMonth').value)).toEqual(
125
+ opt([2020, 12, 1], [2021, 1, 1])
126
+ );
127
+ });
128
+
129
+ it(`should generate comparison for custom interval`, () => {
130
+ dateSpy.mock('2021-01-20T12:24:00');
131
+
132
+ const store = DateRangePickerOptionsTenantAds.create({ UtcOffset: newYorkOffset });
133
+
134
+ expect(store.getComparisonDateRange(opt([2021, 3, 5], [2021, 3, 15]))).toEqual(
135
+ opt([2021, 2, 23], [2021, 3, 5])
136
+ );
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,38 @@
1
+ import { TenantDate } from '../date-tenant';
2
+ import { dateMockSpy } from '../__mocks__/date-mock';
3
+
4
+ describe('TenantDate', () => {
5
+ const dateSpy = dateMockSpy();
6
+
7
+ afterEach(() => {
8
+ dateSpy.clear();
9
+ });
10
+
11
+ describe('tenantTimeFromLocal', () => {
12
+ it('works correct when local tz and tenant tz has same dates', () => {
13
+ dateSpy.mock('2021-08-20T12:24:00');
14
+
15
+ expect(TenantDate.tenantTimeFromLocal(-240)).toEqual(new Date(2021, 7, 20, 4, 24, 0));
16
+ });
17
+
18
+ it('works correct when local tz and tenant tz has different dates', () => {
19
+ dateSpy.mock('2021-08-03T04:24:00');
20
+
21
+ expect(TenantDate.tenantTimeFromLocal(-240)).toEqual(new Date(2021, 7, 2, 20, 24, 0));
22
+ });
23
+ });
24
+
25
+ describe('tenantDateFromLocal', () => {
26
+ it('works correct when local tz and tenant tz has same dates', () => {
27
+ dateSpy.mock('2021-08-20T12:24:00');
28
+
29
+ expect(TenantDate.tenantDateFromLocal(-240)).toEqual(new Date(2021, 7, 20));
30
+ });
31
+
32
+ it('works correct when local tz and tenant tz has different dates', () => {
33
+ dateSpy.mock('2021-08-03T04:24:00');
34
+
35
+ expect(TenantDate.tenantDateFromLocal(-240)).toEqual(new Date(2021, 7, 2));
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,167 @@
1
+ import { TenantDate } from './date-tenant';
2
+ import { DateRange, dateRangesEqual, getComparisonDateRange } from './date';
3
+
4
+ export interface DateRangePickerOption<OptionKeys extends string> {
5
+ key: OptionKeys;
6
+ value: DateRange;
7
+ text: string;
8
+ }
9
+
10
+ /**
11
+ * base class for predefined date range options
12
+ *
13
+ * @note start date is inclusive and end date is exclusive for each option
14
+ */
15
+ export abstract class DateRangePickerOptions<OptionKeys extends string> {
16
+ options: DateRangePickerOption<OptionKeys>[] = [];
17
+ defaultOption?: OptionKeys;
18
+
19
+ get keys(): OptionKeys[] {
20
+ return this.options.map(o => o.key);
21
+ }
22
+
23
+ findOption(key: undefined): undefined;
24
+ findOption(key: OptionKeys): DateRangePickerOption<OptionKeys>;
25
+ findOption(range: DateRange): DateRangePickerOption<OptionKeys> | undefined;
26
+ findOption(arg?: DateRange | OptionKeys): DateRangePickerOption<OptionKeys> | undefined {
27
+ if (!arg) {
28
+ return undefined;
29
+ }
30
+
31
+ if (typeof arg === 'string') {
32
+ return this.options.find(o => o.key === arg);
33
+ }
34
+
35
+ for (const opt of this.options) {
36
+ if (dateRangesEqual(arg, opt.value)) {
37
+ return opt;
38
+ }
39
+ }
40
+
41
+ return undefined;
42
+ }
43
+
44
+ getComparisonDateRange(_range: DateRange): DateRange | undefined {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ export interface TenantTimezone {
50
+ UtcOffset: number;
51
+ }
52
+
53
+ const subtractDays = (dt: Date, days: number) => new Date(dt.getTime() - days * 86400000);
54
+
55
+ export type DateRangePickerOptionsTenantAdsTypes =
56
+ | 'today'
57
+ | 'yesterday'
58
+ | 'last7'
59
+ | 'last30'
60
+ | 'last90'
61
+ | 'currentMonth'
62
+ | 'lastMonth'
63
+ | 'currentYear';
64
+
65
+ /**
66
+ * options wrapper in tenant timezone that is used in ads. it contain following options:
67
+ * Today
68
+ * Yesterday
69
+ * Last 7 Days
70
+ * Last 30 Days
71
+ * Current Month
72
+ * Last Month
73
+ * Last 90 Days
74
+ * Current Year
75
+ */
76
+ export class DateRangePickerOptionsTenantAds extends DateRangePickerOptions<DateRangePickerOptionsTenantAdsTypes> {
77
+ readonly options: DateRangePickerOption<DateRangePickerOptionsTenantAdsTypes>[];
78
+
79
+ private constructor(
80
+ tz: TenantTimezone,
81
+ defaultOption: DateRangePickerOptionsTenantAdsTypes = 'last30'
82
+ ) {
83
+ super();
84
+
85
+ const todayTenant = TenantDate.tenantDateFromLocal(tz.UtcOffset);
86
+
87
+ const tomTenant = new Date(
88
+ todayTenant.getFullYear(),
89
+ todayTenant.getMonth(),
90
+ todayTenant.getDate() + 1
91
+ );
92
+
93
+ const yestTenant = new Date(
94
+ todayTenant.getFullYear(),
95
+ todayTenant.getMonth(),
96
+ todayTenant.getDate() - 1
97
+ );
98
+
99
+ const monTenant = new Date(todayTenant.getFullYear(), todayTenant.getMonth(), 1);
100
+ const lastMonTenant = new Date(todayTenant.getFullYear(), todayTenant.getMonth() - 1, 1);
101
+ const yearTenant = new Date(todayTenant.getFullYear(), 0, 1);
102
+
103
+ this.options = [
104
+ { key: 'today', value: { start: todayTenant, end: tomTenant }, text: 'Today' },
105
+ { key: 'yesterday', value: { start: yestTenant, end: todayTenant }, text: 'Yesterday' },
106
+ {
107
+ key: 'last7',
108
+ value: { start: subtractDays(todayTenant, 6), end: tomTenant },
109
+ text: 'Last 7 Days',
110
+ },
111
+ {
112
+ key: 'last30',
113
+ value: { start: subtractDays(todayTenant, 29), end: tomTenant },
114
+ text: 'Last 30 Days',
115
+ },
116
+ {
117
+ key: 'currentMonth',
118
+ value: { start: monTenant, end: tomTenant },
119
+ text: 'Current Month',
120
+ },
121
+ {
122
+ key: 'lastMonth',
123
+ value: { start: lastMonTenant, end: monTenant },
124
+ text: 'Last Month',
125
+ },
126
+ {
127
+ key: 'last90',
128
+ value: { start: subtractDays(todayTenant, 89), end: tomTenant },
129
+ text: 'Last 90 Days',
130
+ },
131
+ {
132
+ key: 'currentYear',
133
+ value: { start: yearTenant, end: tomTenant },
134
+ text: 'Current Year',
135
+ },
136
+ ];
137
+
138
+ this.defaultOption = defaultOption;
139
+ }
140
+
141
+ static create(
142
+ tz: TenantTimezone,
143
+ defaultOption?: DateRangePickerOptionsTenantAdsTypes
144
+ ): DateRangePickerOptionsTenantAds {
145
+ return new DateRangePickerOptionsTenantAds(tz, defaultOption);
146
+ }
147
+
148
+ getComparisonDateRange = (range: DateRange) => {
149
+ const option = this.findOption(range);
150
+
151
+ switch (option?.key) {
152
+ case 'currentMonth':
153
+ case 'lastMonth':
154
+ return {
155
+ start: new Date(range.start.getFullYear(), range.start.getMonth() - 1, 1),
156
+ end: range.start,
157
+ };
158
+ case 'currentYear':
159
+ return {
160
+ start: new Date(range.start.getFullYear() - 1, 0, 1),
161
+ end: range.start,
162
+ };
163
+ }
164
+
165
+ return getComparisonDateRange(range);
166
+ };
167
+ }
@@ -0,0 +1,62 @@
1
+ import { DateRangePickerOption, DateRangePickerOptions } from './date-range-picker-options';
2
+ import { DateRange } from './date';
3
+ import { action, computed, makeObservable, observable } from 'mobx';
4
+
5
+ interface DateRangePickerStateConfig {
6
+ defaultValue?: DateRange;
7
+ }
8
+
9
+ export interface DateRangePickerStateType {
10
+ value?: DateRange;
11
+ onChange(val?: DateRange): void;
12
+ }
13
+
14
+ export interface DateRangePickerOptionsStateType<OptionKeys extends string>
15
+ extends DateRangePickerStateType {
16
+ readonly options: DateRangePickerOptions<OptionKeys>;
17
+ readonly selectedOption?: DateRangePickerOption<OptionKeys>;
18
+ }
19
+
20
+ export class DateRangePickerState<OptionKeys extends string = never> {
21
+ @observable value?: DateRange;
22
+
23
+ readonly options?: DateRangePickerOptions<OptionKeys>;
24
+
25
+ @computed get selectedOption(): DateRangePickerOption<OptionKeys> | undefined {
26
+ if (!this.value || !this.options) {
27
+ return undefined;
28
+ }
29
+
30
+ return this.options.findOption(this.value);
31
+ }
32
+
33
+ constructor(config?: DateRangePickerStateConfig, options?: DateRangePickerOptions<OptionKeys>) {
34
+ makeObservable(this);
35
+
36
+ this.options = options;
37
+
38
+ if (config?.defaultValue) {
39
+ this.value = config.defaultValue;
40
+ } else if (this.options?.defaultOption) {
41
+ this.value = this.options.findOption(this.options.defaultOption)?.value;
42
+ }
43
+ }
44
+
45
+ static create(config?: DateRangePickerStateConfig): DateRangePickerStateType {
46
+ return new DateRangePickerState(config);
47
+ }
48
+
49
+ static createWithOptions<OptionKeys extends string>(
50
+ options: DateRangePickerOptions<OptionKeys>,
51
+ config?: DateRangePickerStateConfig
52
+ ): DateRangePickerOptionsStateType<OptionKeys> {
53
+ return new DateRangePickerState(
54
+ config,
55
+ options
56
+ ) as DateRangePickerOptionsStateType<OptionKeys>;
57
+ }
58
+
59
+ @action onChange = (val?: DateRange) => {
60
+ this.value = val;
61
+ };
62
+ }
@@ -0,0 +1,49 @@
1
+ /*
2
+ * This class exists because we:
3
+ *
4
+ * (1) want to always use tenant TZ (as specified in the settings) over the browser TZ to display/parse dates
5
+ * (2) have parts which we can't/don't want to change right now which use Date (instead of Moment)
6
+ *
7
+ * The issue with Date is that it doesn't support timezones. So we have to introduce the idea of UI Date --
8
+ * a Date value with ticks set to such value that the local time in the current browser TZ is equal to local time in tenant TZ represented by passed real Date
9
+ *
10
+ * Example:
11
+ *
12
+ * Tenant TZ is PDT UTC-07
13
+ * Browser TZ is EDT UTC-04
14
+ *
15
+ * In DB we have a LaunchDate which is 2020-05-11 04:00:00 (we use UTC in DB)
16
+ * This time gives local time of 2020-05-11 00:00:00 in tenant TZ
17
+ * To display the same time in browser's TZ, we need to create a Date with UTC value of 2020-05-11 07:00:00 (see createUIDate)
18
+ * Note that the UTC value of this Date is useless, we only care about local time it gives in that particular TZ, that's why it's an UI date
19
+ * The same reasoning applies when we get a Date from a DatePicker, for example --
20
+ * it's an UI date, we only care about its local time, thus we need to construct a moment which represents the same local time but in tenant TZ (see momentFromUI)
21
+ *
22
+ * If at any point we're able to get rid of all the components relying on Date, this class won't be needed anymore.
23
+ */
24
+ export class TenantDate {
25
+ /**
26
+ * represent local time in tenant's Timezone
27
+ * @note returned date still have local timezone, so it shouldn't be converted to UTC anywhere
28
+ * @param tenantUtcOffset tenant's timezone offset to UTC (in minutes)
29
+ * @param localDateTime datetime or undefined (for 'now')
30
+ */
31
+ static tenantTimeFromLocal(tenantUtcOffset: number, localDateTime?: Date): Date {
32
+ const dtLocal = localDateTime ?? new Date();
33
+ const dtUtc = new Date(dtLocal.getTime() + dtLocal.getTimezoneOffset() * 60000);
34
+
35
+ return new Date(dtUtc.getTime() + tenantUtcOffset * 60000);
36
+ }
37
+
38
+ /**
39
+ * represent local date in tenant's Timezone (time values will be 0)
40
+ * @note returned date still have local timezone, so it shouldn't be converted to UTC anywhere
41
+ * @param tenantUtcOffset tenant's timezone offset to UTC (in minutes)
42
+ * @param localDateTime datetime or undefined (for 'now')
43
+ */
44
+ static tenantDateFromLocal(tenantUtcOffset: number, localDateTime?: Date): Date {
45
+ const dt = TenantDate.tenantTimeFromLocal(tenantUtcOffset, localDateTime);
46
+
47
+ return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate());
48
+ }
49
+ }
@@ -0,0 +1,29 @@
1
+ export interface DateRange {
2
+ start: Date;
3
+ end: Date;
4
+ }
5
+
6
+ export const datesEqual = (date1: Date, date2: Date) =>
7
+ date1.getFullYear() === date2.getFullYear() &&
8
+ date1.getMonth() === date2.getMonth() &&
9
+ date1.getDate() === date2.getDate() &&
10
+ date1.getHours() === date2.getHours() &&
11
+ date1.getMinutes() === date2.getMinutes() &&
12
+ date1.getSeconds() === date2.getSeconds();
13
+
14
+ export const dateRangesEqual = (dateRange1: DateRange, dateRange2: DateRange) =>
15
+ datesEqual(dateRange1.start, dateRange2.start) && datesEqual(dateRange1.end, dateRange2.end);
16
+
17
+ export const getComparisonDateRange = (range: DateRange) => {
18
+ const diff = range.end.getTime() - range.start.getTime();
19
+ const compareStart = new Date(range.start.getTime() - diff);
20
+
21
+ return {
22
+ start: new Date(
23
+ compareStart.getFullYear(),
24
+ compareStart.getMonth(),
25
+ compareStart.getDate()
26
+ ),
27
+ end: range.start,
28
+ };
29
+ };
@@ -0,0 +1,4 @@
1
+ export * from './date';
2
+ export * from './date-tenant';
3
+ export * from './date-range-picker-options';
4
+ export * from './date-range-picker-state';