@licklist/design 0.78.21 → 0.78.25

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 (186) hide show
  1. package/dist/assets/Trend-Down.svg +3 -0
  2. package/dist/assets/Trend-Up.svg +3 -0
  3. package/dist/auth/Authorizer.d.ts.map +1 -1
  4. package/dist/auth/Authorizer.js +47 -12
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +5 -0
  8. package/dist/v2/components/EntityHeader/EntityHeader.d.ts +13 -0
  9. package/dist/v2/components/EntityHeader/EntityHeader.d.ts.map +1 -0
  10. package/dist/v2/components/EntityHeader/EntityHeader.js +85 -0
  11. package/dist/v2/components/EntityHeader/EntityHeader.scss.js +6 -0
  12. package/dist/v2/components/EntityHeader/index.d.ts +2 -0
  13. package/dist/v2/components/EntityHeader/index.d.ts.map +1 -0
  14. package/dist/v2/components/NPSScore/NPSScore.d.ts +18 -0
  15. package/dist/v2/components/NPSScore/NPSScore.d.ts.map +1 -0
  16. package/dist/v2/components/NPSScore/index.d.ts +3 -0
  17. package/dist/v2/components/NPSScore/index.d.ts.map +1 -0
  18. package/dist/v2/components/Select/Select.d.ts +10 -0
  19. package/dist/v2/components/Select/Select.d.ts.map +1 -0
  20. package/dist/v2/components/Select/index.d.ts +3 -0
  21. package/dist/v2/components/Select/index.d.ts.map +1 -0
  22. package/dist/v2/components/Tooltip/Tooltip.d.ts +21 -0
  23. package/dist/v2/components/Tooltip/Tooltip.d.ts.map +1 -0
  24. package/dist/v2/components/Tooltip/Tooltip.js +103 -0
  25. package/dist/v2/components/Tooltip/Tooltip.scss.js +6 -0
  26. package/dist/v2/components/Tooltip/index.d.ts +2 -0
  27. package/dist/v2/components/Tooltip/index.d.ts.map +1 -0
  28. package/dist/v2/components/UserAvatar/UserAvatar.d.ts +12 -0
  29. package/dist/v2/components/UserAvatar/UserAvatar.d.ts.map +1 -0
  30. package/dist/v2/components/UserAvatar/UserAvatar.js +77 -0
  31. package/dist/v2/components/UserAvatar/UserAvatar.scss.js +6 -0
  32. package/dist/v2/components/UserAvatar/index.d.ts +2 -0
  33. package/dist/v2/components/UserAvatar/index.d.ts.map +1 -0
  34. package/dist/v2/components/UserPanel/UserPanel.d.ts +17 -0
  35. package/dist/v2/components/UserPanel/UserPanel.d.ts.map +1 -0
  36. package/dist/v2/components/UserPanel/UserPanel.js +168 -0
  37. package/dist/v2/components/UserPanel/UserPanel.scss.js +6 -0
  38. package/dist/v2/components/UserPanel/index.d.ts +3 -0
  39. package/dist/v2/components/UserPanel/index.d.ts.map +1 -0
  40. package/dist/v2/dashboard-analytics/blog-posts/Blog.d.ts +15 -0
  41. package/dist/v2/dashboard-analytics/blog-posts/Blog.d.ts.map +1 -0
  42. package/dist/v2/dashboard-analytics/blog-posts/index.d.ts +3 -0
  43. package/dist/v2/dashboard-analytics/blog-posts/index.d.ts.map +1 -0
  44. package/dist/v2/dashboard-analytics/chart/Chart.d.ts +21 -0
  45. package/dist/v2/dashboard-analytics/chart/Chart.d.ts.map +1 -0
  46. package/dist/v2/dashboard-analytics/chart/index.d.ts +3 -0
  47. package/dist/v2/dashboard-analytics/chart/index.d.ts.map +1 -0
  48. package/dist/v2/dashboard-analytics/dashboard/Dashboard.d.ts +57 -0
  49. package/dist/v2/dashboard-analytics/dashboard/Dashboard.d.ts.map +1 -0
  50. package/dist/v2/dashboard-analytics/dashboard/index.d.ts +3 -0
  51. package/dist/v2/dashboard-analytics/dashboard/index.d.ts.map +1 -0
  52. package/dist/v2/dashboard-analytics/index.d.ts +13 -0
  53. package/dist/v2/dashboard-analytics/index.d.ts.map +1 -0
  54. package/dist/v2/dashboard-analytics/metric-card/MetricCard.d.ts +17 -0
  55. package/dist/v2/dashboard-analytics/metric-card/MetricCard.d.ts.map +1 -0
  56. package/dist/v2/dashboard-analytics/metric-card/index.d.ts +3 -0
  57. package/dist/v2/dashboard-analytics/metric-card/index.d.ts.map +1 -0
  58. package/dist/v2/dashboard-analytics/venue-card/VenueCard.d.ts +12 -0
  59. package/dist/v2/dashboard-analytics/venue-card/VenueCard.d.ts.map +1 -0
  60. package/dist/v2/dashboard-analytics/venue-card/index.d.ts +3 -0
  61. package/dist/v2/dashboard-analytics/venue-card/index.d.ts.map +1 -0
  62. package/dist/v2/dashboard-analytics/venue-closed-card/VenueClosedCard.d.ts +25 -0
  63. package/dist/v2/dashboard-analytics/venue-closed-card/VenueClosedCard.d.ts.map +1 -0
  64. package/dist/v2/dashboard-analytics/venue-closed-card/index.d.ts +3 -0
  65. package/dist/v2/dashboard-analytics/venue-closed-card/index.d.ts.map +1 -0
  66. package/dist/v2/index.d.ts +11 -5
  67. package/dist/v2/index.d.ts.map +1 -1
  68. package/dist/v2/navigation/DashboardLayout/AdminSidebar.d.ts +10 -0
  69. package/dist/v2/navigation/DashboardLayout/AdminSidebar.d.ts.map +1 -0
  70. package/dist/v2/navigation/DashboardLayout/AdminSidebar.js +296 -0
  71. package/dist/v2/navigation/DashboardLayout/AdminSidebar.scss.js +6 -0
  72. package/dist/v2/navigation/DashboardLayout/DashboardFooter.d.ts +7 -0
  73. package/dist/v2/navigation/DashboardLayout/DashboardFooter.d.ts.map +1 -0
  74. package/dist/v2/navigation/DashboardLayout/DashboardFooter.js +34 -0
  75. package/dist/v2/navigation/DashboardLayout/DashboardFooter.scss.js +6 -0
  76. package/dist/v2/navigation/DashboardLayout/DashboardLayout.d.ts +42 -0
  77. package/dist/v2/navigation/DashboardLayout/DashboardLayout.d.ts.map +1 -0
  78. package/dist/v2/navigation/DashboardLayout/DashboardLayout.js +154 -0
  79. package/dist/v2/navigation/DashboardLayout/DashboardLayout.scss.js +6 -0
  80. package/dist/v2/navigation/DashboardLayout/ProviderSidebar.d.ts +35 -0
  81. package/dist/v2/navigation/DashboardLayout/ProviderSidebar.d.ts.map +1 -0
  82. package/dist/v2/navigation/DashboardLayout/ProviderSidebar.js +344 -0
  83. package/dist/v2/navigation/DashboardLayout/ProviderSidebar.scss.js +6 -0
  84. package/dist/v2/navigation/DashboardLayout/TopNavigation.d.ts +26 -0
  85. package/dist/v2/navigation/DashboardLayout/TopNavigation.d.ts.map +1 -0
  86. package/dist/v2/navigation/DashboardLayout/TopNavigation.js +468 -0
  87. package/dist/v2/navigation/DashboardLayout/TopNavigation.scss.js +6 -0
  88. package/dist/v2/navigation/DashboardLayout/assets/AdminLogo.png.js +3 -0
  89. package/dist/v2/navigation/DashboardLayout/assets/BookedLogo_Mark.png.js +3 -0
  90. package/dist/v2/navigation/DashboardLayout/index.d.ts +7 -0
  91. package/dist/v2/navigation/DashboardLayout/index.d.ts.map +1 -0
  92. package/package.json +5 -3
  93. package/src/assets/Trend-Down.svg +3 -0
  94. package/src/assets/Trend-Up.svg +3 -0
  95. package/src/auth/Authorizer.tsx +49 -20
  96. package/src/index.ts +2 -1
  97. package/src/v2/components/EntityHeader/EntityHeader.scss +133 -0
  98. package/src/v2/components/EntityHeader/EntityHeader.stories.tsx +103 -0
  99. package/src/v2/components/EntityHeader/EntityHeader.tsx +76 -0
  100. package/src/v2/components/EntityHeader/index.ts +1 -0
  101. package/src/v2/components/NPSScore/NPSScore.scss +330 -0
  102. package/src/v2/components/NPSScore/NPSScore.stories.tsx +29 -0
  103. package/src/v2/components/NPSScore/NPSScore.tsx +209 -0
  104. package/src/v2/components/NPSScore/index.ts +2 -0
  105. package/src/v2/components/Select/Select.scss +188 -0
  106. package/src/v2/components/Select/Select.stories.tsx +164 -0
  107. package/src/v2/components/Select/Select.tsx +56 -0
  108. package/src/v2/components/Select/index.ts +2 -0
  109. package/src/v2/components/Tooltip/Tooltip.scss +92 -0
  110. package/src/v2/components/Tooltip/Tooltip.stories.tsx +164 -0
  111. package/src/v2/components/Tooltip/Tooltip.tsx +64 -0
  112. package/src/v2/components/Tooltip/index.ts +8 -0
  113. package/src/v2/components/UserAvatar/UserAvatar.scss +62 -0
  114. package/src/v2/components/UserAvatar/UserAvatar.stories.tsx +94 -0
  115. package/src/v2/components/UserAvatar/UserAvatar.tsx +96 -0
  116. package/src/v2/components/UserAvatar/index.ts +1 -0
  117. package/src/v2/components/UserPanel/UserPanel.scss +195 -0
  118. package/src/v2/components/UserPanel/UserPanel.stories.tsx +66 -0
  119. package/src/v2/components/UserPanel/UserPanel.tsx +132 -0
  120. package/src/v2/components/UserPanel/index.ts +2 -0
  121. package/src/v2/dashboard-analytics/blog-posts/Blog.scss +92 -0
  122. package/src/v2/dashboard-analytics/blog-posts/Blog.stories.tsx +57 -0
  123. package/src/v2/dashboard-analytics/blog-posts/Blog.tsx +91 -0
  124. package/src/v2/dashboard-analytics/blog-posts/index.ts +2 -0
  125. package/src/v2/dashboard-analytics/chart/Chart.scss +424 -0
  126. package/src/v2/dashboard-analytics/chart/Chart.stories.tsx +157 -0
  127. package/src/v2/dashboard-analytics/chart/Chart.tsx +623 -0
  128. package/src/v2/dashboard-analytics/chart/index.ts +2 -0
  129. package/src/v2/dashboard-analytics/dashboard/Dashboard.scss +254 -0
  130. package/src/v2/dashboard-analytics/dashboard/Dashboard.stories.tsx +298 -0
  131. package/src/v2/dashboard-analytics/dashboard/Dashboard.tsx +248 -0
  132. package/src/v2/dashboard-analytics/dashboard/index.ts +2 -0
  133. package/src/v2/dashboard-analytics/index.ts +12 -0
  134. package/src/v2/dashboard-analytics/metric-card/MetricCard.scss +125 -0
  135. package/src/v2/dashboard-analytics/metric-card/MetricCard.stories.tsx +106 -0
  136. package/src/v2/dashboard-analytics/metric-card/MetricCard.tsx +72 -0
  137. package/src/v2/dashboard-analytics/metric-card/index.ts +2 -0
  138. package/src/v2/dashboard-analytics/venue-card/VenueCard.scss +112 -0
  139. package/src/v2/dashboard-analytics/venue-card/VenueCard.stories.tsx +40 -0
  140. package/src/v2/dashboard-analytics/venue-card/VenueCard.tsx +62 -0
  141. package/src/v2/dashboard-analytics/venue-card/index.ts +2 -0
  142. package/src/v2/dashboard-analytics/venue-closed-card/VenueClosedCard.scss +129 -0
  143. package/src/v2/dashboard-analytics/venue-closed-card/VenueClosedCard.stories.tsx +31 -0
  144. package/src/v2/dashboard-analytics/venue-closed-card/VenueClosedCard.tsx +61 -0
  145. package/src/v2/dashboard-analytics/venue-closed-card/index.ts +2 -0
  146. package/src/v2/design-system/colors/ColorSystem.scss +439 -0
  147. package/src/v2/design-system/colors/ColorSystem.stories.tsx +730 -0
  148. package/src/v2/design-system/typography/Typography.scss +295 -0
  149. package/src/v2/design-system/typography/Typography.stories.tsx +109 -0
  150. package/src/v2/index.ts +43 -7
  151. package/src/v2/navigation/DashboardLayout/AdminSidebar.scss +207 -0
  152. package/src/v2/navigation/DashboardLayout/AdminSidebar.tsx +171 -0
  153. package/src/v2/navigation/DashboardLayout/DashboardFooter.scss +30 -0
  154. package/src/v2/navigation/DashboardLayout/DashboardFooter.tsx +25 -0
  155. package/src/v2/navigation/DashboardLayout/DashboardLayout.scss +91 -0
  156. package/src/v2/navigation/DashboardLayout/DashboardLayout.stories.tsx +370 -0
  157. package/src/v2/navigation/DashboardLayout/DashboardLayout.tsx +215 -0
  158. package/src/v2/navigation/DashboardLayout/ProviderSidebar.scss +266 -0
  159. package/src/v2/navigation/DashboardLayout/ProviderSidebar.tsx +252 -0
  160. package/src/v2/navigation/DashboardLayout/Sidebar.stories.tsx +220 -0
  161. package/src/v2/navigation/DashboardLayout/TopNavigation.scss +206 -0
  162. package/src/v2/navigation/DashboardLayout/TopNavigation.tsx +270 -0
  163. package/src/v2/navigation/DashboardLayout/assets/AdminLogo.png +0 -0
  164. package/src/v2/navigation/DashboardLayout/assets/BookedLogo_Mark.png +0 -0
  165. package/src/v2/navigation/DashboardLayout/index.ts +20 -0
  166. package/src/v2/styles/index.scss +0 -1
  167. package/src/v2/styles/tokens/_colors.scss +531 -98
  168. package/dist/v2/components/Colors/Colors.d.ts +0 -21
  169. package/dist/v2/components/Colors/Colors.d.ts.map +0 -1
  170. package/dist/v2/components/Colors/index.d.ts +0 -3
  171. package/dist/v2/components/Colors/index.d.ts.map +0 -1
  172. package/dist/v2/components/Typography/Typography.d.ts +0 -11
  173. package/dist/v2/components/Typography/Typography.d.ts.map +0 -1
  174. package/dist/v2/components/Typography/index.d.ts +0 -3
  175. package/dist/v2/components/Typography/index.d.ts.map +0 -1
  176. package/src/v2/components/Colors/Colors.scss +0 -64
  177. package/src/v2/components/Colors/Colors.stories.tsx +0 -143
  178. package/src/v2/components/Colors/Colors.tsx +0 -51
  179. package/src/v2/components/Colors/ColorsAliases.stories.tsx +0 -285
  180. package/src/v2/components/Colors/Sizes.stories.tsx +0 -141
  181. package/src/v2/components/Colors/index.ts +0 -2
  182. package/src/v2/components/Typography/Typography.scss +0 -72
  183. package/src/v2/components/Typography/Typography.stories.tsx +0 -266
  184. package/src/v2/components/Typography/Typography.tsx +0 -56
  185. package/src/v2/components/Typography/index.ts +0 -2
  186. package/src/v2/styles/tokens/_aliases.scss +0 -199
@@ -0,0 +1,623 @@
1
+ import React from 'react';
2
+ import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, ReferenceLine } from 'recharts';
3
+ import { TrendingUp, TrendingDown } from 'lucide-react';
4
+ import './Chart.scss';
5
+
6
+ interface ChartProps {
7
+ title: string;
8
+ subtitle: string;
9
+ data: Array<{ time: string; value: number; participants?: number }>;
10
+ isRevenue?: boolean;
11
+ className?: string;
12
+ period?: 'today' | 'yesterday' | 'week';
13
+ bookingType?: 'bookings_for' | 'bookings_made';
14
+ isFullWidth?: boolean;
15
+ isLoading?: boolean;
16
+ isRefreshing?: boolean;
17
+ }
18
+
19
+ export const Chart: React.FC<ChartProps> = ({
20
+ title,
21
+ subtitle,
22
+ data,
23
+ isRevenue = false,
24
+ className = '',
25
+ period = 'today',
26
+ bookingType = 'bookings_for',
27
+ isFullWidth: propIsFullWidth = false,
28
+ isLoading = false,
29
+ isRefreshing = false
30
+ }) => {
31
+ const [animationKey, setAnimationKey] = React.useState(0);
32
+ const [isAnimating, setIsAnimating] = React.useState(false);
33
+ const [forceResize, setForceResize] = React.useState(0);
34
+ const prevDataRef = React.useRef(data);
35
+ const prevPeriodRef = React.useRef(period);
36
+
37
+ React.useEffect(() => {
38
+ const dataChanged = JSON.stringify(prevDataRef.current) !== JSON.stringify(data);
39
+ const periodChanged = prevPeriodRef.current !== period;
40
+
41
+ if (dataChanged || periodChanged) {
42
+ setIsAnimating(true);
43
+ setAnimationKey(prev => prev + 1);
44
+
45
+ if (periodChanged) {
46
+ setForceResize(prev => prev + 1);
47
+ prevPeriodRef.current = period;
48
+ }
49
+
50
+ prevDataRef.current = data;
51
+
52
+ const timer = setTimeout(() => {
53
+ setIsAnimating(false);
54
+ }, 800);
55
+
56
+ return () => clearTimeout(timer);
57
+ }
58
+ }, [data, period]);
59
+ const [isMobile, setIsMobile] = React.useState(false);
60
+ const [isTablet, setIsTablet] = React.useState(false);
61
+ const [detectedFullWidth, setDetectedFullWidth] = React.useState(false);
62
+ const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 });
63
+ const chartRef = React.useRef<HTMLDivElement>(null);
64
+
65
+ const isFullWidth = propIsFullWidth || detectedFullWidth;
66
+
67
+ React.useEffect(() => {
68
+ const checkScreenSize = () => {
69
+ setIsMobile(window.innerWidth < 480);
70
+ setIsTablet(window.innerWidth >= 480 && window.innerWidth < 768);
71
+
72
+ if (chartRef.current) {
73
+ const rect = chartRef.current.getBoundingClientRect();
74
+ setDimensions({ width: rect.width, height: rect.height });
75
+ }
76
+ };
77
+
78
+ checkScreenSize();
79
+ window.addEventListener('resize', checkScreenSize);
80
+ return () => window.removeEventListener('resize', checkScreenSize);
81
+ }, []);
82
+
83
+ const useChartRecalculation = () => {
84
+ const forceRecalculation = React.useCallback(() => {
85
+ if (chartRef.current) {
86
+ const resizeObserver = new ResizeObserver(() => {
87
+ });
88
+
89
+ resizeObserver.observe(chartRef.current);
90
+
91
+ setTimeout(() => {
92
+ resizeObserver.disconnect();
93
+ }, 100);
94
+
95
+ setTimeout(() => {
96
+ window.dispatchEvent(new Event('resize'));
97
+ }, 50);
98
+ }
99
+ }, []);
100
+
101
+ return forceRecalculation;
102
+ };
103
+
104
+ const forceRecalculation = useChartRecalculation();
105
+
106
+ React.useEffect(() => {
107
+ forceRecalculation();
108
+ }, [period, forceRecalculation]);
109
+
110
+ React.useEffect(() => {
111
+ if (propIsFullWidth !== undefined) return;
112
+
113
+ const checkChartWidth = () => {
114
+ if (chartRef.current) {
115
+ const chartWidth = chartRef.current.offsetWidth;
116
+ const viewportWidth = window.innerWidth;
117
+
118
+ const isWide = chartWidth > (viewportWidth * 0.65);
119
+ setDetectedFullWidth(isWide);
120
+ }
121
+ };
122
+
123
+ const timeoutId = setTimeout(checkChartWidth, 100);
124
+
125
+ window.addEventListener('resize', checkChartWidth);
126
+
127
+ const resizeObserver = new ResizeObserver(() => {
128
+ setTimeout(checkChartWidth, 50);
129
+ });
130
+
131
+ if (chartRef.current) {
132
+ resizeObserver.observe(chartRef.current);
133
+ }
134
+
135
+ return () => {
136
+ clearTimeout(timeoutId);
137
+ window.removeEventListener('resize', checkChartWidth);
138
+ resizeObserver.disconnect();
139
+ };
140
+ }, [propIsFullWidth, forceResize]);
141
+
142
+ React.useEffect(() => {
143
+ if (chartRef.current) {
144
+
145
+ const resizeEvent = new Event('resize');
146
+ window.dispatchEvent(resizeEvent);
147
+
148
+ const currentRef = chartRef.current;
149
+ const display = currentRef.style.display;
150
+ currentRef.style.display = 'none';
151
+ currentRef.offsetHeight;
152
+ currentRef.style.display = display;
153
+
154
+ setTimeout(() => {
155
+ const rect = currentRef.getBoundingClientRect();
156
+ setDimensions({ width: rect.width, height: rect.height });
157
+ }, 10);
158
+ }
159
+ }, [period, forceResize, animationKey]);
160
+ const processedData = React.useMemo(() => {
161
+ const transformXAxisForPeriod = (data: Array<{ time: string; value: number; participants?: number }>, period: string) => {
162
+ if (period === 'week') {
163
+ const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
164
+
165
+ return days.map((day, index) => {
166
+ const hourIndex = index * 3;
167
+ const dayData = data[hourIndex] || { time: '', value: 0, participants: 0 };
168
+
169
+ return {
170
+ time: day,
171
+ value: dayData.value,
172
+ participants: dayData.participants || 0
173
+ };
174
+ });
175
+ }
176
+
177
+ if (isFullWidth) {
178
+ if (data.length > 8) {
179
+ return data.length <= 24 ? data : data.filter((_, index) => index % 2 === 0);
180
+ }
181
+ return data;
182
+ }
183
+
184
+ if (period === 'today' && data.length > 12) {
185
+ return data.filter((_, index) => index % 3 === 0);
186
+ }
187
+
188
+ return data;
189
+ };
190
+
191
+ const transformed = transformXAxisForPeriod(data, period);
192
+
193
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
194
+ const isMonthlyData = transformed.some(item => months.some(month => item.time.includes(month)));
195
+
196
+ if (isMonthlyData) {
197
+ const last12Months = transformed.slice(-12);
198
+ return last12Months;
199
+ }
200
+
201
+ return transformed;
202
+ }, [data, period, isFullWidth]);
203
+
204
+ const maxValue = Math.max(...processedData.map(d => d.value));
205
+
206
+ let yAxisMax: number;
207
+ let yAxisMin = 0;
208
+
209
+ const calculateOptimalScale = (max: number) => {
210
+ if (max === 0) return isRevenue ? 10 : 5;
211
+
212
+ const bufferedMax = max * (isRevenue ? 1.15 : 1.1);
213
+
214
+ if (isRevenue) {
215
+ if (bufferedMax <= 1) return 1;
216
+ if (bufferedMax <= 2) return 2;
217
+ if (bufferedMax <= 5) return 5;
218
+ if (bufferedMax <= 10) return 10;
219
+ if (bufferedMax <= 20) return 20;
220
+ if (bufferedMax <= 25) return 25;
221
+ if (bufferedMax <= 50) return 50;
222
+ if (bufferedMax <= 100) return 100;
223
+ if (bufferedMax <= 250) return 250;
224
+ if (bufferedMax <= 500) return 500;
225
+ if (bufferedMax <= 1000) return 1000;
226
+
227
+ const magnitude = Math.pow(10, Math.floor(Math.log10(bufferedMax)));
228
+ const normalized = bufferedMax / magnitude;
229
+
230
+ if (normalized <= 1) return magnitude;
231
+ if (normalized <= 2) return 2 * magnitude;
232
+ if (normalized <= 2.5) return 2.5 * magnitude;
233
+ if (normalized <= 5) return 5 * magnitude;
234
+ return 10 * magnitude;
235
+ } else {
236
+ if (bufferedMax <= 1) return 2;
237
+ if (bufferedMax <= 2) return 3;
238
+ if (bufferedMax <= 3) return 4;
239
+ if (bufferedMax <= 5) return 6;
240
+ if (bufferedMax <= 8) return 10;
241
+ if (bufferedMax <= 12) return 15;
242
+ if (bufferedMax <= 20) return 25;
243
+ if (bufferedMax <= 40) return 50;
244
+ if (bufferedMax <= 80) return 100;
245
+
246
+ const magnitude = Math.pow(10, Math.floor(Math.log10(bufferedMax)));
247
+ const normalized = bufferedMax / magnitude;
248
+
249
+ if (normalized <= 1) return magnitude;
250
+ if (normalized <= 2) return 2 * magnitude;
251
+ if (normalized <= 5) return 5 * magnitude;
252
+ return 10 * magnitude;
253
+ }
254
+ };
255
+
256
+ yAxisMax = calculateOptimalScale(maxValue);
257
+
258
+ const getTickInterval = (max: number) => {
259
+ const targetTickCount = 5;
260
+
261
+ if (isRevenue) {
262
+ if (max <= 1) return 0.25;
263
+ if (max <= 2) return 0.5;
264
+ if (max <= 5) return 1;
265
+ if (max <= 10) return 2;
266
+ if (max <= 20) return 5;
267
+ if (max <= 25) return 5;
268
+ if (max <= 50) return 10;
269
+ if (max <= 100) return 20;
270
+ if (max <= 250) return 50;
271
+ if (max <= 500) return 100;
272
+ if (max <= 1000) return 200;
273
+
274
+ const roughInterval = max / targetTickCount;
275
+ const magnitude = Math.pow(10, Math.floor(Math.log10(roughInterval)));
276
+ const normalized = roughInterval / magnitude;
277
+
278
+ if (normalized <= 1) return magnitude;
279
+ if (normalized <= 2) return 2 * magnitude;
280
+ if (normalized <= 2.5) return 2.5 * magnitude;
281
+ if (normalized <= 5) return 5 * magnitude;
282
+ return 10 * magnitude;
283
+ } else {
284
+ if (max <= 2) return 1;
285
+ if (max <= 3) return 1;
286
+ if (max <= 6) return 2;
287
+ if (max <= 10) return 2;
288
+ if (max <= 15) return 3;
289
+ if (max <= 25) return 5;
290
+ if (max <= 50) return 10;
291
+ if (max <= 100) return 20;
292
+
293
+ const roughInterval = max / targetTickCount;
294
+ const magnitude = Math.pow(10, Math.floor(Math.log10(roughInterval)));
295
+ const normalized = roughInterval / magnitude;
296
+
297
+ if (normalized <= 1) return magnitude;
298
+ if (normalized <= 2) return 2 * magnitude;
299
+ if (normalized <= 5) return 5 * magnitude;
300
+ return 10 * magnitude;
301
+ }
302
+ };
303
+
304
+ const tickInterval = getTickInterval(yAxisMax);
305
+ const ticks = [];
306
+
307
+ for (let i = yAxisMin; i <= yAxisMax; i += tickInterval) {
308
+ const tick = Math.round(i * 100) / 100;
309
+ ticks.push(tick);
310
+ }
311
+
312
+ if (ticks[ticks.length - 1] > yAxisMax) {
313
+ ticks.pop();
314
+ }
315
+
316
+ if (ticks[ticks.length - 1] < yAxisMax) {
317
+ ticks.push(yAxisMax);
318
+ }
319
+
320
+ const getXAxisInterval = (dataLength: number, isMonthly: boolean = false, isWeekly: boolean = false, isHourly: boolean = false) => {
321
+ if (isMobile) {
322
+ if (isFullWidth) {
323
+ if (dataLength <= 6) return 0;
324
+ if (dataLength <= 10) return 1;
325
+ return Math.floor(dataLength / 5);
326
+ }
327
+
328
+ if (isMonthly && dataLength <= 12) return 2;
329
+ if (isWeekly && dataLength <= 7) return 0;
330
+ if (isHourly && dataLength <= 12) return 3;
331
+ if (dataLength <= 4) return 0;
332
+ if (dataLength <= 7) return 1;
333
+ if (dataLength <= 12) return 2;
334
+ return Math.floor(dataLength / 3);
335
+ }
336
+
337
+ if (isTablet) {
338
+ if (isFullWidth) {
339
+ if (isMonthly && dataLength <= 12) return 0;
340
+ if (isWeekly && dataLength <= 7) return 0;
341
+ if (isHourly && dataLength <= 16) return 0;
342
+ if (dataLength <= 10) return 0;
343
+ if (dataLength <= 20) return 1;
344
+ return Math.floor(dataLength / 10);
345
+ }
346
+
347
+ if (isMonthly && dataLength <= 12) return 0;
348
+ if (isWeekly && dataLength <= 7) return 0;
349
+ if (isHourly && dataLength <= 12) return 1;
350
+ if (dataLength <= 4) return 0;
351
+ if (dataLength <= 7) return 0;
352
+ if (dataLength <= 12) return 1;
353
+ return Math.floor(dataLength / 5);
354
+ }
355
+
356
+ if (isFullWidth) {
357
+ if (isMonthly && dataLength <= 12) return 0;
358
+ if (isWeekly && dataLength <= 7) return 0;
359
+ if (isHourly && dataLength <= 24) return 1;
360
+ if (dataLength <= 10) return 0;
361
+ if (dataLength <= 20) return 1;
362
+ return Math.floor(dataLength / 12);
363
+ }
364
+
365
+ if (isMonthly && dataLength <= 12) return 0;
366
+ if (isWeekly && dataLength <= 7) return 0;
367
+ if (isHourly && dataLength <= 12) return 1;
368
+ if (dataLength <= 4) return 0;
369
+ if (dataLength <= 7) return 0;
370
+ if (dataLength <= 12) return 2;
371
+ return Math.floor(dataLength / 6);
372
+ };
373
+
374
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
375
+ const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
376
+ const isMonthlyData = processedData.some(item => months.some(month => item.time.includes(month)));
377
+ const isWeeklyData = processedData.some(item => days.some(day => item.time === day));
378
+ const isHourlyData = processedData.some(item => item.time.includes(':'));
379
+ const xAxisInterval = getXAxisInterval(processedData.length, isMonthlyData, isWeeklyData, isHourlyData);
380
+
381
+ const getCurrentTimeReference = () => {
382
+ if (subtitle && subtitle.includes('Yesterday')) return null;
383
+
384
+ const now = new Date();
385
+
386
+ if (subtitle === 'Today') {
387
+ return `${now.getHours().toString().padStart(2, '0')}:00`;
388
+ }
389
+
390
+ if (subtitle && subtitle.includes(' - ') && (subtitle.includes('Mon') || subtitle.includes('Tue') || subtitle.includes('Wed') || subtitle.includes('Thu') || subtitle.includes('Fri') || subtitle.includes('Sat') || subtitle.includes('Sun'))) {
391
+ const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
392
+ return dayNames[now.getDay() === 0 ? 6 : now.getDay() - 1];
393
+ }
394
+
395
+ if (subtitle && /^[A-Z][a-z]+ \d{4}$/.test(subtitle)) {
396
+ const dayOfMonth = now.getDate();
397
+ const weekNumber = Math.ceil(dayOfMonth / 7);
398
+ const weekStart = ((weekNumber - 1) * 7) + 1;
399
+ const weekEnd = Math.min(weekNumber * 7, new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate());
400
+
401
+ const formatOrdinal = (n: number) => {
402
+ const suffix = n % 10 === 1 && n !== 11 ? 'st' :
403
+ n % 10 === 2 && n !== 12 ? 'nd' :
404
+ n % 10 === 3 && n !== 13 ? 'rd' : 'th';
405
+ return `${n}${suffix}`;
406
+ };
407
+
408
+ return `${formatOrdinal(weekStart)} - ${formatOrdinal(weekEnd)}`;
409
+ }
410
+
411
+ if (subtitle && /^\d{4}$/.test(subtitle)) {
412
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
413
+ return months[now.getMonth()];
414
+ }
415
+
416
+ return null;
417
+ };
418
+
419
+ const currentTimeRef = getCurrentTimeReference();
420
+
421
+ const CustomTooltip = ({ active, payload, label }: any) => {
422
+ if (active && payload && payload.length) {
423
+ const currentValue = payload[0].value;
424
+ const currentIndex = processedData.findIndex(d => d.time === label);
425
+
426
+ let trend = null;
427
+ if (currentIndex >= 0 && currentIndex > 0) {
428
+ const prevValue = processedData[currentIndex - 1].value;
429
+ const prevTime = processedData[currentIndex - 1].time;
430
+ const change = currentValue - prevValue;
431
+
432
+ if (typeof prevValue === 'number' && typeof currentValue === 'number') {
433
+ const previousValue = prevValue === 0 ? 0.1 : Math.abs(prevValue);
434
+ const percentChange = ((change / previousValue) * 100);
435
+
436
+ if (!isNaN(percentChange) && isFinite(percentChange)) {
437
+ trend = {
438
+ change,
439
+ percentChange: Math.abs(percentChange).toFixed(1),
440
+ isPositive: change > 0,
441
+ isEqual: change === 0,
442
+ comparisonText: `vs ${prevTime}`,
443
+ previousValue: prevValue
444
+ };
445
+ }
446
+ }
447
+ }
448
+
449
+ const formatValue = (value: number) => {
450
+ if (isRevenue) {
451
+ return `£${value.toLocaleString("en-GB", {
452
+ minimumFractionDigits: 2,
453
+ maximumFractionDigits: 2
454
+ })}`;
455
+ }
456
+ return value.toLocaleString();
457
+ };
458
+
459
+ return (
460
+ <div className="chart__tooltip">
461
+ <div className="chart__tooltip-label">
462
+ {label}
463
+ </div>
464
+ <div className="chart__tooltip-value">
465
+ {isRevenue ? formatValue(currentValue) : `${currentValue} ${currentValue === 1 ? 'Booking' : 'Bookings'}`}
466
+ </div>
467
+
468
+ {!isRevenue && payload && payload[0] && payload[0].payload.participants !== undefined && (
469
+ <div className="chart__tooltip-participants">
470
+ {payload[0].payload.participants} people
471
+ </div>
472
+ )}
473
+
474
+ {trend && !trend.isEqual && (
475
+ <div className={`chart__tooltip-trend ${
476
+ trend.isEqual ? 'neutral' : trend.isPositive ? 'positive' : 'negative'
477
+ }`}>
478
+ <div className="chart__tooltip-trend-content">
479
+ {trend.isPositive ? (
480
+ <TrendingUp size={12} className="chart__tooltip-trend-icon" />
481
+ ) : (
482
+ <TrendingDown size={12} className="chart__tooltip-trend-icon" />
483
+ )}
484
+ <span className="chart__tooltip-trend-value">
485
+ {(trend.isPositive ? '+' : '') + (isRevenue ? `£${Math.abs(trend.change).toFixed(2)}` : `${trend.change} ${Math.abs(trend.change) === 1 ? 'Booking' : 'Bookings'}`)}
486
+ </span>
487
+ </div>
488
+ </div>
489
+ )}
490
+ {trend && !trend.isEqual && (
491
+ <div className="chart__tooltip-comparison">
492
+ {trend.comparisonText}
493
+ </div>
494
+ )}
495
+ </div>
496
+ );
497
+ }
498
+ return null;
499
+ };
500
+
501
+ return (
502
+ <section
503
+ ref={chartRef}
504
+ className={`chart ${className} ${isLoading || isAnimating ? 'chart--loading' : ''} ${isRefreshing ? 'chart--refreshing' : ''}`.trim()}
505
+ data-full-width={isFullWidth}
506
+ >
507
+ <header className="chart__header">
508
+ <h2 className="chart__title">
509
+ {title}
510
+ {/* Subtle refresh indicator in title */}
511
+ {isRefreshing && !isLoading && (
512
+ <span className="chart__title-refresh-indicator">⟳</span>
513
+ )}
514
+ </h2>
515
+ <p className="chart__subtitle">
516
+ {subtitle}
517
+ </p>
518
+ </header>
519
+
520
+ <div className="chart__container">
521
+ {/* Only show major loading overlay for initial loads */}
522
+ {isLoading && (
523
+ <div className="chart__loading-overlay">
524
+ <div className="chart__loading-spinner"></div>
525
+ </div>
526
+ )}
527
+ <div className={`chart__wrapper ${isAnimating ? 'chart__wrapper--recalculating' : ''}`}>
528
+ <ResponsiveContainer
529
+ key={`responsive-${animationKey}-${forceResize}`}
530
+ width="100%"
531
+ height="100%"
532
+ debounce={1}
533
+ >
534
+ <AreaChart
535
+ key={`chart-${animationKey}-${forceResize}`}
536
+ data={processedData}
537
+ margin={{
538
+ top: 10,
539
+ right: 10,
540
+ left: 10,
541
+ bottom: 16
542
+ }}
543
+ >
544
+ <defs>
545
+ <linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
546
+ <stop offset="0%" stopColor="#1F9AB5" stopOpacity={0.5}/>
547
+ <stop offset="100%" stopColor="#1F9AB5" stopOpacity={0}/>
548
+ </linearGradient>
549
+ </defs>
550
+ <CartesianGrid strokeDasharray="3 3" stroke="#E8E9EF" />
551
+ <XAxis
552
+ dataKey="time"
553
+ type="category"
554
+ axisLine={false}
555
+ tickLine={false}
556
+ tick={{
557
+ fontSize: isMobile ? 9 : (isTablet ? 10 : 11),
558
+ fill: '#626A90'
559
+ }}
560
+ tickMargin={isMobile ? 5 : 10}
561
+ interval={xAxisInterval}
562
+ />
563
+ <YAxis
564
+ domain={[yAxisMin, yAxisMax]}
565
+ ticks={ticks}
566
+ axisLine={false}
567
+ tickLine={false}
568
+ tick={{
569
+ fontSize: isMobile ? 9 : (isTablet ? 10 : 11),
570
+ fill: '#626A90'
571
+ }}
572
+ tickFormatter={(value) => {
573
+ if (isRevenue) {
574
+ return isMobile ? `£${Math.round(value)}` : `£${value}`;
575
+ }
576
+ return value.toString();
577
+ }}
578
+ tickMargin={isMobile ? 3 : 5}
579
+ width={isMobile ? 30 : (isTablet ? 35 : 40)}
580
+ />
581
+ <Tooltip content={CustomTooltip} />
582
+ {currentTimeRef && (
583
+ <ReferenceLine
584
+ x={currentTimeRef}
585
+ stroke="#7C3AED"
586
+ strokeWidth={2}
587
+ strokeDasharray="6 4"
588
+ />
589
+ )}
590
+ <Area
591
+ type="monotone"
592
+ dataKey="value"
593
+ stroke="#1F9AB5"
594
+ strokeWidth={isMobile ? 1.5 : 2}
595
+ fill="url(#chartGradient)"
596
+ dot={{
597
+ fill: '#1F9AB5',
598
+ strokeWidth: 0,
599
+ r: isMobile ? 3 : (isTablet ? 3.5 : 4),
600
+ fillOpacity: 1
601
+ }}
602
+ activeDot={{
603
+ r: isMobile ? 5 : (isTablet ? 5.5 : 6),
604
+ stroke: '#1F9AB5',
605
+ strokeWidth: 2,
606
+ fill: '#fff',
607
+ fillOpacity: 1,
608
+ strokeOpacity: 1
609
+ }}
610
+ animationBegin={0}
611
+ animationDuration={800}
612
+ animationEasing="ease-out"
613
+ isAnimationActive={true}
614
+ />
615
+ </AreaChart>
616
+ </ResponsiveContainer>
617
+ </div>
618
+ </div>
619
+ </section>
620
+ );
621
+ };
622
+
623
+ export type { ChartProps };
@@ -0,0 +1,2 @@
1
+ export { Chart } from './Chart';
2
+ export type { ChartProps } from './Chart';