social-masonry 1.0.0 → 1.0.1

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.
@@ -1,47 +1,57 @@
1
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { forwardRef, useRef, useState, useMemo, useCallback, useEffect, useImperativeHandle } from 'react';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
3
3
 
4
4
  /**
5
5
  * Social Masonry - Utility Functions
6
6
  */
7
+ // ============================================
8
+ // URL Parsing Utilities
9
+ // ============================================
7
10
  /**
8
- * Debounce function execution
11
+ * Extract tweet ID from Twitter/X URL
9
12
  */
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- function debounce(fn, delay) {
12
- let timeoutId = null;
13
- return function (...args) {
14
- if (timeoutId) {
15
- clearTimeout(timeoutId);
16
- }
17
- timeoutId = setTimeout(() => {
18
- fn.apply(this, args);
19
- timeoutId = null;
20
- }, delay);
21
- };
13
+ function extractTweetId(url) {
14
+ if (!url)
15
+ return null;
16
+ const match = url.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/);
17
+ return match ? match[1] : null;
22
18
  }
23
19
  /**
24
- * Throttle function execution
20
+ * Extract post ID from Instagram URL
25
21
  */
26
- function throttle(fn, limit) {
27
- let inThrottle = false;
28
- let lastArgs = null;
29
- return function (...args) {
30
- if (!inThrottle) {
31
- fn.apply(this, args);
32
- inThrottle = true;
33
- setTimeout(() => {
34
- inThrottle = false;
35
- if (lastArgs) {
36
- fn.apply(this, lastArgs);
37
- lastArgs = null;
38
- }
39
- }, limit);
40
- }
41
- else {
42
- lastArgs = args;
43
- }
44
- };
22
+ function extractInstagramId(url) {
23
+ if (!url)
24
+ return null;
25
+ const match = url.match(/instagram\.com\/(?:p|reel)\/([A-Za-z0-9_-]+)/);
26
+ return match ? match[1] : null;
27
+ }
28
+ /**
29
+ * Generate unique ID from post URL
30
+ */
31
+ function generatePostId(post) {
32
+ if (post.id)
33
+ return post.id;
34
+ if (post.platform === 'twitter') {
35
+ const tweetId = extractTweetId(post.url);
36
+ return tweetId ? `tw-${tweetId}` : `tw-${hashString(post.url)}`;
37
+ }
38
+ if (post.platform === 'instagram') {
39
+ const igId = extractInstagramId(post.url);
40
+ return igId ? `ig-${igId}` : `ig-${hashString(post.url)}`;
41
+ }
42
+ return `post-${hashString(post.url)}`;
43
+ }
44
+ /**
45
+ * Simple string hash function
46
+ */
47
+ function hashString(str) {
48
+ let hash = 0;
49
+ for (let i = 0; i < str.length; i++) {
50
+ const char = str.charCodeAt(i);
51
+ hash = ((hash << 5) - hash) + char;
52
+ hash = hash & hash;
53
+ }
54
+ return Math.abs(hash).toString(36);
45
55
  }
46
56
  /**
47
57
  * Get number of columns based on viewport width
@@ -69,660 +79,279 @@ const defaultColumnConfig = [
69
79
  { columns: 2, minWidth: 600 },
70
80
  { columns: 1, minWidth: 0 },
71
81
  ];
72
- /**
73
- * Format number with abbreviations (1K, 1M, etc.)
74
- */
75
- function formatNumber(num) {
76
- if (num >= 1000000) {
77
- return `${(num / 1000000).toFixed(1).replace(/\.0$/, '')}M`;
78
- }
79
- if (num >= 1000) {
80
- return `${(num / 1000).toFixed(1).replace(/\.0$/, '')}K`;
81
- }
82
- return num.toString();
83
- }
84
- /**
85
- * Format relative time
86
- */
87
- function formatRelativeTime(date) {
88
- const now = new Date();
89
- const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
90
- if (diffInSeconds < 60) {
91
- return 'now';
92
- }
93
- const diffInMinutes = Math.floor(diffInSeconds / 60);
94
- if (diffInMinutes < 60) {
95
- return `${diffInMinutes}m`;
96
- }
97
- const diffInHours = Math.floor(diffInMinutes / 60);
98
- if (diffInHours < 24) {
99
- return `${diffInHours}h`;
100
- }
101
- const diffInDays = Math.floor(diffInHours / 24);
102
- if (diffInDays < 7) {
103
- return `${diffInDays}d`;
104
- }
105
- const diffInWeeks = Math.floor(diffInDays / 7);
106
- if (diffInWeeks < 4) {
107
- return `${diffInWeeks}w`;
108
- }
109
- // Format as date for older posts
110
- return date.toLocaleDateString('en-US', {
111
- month: 'short',
112
- day: 'numeric',
113
- });
114
- }
115
- /**
116
- * Parse CSS value to pixels
117
- */
118
- function parseCSSValue(value, containerWidth) {
119
- if (typeof value === 'number') {
120
- return value;
121
- }
122
- const numMatch = value.match(/^([\d.]+)(px|rem|em|%|vw)?$/);
123
- if (!numMatch) {
124
- return 0;
125
- }
126
- const num = parseFloat(numMatch[1]);
127
- const unit = numMatch[2] || 'px';
128
- switch (unit) {
129
- case 'px':
130
- return num;
131
- case 'rem':
132
- return num * 16; // Assume 16px base
133
- case 'em':
134
- return num * 16;
135
- case '%':
136
- return 0;
137
- case 'vw':
138
- return (num / 100) * window.innerWidth;
139
- default:
140
- return num;
141
- }
142
- }
143
- /**
144
- * Check if element is in viewport
145
- */
146
- function isInViewport(element, scrollTop, viewportHeight, overscan = 0) {
147
- const expandedTop = scrollTop - overscan;
148
- const expandedBottom = scrollTop + viewportHeight + overscan;
149
- return element.bottom >= expandedTop && element.top <= expandedBottom;
150
- }
151
- /**
152
- * Get scroll position
153
- */
154
- function getScrollPosition(scrollContainer) {
155
- if (!scrollContainer || scrollContainer === window) {
156
- return {
157
- scrollTop: window.scrollY || document.documentElement.scrollTop,
158
- viewportHeight: window.innerHeight,
159
- };
160
- }
161
- return {
162
- scrollTop: scrollContainer.scrollTop,
163
- viewportHeight: scrollContainer.clientHeight,
164
- };
165
- }
166
- /**
167
- * Type guard for Twitter posts
168
- */
169
- function isTwitterPost(post) {
170
- return post.platform === 'twitter';
171
- }
172
- /**
173
- * Type guard for Instagram posts
174
- */
175
- function isInstagramPost(post) {
176
- return post.platform === 'instagram';
177
- }
178
- /**
179
- * Request animation frame with fallback
180
- */
181
- const raf = typeof requestAnimationFrame !== 'undefined'
182
- ? requestAnimationFrame
183
- : (callback) => setTimeout(callback, 16);
184
- /**
185
- * Cancel animation frame with fallback
186
- */
187
- const cancelRaf = typeof cancelAnimationFrame !== 'undefined'
188
- ? cancelAnimationFrame
189
- : (id) => clearTimeout(id);
190
82
 
191
- /**
192
- * Social Masonry - Layout Engine
193
- * Calculates positions for masonry grid items
194
- */
195
- class LayoutEngine {
196
- constructor(options) {
197
- this.options = {
198
- gap: 16,
199
- columns: defaultColumnConfig,
200
- defaultColumns: 3,
201
- padding: 0,
202
- animationDuration: 300,
203
- animate: true,
204
- easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
205
- ...options,
206
- };
207
- this.state = {
208
- positions: new Map(),
209
- columnHeights: [],
210
- containerHeight: 0,
211
- columnWidth: 0,
212
- };
213
- }
214
- /**
215
- * Calculate layout for all posts
216
- */
217
- calculate(posts) {
218
- const { containerWidth, itemHeights } = this.options;
219
- const gap = parseCSSValue(this.options.gap);
220
- const padding = parseCSSValue(this.options.padding);
221
- // Calculate column count and width
222
- const columnCount = getColumnCount(this.options.columns, containerWidth);
223
- const availableWidth = containerWidth - padding * 2;
224
- const totalGapWidth = gap * (columnCount - 1);
225
- const columnWidth = (availableWidth - totalGapWidth) / columnCount;
226
- // Initialize column heights
227
- const columnHeights = new Array(columnCount).fill(0);
228
- const positions = new Map();
229
- // Place each item in the shortest column
230
- for (const post of posts) {
231
- const itemHeight = itemHeights.get(post.id) ?? this.estimateHeight(post, columnWidth);
232
- // Find shortest column
233
- const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
234
- // Calculate position
235
- const x = padding + shortestColumn * (columnWidth + gap);
236
- const y = columnHeights[shortestColumn];
237
- positions.set(post.id, {
238
- id: post.id,
239
- x,
240
- y,
241
- width: columnWidth,
242
- height: itemHeight,
243
- column: shortestColumn,
244
- });
245
- // Update column height
246
- columnHeights[shortestColumn] = y + itemHeight + gap;
83
+ // ============================================
84
+ // Script Loading
85
+ // ============================================
86
+ let twitterScriptLoaded = false;
87
+ let instagramScriptLoaded = false;
88
+ const loadTwitterScript = () => {
89
+ if (twitterScriptLoaded)
90
+ return Promise.resolve();
91
+ if (typeof window === 'undefined')
92
+ return Promise.resolve();
93
+ return new Promise((resolve) => {
94
+ if (window.twttr) {
95
+ twitterScriptLoaded = true;
96
+ resolve();
97
+ return;
247
98
  }
248
- // Remove last gap from column heights
249
- const containerHeight = Math.max(...columnHeights.map(h => h - gap), 0);
250
- this.state = {
251
- positions,
252
- columnHeights,
253
- containerHeight,
254
- columnWidth,
99
+ const script = document.createElement('script');
100
+ script.src = 'https://platform.twitter.com/widgets.js';
101
+ script.async = true;
102
+ script.onload = () => {
103
+ twitterScriptLoaded = true;
104
+ resolve();
255
105
  };
256
- return this.state;
257
- }
258
- /**
259
- * Estimate item height based on content
260
- */
261
- estimateHeight(post, columnWidth) {
262
- let height = 0;
263
- // Header (avatar, name, etc.)
264
- height += 56;
265
- // Text content
266
- if (post.platform === 'twitter') {
267
- const textLength = post.content.text.length;
268
- const avgCharsPerLine = Math.floor(columnWidth / 8); // ~8px per char
269
- const lines = Math.ceil(textLength / avgCharsPerLine);
270
- height += lines * 24; // ~24px per line
271
- }
272
- else if (post.content.caption) {
273
- const captionLength = post.content.caption.length;
274
- const avgCharsPerLine = Math.floor(columnWidth / 8);
275
- const lines = Math.min(Math.ceil(captionLength / avgCharsPerLine), 4); // Max 4 lines
276
- height += lines * 20;
277
- }
278
- // Media
279
- if (post.platform === 'twitter' && post.media?.length) {
280
- const media = post.media[0];
281
- const aspectRatio = media.aspectRatio ?? 16 / 9;
282
- height += columnWidth / aspectRatio;
283
- }
284
- else if (post.platform === 'instagram') {
285
- const aspectRatio = post.media.aspectRatio ?? 1;
286
- height += columnWidth / aspectRatio;
106
+ document.head.appendChild(script);
107
+ });
108
+ };
109
+ const loadInstagramScript = () => {
110
+ if (instagramScriptLoaded)
111
+ return Promise.resolve();
112
+ if (typeof window === 'undefined')
113
+ return Promise.resolve();
114
+ return new Promise((resolve) => {
115
+ if (window.instgrm) {
116
+ instagramScriptLoaded = true;
117
+ resolve();
118
+ return;
287
119
  }
288
- // Footer (metrics, timestamp)
289
- height += 44;
290
- // Padding
291
- height += 24;
292
- return Math.round(height);
293
- }
294
- /**
295
- * Update single item height and recalculate affected items
296
- */
297
- updateItemHeight(id, height) {
298
- this.options.itemHeights.set(id, height);
299
- }
300
- /**
301
- * Get current layout state
302
- */
303
- getState() {
304
- return this.state;
305
- }
306
- /**
307
- * Get position for specific item
308
- */
309
- getPosition(id) {
310
- return this.state.positions.get(id);
311
- }
312
- /**
313
- * Update container width
314
- */
315
- setContainerWidth(width) {
316
- this.options.containerWidth = width;
317
- }
318
- /**
319
- * Get current column count
320
- */
321
- getColumnCount() {
322
- return getColumnCount(this.options.columns, this.options.containerWidth);
323
- }
324
- /**
325
- * Get CSS variables for animations
326
- */
327
- getCSSVariables() {
328
- return {
329
- '--sm-animation-duration': `${this.options.animationDuration}ms`,
330
- '--sm-easing': this.options.easing,
331
- '--sm-gap': typeof this.options.gap === 'number'
332
- ? `${this.options.gap}px`
333
- : this.options.gap,
334
- '--sm-column-width': `${this.state.columnWidth}px`,
335
- '--sm-container-height': `${this.state.containerHeight}px`,
120
+ const script = document.createElement('script');
121
+ script.src = 'https://www.instagram.com/embed.js';
122
+ script.async = true;
123
+ script.onload = () => {
124
+ instagramScriptLoaded = true;
125
+ resolve();
336
126
  };
337
- }
338
- }
339
-
340
- /**
341
- * Social Masonry - Virtualization Engine
342
- * Handles virtual scrolling for large lists
343
- */
344
- class VirtualizationEngine {
345
- constructor(options) {
346
- this.visibleItems = [];
347
- this.rafId = null;
348
- this.lastScrollTop = 0;
349
- this.isScrolling = false;
350
- this.scrollEndTimeout = null;
351
- this.options = {
352
- enabled: true,
353
- overscan: 3,
354
- estimatedItemHeight: 400,
355
- scrollContainer: null,
356
- ...options,
127
+ document.head.appendChild(script);
128
+ });
129
+ };
130
+ const TwitterEmbed = ({ url, theme = 'light', onLoad, onError, }) => {
131
+ const containerRef = useRef(null);
132
+ const embedRef = useRef(null);
133
+ const [loading, setLoading] = useState(true);
134
+ const mountedRef = useRef(true);
135
+ useEffect(() => {
136
+ mountedRef.current = true;
137
+ return () => {
138
+ mountedRef.current = false;
357
139
  };
358
- this.posts = options.posts;
359
- this.positions = options.positions;
360
- this.onVisibleItemsChange = options.onVisibleItemsChange;
361
- this.scrollHandler = throttle(this.handleScroll.bind(this), 16);
362
- }
363
- /**
364
- * Initialize scroll listener
365
- */
366
- init() {
367
- if (!this.options.enabled)
368
- return;
369
- const container = this.options.scrollContainer ?? window;
370
- container.addEventListener('scroll', this.scrollHandler, { passive: true });
371
- // Initial calculation
372
- this.calculateVisibleItems();
373
- }
374
- /**
375
- * Destroy and cleanup
376
- */
377
- destroy() {
378
- const container = this.options.scrollContainer ?? window;
379
- container.removeEventListener('scroll', this.scrollHandler);
380
- if (this.rafId !== null) {
381
- cancelRaf(this.rafId);
382
- }
383
- if (this.scrollEndTimeout) {
384
- clearTimeout(this.scrollEndTimeout);
385
- }
386
- }
387
- /**
388
- * Handle scroll event
389
- */
390
- handleScroll() {
391
- if (this.rafId !== null)
140
+ }, []);
141
+ useEffect(() => {
142
+ const embedContainer = embedRef.current;
143
+ if (!embedContainer)
392
144
  return;
393
- this.isScrolling = true;
394
- // Clear existing scroll end timeout
395
- if (this.scrollEndTimeout) {
396
- clearTimeout(this.scrollEndTimeout);
397
- }
398
- // Set scroll end detection
399
- this.scrollEndTimeout = setTimeout(() => {
400
- this.isScrolling = false;
401
- }, 150);
402
- this.rafId = raf(() => {
403
- this.rafId = null;
404
- this.calculateVisibleItems();
405
- });
406
- }
407
- /**
408
- * Calculate which items are visible
409
- */
410
- calculateVisibleItems() {
411
- const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
412
- const overscanPx = this.options.overscan * this.options.estimatedItemHeight;
413
- this.lastScrollTop = scrollTop;
414
- const newVisibleItems = [];
415
- for (let i = 0; i < this.posts.length; i++) {
416
- const post = this.posts[i];
417
- const position = this.positions.get(post.id);
418
- if (!position)
419
- continue;
420
- const isVisible = isInViewport({
421
- top: position.y,
422
- bottom: position.y + position.height,
423
- }, scrollTop, viewportHeight, overscanPx);
424
- if (isVisible) {
425
- newVisibleItems.push({
426
- index: i,
427
- post,
428
- position,
429
- isVisible: true,
430
- });
145
+ // Create a fresh container for the widget
146
+ const widgetContainer = document.createElement('div');
147
+ embedContainer.appendChild(widgetContainer);
148
+ setLoading(true);
149
+ loadTwitterScript().then(() => {
150
+ if (!mountedRef.current)
151
+ return;
152
+ const twttr = window.twttr;
153
+ if (!twttr) {
154
+ onError?.(new Error('Twitter widgets not loaded'));
155
+ setLoading(false);
156
+ return;
431
157
  }
432
- }
433
- // Check if visible items changed
434
- const hasChanged = this.hasVisibleItemsChanged(newVisibleItems);
435
- if (hasChanged) {
436
- this.visibleItems = newVisibleItems;
437
- this.onVisibleItemsChange?.(newVisibleItems);
438
- }
439
- return this.visibleItems;
440
- }
441
- /**
442
- * Check if visible items have changed
443
- */
444
- hasVisibleItemsChanged(newItems) {
445
- if (newItems.length !== this.visibleItems.length) {
446
- return true;
447
- }
448
- for (let i = 0; i < newItems.length; i++) {
449
- if (newItems[i].post.id !== this.visibleItems[i].post.id) {
450
- return true;
158
+ // Extract tweet ID from URL
159
+ const match = url.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/);
160
+ if (!match) {
161
+ onError?.(new Error('Invalid Twitter URL'));
162
+ setLoading(false);
163
+ return;
451
164
  }
452
- }
453
- return false;
454
- }
455
- /**
456
- * Update posts and positions
457
- */
458
- update(posts, positions) {
459
- this.posts = posts;
460
- this.positions = positions;
461
- this.calculateVisibleItems();
462
- }
463
- /**
464
- * Get visible items
465
- */
466
- getVisibleItems() {
467
- return this.visibleItems;
468
- }
469
- /**
470
- * Get all items (for non-virtualized mode)
471
- */
472
- getAllItems() {
473
- return this.posts.map((post, index) => ({
474
- index,
475
- post,
476
- position: this.positions.get(post.id),
477
- isVisible: true,
478
- }));
479
- }
480
- /**
481
- * Check if scrolling
482
- */
483
- getIsScrolling() {
484
- return this.isScrolling;
485
- }
486
- /**
487
- * Get scroll direction
488
- */
489
- getScrollDirection() {
490
- const { scrollTop } = getScrollPosition(this.options.scrollContainer);
491
- if (scrollTop > this.lastScrollTop) {
492
- return 'down';
493
- }
494
- else if (scrollTop < this.lastScrollTop) {
495
- return 'up';
496
- }
497
- return 'none';
498
- }
499
- /**
500
- * Check if near bottom (for infinite scroll)
501
- */
502
- isNearBottom(threshold = 500) {
503
- const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
504
- // Get container height from positions
505
- let maxBottom = 0;
506
- for (const position of this.positions.values()) {
507
- maxBottom = Math.max(maxBottom, position.y + position.height);
508
- }
509
- return scrollTop + viewportHeight >= maxBottom - threshold;
510
- }
511
- /**
512
- * Scroll to item
513
- */
514
- scrollToItem(id, behavior = 'smooth') {
515
- const position = this.positions.get(id);
516
- if (!position)
517
- return;
518
- const container = this.options.scrollContainer ?? window;
519
- if (container === window) {
520
- window.scrollTo({
521
- top: position.y,
522
- behavior,
523
- });
524
- }
525
- else {
526
- container.scrollTo({
527
- top: position.y,
528
- behavior,
165
+ twttr.widgets.createTweet(match[1], widgetContainer, {
166
+ theme,
167
+ conversation: 'none',
168
+ dnt: true,
169
+ }).then((el) => {
170
+ if (!mountedRef.current)
171
+ return;
172
+ setLoading(false);
173
+ if (el) {
174
+ onLoad?.();
175
+ }
176
+ else {
177
+ onError?.(new Error('Tweet not found or unavailable'));
178
+ }
179
+ }).catch((err) => {
180
+ if (!mountedRef.current)
181
+ return;
182
+ setLoading(false);
183
+ onError?.(err);
529
184
  });
530
- }
531
- }
532
- /**
533
- * Get render range for optimization
534
- */
535
- getRenderRange() {
536
- if (this.visibleItems.length === 0) {
537
- return { start: 0, end: 0 };
538
- }
539
- const indices = this.visibleItems.map(item => item.index);
540
- return {
541
- start: Math.min(...indices),
542
- end: Math.max(...indices) + 1,
185
+ });
186
+ return () => {
187
+ // Safely remove the widget container
188
+ if (embedContainer && widgetContainer.parentNode === embedContainer) {
189
+ embedContainer.removeChild(widgetContainer);
190
+ }
543
191
  };
544
- }
545
- }
546
-
547
- // ============================================
548
- // Icons
549
- // ============================================
550
- const TwitterIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" }) }));
551
- const InstagramIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M12 2c2.717 0 3.056.01 4.122.06 1.065.05 1.79.217 2.428.465.66.254 1.216.598 1.772 1.153a4.908 4.908 0 0 1 1.153 1.772c.247.637.415 1.363.465 2.428.047 1.066.06 1.405.06 4.122 0 2.717-.01 3.056-.06 4.122-.05 1.065-.218 1.79-.465 2.428a4.883 4.883 0 0 1-1.153 1.772 4.915 4.915 0 0 1-1.772 1.153c-.637.247-1.363.415-2.428.465-1.066.047-1.405.06-4.122.06-2.717 0-3.056-.01-4.122-.06-1.065-.05-1.79-.218-2.428-.465a4.89 4.89 0 0 1-1.772-1.153 4.904 4.904 0 0 1-1.153-1.772c-.248-.637-.415-1.363-.465-2.428C2.013 15.056 2 14.717 2 12c0-2.717.01-3.056.06-4.122.05-1.066.217-1.79.465-2.428a4.88 4.88 0 0 1 1.153-1.772A4.897 4.897 0 0 1 5.45 2.525c.638-.248 1.362-.415 2.428-.465C8.944 2.013 9.283 2 12 2zm0 1.802c-2.67 0-2.986.01-4.04.058-.976.045-1.505.207-1.858.344-.466.182-.8.398-1.15.748-.35.35-.566.684-.748 1.15-.137.353-.3.882-.344 1.857-.048 1.055-.058 1.37-.058 4.041 0 2.67.01 2.986.058 4.04.045.976.207 1.505.344 1.858.182.466.399.8.748 1.15.35.35.684.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058 2.67 0 2.987-.01 4.04-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.684.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041 0-2.67-.01-2.986-.058-4.04-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 0 0-.748-1.15 3.098 3.098 0 0 0-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.055-.048-1.37-.058-4.041-.058zm0 3.063a5.135 5.135 0 1 1 0 10.27 5.135 5.135 0 0 1 0-10.27zm0 8.468a3.333 3.333 0 1 0 0-6.666 3.333 3.333 0 0 0 0 6.666zm6.538-8.671a1.2 1.2 0 1 1-2.4 0 1.2 1.2 0 0 1 2.4 0z" }) }));
552
- const VerifiedIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.34 2.19c-1.39-.46-2.9-.2-3.91.81s-1.27 2.52-.81 3.91C2.63 9.33 1.75 10.57 1.75 12s.88 2.67 2.19 3.34c-.46 1.39-.2 2.9.81 3.91s2.52 1.27 3.91.81c.66 1.31 1.91 2.19 3.34 2.19s2.67-.88 3.34-2.19c1.39.46 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34zm-11.71 4.2L6.8 12.46l1.41-1.42 2.26 2.26 4.8-5.23 1.47 1.36-6.2 6.77z" }) }));
553
- const PlayIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M8 5v14l11-7z" }) }));
554
- const Card = ({ post, position, variant = 'default', theme = 'auto', borderRadius = 12, showPlatformIcon = true, showAuthor = true, showMetrics = true, showTimestamp = true, formatDate = formatRelativeTime, formatNumber: formatNum = formatNumber, className = '', hoverEffect = true, imageLoading = 'lazy', fallbackImage = '', onPostClick, onAuthorClick, onMediaClick, onHeightChange, }) => {
555
- const cardRef = useRef(null);
192
+ }, [url, theme, onLoad, onError]);
193
+ return (jsxs("div", { ref: containerRef, className: "sm-twitter-embed", children: [loading && (jsx("div", { style: {
194
+ display: 'flex',
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ minHeight: 200,
198
+ backgroundColor: theme === 'dark' ? '#15202b' : '#f5f5f5',
199
+ borderRadius: 12,
200
+ }, children: jsx("div", { style: { color: theme === 'dark' ? '#8899a6' : '#666' }, children: "Loading..." }) })), jsx("div", { ref: embedRef })] }));
201
+ };
202
+ const InstagramEmbed = ({ url, onLoad, onError, }) => {
203
+ const containerRef = useRef(null);
204
+ const embedRef = useRef(null);
205
+ const [loading, setLoading] = useState(true);
206
+ const mountedRef = useRef(true);
556
207
  useEffect(() => {
557
- if (cardRef.current && onHeightChange) {
558
- const height = cardRef.current.offsetHeight;
559
- if (height !== position.height) {
560
- onHeightChange(post.id, height);
561
- }
562
- }
563
- }, [post.id, position.height, onHeightChange]);
564
- const handlePostClick = useCallback((e) => {
565
- const target = e.target;
566
- if (target.tagName === 'A' || target.closest('a'))
208
+ mountedRef.current = true;
209
+ return () => {
210
+ mountedRef.current = false;
211
+ };
212
+ }, []);
213
+ useEffect(() => {
214
+ const embedContainer = embedRef.current;
215
+ if (!embedContainer)
216
+ return;
217
+ // Extract post ID from URL
218
+ const match = url.match(/instagram\.com\/(?:p|reel)\/([A-Za-z0-9_-]+)/);
219
+ if (!match) {
220
+ onError?.(new Error('Invalid Instagram URL'));
221
+ setLoading(false);
567
222
  return;
568
- onPostClick?.(post, e);
569
- }, [post, onPostClick]);
570
- const handleAuthorClick = useCallback((e) => {
571
- e.stopPropagation();
572
- onAuthorClick?.(post, e);
573
- }, [post, onAuthorClick]);
574
- const handleMediaClick = useCallback((index) => (e) => {
575
- e.stopPropagation();
576
- onMediaClick?.(post, index, e);
577
- }, [post, onMediaClick]);
578
- const classes = [
579
- 'sm-card',
580
- `sm-card--${post.platform}`,
581
- `sm-card--${variant}`,
582
- `sm-card--${theme}`,
583
- hoverEffect && 'sm-card--hover',
584
- className,
585
- ].filter(Boolean).join(' ');
586
- const style = {
587
- position: 'absolute',
588
- left: position.x,
589
- top: position.y,
590
- width: position.width,
591
- borderRadius: typeof borderRadius === 'number' ? borderRadius : borderRadius,
592
- cursor: onPostClick ? 'pointer' : undefined,
593
- };
594
- const date = post.createdAt instanceof Date ? post.createdAt : new Date(post.createdAt);
595
- return (jsxs("div", { ref: cardRef, className: classes, style: style, onClick: handlePostClick, "data-post-id": post.id, "data-platform": post.platform, children: [showPlatformIcon && (jsx("div", { className: "sm-card__platform-icon", children: post.platform === 'twitter' ? (jsx(TwitterIcon, { className: "sm-platform-icon sm-platform-icon--twitter" })) : (jsx(InstagramIcon, { className: "sm-platform-icon sm-platform-icon--instagram" })) })), showAuthor && (jsxs("div", { className: "sm-card__header", onClick: onAuthorClick ? handleAuthorClick : undefined, style: { cursor: onAuthorClick ? 'pointer' : undefined }, children: [jsx("img", { src: post.author.avatarUrl || fallbackImage, alt: post.author.displayName || post.author.username, className: "sm-card__avatar", loading: imageLoading, onError: (e) => {
596
- if (fallbackImage) {
597
- e.target.src = fallbackImage;
598
- }
599
- } }), jsxs("div", { className: "sm-card__author", children: [jsxs("span", { className: "sm-card__author-name", children: [post.author.displayName || post.author.username, post.author.verified && jsx(VerifiedIcon, { className: "sm-card__verified" })] }), jsxs("span", { className: "sm-card__author-handle", children: ["@", post.author.username] })] })] })), jsxs("div", { className: "sm-card__content", children: [isTwitterPost(post) && (jsx("p", { className: "sm-card__text", children: post.content.text })), isInstagramPost(post) && post.content.caption && (jsx("p", { className: "sm-card__caption", children: post.content.caption.length > 150
600
- ? `${post.content.caption.substring(0, 150)}...`
601
- : post.content.caption }))] }), isTwitterPost(post) && post.media && post.media.length > 0 && (jsx("div", { className: `sm-card__media ${post.media.length > 1 ? `sm-card__media-grid sm-card__media-grid--${Math.min(post.media.length, 4)}` : ''}`, children: post.media.slice(0, 4).map((media, index) => (jsxs("div", { className: `sm-card__media-item ${media.type !== 'image' ? 'sm-card__media-item--video' : ''}`, onClick: onMediaClick ? handleMediaClick(index) : undefined, style: { cursor: onMediaClick ? 'pointer' : undefined }, children: [jsx("img", { src: media.thumbnailUrl || media.url, alt: "Post media", loading: imageLoading, onError: (e) => {
602
- if (fallbackImage) {
603
- e.target.src = fallbackImage;
604
- }
605
- } }), (media.type === 'video' || media.type === 'gif') && (jsx("div", { className: "sm-card__play-button", children: jsx(PlayIcon, {}) }))] }, index))) })), isInstagramPost(post) && (jsxs("div", { className: "sm-card__media", style: { aspectRatio: post.media.aspectRatio || 1 }, children: [jsx("img", { src: post.media.thumbnailUrl || post.media.url, alt: "Post media", loading: imageLoading, onError: (e) => {
606
- if (fallbackImage) {
607
- e.target.src = fallbackImage;
608
- }
609
- } }), post.media.type === 'video' && (jsx("div", { className: "sm-card__play-button", children: jsx(PlayIcon, {}) })), post.media.type === 'carousel' && (jsx("div", { className: "sm-card__carousel-indicator", children: jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M6 13c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm12 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm-6 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z" }) }) }))] })), (showMetrics || showTimestamp) && (jsxs("div", { className: "sm-card__footer", children: [showMetrics && post.metrics && (jsxs("div", { className: "sm-card__metrics", children: [isTwitterPost(post) && (jsxs(Fragment, { children: [post.metrics.replies !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01zm8.005-6c-3.317 0-6.005 2.69-6.005 6 0 3.37 2.77 6.08 6.138 6.01l.351-.01h1.761v2.3l5.087-2.81c1.951-1.08 3.163-3.13 3.163-5.36 0-3.39-2.744-6.13-6.129-6.13H9.756z" }) }), formatNum(post.metrics.replies)] })), post.metrics.retweets !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M4.5 3.88l4.432 4.14-1.364 1.46L5.5 7.55V16c0 1.1.896 2 2 2H13v2H7.5c-2.209 0-4-1.79-4-4V7.55L1.432 9.48.068 8.02 4.5 3.88zM16.5 6H11V4h5.5c2.209 0 4 1.79 4 4v8.45l2.068-1.93 1.364 1.46-4.432 4.14-4.432-4.14 1.364-1.46 2.068 1.93V8c0-1.1-.896-2-2-2z" }) }), formatNum(post.metrics.retweets)] })), post.metrics.likes !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91zm4.187 7.69c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z" }) }), formatNum(post.metrics.likes)] }))] })), isInstagramPost(post) && (jsxs(Fragment, { children: [post.metrics.likes !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M16.792 3.904A4.989 4.989 0 0 1 21.5 9.122c0 3.072-2.652 4.959-5.197 7.222-2.512 2.243-3.865 3.469-4.303 3.752-.477-.309-2.143-1.823-4.303-3.752C5.141 14.072 2.5 12.167 2.5 9.122a4.989 4.989 0 0 1 4.708-5.218 4.21 4.21 0 0 1 3.675 1.941c.84 1.175.98 1.763 1.12 1.763s.278-.588 1.11-1.766a4.17 4.17 0 0 1 3.679-1.938m0-2a6.04 6.04 0 0 0-4.797 2.127 6.052 6.052 0 0 0-4.787-2.127A6.985 6.985 0 0 0 .5 9.122c0 3.61 2.55 5.827 5.015 7.97.283.246.569.494.853.747l1.027.918a44.998 44.998 0 0 0 3.518 3.018 2 2 0 0 0 2.174 0 45.263 45.263 0 0 0 3.626-3.115l.922-.824c.293-.26.59-.519.885-.774 2.334-2.025 4.98-4.32 4.98-7.94a6.985 6.985 0 0 0-6.708-7.218z" }) }), formatNum(post.metrics.likes)] })), post.metrics.comments !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M20.656 17.008a9.993 9.993 0 1 0-3.59 3.615L22 22l-1.344-4.992zM10 12a1 1 0 1 1 1 1 1 1 0 0 1-1-1zm-4 0a1 1 0 1 1 1 1 1 1 0 0 1-1-1zm8 0a1 1 0 1 1 1 1 1 1 0 0 1-1-1z" }) }), formatNum(post.metrics.comments)] }))] }))] })), showTimestamp && (jsx("time", { className: "sm-card__timestamp", dateTime: date.toISOString(), children: formatDate(date) }))] }))] }));
223
+ }
224
+ // Create a fresh container for the widget
225
+ const widgetContainer = document.createElement('div');
226
+ widgetContainer.innerHTML = `
227
+ <blockquote
228
+ class="instagram-media"
229
+ data-instgrm-captioned
230
+ data-instgrm-permalink="https://www.instagram.com/p/${match[1]}/"
231
+ data-instgrm-version="14"
232
+ style="
233
+ background:#FFF;
234
+ border:0;
235
+ border-radius:12px;
236
+ box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15);
237
+ margin: 0;
238
+ max-width:100%;
239
+ min-width:100%;
240
+ padding:0;
241
+ width:100%;
242
+ "
243
+ >
244
+ </blockquote>
245
+ `;
246
+ embedContainer.appendChild(widgetContainer);
247
+ loadInstagramScript().then(() => {
248
+ if (!mountedRef.current)
249
+ return;
250
+ const instgrm = window.instgrm;
251
+ if (instgrm) {
252
+ instgrm.Embeds.process();
253
+ setLoading(false);
254
+ onLoad?.();
255
+ }
256
+ });
257
+ return () => {
258
+ // Safely remove the widget container
259
+ if (embedContainer && widgetContainer.parentNode === embedContainer) {
260
+ embedContainer.removeChild(widgetContainer);
261
+ }
262
+ };
263
+ }, [url, onLoad, onError]);
264
+ return (jsxs("div", { ref: containerRef, className: "sm-instagram-embed", children: [loading && (jsx("div", { style: {
265
+ display: 'flex',
266
+ alignItems: 'center',
267
+ justifyContent: 'center',
268
+ minHeight: 400,
269
+ backgroundColor: '#fafafa',
270
+ borderRadius: 12,
271
+ }, children: jsx("div", { style: { color: '#666' }, children: "Loading..." }) })), jsx("div", { ref: embedRef })] }));
610
272
  };
611
273
  const SocialMasonry = forwardRef((props, ref) => {
612
- const { posts: initialPosts = [], gap = 16, columns = defaultColumnConfig, padding = 0, animationDuration = 300, animate = true, easing = 'cubic-bezier(0.4, 0, 0.2, 1)', virtualization, loadMoreThreshold: _loadMoreThreshold = 500, onLoadMore: _onLoadMore, onLayoutComplete, className, style, ...cardProps } = props;
274
+ const { posts: initialPosts = [], gap = 16, columns = defaultColumnConfig, theme = 'light', onEmbedLoad, onEmbedError, className, style, } = props;
613
275
  const containerRef = useRef(null);
614
276
  const [posts, setPosts] = useState(initialPosts);
615
- const [positions, setPositions] = useState(new Map());
616
- const [containerHeight, setContainerHeight] = useState(0);
617
- const [itemHeights] = useState(() => new Map());
618
- const [visibleItems, setVisibleItems] = useState([]);
619
- const layoutEngine = useMemo(() => {
620
- return new LayoutEngine({
621
- gap,
622
- columns,
623
- padding,
624
- animationDuration,
625
- animate,
626
- easing,
627
- containerWidth: containerRef.current?.clientWidth || 800,
628
- itemHeights,
629
- });
630
- }, [gap, columns, padding, animationDuration, animate, easing]);
631
- const virtualizationEngine = useMemo(() => {
632
- if (!virtualization?.enabled)
633
- return null;
634
- return new VirtualizationEngine({
635
- ...virtualization,
636
- posts,
637
- positions,
638
- onVisibleItemsChange: setVisibleItems,
639
- });
640
- }, [virtualization?.enabled]);
641
- // Calculate layout
642
- const calculateLayout = useCallback(() => {
643
- if (!containerRef.current)
644
- return;
645
- layoutEngine.setContainerWidth(containerRef.current.clientWidth);
646
- const state = layoutEngine.calculate(posts);
647
- setPositions(state.positions);
648
- setContainerHeight(state.containerHeight);
649
- if (virtualizationEngine) {
650
- virtualizationEngine.update(posts, state.positions);
651
- setVisibleItems(virtualizationEngine.calculateVisibleItems());
652
- }
653
- onLayoutComplete?.(Array.from(state.positions.values()));
654
- }, [posts, layoutEngine, virtualizationEngine, onLayoutComplete]);
655
- // Handle resize
277
+ const [columnCount, setColumnCount] = useState(3);
278
+ // Update posts when initialPosts changes
279
+ useEffect(() => {
280
+ setPosts(initialPosts);
281
+ }, [initialPosts]);
282
+ // Calculate column count based on container width
656
283
  useEffect(() => {
657
- const handleResize = debounce(() => {
658
- calculateLayout();
659
- }, 150);
660
- const resizeObserver = new ResizeObserver(handleResize);
284
+ const updateColumnCount = () => {
285
+ if (!containerRef.current)
286
+ return;
287
+ const width = containerRef.current.clientWidth;
288
+ const count = getColumnCount(columns, width);
289
+ setColumnCount(count);
290
+ };
291
+ updateColumnCount();
292
+ const resizeObserver = new ResizeObserver(updateColumnCount);
661
293
  if (containerRef.current) {
662
294
  resizeObserver.observe(containerRef.current);
663
295
  }
664
- return () => {
665
- resizeObserver.disconnect();
666
- };
667
- }, [calculateLayout]);
668
- // Initial layout
669
- useEffect(() => {
670
- calculateLayout();
671
- }, [posts, calculateLayout]);
672
- // Initialize virtualization
673
- useEffect(() => {
674
- if (virtualizationEngine) {
675
- virtualizationEngine.init();
676
- return () => virtualizationEngine.destroy();
677
- }
678
- return undefined;
679
- }, [virtualizationEngine]);
680
- // Handle height change
681
- const handleHeightChange = useCallback((id, height) => {
682
- itemHeights.set(id, height);
683
- calculateLayout();
684
- }, [itemHeights, calculateLayout]);
296
+ return () => resizeObserver.disconnect();
297
+ }, [columns]);
685
298
  // Expose methods via ref
686
299
  useImperativeHandle(ref, () => ({
687
300
  addPosts: (newPosts) => {
688
301
  setPosts(prev => [...prev, ...newPosts]);
689
302
  },
690
303
  setPosts: (newPosts) => {
691
- itemHeights.clear();
692
304
  setPosts(newPosts);
693
305
  },
694
306
  removePost: (id) => {
695
- itemHeights.delete(id);
696
- setPosts(prev => prev.filter(p => p.id !== id));
307
+ setPosts(prev => prev.filter(p => generatePostId(p) !== id));
697
308
  },
698
309
  refresh: () => {
699
- itemHeights.clear();
700
- calculateLayout();
701
- },
702
- scrollToPost: (id, behavior = 'smooth') => {
703
- const position = positions.get(id);
704
- if (position) {
705
- window.scrollTo({ top: position.y, behavior });
310
+ // Re-process embeds
311
+ const instgrm = window.instgrm;
312
+ if (instgrm) {
313
+ instgrm.Embeds.process();
706
314
  }
707
315
  },
708
- }), [positions, calculateLayout, itemHeights]);
709
- // Determine which posts to render
710
- const postsToRender = virtualizationEngine
711
- ? visibleItems.map(item => item.post)
712
- : posts;
316
+ }), []);
317
+ // Distribute posts into columns (CSS-based masonry simulation)
318
+ const distributeToColumns = useCallback(() => {
319
+ const cols = Array.from({ length: columnCount }, () => []);
320
+ posts.forEach((post, index) => {
321
+ cols[index % columnCount].push(post);
322
+ });
323
+ return cols;
324
+ }, [posts, columnCount]);
325
+ const postColumns = distributeToColumns();
713
326
  return (jsxs("div", { ref: containerRef, className: `sm-container ${className || ''}`, style: {
714
- position: 'relative',
327
+ display: 'flex',
328
+ gap,
715
329
  width: '100%',
716
- height: containerHeight,
717
330
  ...style,
718
- }, children: [postsToRender.map(post => {
719
- const position = positions.get(post.id);
720
- if (!position)
331
+ }, children: [postColumns.map((columnPosts, colIndex) => (jsx("div", { className: "sm-column", style: {
332
+ flex: 1,
333
+ display: 'flex',
334
+ flexDirection: 'column',
335
+ gap,
336
+ minWidth: 0,
337
+ }, children: columnPosts.map(post => {
338
+ const postId = generatePostId(post);
339
+ if (post.platform === 'twitter') {
340
+ return (jsx("div", { className: "sm-embed sm-embed--twitter", children: jsx(TwitterEmbed, { url: post.url, theme: theme, onLoad: () => onEmbedLoad?.(post), onError: (error) => onEmbedError?.(post, error) }) }, postId));
341
+ }
342
+ if (post.platform === 'instagram') {
343
+ return (jsx("div", { className: "sm-embed sm-embed--instagram", children: jsx(InstagramEmbed, { url: post.url, onLoad: () => onEmbedLoad?.(post), onError: (error) => onEmbedError?.(post, error) }) }, postId));
344
+ }
721
345
  return null;
722
- // Extract event handlers and adapt types
723
- const { onPostClick, onAuthorClick, onMediaClick, ...restCardProps } = cardProps;
724
- return (jsx(Card, { post: post, position: position, onHeightChange: handleHeightChange, onPostClick: onPostClick ? (p, e) => onPostClick(p, e.nativeEvent) : undefined, onAuthorClick: onAuthorClick ? (p, e) => onAuthorClick(p, e.nativeEvent) : undefined, onMediaClick: onMediaClick ? (p, i, e) => onMediaClick(p, i, e.nativeEvent) : undefined, ...restCardProps }, post.id));
725
- }), posts.length === 0 && (jsx("div", { className: "sm-empty", children: "No posts to display" }))] }));
346
+ }) }, colIndex))), posts.length === 0 && (jsx("div", { className: "sm-empty", style: {
347
+ display: 'flex',
348
+ alignItems: 'center',
349
+ justifyContent: 'center',
350
+ padding: 40,
351
+ color: '#666',
352
+ fontSize: 14,
353
+ width: '100%',
354
+ }, children: "No posts to display" }))] }));
726
355
  });
727
356
  SocialMasonry.displayName = 'SocialMasonry';
728
357