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.
- package/README.md +97 -308
- package/dist/index.d.ts +62 -437
- package/dist/index.esm.js +93 -1354
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +101 -1366
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +35 -141
- package/dist/react/index.esm.js +280 -651
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/index.js +278 -649
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -8,44 +8,54 @@ var react = require('react');
|
|
|
8
8
|
/**
|
|
9
9
|
* Social Masonry - Utility Functions
|
|
10
10
|
*/
|
|
11
|
+
// ============================================
|
|
12
|
+
// URL Parsing Utilities
|
|
13
|
+
// ============================================
|
|
11
14
|
/**
|
|
12
|
-
*
|
|
15
|
+
* Extract tweet ID from Twitter/X URL
|
|
13
16
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
clearTimeout(timeoutId);
|
|
20
|
-
}
|
|
21
|
-
timeoutId = setTimeout(() => {
|
|
22
|
-
fn.apply(this, args);
|
|
23
|
-
timeoutId = null;
|
|
24
|
-
}, delay);
|
|
25
|
-
};
|
|
17
|
+
function extractTweetId(url) {
|
|
18
|
+
if (!url)
|
|
19
|
+
return null;
|
|
20
|
+
const match = url.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/);
|
|
21
|
+
return match ? match[1] : null;
|
|
26
22
|
}
|
|
27
23
|
/**
|
|
28
|
-
*
|
|
24
|
+
* Extract post ID from Instagram URL
|
|
29
25
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
function extractInstagramId(url) {
|
|
27
|
+
if (!url)
|
|
28
|
+
return null;
|
|
29
|
+
const match = url.match(/instagram\.com\/(?:p|reel)\/([A-Za-z0-9_-]+)/);
|
|
30
|
+
return match ? match[1] : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate unique ID from post URL
|
|
34
|
+
*/
|
|
35
|
+
function generatePostId(post) {
|
|
36
|
+
if (post.id)
|
|
37
|
+
return post.id;
|
|
38
|
+
if (post.platform === 'twitter') {
|
|
39
|
+
const tweetId = extractTweetId(post.url);
|
|
40
|
+
return tweetId ? `tw-${tweetId}` : `tw-${hashString(post.url)}`;
|
|
41
|
+
}
|
|
42
|
+
if (post.platform === 'instagram') {
|
|
43
|
+
const igId = extractInstagramId(post.url);
|
|
44
|
+
return igId ? `ig-${igId}` : `ig-${hashString(post.url)}`;
|
|
45
|
+
}
|
|
46
|
+
return `post-${hashString(post.url)}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Simple string hash function
|
|
50
|
+
*/
|
|
51
|
+
function hashString(str) {
|
|
52
|
+
let hash = 0;
|
|
53
|
+
for (let i = 0; i < str.length; i++) {
|
|
54
|
+
const char = str.charCodeAt(i);
|
|
55
|
+
hash = ((hash << 5) - hash) + char;
|
|
56
|
+
hash = hash & hash;
|
|
57
|
+
}
|
|
58
|
+
return Math.abs(hash).toString(36);
|
|
49
59
|
}
|
|
50
60
|
/**
|
|
51
61
|
* Get number of columns based on viewport width
|
|
@@ -73,660 +83,279 @@ const defaultColumnConfig = [
|
|
|
73
83
|
{ columns: 2, minWidth: 600 },
|
|
74
84
|
{ columns: 1, minWidth: 0 },
|
|
75
85
|
];
|
|
76
|
-
/**
|
|
77
|
-
* Format number with abbreviations (1K, 1M, etc.)
|
|
78
|
-
*/
|
|
79
|
-
function formatNumber(num) {
|
|
80
|
-
if (num >= 1000000) {
|
|
81
|
-
return `${(num / 1000000).toFixed(1).replace(/\.0$/, '')}M`;
|
|
82
|
-
}
|
|
83
|
-
if (num >= 1000) {
|
|
84
|
-
return `${(num / 1000).toFixed(1).replace(/\.0$/, '')}K`;
|
|
85
|
-
}
|
|
86
|
-
return num.toString();
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Format relative time
|
|
90
|
-
*/
|
|
91
|
-
function formatRelativeTime(date) {
|
|
92
|
-
const now = new Date();
|
|
93
|
-
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
94
|
-
if (diffInSeconds < 60) {
|
|
95
|
-
return 'now';
|
|
96
|
-
}
|
|
97
|
-
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
98
|
-
if (diffInMinutes < 60) {
|
|
99
|
-
return `${diffInMinutes}m`;
|
|
100
|
-
}
|
|
101
|
-
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
102
|
-
if (diffInHours < 24) {
|
|
103
|
-
return `${diffInHours}h`;
|
|
104
|
-
}
|
|
105
|
-
const diffInDays = Math.floor(diffInHours / 24);
|
|
106
|
-
if (diffInDays < 7) {
|
|
107
|
-
return `${diffInDays}d`;
|
|
108
|
-
}
|
|
109
|
-
const diffInWeeks = Math.floor(diffInDays / 7);
|
|
110
|
-
if (diffInWeeks < 4) {
|
|
111
|
-
return `${diffInWeeks}w`;
|
|
112
|
-
}
|
|
113
|
-
// Format as date for older posts
|
|
114
|
-
return date.toLocaleDateString('en-US', {
|
|
115
|
-
month: 'short',
|
|
116
|
-
day: 'numeric',
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Parse CSS value to pixels
|
|
121
|
-
*/
|
|
122
|
-
function parseCSSValue(value, containerWidth) {
|
|
123
|
-
if (typeof value === 'number') {
|
|
124
|
-
return value;
|
|
125
|
-
}
|
|
126
|
-
const numMatch = value.match(/^([\d.]+)(px|rem|em|%|vw)?$/);
|
|
127
|
-
if (!numMatch) {
|
|
128
|
-
return 0;
|
|
129
|
-
}
|
|
130
|
-
const num = parseFloat(numMatch[1]);
|
|
131
|
-
const unit = numMatch[2] || 'px';
|
|
132
|
-
switch (unit) {
|
|
133
|
-
case 'px':
|
|
134
|
-
return num;
|
|
135
|
-
case 'rem':
|
|
136
|
-
return num * 16; // Assume 16px base
|
|
137
|
-
case 'em':
|
|
138
|
-
return num * 16;
|
|
139
|
-
case '%':
|
|
140
|
-
return 0;
|
|
141
|
-
case 'vw':
|
|
142
|
-
return (num / 100) * window.innerWidth;
|
|
143
|
-
default:
|
|
144
|
-
return num;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Check if element is in viewport
|
|
149
|
-
*/
|
|
150
|
-
function isInViewport(element, scrollTop, viewportHeight, overscan = 0) {
|
|
151
|
-
const expandedTop = scrollTop - overscan;
|
|
152
|
-
const expandedBottom = scrollTop + viewportHeight + overscan;
|
|
153
|
-
return element.bottom >= expandedTop && element.top <= expandedBottom;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Get scroll position
|
|
157
|
-
*/
|
|
158
|
-
function getScrollPosition(scrollContainer) {
|
|
159
|
-
if (!scrollContainer || scrollContainer === window) {
|
|
160
|
-
return {
|
|
161
|
-
scrollTop: window.scrollY || document.documentElement.scrollTop,
|
|
162
|
-
viewportHeight: window.innerHeight,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
scrollTop: scrollContainer.scrollTop,
|
|
167
|
-
viewportHeight: scrollContainer.clientHeight,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Type guard for Twitter posts
|
|
172
|
-
*/
|
|
173
|
-
function isTwitterPost(post) {
|
|
174
|
-
return post.platform === 'twitter';
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Type guard for Instagram posts
|
|
178
|
-
*/
|
|
179
|
-
function isInstagramPost(post) {
|
|
180
|
-
return post.platform === 'instagram';
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Request animation frame with fallback
|
|
184
|
-
*/
|
|
185
|
-
const raf = typeof requestAnimationFrame !== 'undefined'
|
|
186
|
-
? requestAnimationFrame
|
|
187
|
-
: (callback) => setTimeout(callback, 16);
|
|
188
|
-
/**
|
|
189
|
-
* Cancel animation frame with fallback
|
|
190
|
-
*/
|
|
191
|
-
const cancelRaf = typeof cancelAnimationFrame !== 'undefined'
|
|
192
|
-
? cancelAnimationFrame
|
|
193
|
-
: (id) => clearTimeout(id);
|
|
194
86
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
};
|
|
211
|
-
this.state = {
|
|
212
|
-
positions: new Map(),
|
|
213
|
-
columnHeights: [],
|
|
214
|
-
containerHeight: 0,
|
|
215
|
-
columnWidth: 0,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Calculate layout for all posts
|
|
220
|
-
*/
|
|
221
|
-
calculate(posts) {
|
|
222
|
-
const { containerWidth, itemHeights } = this.options;
|
|
223
|
-
const gap = parseCSSValue(this.options.gap);
|
|
224
|
-
const padding = parseCSSValue(this.options.padding);
|
|
225
|
-
// Calculate column count and width
|
|
226
|
-
const columnCount = getColumnCount(this.options.columns, containerWidth);
|
|
227
|
-
const availableWidth = containerWidth - padding * 2;
|
|
228
|
-
const totalGapWidth = gap * (columnCount - 1);
|
|
229
|
-
const columnWidth = (availableWidth - totalGapWidth) / columnCount;
|
|
230
|
-
// Initialize column heights
|
|
231
|
-
const columnHeights = new Array(columnCount).fill(0);
|
|
232
|
-
const positions = new Map();
|
|
233
|
-
// Place each item in the shortest column
|
|
234
|
-
for (const post of posts) {
|
|
235
|
-
const itemHeight = itemHeights.get(post.id) ?? this.estimateHeight(post, columnWidth);
|
|
236
|
-
// Find shortest column
|
|
237
|
-
const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
|
|
238
|
-
// Calculate position
|
|
239
|
-
const x = padding + shortestColumn * (columnWidth + gap);
|
|
240
|
-
const y = columnHeights[shortestColumn];
|
|
241
|
-
positions.set(post.id, {
|
|
242
|
-
id: post.id,
|
|
243
|
-
x,
|
|
244
|
-
y,
|
|
245
|
-
width: columnWidth,
|
|
246
|
-
height: itemHeight,
|
|
247
|
-
column: shortestColumn,
|
|
248
|
-
});
|
|
249
|
-
// Update column height
|
|
250
|
-
columnHeights[shortestColumn] = y + itemHeight + gap;
|
|
87
|
+
// ============================================
|
|
88
|
+
// Script Loading
|
|
89
|
+
// ============================================
|
|
90
|
+
let twitterScriptLoaded = false;
|
|
91
|
+
let instagramScriptLoaded = false;
|
|
92
|
+
const loadTwitterScript = () => {
|
|
93
|
+
if (twitterScriptLoaded)
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
if (typeof window === 'undefined')
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
if (window.twttr) {
|
|
99
|
+
twitterScriptLoaded = true;
|
|
100
|
+
resolve();
|
|
101
|
+
return;
|
|
251
102
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
columnWidth,
|
|
103
|
+
const script = document.createElement('script');
|
|
104
|
+
script.src = 'https://platform.twitter.com/widgets.js';
|
|
105
|
+
script.async = true;
|
|
106
|
+
script.onload = () => {
|
|
107
|
+
twitterScriptLoaded = true;
|
|
108
|
+
resolve();
|
|
259
109
|
};
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const lines = Math.ceil(textLength / avgCharsPerLine);
|
|
274
|
-
height += lines * 24; // ~24px per line
|
|
275
|
-
}
|
|
276
|
-
else if (post.content.caption) {
|
|
277
|
-
const captionLength = post.content.caption.length;
|
|
278
|
-
const avgCharsPerLine = Math.floor(columnWidth / 8);
|
|
279
|
-
const lines = Math.min(Math.ceil(captionLength / avgCharsPerLine), 4); // Max 4 lines
|
|
280
|
-
height += lines * 20;
|
|
281
|
-
}
|
|
282
|
-
// Media
|
|
283
|
-
if (post.platform === 'twitter' && post.media?.length) {
|
|
284
|
-
const media = post.media[0];
|
|
285
|
-
const aspectRatio = media.aspectRatio ?? 16 / 9;
|
|
286
|
-
height += columnWidth / aspectRatio;
|
|
287
|
-
}
|
|
288
|
-
else if (post.platform === 'instagram') {
|
|
289
|
-
const aspectRatio = post.media.aspectRatio ?? 1;
|
|
290
|
-
height += columnWidth / aspectRatio;
|
|
110
|
+
document.head.appendChild(script);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const loadInstagramScript = () => {
|
|
114
|
+
if (instagramScriptLoaded)
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
if (typeof window === 'undefined')
|
|
117
|
+
return Promise.resolve();
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
if (window.instgrm) {
|
|
120
|
+
instagramScriptLoaded = true;
|
|
121
|
+
resolve();
|
|
122
|
+
return;
|
|
291
123
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Update single item height and recalculate affected items
|
|
300
|
-
*/
|
|
301
|
-
updateItemHeight(id, height) {
|
|
302
|
-
this.options.itemHeights.set(id, height);
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Get current layout state
|
|
306
|
-
*/
|
|
307
|
-
getState() {
|
|
308
|
-
return this.state;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Get position for specific item
|
|
312
|
-
*/
|
|
313
|
-
getPosition(id) {
|
|
314
|
-
return this.state.positions.get(id);
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Update container width
|
|
318
|
-
*/
|
|
319
|
-
setContainerWidth(width) {
|
|
320
|
-
this.options.containerWidth = width;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Get current column count
|
|
324
|
-
*/
|
|
325
|
-
getColumnCount() {
|
|
326
|
-
return getColumnCount(this.options.columns, this.options.containerWidth);
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Get CSS variables for animations
|
|
330
|
-
*/
|
|
331
|
-
getCSSVariables() {
|
|
332
|
-
return {
|
|
333
|
-
'--sm-animation-duration': `${this.options.animationDuration}ms`,
|
|
334
|
-
'--sm-easing': this.options.easing,
|
|
335
|
-
'--sm-gap': typeof this.options.gap === 'number'
|
|
336
|
-
? `${this.options.gap}px`
|
|
337
|
-
: this.options.gap,
|
|
338
|
-
'--sm-column-width': `${this.state.columnWidth}px`,
|
|
339
|
-
'--sm-container-height': `${this.state.containerHeight}px`,
|
|
124
|
+
const script = document.createElement('script');
|
|
125
|
+
script.src = 'https://www.instagram.com/embed.js';
|
|
126
|
+
script.async = true;
|
|
127
|
+
script.onload = () => {
|
|
128
|
+
instagramScriptLoaded = true;
|
|
129
|
+
resolve();
|
|
340
130
|
};
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
this.isScrolling = false;
|
|
354
|
-
this.scrollEndTimeout = null;
|
|
355
|
-
this.options = {
|
|
356
|
-
enabled: true,
|
|
357
|
-
overscan: 3,
|
|
358
|
-
estimatedItemHeight: 400,
|
|
359
|
-
scrollContainer: null,
|
|
360
|
-
...options,
|
|
131
|
+
document.head.appendChild(script);
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
const TwitterEmbed = ({ url, theme = 'light', onLoad, onError, }) => {
|
|
135
|
+
const containerRef = react.useRef(null);
|
|
136
|
+
const embedRef = react.useRef(null);
|
|
137
|
+
const [loading, setLoading] = react.useState(true);
|
|
138
|
+
const mountedRef = react.useRef(true);
|
|
139
|
+
react.useEffect(() => {
|
|
140
|
+
mountedRef.current = true;
|
|
141
|
+
return () => {
|
|
142
|
+
mountedRef.current = false;
|
|
361
143
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Initialize scroll listener
|
|
369
|
-
*/
|
|
370
|
-
init() {
|
|
371
|
-
if (!this.options.enabled)
|
|
372
|
-
return;
|
|
373
|
-
const container = this.options.scrollContainer ?? window;
|
|
374
|
-
container.addEventListener('scroll', this.scrollHandler, { passive: true });
|
|
375
|
-
// Initial calculation
|
|
376
|
-
this.calculateVisibleItems();
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Destroy and cleanup
|
|
380
|
-
*/
|
|
381
|
-
destroy() {
|
|
382
|
-
const container = this.options.scrollContainer ?? window;
|
|
383
|
-
container.removeEventListener('scroll', this.scrollHandler);
|
|
384
|
-
if (this.rafId !== null) {
|
|
385
|
-
cancelRaf(this.rafId);
|
|
386
|
-
}
|
|
387
|
-
if (this.scrollEndTimeout) {
|
|
388
|
-
clearTimeout(this.scrollEndTimeout);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Handle scroll event
|
|
393
|
-
*/
|
|
394
|
-
handleScroll() {
|
|
395
|
-
if (this.rafId !== null)
|
|
144
|
+
}, []);
|
|
145
|
+
react.useEffect(() => {
|
|
146
|
+
const embedContainer = embedRef.current;
|
|
147
|
+
if (!embedContainer)
|
|
396
148
|
return;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Calculate which items are visible
|
|
413
|
-
*/
|
|
414
|
-
calculateVisibleItems() {
|
|
415
|
-
const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
|
|
416
|
-
const overscanPx = this.options.overscan * this.options.estimatedItemHeight;
|
|
417
|
-
this.lastScrollTop = scrollTop;
|
|
418
|
-
const newVisibleItems = [];
|
|
419
|
-
for (let i = 0; i < this.posts.length; i++) {
|
|
420
|
-
const post = this.posts[i];
|
|
421
|
-
const position = this.positions.get(post.id);
|
|
422
|
-
if (!position)
|
|
423
|
-
continue;
|
|
424
|
-
const isVisible = isInViewport({
|
|
425
|
-
top: position.y,
|
|
426
|
-
bottom: position.y + position.height,
|
|
427
|
-
}, scrollTop, viewportHeight, overscanPx);
|
|
428
|
-
if (isVisible) {
|
|
429
|
-
newVisibleItems.push({
|
|
430
|
-
index: i,
|
|
431
|
-
post,
|
|
432
|
-
position,
|
|
433
|
-
isVisible: true,
|
|
434
|
-
});
|
|
149
|
+
// Create a fresh container for the widget
|
|
150
|
+
const widgetContainer = document.createElement('div');
|
|
151
|
+
embedContainer.appendChild(widgetContainer);
|
|
152
|
+
setLoading(true);
|
|
153
|
+
loadTwitterScript().then(() => {
|
|
154
|
+
if (!mountedRef.current)
|
|
155
|
+
return;
|
|
156
|
+
const twttr = window.twttr;
|
|
157
|
+
if (!twttr) {
|
|
158
|
+
onError?.(new Error('Twitter widgets not loaded'));
|
|
159
|
+
setLoading(false);
|
|
160
|
+
return;
|
|
435
161
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
return this.visibleItems;
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Check if visible items have changed
|
|
447
|
-
*/
|
|
448
|
-
hasVisibleItemsChanged(newItems) {
|
|
449
|
-
if (newItems.length !== this.visibleItems.length) {
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
|
-
for (let i = 0; i < newItems.length; i++) {
|
|
453
|
-
if (newItems[i].post.id !== this.visibleItems[i].post.id) {
|
|
454
|
-
return true;
|
|
162
|
+
// Extract tweet ID from URL
|
|
163
|
+
const match = url.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/);
|
|
164
|
+
if (!match) {
|
|
165
|
+
onError?.(new Error('Invalid Twitter URL'));
|
|
166
|
+
setLoading(false);
|
|
167
|
+
return;
|
|
455
168
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
*/
|
|
476
|
-
getAllItems() {
|
|
477
|
-
return this.posts.map((post, index) => ({
|
|
478
|
-
index,
|
|
479
|
-
post,
|
|
480
|
-
position: this.positions.get(post.id),
|
|
481
|
-
isVisible: true,
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Check if scrolling
|
|
486
|
-
*/
|
|
487
|
-
getIsScrolling() {
|
|
488
|
-
return this.isScrolling;
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Get scroll direction
|
|
492
|
-
*/
|
|
493
|
-
getScrollDirection() {
|
|
494
|
-
const { scrollTop } = getScrollPosition(this.options.scrollContainer);
|
|
495
|
-
if (scrollTop > this.lastScrollTop) {
|
|
496
|
-
return 'down';
|
|
497
|
-
}
|
|
498
|
-
else if (scrollTop < this.lastScrollTop) {
|
|
499
|
-
return 'up';
|
|
500
|
-
}
|
|
501
|
-
return 'none';
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Check if near bottom (for infinite scroll)
|
|
505
|
-
*/
|
|
506
|
-
isNearBottom(threshold = 500) {
|
|
507
|
-
const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
|
|
508
|
-
// Get container height from positions
|
|
509
|
-
let maxBottom = 0;
|
|
510
|
-
for (const position of this.positions.values()) {
|
|
511
|
-
maxBottom = Math.max(maxBottom, position.y + position.height);
|
|
512
|
-
}
|
|
513
|
-
return scrollTop + viewportHeight >= maxBottom - threshold;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Scroll to item
|
|
517
|
-
*/
|
|
518
|
-
scrollToItem(id, behavior = 'smooth') {
|
|
519
|
-
const position = this.positions.get(id);
|
|
520
|
-
if (!position)
|
|
521
|
-
return;
|
|
522
|
-
const container = this.options.scrollContainer ?? window;
|
|
523
|
-
if (container === window) {
|
|
524
|
-
window.scrollTo({
|
|
525
|
-
top: position.y,
|
|
526
|
-
behavior,
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
else {
|
|
530
|
-
container.scrollTo({
|
|
531
|
-
top: position.y,
|
|
532
|
-
behavior,
|
|
169
|
+
twttr.widgets.createTweet(match[1], widgetContainer, {
|
|
170
|
+
theme,
|
|
171
|
+
conversation: 'none',
|
|
172
|
+
dnt: true,
|
|
173
|
+
}).then((el) => {
|
|
174
|
+
if (!mountedRef.current)
|
|
175
|
+
return;
|
|
176
|
+
setLoading(false);
|
|
177
|
+
if (el) {
|
|
178
|
+
onLoad?.();
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
onError?.(new Error('Tweet not found or unavailable'));
|
|
182
|
+
}
|
|
183
|
+
}).catch((err) => {
|
|
184
|
+
if (!mountedRef.current)
|
|
185
|
+
return;
|
|
186
|
+
setLoading(false);
|
|
187
|
+
onError?.(err);
|
|
533
188
|
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (this.visibleItems.length === 0) {
|
|
541
|
-
return { start: 0, end: 0 };
|
|
542
|
-
}
|
|
543
|
-
const indices = this.visibleItems.map(item => item.index);
|
|
544
|
-
return {
|
|
545
|
-
start: Math.min(...indices),
|
|
546
|
-
end: Math.max(...indices) + 1,
|
|
189
|
+
});
|
|
190
|
+
return () => {
|
|
191
|
+
// Safely remove the widget container
|
|
192
|
+
if (embedContainer && widgetContainer.parentNode === embedContainer) {
|
|
193
|
+
embedContainer.removeChild(widgetContainer);
|
|
194
|
+
}
|
|
547
195
|
};
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
const
|
|
196
|
+
}, [url, theme, onLoad, onError]);
|
|
197
|
+
return (jsxRuntime.jsxs("div", { ref: containerRef, className: "sm-twitter-embed", children: [loading && (jsxRuntime.jsx("div", { style: {
|
|
198
|
+
display: 'flex',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
justifyContent: 'center',
|
|
201
|
+
minHeight: 200,
|
|
202
|
+
backgroundColor: theme === 'dark' ? '#15202b' : '#f5f5f5',
|
|
203
|
+
borderRadius: 12,
|
|
204
|
+
}, children: jsxRuntime.jsx("div", { style: { color: theme === 'dark' ? '#8899a6' : '#666' }, children: "Loading..." }) })), jsxRuntime.jsx("div", { ref: embedRef })] }));
|
|
205
|
+
};
|
|
206
|
+
const InstagramEmbed = ({ url, onLoad, onError, }) => {
|
|
207
|
+
const containerRef = react.useRef(null);
|
|
208
|
+
const embedRef = react.useRef(null);
|
|
209
|
+
const [loading, setLoading] = react.useState(true);
|
|
210
|
+
const mountedRef = react.useRef(true);
|
|
560
211
|
react.useEffect(() => {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
212
|
+
mountedRef.current = true;
|
|
213
|
+
return () => {
|
|
214
|
+
mountedRef.current = false;
|
|
215
|
+
};
|
|
216
|
+
}, []);
|
|
217
|
+
react.useEffect(() => {
|
|
218
|
+
const embedContainer = embedRef.current;
|
|
219
|
+
if (!embedContainer)
|
|
220
|
+
return;
|
|
221
|
+
// Extract post ID from URL
|
|
222
|
+
const match = url.match(/instagram\.com\/(?:p|reel)\/([A-Za-z0-9_-]+)/);
|
|
223
|
+
if (!match) {
|
|
224
|
+
onError?.(new Error('Invalid Instagram URL'));
|
|
225
|
+
setLoading(false);
|
|
571
226
|
return;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
227
|
+
}
|
|
228
|
+
// Create a fresh container for the widget
|
|
229
|
+
const widgetContainer = document.createElement('div');
|
|
230
|
+
widgetContainer.innerHTML = `
|
|
231
|
+
<blockquote
|
|
232
|
+
class="instagram-media"
|
|
233
|
+
data-instgrm-captioned
|
|
234
|
+
data-instgrm-permalink="https://www.instagram.com/p/${match[1]}/"
|
|
235
|
+
data-instgrm-version="14"
|
|
236
|
+
style="
|
|
237
|
+
background:#FFF;
|
|
238
|
+
border:0;
|
|
239
|
+
border-radius:12px;
|
|
240
|
+
box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15);
|
|
241
|
+
margin: 0;
|
|
242
|
+
max-width:100%;
|
|
243
|
+
min-width:100%;
|
|
244
|
+
padding:0;
|
|
245
|
+
width:100%;
|
|
246
|
+
"
|
|
247
|
+
>
|
|
248
|
+
</blockquote>
|
|
249
|
+
`;
|
|
250
|
+
embedContainer.appendChild(widgetContainer);
|
|
251
|
+
loadInstagramScript().then(() => {
|
|
252
|
+
if (!mountedRef.current)
|
|
253
|
+
return;
|
|
254
|
+
const instgrm = window.instgrm;
|
|
255
|
+
if (instgrm) {
|
|
256
|
+
instgrm.Embeds.process();
|
|
257
|
+
setLoading(false);
|
|
258
|
+
onLoad?.();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
return () => {
|
|
262
|
+
// Safely remove the widget container
|
|
263
|
+
if (embedContainer && widgetContainer.parentNode === embedContainer) {
|
|
264
|
+
embedContainer.removeChild(widgetContainer);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}, [url, onLoad, onError]);
|
|
268
|
+
return (jsxRuntime.jsxs("div", { ref: containerRef, className: "sm-instagram-embed", children: [loading && (jsxRuntime.jsx("div", { style: {
|
|
269
|
+
display: 'flex',
|
|
270
|
+
alignItems: 'center',
|
|
271
|
+
justifyContent: 'center',
|
|
272
|
+
minHeight: 400,
|
|
273
|
+
backgroundColor: '#fafafa',
|
|
274
|
+
borderRadius: 12,
|
|
275
|
+
}, children: jsxRuntime.jsx("div", { style: { color: '#666' }, children: "Loading..." }) })), jsxRuntime.jsx("div", { ref: embedRef })] }));
|
|
614
276
|
};
|
|
615
277
|
const SocialMasonry = react.forwardRef((props, ref) => {
|
|
616
|
-
const { posts: initialPosts = [], gap = 16, columns = defaultColumnConfig,
|
|
278
|
+
const { posts: initialPosts = [], gap = 16, columns = defaultColumnConfig, theme = 'light', onEmbedLoad, onEmbedError, className, style, } = props;
|
|
617
279
|
const containerRef = react.useRef(null);
|
|
618
280
|
const [posts, setPosts] = react.useState(initialPosts);
|
|
619
|
-
const [
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
gap,
|
|
626
|
-
columns,
|
|
627
|
-
padding,
|
|
628
|
-
animationDuration,
|
|
629
|
-
animate,
|
|
630
|
-
easing,
|
|
631
|
-
containerWidth: containerRef.current?.clientWidth || 800,
|
|
632
|
-
itemHeights,
|
|
633
|
-
});
|
|
634
|
-
}, [gap, columns, padding, animationDuration, animate, easing]);
|
|
635
|
-
const virtualizationEngine = react.useMemo(() => {
|
|
636
|
-
if (!virtualization?.enabled)
|
|
637
|
-
return null;
|
|
638
|
-
return new VirtualizationEngine({
|
|
639
|
-
...virtualization,
|
|
640
|
-
posts,
|
|
641
|
-
positions,
|
|
642
|
-
onVisibleItemsChange: setVisibleItems,
|
|
643
|
-
});
|
|
644
|
-
}, [virtualization?.enabled]);
|
|
645
|
-
// Calculate layout
|
|
646
|
-
const calculateLayout = react.useCallback(() => {
|
|
647
|
-
if (!containerRef.current)
|
|
648
|
-
return;
|
|
649
|
-
layoutEngine.setContainerWidth(containerRef.current.clientWidth);
|
|
650
|
-
const state = layoutEngine.calculate(posts);
|
|
651
|
-
setPositions(state.positions);
|
|
652
|
-
setContainerHeight(state.containerHeight);
|
|
653
|
-
if (virtualizationEngine) {
|
|
654
|
-
virtualizationEngine.update(posts, state.positions);
|
|
655
|
-
setVisibleItems(virtualizationEngine.calculateVisibleItems());
|
|
656
|
-
}
|
|
657
|
-
onLayoutComplete?.(Array.from(state.positions.values()));
|
|
658
|
-
}, [posts, layoutEngine, virtualizationEngine, onLayoutComplete]);
|
|
659
|
-
// Handle resize
|
|
281
|
+
const [columnCount, setColumnCount] = react.useState(3);
|
|
282
|
+
// Update posts when initialPosts changes
|
|
283
|
+
react.useEffect(() => {
|
|
284
|
+
setPosts(initialPosts);
|
|
285
|
+
}, [initialPosts]);
|
|
286
|
+
// Calculate column count based on container width
|
|
660
287
|
react.useEffect(() => {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
288
|
+
const updateColumnCount = () => {
|
|
289
|
+
if (!containerRef.current)
|
|
290
|
+
return;
|
|
291
|
+
const width = containerRef.current.clientWidth;
|
|
292
|
+
const count = getColumnCount(columns, width);
|
|
293
|
+
setColumnCount(count);
|
|
294
|
+
};
|
|
295
|
+
updateColumnCount();
|
|
296
|
+
const resizeObserver = new ResizeObserver(updateColumnCount);
|
|
665
297
|
if (containerRef.current) {
|
|
666
298
|
resizeObserver.observe(containerRef.current);
|
|
667
299
|
}
|
|
668
|
-
return () =>
|
|
669
|
-
|
|
670
|
-
};
|
|
671
|
-
}, [calculateLayout]);
|
|
672
|
-
// Initial layout
|
|
673
|
-
react.useEffect(() => {
|
|
674
|
-
calculateLayout();
|
|
675
|
-
}, [posts, calculateLayout]);
|
|
676
|
-
// Initialize virtualization
|
|
677
|
-
react.useEffect(() => {
|
|
678
|
-
if (virtualizationEngine) {
|
|
679
|
-
virtualizationEngine.init();
|
|
680
|
-
return () => virtualizationEngine.destroy();
|
|
681
|
-
}
|
|
682
|
-
return undefined;
|
|
683
|
-
}, [virtualizationEngine]);
|
|
684
|
-
// Handle height change
|
|
685
|
-
const handleHeightChange = react.useCallback((id, height) => {
|
|
686
|
-
itemHeights.set(id, height);
|
|
687
|
-
calculateLayout();
|
|
688
|
-
}, [itemHeights, calculateLayout]);
|
|
300
|
+
return () => resizeObserver.disconnect();
|
|
301
|
+
}, [columns]);
|
|
689
302
|
// Expose methods via ref
|
|
690
303
|
react.useImperativeHandle(ref, () => ({
|
|
691
304
|
addPosts: (newPosts) => {
|
|
692
305
|
setPosts(prev => [...prev, ...newPosts]);
|
|
693
306
|
},
|
|
694
307
|
setPosts: (newPosts) => {
|
|
695
|
-
itemHeights.clear();
|
|
696
308
|
setPosts(newPosts);
|
|
697
309
|
},
|
|
698
310
|
removePost: (id) => {
|
|
699
|
-
|
|
700
|
-
setPosts(prev => prev.filter(p => p.id !== id));
|
|
311
|
+
setPosts(prev => prev.filter(p => generatePostId(p) !== id));
|
|
701
312
|
},
|
|
702
313
|
refresh: () => {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const position = positions.get(id);
|
|
708
|
-
if (position) {
|
|
709
|
-
window.scrollTo({ top: position.y, behavior });
|
|
314
|
+
// Re-process embeds
|
|
315
|
+
const instgrm = window.instgrm;
|
|
316
|
+
if (instgrm) {
|
|
317
|
+
instgrm.Embeds.process();
|
|
710
318
|
}
|
|
711
319
|
},
|
|
712
|
-
}), [
|
|
713
|
-
//
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
320
|
+
}), []);
|
|
321
|
+
// Distribute posts into columns (CSS-based masonry simulation)
|
|
322
|
+
const distributeToColumns = react.useCallback(() => {
|
|
323
|
+
const cols = Array.from({ length: columnCount }, () => []);
|
|
324
|
+
posts.forEach((post, index) => {
|
|
325
|
+
cols[index % columnCount].push(post);
|
|
326
|
+
});
|
|
327
|
+
return cols;
|
|
328
|
+
}, [posts, columnCount]);
|
|
329
|
+
const postColumns = distributeToColumns();
|
|
717
330
|
return (jsxRuntime.jsxs("div", { ref: containerRef, className: `sm-container ${className || ''}`, style: {
|
|
718
|
-
|
|
331
|
+
display: 'flex',
|
|
332
|
+
gap,
|
|
719
333
|
width: '100%',
|
|
720
|
-
height: containerHeight,
|
|
721
334
|
...style,
|
|
722
|
-
}, children: [
|
|
723
|
-
|
|
724
|
-
|
|
335
|
+
}, children: [postColumns.map((columnPosts, colIndex) => (jsxRuntime.jsx("div", { className: "sm-column", style: {
|
|
336
|
+
flex: 1,
|
|
337
|
+
display: 'flex',
|
|
338
|
+
flexDirection: 'column',
|
|
339
|
+
gap,
|
|
340
|
+
minWidth: 0,
|
|
341
|
+
}, children: columnPosts.map(post => {
|
|
342
|
+
const postId = generatePostId(post);
|
|
343
|
+
if (post.platform === 'twitter') {
|
|
344
|
+
return (jsxRuntime.jsx("div", { className: "sm-embed sm-embed--twitter", children: jsxRuntime.jsx(TwitterEmbed, { url: post.url, theme: theme, onLoad: () => onEmbedLoad?.(post), onError: (error) => onEmbedError?.(post, error) }) }, postId));
|
|
345
|
+
}
|
|
346
|
+
if (post.platform === 'instagram') {
|
|
347
|
+
return (jsxRuntime.jsx("div", { className: "sm-embed sm-embed--instagram", children: jsxRuntime.jsx(InstagramEmbed, { url: post.url, onLoad: () => onEmbedLoad?.(post), onError: (error) => onEmbedError?.(post, error) }) }, postId));
|
|
348
|
+
}
|
|
725
349
|
return null;
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
350
|
+
}) }, colIndex))), posts.length === 0 && (jsxRuntime.jsx("div", { className: "sm-empty", style: {
|
|
351
|
+
display: 'flex',
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
justifyContent: 'center',
|
|
354
|
+
padding: 40,
|
|
355
|
+
color: '#666',
|
|
356
|
+
fontSize: 14,
|
|
357
|
+
width: '100%',
|
|
358
|
+
}, children: "No posts to display" }))] }));
|
|
730
359
|
});
|
|
731
360
|
SocialMasonry.displayName = 'SocialMasonry';
|
|
732
361
|
|