@wazobiatech/wazui 0.0.4 → 0.0.6

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 (75) hide show
  1. package/README.md +1 -1
  2. package/USAGE.md +96 -0
  3. package/package.json +8 -4
  4. package/src/assets/dashboard/book.svg +0 -6
  5. package/src/assets/dashboard/edit.svg +0 -5
  6. package/src/assets/dashboard/emerald.svg +0 -9
  7. package/src/assets/dashboard/eye.svg +0 -4
  8. package/src/assets/dashboard/gold.svg +0 -9
  9. package/src/assets/dashboard/percentage-square.svg +0 -6
  10. package/src/assets/dashboard/sapphire.svg +0 -9
  11. package/src/assets/dashboard/send-2.svg +0 -4
  12. package/src/assets/desktop.svg +0 -7
  13. package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.otf +0 -0
  14. package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.woff +0 -0
  15. package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.woff2 +0 -0
  16. package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.otf +0 -0
  17. package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.woff +0 -0
  18. package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.woff2 +0 -0
  19. package/src/assets/fonts/UniversalJungle/UniversalJungle-420.otf +0 -0
  20. package/src/assets/fonts/UniversalJungle/UniversalJungle-420.woff +0 -0
  21. package/src/assets/fonts/UniversalJungle/UniversalJungle-420.woff2 +0 -0
  22. package/src/assets/fonts/UniversalJungle/UniversalJungle-515.otf +0 -0
  23. package/src/assets/fonts/UniversalJungle/UniversalJungle-515.woff +0 -0
  24. package/src/assets/fonts/UniversalJungle/UniversalJungle-515.woff2 +0 -0
  25. package/src/assets/instagram.svg +0 -9
  26. package/src/assets/mobile.svg +0 -4
  27. package/src/assets/no-data-image.svg +0 -41
  28. package/src/assets/no-image-icon.svg +0 -84
  29. package/src/assets/pexels.svg +0 -9
  30. package/src/assets/react.svg +0 -1
  31. package/src/assets/sounds/draft.mp3 +0 -0
  32. package/src/assets/sounds/error.mp3 +0 -0
  33. package/src/assets/sounds/publish.mp3 +0 -0
  34. package/src/assets/tablet.svg +0 -4
  35. package/src/assets/unsplash.svg +0 -3
  36. package/src/assets/upload-image.svg +0 -43
  37. package/src/assets/user.svg +0 -4
  38. package/src/components/shadcn/index.ts +0 -31
  39. package/src/config/env.ts +0 -15
  40. package/src/contexts/ThemeContext.tsx +0 -11
  41. package/src/data/users.json +0 -6014
  42. package/src/decorators/ActionModalDecorator.tsx +0 -10
  43. package/src/decorators/ApolloDecorator.tsx +0 -19
  44. package/src/decorators/DolosApolloDecorator.tsx +0 -31
  45. package/src/decorators/JestDecorators.tsx +0 -34
  46. package/src/decorators/ReactQueryDecorator.tsx +0 -13
  47. package/src/decorators/RouterDecorator.tsx +0 -5
  48. package/src/decorators/UnsafePropsDecorator.tsx +0 -0
  49. package/src/env.ts +0 -99
  50. package/src/hooks/use-autogrow.ts +0 -28
  51. package/src/hooks/use-mobile.ts +0 -19
  52. package/src/hooks/useAuthors.ts +0 -78
  53. package/src/hooks/useDebounce.ts +0 -18
  54. package/src/hooks/useMercuryProjectToken.ts +0 -112
  55. package/src/hooks/useProjectToken.ts +0 -21
  56. package/src/hooks/useTheme.ts +0 -12
  57. package/src/index.css +0 -397
  58. package/src/index.ts +0 -62
  59. package/src/mocks/env.ts +0 -12
  60. package/src/providers/QueryProvider/index.tsx +0 -10
  61. package/src/providers/ThemeProvider/index.tsx +0 -52
  62. package/src/types/category.types.ts +0 -13
  63. package/src/types/image.types.ts +0 -44
  64. package/src/types/post.types.ts +0 -121
  65. package/src/types/theme.types.ts +0 -10
  66. package/src/types/users.types.ts +0 -5
  67. package/src/utils/apolloClient.ts +0 -160
  68. package/src/utils/articleMetadata.ts +0 -92
  69. package/src/utils/audio.ts +0 -10
  70. package/src/utils/imageProcessing.ts +0 -399
  71. package/src/utils/images.ts +0 -279
  72. package/src/utils/mercury.ts +0 -0
  73. package/src/utils/utils.ts +0 -638
  74. package/src/utils/videos.ts +0 -29
  75. package/src/vite-env.d.ts +0 -1
@@ -1,638 +0,0 @@
1
- import { clsx, type ClassValue } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
- import he from 'he';
4
- import { Column } from '@tanstack/react-table';
5
- export function cn(...inputs: ClassValue[]) {
6
- return twMerge(clsx(inputs));
7
- }
8
- export function generateAutoGridColsClass<TData>(columns: Column<TData, unknown>[]) {
9
- const colDefs = columns.map(col => {
10
- const widthMeta = col.columnDef.meta?.width;
11
- return widthMeta === 'grow' ? '1fr' : 'auto';
12
- });
13
-
14
- const tailwindClass = `grid-cols-[${colDefs.join('_')}]`;
15
- const cssValue = colDefs.join(' '); // e.g., "auto 1fr auto"
16
-
17
- return {
18
- tailwindClass,
19
- cssValue,
20
- };
21
- }
22
-
23
- export function cleanHTML(input: string = ''): string {
24
- if (!input) return '';
25
- const noTags = input?.replace?.(/<[^>]*>/g, '');
26
- return he.decode(noTags);
27
- }
28
-
29
- export const getNameInitials = (name: string): string => {
30
- if (!name) return '';
31
-
32
- return name
33
- .split(' ')
34
- .map(word => word[0]?.toUpperCase())
35
- .filter(Boolean)
36
- .slice(0, 2)
37
- .join('');
38
- };
39
-
40
- export function htmlToExcerpt(
41
- html: string = '',
42
- options: {
43
- maxLength?: number; // Maximum excerpt length
44
- minLength?: number; // Minimum length before truncating
45
- stripImages?: boolean; // Remove image tags
46
- stripLinks?: boolean; // Keep link text but remove href
47
- preserveFormatting?: boolean; // Keep basic formatting like <strong>, <em>
48
- ellipsis?: string; // What to add at the end when truncated
49
- sentenceAware?: boolean; // Try to end at sentence boundaries
50
- removeEmptyParagraphs?: boolean; // Remove empty <p> tags
51
- } = {}
52
- ) {
53
- const {
54
- maxLength = 160,
55
- minLength = 50,
56
- stripImages = true,
57
- stripLinks = false,
58
- preserveFormatting = false,
59
- ellipsis = '...',
60
- sentenceAware = true,
61
- removeEmptyParagraphs = true,
62
- } = options;
63
-
64
- if (!html || typeof html !== 'string') {
65
- return '';
66
- }
67
-
68
- let cleanText = html;
69
-
70
- // Remove script and style tags completely
71
- cleanText = cleanText.replace(/<(script|style)[^>]*>[\s\S]*?<\/(script|style)>/gi, '');
72
-
73
- // Remove comments
74
- cleanText = cleanText.replace(/<!--[\s\S]*?-->/g, '');
75
-
76
- // Remove images if specified
77
- if (stripImages) {
78
- cleanText = cleanText.replace(/<img[^>]*>/gi, '');
79
- }
80
-
81
- // Handle links
82
- if (stripLinks) {
83
- // Remove links but keep the text content
84
- cleanText = cleanText.replace(/<a[^>]*>(.*?)<\/a>/gi, '$1');
85
- } else {
86
- // Keep links as text (remove href but keep content)
87
- cleanText = cleanText.replace(/<a[^>]*>(.*?)<\/a>/gi, '$1');
88
- }
89
-
90
- // Remove empty paragraphs
91
- if (removeEmptyParagraphs) {
92
- cleanText = cleanText.replace(/<p[^>]*>\s*<\/p>/gi, '');
93
- }
94
-
95
- // Preserve basic formatting if specified
96
- if (preserveFormatting) {
97
- // Convert basic formatting to plain text equivalents
98
- cleanText = cleanText.replace(/<(strong|b)[^>]*>(.*?)<\/(strong|b)>/gi, '**$2**');
99
- cleanText = cleanText.replace(/<(em|i)[^>]*>(.*?)<\/(em|i)>/gi, '_$2_');
100
- }
101
-
102
- // Convert block elements to spaces before removing tags
103
- cleanText = cleanText.replace(
104
- /<\/(p|div|h[1-6]|li|td|th|blockquote|section|article)[^>]*>/gi,
105
- '</$1> '
106
- );
107
- cleanText = cleanText.replace(/<(br|hr)[^>]*\/?>/gi, ' ');
108
- cleanText = cleanText.replace(/<\/(ul|ol|dl)[^>]*>/gi, '</$1> ');
109
-
110
- // Remove all remaining HTML tags
111
- cleanText = cleanText.replace(/<[^>]*>/g, '');
112
-
113
- // Decode HTML entities
114
- cleanText = cleanText
115
- .replace(/&nbsp;/g, ' ')
116
- .replace(/&amp;/g, '&')
117
- .replace(/&lt;/g, '<')
118
- .replace(/&gt;/g, '>')
119
- .replace(/&quot;/g, '"')
120
- // eslint-disable-next-line quotes
121
- .replace(/&#39;/g, "'")
122
- // eslint-disable-next-line quotes
123
- .replace(/&apos;/g, "'");
124
-
125
- // Clean up whitespace but preserve sentence spacing
126
- cleanText = cleanText
127
- .replace(/\s+/g, ' ') // Multiple spaces to single space
128
- .replace(/\.\s+/g, '. ') // Ensure space after periods
129
- .replace(/!\s+/g, '! ') // Ensure space after exclamation marks
130
- .replace(/\?\s+/g, '? ') // Ensure space after question marks
131
- .trim(); // Remove leading/trailing spaces
132
-
133
- // If text is shorter than minLength, return as is
134
- if (cleanText.length <= minLength) {
135
- return cleanText;
136
- }
137
-
138
- // If text is within maxLength, return as is
139
- if (cleanText.length <= maxLength) {
140
- return cleanText;
141
- }
142
-
143
- // Truncate text
144
- let truncated = cleanText.substring(0, maxLength);
145
-
146
- // If sentence-aware truncation is enabled
147
- if (sentenceAware) {
148
- // Try to end at sentence boundary
149
- const lastSentence = truncated.lastIndexOf('.');
150
- const lastExclamation = truncated.lastIndexOf('!');
151
- const lastQuestion = truncated.lastIndexOf('?');
152
-
153
- const lastPunctuation = Math.max(lastSentence, lastExclamation, lastQuestion);
154
-
155
- // If we found sentence-ending punctuation and it's not too early
156
- if (lastPunctuation > minLength) {
157
- truncated = truncated.substring(0, lastPunctuation + 1);
158
- return truncated;
159
- }
160
-
161
- // Otherwise, try to end at word boundary
162
- const lastSpace = truncated.lastIndexOf(' ');
163
- if (lastSpace > minLength) {
164
- truncated = truncated.substring(0, lastSpace);
165
- }
166
- } else {
167
- // Simple word boundary truncation
168
- const lastSpace = truncated.lastIndexOf(' ');
169
- if (lastSpace > minLength) {
170
- truncated = truncated.substring(0, lastSpace);
171
- }
172
- }
173
-
174
- return truncated + ellipsis;
175
- }
176
-
177
- export const shortenLargeNumber = (num: number): string => {
178
- if (num >= 100000000) {
179
- return (num / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B';
180
- }
181
- if (num >= 1000000) {
182
- return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
183
- }
184
- if (num >= 1000) {
185
- return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
186
- }
187
- return num.toString();
188
- };
189
-
190
- // =============================================================================
191
- // DATE UTILITIES
192
- // =============================================================================
193
-
194
- interface DateRange {
195
- startDate: string; // Format: "YYYY-MM-DD HH:mm:ss"
196
- endDate: string;
197
- }
198
-
199
- interface FilterOptions {
200
- search?: string;
201
- categories?: string[];
202
- dateRange?: DateRange;
203
- }
204
-
205
- /**
206
- * Get default date range (last month) in Muse datetime format
207
- */
208
- export function getLastMonthDateRange(): DateRange {
209
- const now = new Date();
210
- const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
211
- const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59);
212
-
213
- return {
214
- startDate: formatDateForMuse(lastMonth),
215
- endDate: formatDateForMuse(lastMonthEnd),
216
- };
217
- }
218
-
219
- /**
220
- * Format date for Muse API (YYYY-MM-DD HH:mm:ss)
221
- */
222
- export function formatDateForMuse(date: Date): string {
223
- const year = date.getFullYear();
224
- const month = String(date.getMonth() + 1).padStart(2, '0');
225
- const day = String(date.getDate()).padStart(2, '0');
226
- const hours = String(date.getHours()).padStart(2, '0');
227
- const minutes = String(date.getMinutes()).padStart(2, '0');
228
- const seconds = String(date.getSeconds()).padStart(2, '0');
229
-
230
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
231
- }
232
-
233
- /**
234
- * Convert FilterOptions dateRange to Muse format
235
- */
236
- export function getEffectiveDateRange(filters?: FilterOptions): DateRange {
237
- if (filters?.dateRange) {
238
- // Ensure datetime format
239
- return {
240
- startDate: filters.dateRange.startDate.includes(' ')
241
- ? filters.dateRange.startDate
242
- : `${filters.dateRange.startDate} 00:00:00`,
243
- endDate: filters.dateRange.endDate.includes(' ')
244
- ? filters.dateRange.endDate
245
- : `${filters.dateRange.endDate} 23:59:59`,
246
- };
247
- }
248
- return getLastMonthDateRange();
249
- }
250
-
251
- /**
252
- * Convert date range with start/end Date objects to DateRange format
253
- */
254
- export function toFilterDateRange(dateRange: { start: Date; end: Date }): DateRange {
255
- return {
256
- startDate: formatDateForMuse(dateRange.start),
257
- endDate: formatDateForMuse(dateRange.end),
258
- };
259
- }
260
-
261
- /**
262
- * Convert DateRange format back to Date objects
263
- */
264
- export function fromFilterDateRange(dateRange: DateRange): { start: Date; end: Date } {
265
- return {
266
- start: new Date(dateRange.startDate),
267
- end: new Date(dateRange.endDate),
268
- };
269
- }
270
-
271
- /**
272
- * Format duration from seconds to readable format (e.g., "12.5 min" or "2.3 hrs")
273
- */
274
- export function formatDuration(seconds: number): string {
275
- if (seconds < 60) {
276
- return `${Math.round(seconds)}s`;
277
- } else if (seconds < 3600) {
278
- return `${(seconds / 60).toFixed(1)} min`;
279
- } else {
280
- return `${(seconds / 3600).toFixed(1)} hrs`;
281
- }
282
- }
283
-
284
- /**
285
- * Calculate time ago from date string
286
- */
287
- export function calculateTimeAgo(dateString: string): string {
288
- try {
289
- const diffInDays = Math.floor(
290
- (Date.now() - new Date(dateString).getTime()) / (1000 * 60 * 60 * 24)
291
- );
292
- if (diffInDays === 0) return 'Today';
293
- if (diffInDays === 1) return '1 day ago';
294
- if (diffInDays < 7) return `${diffInDays} days ago`;
295
- if (diffInDays < 30) return `${Math.floor(diffInDays / 7)} weeks ago`;
296
- if (diffInDays < 365) return `${Math.floor(diffInDays / 30)} months ago`;
297
- return `${Math.floor(diffInDays / 365)} years ago`;
298
- } catch {
299
- return 'Recently';
300
- }
301
- }
302
-
303
- // =============================================================================
304
- // PARSING UTILITIES
305
- // =============================================================================
306
-
307
- /**
308
- * Parse author ID from metadata JSON
309
- */
310
- export function parseAuthorId(metadata: string | undefined): string | undefined {
311
- if (!metadata) return undefined;
312
- try {
313
- const parsed = JSON.parse(metadata);
314
- return parsed.author_uuid || parsed.author_id;
315
- } catch {
316
- return undefined;
317
- }
318
- }
319
-
320
- /**
321
- * Parse featured image from various formats
322
- */
323
- export function parseFeaturedImage(img: string | { src?: string } | undefined): string {
324
- const fallback =
325
- 'https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=400&h=300&fit=crop';
326
- if (!img) return fallback;
327
- try {
328
- if (typeof img === 'string') {
329
- const parsed = JSON.parse(img);
330
- return parsed.src || fallback;
331
- }
332
- return img.src || fallback;
333
- } catch {
334
- return typeof img === 'string' ? img : fallback;
335
- }
336
- }
337
-
338
- /**
339
- * Parse user avatar from users.json format
340
- * Avatar is stored as double-escaped JSON: "{\"src\":\"https://...\"}"
341
- */
342
- export function parseUserAvatar(avatar: string | null | undefined): string | undefined {
343
- if (!avatar) return undefined;
344
- try {
345
- // First parse removes outer quotes, second parse gets the object
346
- let parsed = avatar;
347
- if (typeof parsed === 'string') {
348
- // Try to parse as JSON (might be double-encoded)
349
- try {
350
- parsed = JSON.parse(parsed);
351
- } catch {
352
- // Not JSON, might be plain URL
353
- }
354
- }
355
- if (typeof parsed === 'string') {
356
- // Try second parse for double-encoded JSON
357
- try {
358
- parsed = JSON.parse(parsed);
359
- } catch {
360
- // Still not JSON, return as-is if it looks like URL
361
- return parsed.startsWith('http') ? parsed : undefined;
362
- }
363
- }
364
- // Now parsed should be an object with src
365
- if (typeof parsed === 'object' && parsed !== null && 'src' in parsed) {
366
- return (parsed as { src: string }).src;
367
- }
368
- return undefined;
369
- } catch {
370
- return undefined;
371
- }
372
- }
373
-
374
- /**
375
- * Parse analytics from post metadata (if synced from Phoenix)
376
- */
377
- export function parseAnalyticsFromMetadata(metadata: string | undefined): {
378
- totalPageViews: number;
379
- uniqueUsers: number;
380
- averageSessionDuration: number | null;
381
- } | null {
382
- if (!metadata) return null;
383
- try {
384
- const parsed = JSON.parse(metadata);
385
- if (parsed.analytics) {
386
- return {
387
- totalPageViews: parsed.analytics.totalPageViews || 0,
388
- uniqueUsers: parsed.analytics.uniqueUsers || 0,
389
- averageSessionDuration: parsed.analytics.averageSessionDuration,
390
- };
391
- }
392
- return null;
393
- } catch {
394
- return null;
395
- }
396
- }
397
-
398
- // =============================================================================
399
- // FILTER BUILDERS
400
- // =============================================================================
401
-
402
- /**
403
- * Build Muse filter for published posts in date range (org-wide)
404
- * Note: We only filter by published_at - if a post has published_at, it's published
405
- */
406
- export function buildPublishedPostsFilter(dateRange: DateRange) {
407
- return {
408
- published_at: {
409
- gte: dateRange.startDate,
410
- lte: dateRange.endDate,
411
- },
412
- };
413
- }
414
-
415
- /**
416
- * Build Muse filter for draft posts in date range (org-wide)
417
- * Note: Drafts have created_at but no published_at
418
- */
419
- export function buildDraftPostsFilter(dateRange: DateRange) {
420
- return {
421
- status: { eq: 1 }, // Draft - may need adjustment based on actual Muse status values
422
- created_at: {
423
- gte: dateRange.startDate,
424
- lte: dateRange.endDate,
425
- },
426
- };
427
- }
428
-
429
- // =============================================================================
430
- // ANALYTICS UTILITIES
431
- // =============================================================================
432
-
433
- interface PhoenixPageStats {
434
- pageId: string;
435
- pageUrl: string;
436
- totalPageViews: number;
437
- uniqueUsers: number;
438
- averageSessionDuration?: number | null;
439
- totalEvents: number;
440
- lastActivity?: string;
441
- }
442
-
443
- /**
444
- * Get engagement value from Phoenix stats (in seconds)
445
- */
446
- export function getEngagementValue(stat: PhoenixPageStats): number {
447
- return stat.averageSessionDuration || 0;
448
- }
449
-
450
- /**
451
- * Strips invalid characters from a URL string
452
- * @param url - The URL string to sanitize
453
- * @param options - Configuration options for sanitization
454
- * @returns Sanitized URL string
455
- */
456
- export function stripInvalidArticleSlugChars(
457
- url: string,
458
- options: {
459
- allowFragments?: boolean;
460
- allowQueryParams?: boolean;
461
- strictMode?: boolean;
462
- } = {}
463
- ): string {
464
- const { allowFragments = true, allowQueryParams = true, strictMode = false } = options;
465
-
466
- if (!url || typeof url !== 'string') {
467
- return '';
468
- }
469
-
470
- let sanitizedUrl = url.trim();
471
-
472
- // Handle different sanitization levels
473
- if (strictMode) {
474
- // Strict mode: only allow basic URL characters
475
- sanitizedUrl = sanitizedUrl.replace(/\s+/g, '-'); // Replace whitespace with hyphens
476
- sanitizedUrl = sanitizedUrl.replace(/'s\b/g, ''); // Replace 's at word boundaries with just 's'
477
- sanitizedUrl = sanitizedUrl.replace(/[^a-zA-Z0-9\-_~/#[\]()]/g, '');
478
- } else {
479
- // Standard mode: remove common problematic characters
480
- sanitizedUrl = sanitizedUrl
481
- .replace(/[\s]+/g, '-') // Replace spaces with hyphens
482
- .replace(/[<>"{}|\\^`]/g, '') // Remove clearly invalid characters
483
- .replace(/[^\x20-\x7E]/g, '') // Remove non-printable ASCII characters
484
- .replace(/\s/g, ''); // Remove any remaining whitespace
485
- }
486
-
487
- // Handle fragments and query parameters based on options
488
- if (!allowFragments) {
489
- sanitizedUrl = sanitizedUrl.split('#')[0] as string;
490
- }
491
-
492
- if (!allowQueryParams) {
493
- sanitizedUrl = sanitizedUrl.split('?')[0] as string;
494
- }
495
-
496
- // Clean up multiple consecutive special characters
497
- sanitizedUrl = sanitizedUrl
498
- .replace(/[-]{2,}/g, '-') // Multiple hyphens to single hyphen
499
- .replace(/[.]{2,}/g, '.') // Multiple dots to single dot
500
- .replace(/[/]{2,}/g, '//') // Keep double slashes for protocol, but remove more
501
- .replace(/^[-.]|[-.]$/g, ''); // Remove leading/trailing hyphens or dots
502
-
503
- return sanitizedUrl;
504
- }
505
-
506
- export function extractArticleSlugComponents(input: string) {
507
- if (!input) {
508
- return { categorySlug: '', articleSlug: '' };
509
- }
510
-
511
- const lastSlashIndex = input.lastIndexOf('/');
512
-
513
- if (lastSlashIndex === -1) {
514
- return {
515
- categorySlug: '',
516
- articleSlug: input.replace('/', ''),
517
- };
518
- }
519
-
520
- const categorySlug = input.substring(0, lastSlashIndex);
521
- const articleSlug = input.substring(lastSlashIndex + 1);
522
-
523
- return {
524
- categorySlug,
525
- articleSlug,
526
- };
527
- }
528
-
529
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
530
- export function parseRecursiveHelper(data: string): any {
531
- if (typeof data !== 'string' || data === '') return data;
532
-
533
- try {
534
- const parsedData = JSON.parse(data);
535
- if (typeof parsedData === 'string') {
536
- return parseRecursiveHelper(parsedData);
537
- }
538
- return parsedData;
539
- } catch {
540
- return { src: data };
541
- }
542
- }
543
-
544
- export const parseRecursive = (data: string) => {
545
- const parsedData = parseRecursiveHelper(data);
546
- try {
547
- const temp = JSON.parse(parsedData?.src);
548
- return temp;
549
- } catch {
550
- return parsedData;
551
- }
552
- };
553
-
554
- interface DateRange {
555
- startDate: string; // Format: "YYYY-MM-DD HH:mm:ss"
556
- endDate: string;
557
- }
558
-
559
- interface MetricChangeResult {
560
- change: string;
561
- changeType: 'positive' | 'negative';
562
- }
563
-
564
- /**
565
- * Calculate the previous period date range based on current period
566
- *
567
- * Example:
568
- * - Current: Nov 1-30 → Previous: Oct 1-30
569
- * - Current: Nov 15-30 (15 days) → Previous: Oct 31 - Nov 14 (15 days before)
570
- */
571
- export function getPreviousPeriodDateRange(currentDateRange: DateRange): DateRange {
572
- const start = new Date(currentDateRange.startDate);
573
- const end = new Date(currentDateRange.endDate);
574
-
575
- // Calculate the duration in milliseconds
576
- const durationMs = end.getTime() - start.getTime();
577
-
578
- // Shift back by the same duration
579
- // Previous period ends 1ms before current period starts
580
- const prevEnd = new Date(start.getTime() - 1);
581
- const prevStart = new Date(prevEnd.getTime() - durationMs);
582
-
583
- // Use your existing formatDateForMuse function or format manually
584
- const formatDate = (date: Date): string => {
585
- const year = date.getFullYear();
586
- const month = String(date.getMonth() + 1).padStart(2, '0');
587
- const day = String(date.getDate()).padStart(2, '0');
588
- const hours = String(date.getHours()).padStart(2, '0');
589
- const minutes = String(date.getMinutes()).padStart(2, '0');
590
- const seconds = String(date.getSeconds()).padStart(2, '0');
591
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
592
- };
593
-
594
- return {
595
- startDate: formatDate(prevStart),
596
- endDate: formatDate(prevEnd),
597
- };
598
- }
599
-
600
- /**
601
- * Format the change text for dashboard metrics
602
- *
603
- * Examples:
604
- * - (45, 33) → { change: "+12 from last month", changeType: "positive" }
605
- * - (20, 32) → { change: "-12 from last month", changeType: "negative" }
606
- * - (0, 0) → { change: "No change from last month", changeType: "positive" }
607
- * - (50, 0) → { change: "+50 from last month", changeType: "positive" }
608
- */
609
- export function formatMetricChangeText(
610
- current: number,
611
- previous: number,
612
- suffix: string = ''
613
- ): MetricChangeResult {
614
- const diff = current - previous;
615
-
616
- // Both zero - no change
617
- if (previous === 0 && current === 0) {
618
- return {
619
- change: 'No change from last month',
620
- changeType: 'positive',
621
- };
622
- }
623
-
624
- // Previous was zero but now has value - all positive
625
- if (previous === 0) {
626
- return {
627
- change: `+${current}${suffix} from last month`,
628
- changeType: 'positive',
629
- };
630
- }
631
-
632
- // Normal case - calculate difference
633
- const sign = diff >= 0 ? '+' : '';
634
- return {
635
- change: `${sign}${diff}${suffix} from last month`,
636
- changeType: diff >= 0 ? 'positive' : 'negative',
637
- };
638
- }
@@ -1,29 +0,0 @@
1
- export const getYoutubeMatch = (url: string) => {
2
- const match =
3
- url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/) ||
4
- url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/) ||
5
- url.match(/^.*(youtu.be\/|v\/|e\/|u\/\w+\/|embed\/|v=)([^#&?]*).*/);
6
- return match;
7
- };
8
- export const getVimeoMatch = (url: string) => {
9
- const match = url.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/);
10
- return match;
11
- };
12
- export const getVideoUrl = (url: string) => {
13
- const youtubeMatch = getYoutubeMatch(url);
14
-
15
- if (youtubeMatch && youtubeMatch[2]?.length === 11) {
16
- return {
17
- embed: true,
18
- url: 'https' + '://www.youtube.com/embed/' + youtubeMatch[2] + '?showinfo=0',
19
- };
20
- }
21
- const vimeoMatch = getVimeoMatch(url);
22
- if (vimeoMatch) {
23
- return {
24
- embed: true,
25
- url: (vimeoMatch[1] || 'https') + '://player.vimeo.com/video/' + vimeoMatch[2] + '/',
26
- };
27
- }
28
- return { url: url, embed: false };
29
- };
package/src/vite-env.d.ts DELETED
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />