react-media-optimizer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +405 -0
- package/dist/index.d.mts +90 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yared Abebe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
markdown
|
|
2
|
+
|
|
3
|
+
# React Media Optimizer 🚀
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/react-media-optimizer)
|
|
6
|
+
[](https://npmjs.com/package/react-media-optimizer)
|
|
7
|
+
[](https://bundlephobia.com/package/react-media-optimizer)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
**Drop-in image & video optimization for React applications.** Automatically compress, lazy-load, and convert media to improve performance and user experience.
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- **🖼️ Automatic Image Optimization** - Auto WebP conversion, compression, and lazy loading
|
|
16
|
+
- **🎬 Smart Video Handling** - Lazy-loaded videos with format optimization
|
|
17
|
+
- **⚡ Performance Focused** - Improves Core Web Vitals (LCP, CLS)
|
|
18
|
+
- **🔧 Zero Configuration** - Works out of the box with sensible defaults
|
|
19
|
+
- **📱 Responsive by Design** - Automatic srcset generation and responsive handling
|
|
20
|
+
- **🔄 Multi-Format Support** - WebP, AVIF, MP4, WebM
|
|
21
|
+
- **🎯 TypeScript Ready** - Full type safety with comprehensive TypeScript support
|
|
22
|
+
- **🌐 CDN Integration Ready** - Easily connect with Cloudinary, Imgix, etc.
|
|
23
|
+
- **♿ Accessibility First** - Automatic alt text handling and focus management
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install react-media-optimizer
|
|
29
|
+
# or
|
|
30
|
+
yarn add react-media-optimizer
|
|
31
|
+
# or
|
|
32
|
+
pnpm add react-media-optimizer
|
|
33
|
+
|
|
34
|
+
🚀 Quick Start
|
|
35
|
+
jsx
|
|
36
|
+
|
|
37
|
+
import { OptimizedImage, OptimizedVideo } from 'react-media-optimizer';
|
|
38
|
+
|
|
39
|
+
function App() {
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
<OptimizedImage
|
|
43
|
+
src="/large-image.jpg"
|
|
44
|
+
alt="Beautiful landscape"
|
|
45
|
+
width={800}
|
|
46
|
+
height={600}
|
|
47
|
+
lazy={true}
|
|
48
|
+
quality={85}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<OptimizedVideo
|
|
52
|
+
src="/video.mp4"
|
|
53
|
+
poster="/video-poster.jpg"
|
|
54
|
+
width={1280}
|
|
55
|
+
height={720}
|
|
56
|
+
controls
|
|
57
|
+
lazy={true}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
📖 API Reference
|
|
64
|
+
<OptimizedImage />
|
|
65
|
+
|
|
66
|
+
The main image optimization component with smart defaults.
|
|
67
|
+
jsx
|
|
68
|
+
|
|
69
|
+
import { OptimizedImage } from 'react-media-optimizer';
|
|
70
|
+
|
|
71
|
+
<OptimizedImage
|
|
72
|
+
src="/path/to/image.jpg"
|
|
73
|
+
alt="Description"
|
|
74
|
+
width={400}
|
|
75
|
+
height={300}
|
|
76
|
+
lazy={true} // Enable lazy loading
|
|
77
|
+
webp={true} // Convert to WebP if supported
|
|
78
|
+
quality={85} // Image quality (1-100)
|
|
79
|
+
placeholderSrc="/placeholder.jpg" // Loading placeholder
|
|
80
|
+
fallbackSrc="/fallback.jpg" // Fallback on error
|
|
81
|
+
showLoadingIndicator={true} // Show loading state
|
|
82
|
+
className="custom-class" // Additional CSS classes
|
|
83
|
+
// All standard img props supported
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
Props
|
|
87
|
+
Prop Type Default Description
|
|
88
|
+
src string Required Image source URL
|
|
89
|
+
alt string "" Alternative text for accessibility
|
|
90
|
+
lazy boolean true Enable lazy loading
|
|
91
|
+
webp boolean true Convert to WebP format when supported
|
|
92
|
+
quality number 85 Image quality (1-100)
|
|
93
|
+
placeholderSrc string undefined Placeholder image during loading
|
|
94
|
+
fallbackSrc string undefined Fallback image on error
|
|
95
|
+
showLoadingIndicator boolean true Show loading state visually
|
|
96
|
+
<OptimizedVideo />
|
|
97
|
+
|
|
98
|
+
Intelligent video component with lazy loading and format optimization.
|
|
99
|
+
jsx
|
|
100
|
+
|
|
101
|
+
import { OptimizedVideo } from 'react-media-optimizer';
|
|
102
|
+
|
|
103
|
+
<OptimizedVideo
|
|
104
|
+
src="/video.mp4"
|
|
105
|
+
poster="/poster.jpg"
|
|
106
|
+
width={1280}
|
|
107
|
+
height={720}
|
|
108
|
+
lazy={true}
|
|
109
|
+
webm={true} // Provide WebM version for better compression
|
|
110
|
+
mp4={true} // Provide MP4 version for compatibility
|
|
111
|
+
controls
|
|
112
|
+
autoPlay
|
|
113
|
+
muted
|
|
114
|
+
// All standard video props supported
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
Props
|
|
118
|
+
Prop Type Default Description
|
|
119
|
+
src string Required Video source URL
|
|
120
|
+
poster string undefined Video poster image
|
|
121
|
+
lazy boolean true Enable lazy loading
|
|
122
|
+
webm boolean true Use WebM format for better compression
|
|
123
|
+
mp4 boolean true Use MP4 format for compatibility
|
|
124
|
+
useOptimizedImage() Hook
|
|
125
|
+
|
|
126
|
+
For advanced usage and custom implementations.
|
|
127
|
+
jsx
|
|
128
|
+
|
|
129
|
+
import { useOptimizedImage } from 'react-media-optimizer';
|
|
130
|
+
|
|
131
|
+
function CustomImageComponent({ src, alt }) {
|
|
132
|
+
const {
|
|
133
|
+
src: optimizedSrc,
|
|
134
|
+
isLoading,
|
|
135
|
+
error,
|
|
136
|
+
elementRef
|
|
137
|
+
} = useOptimizedImage({
|
|
138
|
+
src,
|
|
139
|
+
lazy: true,
|
|
140
|
+
webp: true,
|
|
141
|
+
quality: 80,
|
|
142
|
+
fallbackSrc: '/fallback.jpg',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (isLoading) {
|
|
146
|
+
return <div className="loading">Loading...</div>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (error) {
|
|
150
|
+
return <img src="/fallback.jpg" alt={alt} />;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<img
|
|
155
|
+
ref={elementRef}
|
|
156
|
+
src={optimizedSrc}
|
|
157
|
+
alt={alt}
|
|
158
|
+
loading="lazy"
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
Hook Options
|
|
164
|
+
typescript
|
|
165
|
+
|
|
166
|
+
interface UseOptimizedImageOptions {
|
|
167
|
+
src: string;
|
|
168
|
+
lazy?: boolean; // default: true
|
|
169
|
+
webp?: boolean; // default: true
|
|
170
|
+
quality?: number; // default: 85
|
|
171
|
+
fallbackSrc?: string;
|
|
172
|
+
onLoad?: () => void;
|
|
173
|
+
onError?: () => void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
📊 Performance Impact
|
|
177
|
+
Before & After Example
|
|
178
|
+
Metric Before After Improvement
|
|
179
|
+
Image Size 2.4 MB 450 KB ⬇️ 81% smaller
|
|
180
|
+
LCP (Largest Contentful Paint) 4.2s 1.1s ⬇️ 74% faster
|
|
181
|
+
Page Load Time 5.8s 2.3s ⬇️ 60% faster
|
|
182
|
+
Bandwidth Usage 8.7 MB 1.9 MB ⬇️ 78% less
|
|
183
|
+
🎯 Real-World Examples
|
|
184
|
+
E-commerce Product Gallery
|
|
185
|
+
jsx
|
|
186
|
+
|
|
187
|
+
import { OptimizedImage } from 'react-media-optimizer';
|
|
188
|
+
|
|
189
|
+
function ProductGallery({ products }) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="product-grid">
|
|
192
|
+
{products.map((product) => (
|
|
193
|
+
<div key={product.id} className="product-card">
|
|
194
|
+
<OptimizedImage
|
|
195
|
+
src={product.imageUrl}
|
|
196
|
+
alt={product.name}
|
|
197
|
+
width={400}
|
|
198
|
+
height={400}
|
|
199
|
+
lazy={true}
|
|
200
|
+
quality={90}
|
|
201
|
+
placeholderSrc="/product-placeholder.jpg"
|
|
202
|
+
className="product-image"
|
|
203
|
+
/>
|
|
204
|
+
<h3>{product.name}</h3>
|
|
205
|
+
<p>{product.price}</p>
|
|
206
|
+
</div>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Blog with Hero Image
|
|
213
|
+
jsx
|
|
214
|
+
|
|
215
|
+
function BlogPost({ post }) {
|
|
216
|
+
return (
|
|
217
|
+
<article>
|
|
218
|
+
<OptimizedImage
|
|
219
|
+
src={post.heroImage}
|
|
220
|
+
alt={post.title}
|
|
221
|
+
width={1200}
|
|
222
|
+
height={630}
|
|
223
|
+
lazy={false} // Hero image should load immediately
|
|
224
|
+
quality={95}
|
|
225
|
+
className="hero-image"
|
|
226
|
+
/>
|
|
227
|
+
<h1>{post.title}</h1>
|
|
228
|
+
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
|
229
|
+
</article>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
🔧 Advanced Usage
|
|
234
|
+
Custom Loading Component
|
|
235
|
+
jsx
|
|
236
|
+
|
|
237
|
+
function CustomOptimizedImage({ src, alt, ...props }) {
|
|
238
|
+
const { src: optimizedSrc, isLoading } = useOptimizedImage({ src });
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div className="image-wrapper">
|
|
242
|
+
{isLoading && (
|
|
243
|
+
<div className="custom-loader">
|
|
244
|
+
<Spinner />
|
|
245
|
+
<span>Loading image...</span>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
<img
|
|
249
|
+
src={optimizedSrc}
|
|
250
|
+
alt={alt}
|
|
251
|
+
style={{ opacity: isLoading ? 0 : 1 }}
|
|
252
|
+
{...props}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
CDN Integration
|
|
259
|
+
jsx
|
|
260
|
+
|
|
261
|
+
import { OptimizedImage } from 'react-media-optimizer';
|
|
262
|
+
|
|
263
|
+
// With Cloudinary-like transformations
|
|
264
|
+
<OptimizedImage
|
|
265
|
+
src="https://res.cloudinary.com/demo/image/upload/sample.jpg"
|
|
266
|
+
alt="Sample"
|
|
267
|
+
width={800}
|
|
268
|
+
height={600}
|
|
269
|
+
cdnTransformations={{
|
|
270
|
+
quality: 'auto',
|
|
271
|
+
format: 'auto',
|
|
272
|
+
fetch_format: 'auto',
|
|
273
|
+
}}
|
|
274
|
+
/>
|
|
275
|
+
|
|
276
|
+
🏗️ Integration Guides
|
|
277
|
+
Next.js Integration
|
|
278
|
+
jsx
|
|
279
|
+
|
|
280
|
+
// next.config.js
|
|
281
|
+
module.exports = {
|
|
282
|
+
images: {
|
|
283
|
+
domains: ['your-cdn-domain.com'],
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// pages/index.js
|
|
288
|
+
import { OptimizedImage } from 'react-media-optimizer';
|
|
289
|
+
|
|
290
|
+
export default function HomePage() {
|
|
291
|
+
return (
|
|
292
|
+
<OptimizedImage
|
|
293
|
+
src="/nextjs-image.jpg"
|
|
294
|
+
alt="Next.js optimized"
|
|
295
|
+
width={1920}
|
|
296
|
+
height={1080}
|
|
297
|
+
priority // Next.js specific prop
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
Gatsby Integration
|
|
303
|
+
jsx
|
|
304
|
+
|
|
305
|
+
// Install with Gatsby
|
|
306
|
+
npm install react-media-optimizer gatsby-plugin-image
|
|
307
|
+
|
|
308
|
+
// Use in Gatsby page
|
|
309
|
+
import { OptimizedImage } from 'react-media-optimizer';
|
|
310
|
+
|
|
311
|
+
const Page = () => (
|
|
312
|
+
<OptimizedImage
|
|
313
|
+
src={data.file.publicURL}
|
|
314
|
+
alt="Gatsby image"
|
|
315
|
+
width={800}
|
|
316
|
+
height={600}
|
|
317
|
+
/>
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
⚡ Performance Tips
|
|
321
|
+
|
|
322
|
+
Set appropriate sizes: Always specify width and height to prevent layout shifts
|
|
323
|
+
|
|
324
|
+
Use placeholders: Implement blur-up or color placeholders for better UX
|
|
325
|
+
|
|
326
|
+
Prioritize critical images: Set lazy={false} for above-the-fold images
|
|
327
|
+
|
|
328
|
+
Monitor performance: Use Lighthouse and Web Vitals to track improvements
|
|
329
|
+
|
|
330
|
+
Test formats: Different images compress better in different formats
|
|
331
|
+
|
|
332
|
+
🔄 Migration Guide
|
|
333
|
+
From standard <img> tags
|
|
334
|
+
diff
|
|
335
|
+
|
|
336
|
+
- <img src="/image.jpg" alt="Example" />
|
|
337
|
+
+ <OptimizedImage src="/image.jpg" alt="Example" width={800} height={600} />
|
|
338
|
+
|
|
339
|
+
From Next.js Image
|
|
340
|
+
diff
|
|
341
|
+
|
|
342
|
+
- import Image from 'next/image';
|
|
343
|
+
+ import { OptimizedImage } from 'react-media-optimizer';
|
|
344
|
+
|
|
345
|
+
- <Image src="/image.jpg" alt="Example" width={800} height={600} />
|
|
346
|
+
+ <OptimizedImage src="/image.jpg" alt="Example" width={800} height={600} />
|
|
347
|
+
|
|
348
|
+
📈 Benchmarks
|
|
349
|
+
|
|
350
|
+
Testing with 100 product images (1920x1080):
|
|
351
|
+
Solution Load Time Bandwidth LCP Score
|
|
352
|
+
Standard <img> 12.4s 24.8 MB 2.8s
|
|
353
|
+
Next.js Image 6.2s 8.3 MB 1.4s
|
|
354
|
+
React Media Optimizer 4.1s 5.7 MB 0.9s
|
|
355
|
+
🐛 Troubleshooting
|
|
356
|
+
Common Issues
|
|
357
|
+
|
|
358
|
+
Images not loading
|
|
359
|
+
|
|
360
|
+
Check if the source URL is accessible
|
|
361
|
+
|
|
362
|
+
Verify CORS policies for external images
|
|
363
|
+
|
|
364
|
+
Ensure proper image format support
|
|
365
|
+
|
|
366
|
+
Lazy loading not working
|
|
367
|
+
|
|
368
|
+
Verify the Intersection Observer API is supported (polyfill available)
|
|
369
|
+
|
|
370
|
+
Check if lazy={true} is set
|
|
371
|
+
|
|
372
|
+
Ensure images are in scrollable containers
|
|
373
|
+
|
|
374
|
+
WebP conversion issues
|
|
375
|
+
|
|
376
|
+
Some CDNs auto-convert formats
|
|
377
|
+
|
|
378
|
+
Check browser WebP support with supportsWebP() utility
|
|
379
|
+
|
|
380
|
+
Debug Mode
|
|
381
|
+
jsx
|
|
382
|
+
|
|
383
|
+
<OptimizedImage
|
|
384
|
+
src="/image.jpg"
|
|
385
|
+
alt="Debug"
|
|
386
|
+
debug={true} // Logs optimization process to console
|
|
387
|
+
/>
|
|
388
|
+
|
|
389
|
+
🤝 Contributing
|
|
390
|
+
|
|
391
|
+
We welcome contributions! Please see our Contributing Guide for details.
|
|
392
|
+
|
|
393
|
+
Fork the repository
|
|
394
|
+
|
|
395
|
+
Create a feature branch (git checkout -b feature/AmazingFeature)
|
|
396
|
+
|
|
397
|
+
Commit your changes (git commit -m 'Add some AmazingFeature')
|
|
398
|
+
|
|
399
|
+
Push to the branch (git push origin feature/AmazingFeature)
|
|
400
|
+
|
|
401
|
+
Open a Pull Request
|
|
402
|
+
|
|
403
|
+
📄 License
|
|
404
|
+
|
|
405
|
+
MIT © Yared Abebe.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import React__default, { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal hook options
|
|
6
|
+
* NOTE:
|
|
7
|
+
* - This hook does NOT accept React event handlers
|
|
8
|
+
* - It exposes simple lifecycle callbacks instead
|
|
9
|
+
*/
|
|
10
|
+
interface UseOptimizedImageOptions {
|
|
11
|
+
src: string;
|
|
12
|
+
lazy?: boolean;
|
|
13
|
+
webp?: boolean;
|
|
14
|
+
quality?: number;
|
|
15
|
+
fallbackSrc?: string;
|
|
16
|
+
onOptimizedLoad?: () => void;
|
|
17
|
+
onOptimizedError?: () => void;
|
|
18
|
+
}
|
|
19
|
+
declare function useOptimizedImage(options: UseOptimizedImageOptions): {
|
|
20
|
+
src: string;
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
elementRef: React.RefObject<HTMLImageElement>;
|
|
24
|
+
isVisible: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface UseLazyLoadOptions {
|
|
28
|
+
threshold?: number;
|
|
29
|
+
rootMargin?: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* useLazyLoad
|
|
34
|
+
* - Observes element visibility using IntersectionObserver
|
|
35
|
+
* - Safe for SSR
|
|
36
|
+
* - Stable ref ownership
|
|
37
|
+
*/
|
|
38
|
+
declare function useLazyLoad<T extends HTMLElement = HTMLElement>(options?: UseLazyLoadOptions): {
|
|
39
|
+
isVisible: boolean;
|
|
40
|
+
elementRef: RefObject<T>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface OptimizedImageProps extends React__default.ImgHTMLAttributes<HTMLImageElement> {
|
|
44
|
+
src: string;
|
|
45
|
+
lazy?: boolean;
|
|
46
|
+
webp?: boolean;
|
|
47
|
+
quality?: number;
|
|
48
|
+
placeholderSrc?: string;
|
|
49
|
+
fallbackSrc?: string;
|
|
50
|
+
showLoadingIndicator?: boolean;
|
|
51
|
+
}
|
|
52
|
+
declare const OptimizedImage: React__default.FC<OptimizedImageProps>;
|
|
53
|
+
|
|
54
|
+
interface OptimizedVideoProps extends React__default.VideoHTMLAttributes<HTMLVideoElement> {
|
|
55
|
+
src: string;
|
|
56
|
+
poster?: string;
|
|
57
|
+
lazy?: boolean;
|
|
58
|
+
webm?: boolean;
|
|
59
|
+
mp4?: boolean;
|
|
60
|
+
}
|
|
61
|
+
declare const OptimizedVideo: React__default.FC<OptimizedVideoProps>;
|
|
62
|
+
|
|
63
|
+
interface CompressionOptions {
|
|
64
|
+
quality?: number;
|
|
65
|
+
maxWidth?: number;
|
|
66
|
+
maxHeight?: number;
|
|
67
|
+
mimeType?: 'image/jpeg' | 'image/png' | 'image/webp';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compress image using Canvas
|
|
71
|
+
* Browser-only utility
|
|
72
|
+
*/
|
|
73
|
+
declare function compressImage(file: File, options?: CompressionOptions): Promise<File>;
|
|
74
|
+
/**
|
|
75
|
+
* Calculate compression reduction
|
|
76
|
+
*/
|
|
77
|
+
declare function calculateSizeReduction(originalSize: number, newSize: number): string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detects WebP support (cached)
|
|
81
|
+
* Safe for SSR
|
|
82
|
+
*/
|
|
83
|
+
declare function supportsWebP(): Promise<boolean>;
|
|
84
|
+
/**
|
|
85
|
+
* Converts image URL to .webp by rewriting extension
|
|
86
|
+
* NOTE: Only works if server/CDN supports WebP
|
|
87
|
+
*/
|
|
88
|
+
declare function convertToWebP(src: string): string;
|
|
89
|
+
|
|
90
|
+
export { OptimizedImage, OptimizedVideo, calculateSizeReduction, compressImage, convertToWebP, supportsWebP, useLazyLoad, useOptimizedImage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import React__default, { RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal hook options
|
|
6
|
+
* NOTE:
|
|
7
|
+
* - This hook does NOT accept React event handlers
|
|
8
|
+
* - It exposes simple lifecycle callbacks instead
|
|
9
|
+
*/
|
|
10
|
+
interface UseOptimizedImageOptions {
|
|
11
|
+
src: string;
|
|
12
|
+
lazy?: boolean;
|
|
13
|
+
webp?: boolean;
|
|
14
|
+
quality?: number;
|
|
15
|
+
fallbackSrc?: string;
|
|
16
|
+
onOptimizedLoad?: () => void;
|
|
17
|
+
onOptimizedError?: () => void;
|
|
18
|
+
}
|
|
19
|
+
declare function useOptimizedImage(options: UseOptimizedImageOptions): {
|
|
20
|
+
src: string;
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
elementRef: React.RefObject<HTMLImageElement>;
|
|
24
|
+
isVisible: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface UseLazyLoadOptions {
|
|
28
|
+
threshold?: number;
|
|
29
|
+
rootMargin?: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* useLazyLoad
|
|
34
|
+
* - Observes element visibility using IntersectionObserver
|
|
35
|
+
* - Safe for SSR
|
|
36
|
+
* - Stable ref ownership
|
|
37
|
+
*/
|
|
38
|
+
declare function useLazyLoad<T extends HTMLElement = HTMLElement>(options?: UseLazyLoadOptions): {
|
|
39
|
+
isVisible: boolean;
|
|
40
|
+
elementRef: RefObject<T>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface OptimizedImageProps extends React__default.ImgHTMLAttributes<HTMLImageElement> {
|
|
44
|
+
src: string;
|
|
45
|
+
lazy?: boolean;
|
|
46
|
+
webp?: boolean;
|
|
47
|
+
quality?: number;
|
|
48
|
+
placeholderSrc?: string;
|
|
49
|
+
fallbackSrc?: string;
|
|
50
|
+
showLoadingIndicator?: boolean;
|
|
51
|
+
}
|
|
52
|
+
declare const OptimizedImage: React__default.FC<OptimizedImageProps>;
|
|
53
|
+
|
|
54
|
+
interface OptimizedVideoProps extends React__default.VideoHTMLAttributes<HTMLVideoElement> {
|
|
55
|
+
src: string;
|
|
56
|
+
poster?: string;
|
|
57
|
+
lazy?: boolean;
|
|
58
|
+
webm?: boolean;
|
|
59
|
+
mp4?: boolean;
|
|
60
|
+
}
|
|
61
|
+
declare const OptimizedVideo: React__default.FC<OptimizedVideoProps>;
|
|
62
|
+
|
|
63
|
+
interface CompressionOptions {
|
|
64
|
+
quality?: number;
|
|
65
|
+
maxWidth?: number;
|
|
66
|
+
maxHeight?: number;
|
|
67
|
+
mimeType?: 'image/jpeg' | 'image/png' | 'image/webp';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compress image using Canvas
|
|
71
|
+
* Browser-only utility
|
|
72
|
+
*/
|
|
73
|
+
declare function compressImage(file: File, options?: CompressionOptions): Promise<File>;
|
|
74
|
+
/**
|
|
75
|
+
* Calculate compression reduction
|
|
76
|
+
*/
|
|
77
|
+
declare function calculateSizeReduction(originalSize: number, newSize: number): string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detects WebP support (cached)
|
|
81
|
+
* Safe for SSR
|
|
82
|
+
*/
|
|
83
|
+
declare function supportsWebP(): Promise<boolean>;
|
|
84
|
+
/**
|
|
85
|
+
* Converts image URL to .webp by rewriting extension
|
|
86
|
+
* NOTE: Only works if server/CDN supports WebP
|
|
87
|
+
*/
|
|
88
|
+
declare function convertToWebP(src: string): string;
|
|
89
|
+
|
|
90
|
+
export { OptimizedImage, OptimizedVideo, calculateSizeReduction, compressImage, convertToWebP, supportsWebP, useLazyLoad, useOptimizedImage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');function z(e={}){let{threshold:t=.1,rootMargin:r="50px",enabled:o=true}=e,[a,i]=react.useState(!o),c=react.useRef(null);return react.useEffect(()=>{if(!o){i(true);return}if(typeof window>"u"||!("IntersectionObserver"in window)){i(true);return}let m=c.current;if(!m)return;let n=new IntersectionObserver(([s])=>{s.isIntersecting&&(i(true),n.disconnect());},{threshold:t,rootMargin:r});return n.observe(m),()=>{n.disconnect();}},[o,t,r]),{isVisible:a,elementRef:c}}var b=null;function I(){return b!==null?Promise.resolve(b):typeof window>"u"?(b=false,Promise.resolve(false)):new Promise(e=>{let t=new Image;t.onload=()=>{b=t.width>0&&t.height>0,e(b);},t.onerror=()=>{b=false,e(false);},t.src="data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA";})}function h(e){return M(e)?e.replace(/\.(jpe?g|png)(\?.*)?$/i,".webp$2"):e}function M(e){return /^https?:\/\//.test(e)}var O=null;function v(e){let{src:t,lazy:r=true,webp:o=true,quality:a=80,fallbackSrc:i,onOptimizedLoad:c,onOptimizedError:m}=e,[n,s]=react.useState(t),[d,l]=react.useState(true),[g,p]=react.useState(null),{isVisible:w,elementRef:y}=z({enabled:r}),u=react.useRef(false);return react.useEffect(()=>r&&!w?void 0:(u.current=false,(async()=>{l(true),p(null);try{O===null&&(O=await I());let f=t;if(o&&O&&R(t)&&(f=h(t)),R(f)&&typeof a=="number"){let A=f.includes("?")?"&":"?";f+=`${A}q=${a}`;}await V(f),u.current||(s(f),l(!1),c?.());}catch(f){u.current||(i?(s(i),l(false)):(p(f),l(false),m?.()));}})(),()=>{u.current=true;}),[t,w,r,o,a,i]),{src:n,isLoading:d,error:g,elementRef:y,isVisible:w}}function R(e){return /^https?:\/\//.test(e)}function V(e){return new Promise((t,r)=>{let o=new Image;o.onload=()=>t(),o.onerror=()=>r(new Error(`Failed to load image: ${e}`)),o.src=e;})}var U=({src:e,lazy:t=true,webp:r=true,quality:o=80,placeholderSrc:a,fallbackSrc:i,showLoadingIndicator:c=true,className:m="",alt:n,onLoad:s,onError:d,...l})=>{let{src:g,isLoading:p,error:w,elementRef:y}=v({src:e,lazy:t,webp:r,quality:o,fallbackSrc:i,onOptimizedLoad:()=>{},onOptimizedError:()=>{}}),u=n||e.split("/").pop()?.replace(/[-_]/g," ")||"image";return p&&c&&a?jsxRuntime.jsx("img",{ref:y,src:a,alt:`Loading ${u}`,className:`${m} media-optimizer-loading`,"aria-busy":"true",...l}):w&&i?jsxRuntime.jsx("img",{ref:y,src:i,alt:`Fallback for ${u}`,className:`${m} media-optimizer-error`,onLoad:s,onError:d,...l}):jsxRuntime.jsx("img",{ref:y,src:g,alt:u,className:`${m} media-optimizer-loaded`,loading:t?void 0:"eager",decoding:"async",onLoad:s,onError:d,...l})};var H=({src:e,poster:t,lazy:r=true,webm:o=true,mp4:a=true,className:i="",autoPlay:c,muted:m,...n})=>{let{isVisible:s,elementRef:d}=z({enabled:r}),l=c?true:m,g=C(e,{webm:o,mp4:a});return r&&!s?jsxRuntime.jsx("div",{className:`${i} media-optimizer-video-placeholder`,style:{background:t?`url(${t}) center / cover no-repeat`:"#f0f0f0",width:n.width||"100%",height:n.height||"300px",borderRadius:"4px"},"aria-label":"Video loading"}):jsxRuntime.jsxs("video",{ref:d,className:`${i} media-optimizer-video`,poster:t,preload:"metadata",playsInline:true,autoPlay:c,muted:l,...n,children:[g.map(p=>jsxRuntime.jsx("source",{src:p.src,type:p.type},p.src)),"Your browser does not support the video tag."]})};function C(e,t){let r=[],o=e.split("?")[0].toLowerCase(),a=o.endsWith(".mp4"),i=o.endsWith(".webm");return a?(t.webm&&r.push({src:e.replace(/\.mp4(\?.*)?$/,".webm$1"),type:"video/webm"}),t.mp4&&r.push({src:e,type:"video/mp4"})):i?r.push({src:e,type:"video/webm"}):r.push({src:e,type:"video/mp4"}),r}async function k(e,t={}){if(typeof window>"u")throw new Error("compressImage can only run in the browser");let{quality:r=.8,maxWidth:o=1920,maxHeight:a=1080,mimeType:i="image/jpeg"}=t,c=URL.createObjectURL(e);try{let m=await B(c),{width:n,height:s}=m;n>o&&(s=s*o/n,n=o),s>a&&(n=n*a/s,s=a);let d=document.createElement("canvas");d.width=n,d.height=s;let l=d.getContext("2d");if(!l)throw new Error("Canvas context not available");l.drawImage(m,0,0,n,s);let g=await q(d,i,r);return new File([g],D(e.name,i),{type:i,lastModified:Date.now()})}finally{URL.revokeObjectURL(c);}}function j(e,t){return `${((e-t)/e*100).toFixed(1)}% smaller`}function B(e){return new Promise((t,r)=>{let o=new Image;o.onload=()=>t(o),o.onerror=()=>r(new Error("Failed to load image")),o.src=e;})}function q(e,t,r){return new Promise((o,a)=>{e.toBlob(i=>{i?o(i):a(new Error("Image compression failed"));},t,r);})}function D(e,t){let r=t==="image/png"?"png":t==="image/webp"?"webp":"jpg";return e.replace(/\.\w+$/,`.${r}`)}
|
|
2
|
+
exports.OptimizedImage=U;exports.OptimizedVideo=H;exports.calculateSizeReduction=j;exports.compressImage=k;exports.convertToWebP=h;exports.supportsWebP=I;exports.useLazyLoad=z;exports.useOptimizedImage=v;//# sourceMappingURL=index.js.map
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useLazyLoad.ts","../src/utils/webpConverter.ts","../src/hooks/useOptimizedImage.ts","../src/components/OptimizedImage.tsx","../src/components/OptimizedVideo.tsx","../src/utils/compress.ts"],"names":["useLazyLoad","options","threshold","rootMargin","enabled","isVisible","setIsVisible","useState","elementRef","useRef","useEffect","node","observer","entry","cachedWebPSupport","supportsWebP","resolve","img","convertToWebP","src","isRemoteUrl","url","useOptimizedImage","lazy","webp","quality","fallbackSrc","onOptimizedLoad","onOptimizedError","optimizedSrc","setOptimizedSrc","isLoading","setIsLoading","error","setError","isCancelled","finalSrc","separator","preloadImage","err","reject","OptimizedImage","placeholderSrc","showLoadingIndicator","className","alt","onLoad","onError","imgProps","resolvedAlt","jsx","OptimizedVideo","poster","webm","mp4","autoPlay","muted","videoProps","resolvedMuted","sources","buildVideoSources","jsxs","source","cleanSrc","isMp4","isWebm","compressImage","file","maxWidth","maxHeight","mimeType","objectUrl","loadImage","width","height","canvas","ctx","blob","canvasToBlob","replaceExtension","calculateSizeReduction","originalSize","newSize","type","filename","ext"],"mappings":"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","file":"index.js","sourcesContent":["// src/hooks/useLazyLoad.ts\r\nimport { useState, useEffect, useRef, RefObject } from 'react';\r\n\r\ninterface UseLazyLoadOptions {\r\n threshold?: number;\r\n rootMargin?: string;\r\n enabled?: boolean;\r\n}\r\n\r\n/**\r\n * useLazyLoad\r\n * - Observes element visibility using IntersectionObserver\r\n * - Safe for SSR\r\n * - Stable ref ownership\r\n */\r\nexport function useLazyLoad<T extends HTMLElement = HTMLElement>(\r\n options: UseLazyLoadOptions = {}\r\n) {\r\n const {\r\n threshold = 0.1,\r\n rootMargin = '50px',\r\n enabled = true,\r\n } = options;\r\n\r\n const [isVisible, setIsVisible] = useState(!enabled);\r\n const elementRef = useRef<T | null>(null);\r\n\r\n useEffect(() => {\r\n if (!enabled) {\r\n setIsVisible(true);\r\n return;\r\n }\r\n\r\n // SSR / legacy browser safety\r\n if (typeof window === 'undefined' || !('IntersectionObserver' in window)) {\r\n setIsVisible(true);\r\n return;\r\n }\r\n\r\n const node = elementRef.current;\r\n if (!node) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setIsVisible(true);\r\n observer.disconnect();\r\n }\r\n },\r\n { threshold, rootMargin }\r\n );\r\n\r\n observer.observe(node);\r\n\r\n return () => {\r\n observer.disconnect();\r\n };\r\n }, [enabled, threshold, rootMargin]);\r\n\r\n return {\r\n isVisible,\r\n elementRef: elementRef as RefObject<T>,\r\n };\r\n}\r\n","// src/utils/webpConverter.ts\r\n\r\nlet cachedWebPSupport: boolean | null = null;\r\n\r\n/**\r\n * Detects WebP support (cached)\r\n * Safe for SSR\r\n */\r\nexport function supportsWebP(): Promise<boolean> {\r\n if (cachedWebPSupport !== null) {\r\n return Promise.resolve(cachedWebPSupport);\r\n }\r\n\r\n if (typeof window === 'undefined') {\r\n cachedWebPSupport = false;\r\n return Promise.resolve(false);\r\n }\r\n\r\n return new Promise((resolve) => {\r\n const img = new Image();\r\n img.onload = () => {\r\n cachedWebPSupport = img.width > 0 && img.height > 0;\r\n resolve(cachedWebPSupport);\r\n };\r\n img.onerror = () => {\r\n cachedWebPSupport = false;\r\n resolve(false);\r\n };\r\n img.src =\r\n 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';\r\n });\r\n}\r\n\r\n/**\r\n * Converts image URL to .webp by rewriting extension\r\n * NOTE: Only works if server/CDN supports WebP\r\n */\r\nexport function convertToWebP(src: string): string {\r\n if (!isRemoteUrl(src)) return src;\r\n\r\n return src.replace(/\\.(jpe?g|png)(\\?.*)?$/i, '.webp$2');\r\n}\r\n\r\n/**\r\n * Returns optimal image format (simple heuristic)\r\n */\r\nexport function getOptimalFormat(): 'webp' | 'jpeg' | 'png' {\r\n return 'webp';\r\n}\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction isRemoteUrl(url: string) {\r\n return /^https?:\\/\\//.test(url);\r\n}\r\n","import { useState, useEffect, useRef } from 'react';\r\nimport { useLazyLoad } from './useLazyLoad';\r\nimport { supportsWebP, convertToWebP } from '../utils/webpConverter';\r\n\r\n/**\r\n * Internal hook options\r\n * NOTE:\r\n * - This hook does NOT accept React event handlers\r\n * - It exposes simple lifecycle callbacks instead\r\n */\r\ninterface UseOptimizedImageOptions {\r\n src: string;\r\n lazy?: boolean;\r\n webp?: boolean;\r\n quality?: number;\r\n fallbackSrc?: string;\r\n onOptimizedLoad?: () => void;\r\n onOptimizedError?: () => void;\r\n}\r\n\r\nlet cachedWebPSupport: boolean | null = null;\r\n\r\nexport function useOptimizedImage(options: UseOptimizedImageOptions) {\r\n const {\r\n src,\r\n lazy = true,\r\n webp = true,\r\n quality = 80,\r\n fallbackSrc,\r\n onOptimizedLoad,\r\n onOptimizedError,\r\n } = options;\r\n\r\n const [optimizedSrc, setOptimizedSrc] = useState(src);\r\n const [isLoading, setIsLoading] = useState(true);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const { isVisible, elementRef } =\r\n useLazyLoad<HTMLImageElement>({ enabled: lazy });\r\n\r\n const isCancelled = useRef(false);\r\n\r\n useEffect(() => {\r\n if (lazy && !isVisible) return;\r\n\r\n isCancelled.current = false;\r\n\r\n const optimizeImage = async () => {\r\n setIsLoading(true);\r\n setError(null);\r\n\r\n try {\r\n // Cache WebP support (run once per app)\r\n if (cachedWebPSupport === null) {\r\n cachedWebPSupport = await supportsWebP();\r\n }\r\n\r\n let finalSrc = src;\r\n\r\n // Convert to WebP if supported and remote\r\n if (webp && cachedWebPSupport && isRemoteUrl(src)) {\r\n finalSrc = convertToWebP(src);\r\n }\r\n\r\n // Append quality only for remote URLs\r\n if (isRemoteUrl(finalSrc) && typeof quality === 'number') {\r\n const separator = finalSrc.includes('?') ? '&' : '?';\r\n finalSrc += `${separator}q=${quality}`;\r\n }\r\n\r\n // Preload image\r\n await preloadImage(finalSrc);\r\n\r\n if (!isCancelled.current) {\r\n setOptimizedSrc(finalSrc);\r\n setIsLoading(false);\r\n onOptimizedLoad?.();\r\n }\r\n } catch (err) {\r\n if (!isCancelled.current) {\r\n if (fallbackSrc) {\r\n setOptimizedSrc(fallbackSrc);\r\n setIsLoading(false);\r\n } else {\r\n setError(err as Error);\r\n setIsLoading(false);\r\n onOptimizedError?.();\r\n }\r\n }\r\n }\r\n };\r\n\r\n optimizeImage();\r\n\r\n return () => {\r\n isCancelled.current = true;\r\n };\r\n }, [src, isVisible, lazy, webp, quality, fallbackSrc]);\r\n\r\n return {\r\n src: optimizedSrc,\r\n isLoading,\r\n error,\r\n elementRef,\r\n isVisible,\r\n };\r\n}\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction isRemoteUrl(url: string) {\r\n return /^https?:\\/\\//.test(url);\r\n}\r\n\r\nfunction preloadImage(src: string) {\r\n return new Promise<void>((resolve, reject) => {\r\n const img = new Image();\r\n img.onload = () => resolve();\r\n img.onerror = () => reject(new Error(`Failed to load image: ${src}`));\r\n img.src = src;\r\n });\r\n}\r\n","import React from 'react';\r\nimport { useOptimizedImage } from '../hooks/useOptimizedImage';\r\n\r\ninterface OptimizedImageProps\r\n extends React.ImgHTMLAttributes<HTMLImageElement> {\r\n src: string;\r\n lazy?: boolean;\r\n webp?: boolean;\r\n quality?: number;\r\n placeholderSrc?: string;\r\n fallbackSrc?: string;\r\n showLoadingIndicator?: boolean;\r\n}\r\n\r\nexport const OptimizedImage: React.FC<OptimizedImageProps> = ({\r\n src,\r\n lazy = true,\r\n webp = true,\r\n quality = 80,\r\n placeholderSrc,\r\n fallbackSrc,\r\n showLoadingIndicator = true,\r\n className = '',\r\n alt,\r\n onLoad, \r\n onError,\r\n ...imgProps\r\n}) => {\r\n const {\r\n src: optimizedSrc,\r\n isLoading,\r\n error,\r\n elementRef,\r\n } = useOptimizedImage({\r\n src,\r\n lazy,\r\n webp,\r\n quality,\r\n fallbackSrc,\r\n // 👇 internal lifecycle hooks (NOT React events)\r\n onOptimizedLoad: () => {\r\n // optional: internal tracking / analytics\r\n },\r\n onOptimizedError: () => {\r\n // optional: internal error handling\r\n },\r\n });\r\n\r\n const resolvedAlt =\r\n alt ||\r\n src.split('/').pop()?.replace(/[-_]/g, ' ') ||\r\n 'image';\r\n\r\n /* ---------------- Loading / Placeholder ---------------- */\r\n\r\n if (isLoading && showLoadingIndicator && placeholderSrc) {\r\n return (\r\n <img\r\n ref={elementRef}\r\n src={placeholderSrc}\r\n alt={`Loading ${resolvedAlt}`}\r\n className={`${className} media-optimizer-loading`}\r\n aria-busy=\"true\"\r\n {...imgProps}\r\n />\r\n );\r\n }\r\n\r\n /* ---------------- Error fallback ---------------- */\r\n\r\n if (error && fallbackSrc) {\r\n return (\r\n <img\r\n ref={elementRef}\r\n src={fallbackSrc}\r\n alt={`Fallback for ${resolvedAlt}`}\r\n className={`${className} media-optimizer-error`}\r\n onLoad={onLoad}\r\n onError={onError}\r\n {...imgProps}\r\n />\r\n );\r\n }\r\n\r\n /* ---------------- Optimized image ---------------- */\r\n\r\n return (\r\n <img\r\n ref={elementRef}\r\n src={optimizedSrc}\r\n alt={resolvedAlt}\r\n className={`${className} media-optimizer-loaded`}\r\n loading={lazy ? undefined : 'eager'}\r\n decoding=\"async\"\r\n onLoad={onLoad} // React DOM event\r\n onError={onError} // React DOM event\r\n {...imgProps}\r\n />\r\n );\r\n};\r\n\r\n/* ---------------- Optional default styles ---------------- */\r\n\r\nexport const OptimizedImageStyles = `\r\n.media-optimizer-loading {\r\n opacity: 0.6;\r\n}\r\n\r\n.media-optimizer-loaded {\r\n animation: fadeIn 0.4s ease;\r\n}\r\n\r\n@keyframes fadeIn {\r\n from { opacity: 0.6; }\r\n to { opacity: 1; }\r\n}\r\n`;\r\n","// src/components/OptimizedVideo.tsx\r\nimport React from 'react';\r\nimport { useLazyLoad } from '../hooks/useLazyLoad';\r\n\r\ninterface OptimizedVideoProps\r\n extends React.VideoHTMLAttributes<HTMLVideoElement> {\r\n src: string;\r\n poster?: string;\r\n lazy?: boolean;\r\n webm?: boolean;\r\n mp4?: boolean;\r\n}\r\n\r\nexport const OptimizedVideo: React.FC<OptimizedVideoProps> = ({\r\n src,\r\n poster,\r\n lazy = true,\r\n webm = true,\r\n mp4 = true,\r\n className = '',\r\n autoPlay,\r\n muted,\r\n ...videoProps\r\n}) => {\r\n const { isVisible, elementRef } = useLazyLoad<HTMLVideoElement>({\r\n enabled: lazy,\r\n });\r\n\r\n const resolvedMuted = autoPlay ? true : muted;\r\n\r\n const sources = buildVideoSources(src, { webm, mp4 });\r\n\r\n // Placeholder (lazy)\r\n if (lazy && !isVisible) {\r\n return (\r\n <div\r\n className={`${className} media-optimizer-video-placeholder`}\r\n style={{\r\n background: poster\r\n ? `url(${poster}) center / cover no-repeat`\r\n : '#f0f0f0',\r\n width: videoProps.width || '100%',\r\n height: videoProps.height || '300px',\r\n borderRadius: '4px',\r\n }}\r\n aria-label=\"Video loading\"\r\n />\r\n );\r\n }\r\n\r\n return (\r\n <video\r\n ref={elementRef}\r\n className={`${className} media-optimizer-video`}\r\n poster={poster}\r\n preload=\"metadata\"\r\n playsInline\r\n autoPlay={autoPlay}\r\n muted={resolvedMuted}\r\n {...videoProps}\r\n >\r\n {sources.map((source) => (\r\n <source key={source.src} src={source.src} type={source.type} />\r\n ))}\r\n Your browser does not support the video tag.\r\n </video>\r\n );\r\n};\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction buildVideoSources(\r\n src: string,\r\n options: { webm: boolean; mp4: boolean }\r\n) {\r\n const sources: { src: string; type: string }[] = [];\r\n const cleanSrc = src.split('?')[0].toLowerCase();\r\n\r\n const isMp4 = cleanSrc.endsWith('.mp4');\r\n const isWebm = cleanSrc.endsWith('.webm');\r\n\r\n if (isMp4) {\r\n if (options.webm) {\r\n sources.push({\r\n src: src.replace(/\\.mp4(\\?.*)?$/, '.webm$1'),\r\n type: 'video/webm',\r\n });\r\n }\r\n\r\n if (options.mp4) {\r\n sources.push({\r\n src,\r\n type: 'video/mp4',\r\n });\r\n }\r\n } else if (isWebm) {\r\n sources.push({\r\n src,\r\n type: 'video/webm',\r\n });\r\n } else {\r\n // Unknown format fallback\r\n sources.push({\r\n src,\r\n type: 'video/mp4',\r\n });\r\n }\r\n\r\n return sources;\r\n}\r\n","// src/utils/compress.ts\r\n\r\ninterface CompressionOptions {\r\n quality?: number;\r\n maxWidth?: number;\r\n maxHeight?: number;\r\n mimeType?: 'image/jpeg' | 'image/png' | 'image/webp';\r\n}\r\n\r\n/**\r\n * Compress image using Canvas\r\n * Browser-only utility\r\n */\r\nexport async function compressImage(\r\n file: File,\r\n options: CompressionOptions = {}\r\n): Promise<File> {\r\n if (typeof window === 'undefined') {\r\n throw new Error('compressImage can only run in the browser');\r\n }\r\n\r\n const {\r\n quality = 0.8,\r\n maxWidth = 1920,\r\n maxHeight = 1080,\r\n mimeType = 'image/jpeg',\r\n } = options;\r\n\r\n const objectUrl = URL.createObjectURL(file);\r\n\r\n try {\r\n const img = await loadImage(objectUrl);\r\n\r\n let { width, height } = img;\r\n\r\n // Resize proportionally\r\n if (width > maxWidth) {\r\n height = (height * maxWidth) / width;\r\n width = maxWidth;\r\n }\r\n\r\n if (height > maxHeight) {\r\n width = (width * maxHeight) / height;\r\n height = maxHeight;\r\n }\r\n\r\n const canvas = document.createElement('canvas');\r\n canvas.width = width;\r\n canvas.height = height;\r\n\r\n const ctx = canvas.getContext('2d');\r\n if (!ctx) {\r\n throw new Error('Canvas context not available');\r\n }\r\n\r\n ctx.drawImage(img, 0, 0, width, height);\r\n\r\n const blob = await canvasToBlob(canvas, mimeType, quality);\r\n\r\n return new File([blob], replaceExtension(file.name, mimeType), {\r\n type: mimeType,\r\n lastModified: Date.now(),\r\n });\r\n } finally {\r\n URL.revokeObjectURL(objectUrl);\r\n }\r\n}\r\n\r\n/**\r\n * Calculate compression reduction\r\n */\r\nexport function calculateSizeReduction(\r\n originalSize: number,\r\n newSize: number\r\n): string {\r\n const reduction = ((originalSize - newSize) / originalSize) * 100;\r\n return `${reduction.toFixed(1)}% smaller`;\r\n}\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction loadImage(src: string): Promise<HTMLImageElement> {\r\n return new Promise((resolve, reject) => {\r\n const img = new Image();\r\n img.onload = () => resolve(img);\r\n img.onerror = () => reject(new Error('Failed to load image'));\r\n img.src = src;\r\n });\r\n}\r\n\r\nfunction canvasToBlob(\r\n canvas: HTMLCanvasElement,\r\n type: string,\r\n quality: number\r\n): Promise<Blob> {\r\n return new Promise((resolve, reject) => {\r\n canvas.toBlob((blob) => {\r\n if (blob) resolve(blob);\r\n else reject(new Error('Image compression failed'));\r\n }, type, quality);\r\n });\r\n}\r\n\r\nfunction replaceExtension(\r\n filename: string,\r\n mimeType: string\r\n): string {\r\n const ext =\r\n mimeType === 'image/png'\r\n ? 'png'\r\n : mimeType === 'image/webp'\r\n ? 'webp'\r\n : 'jpg';\r\n\r\n return filename.replace(/\\.\\w+$/, `.${ext}`);\r\n}\r\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import {useState,useRef,useEffect}from'react';import {jsx,jsxs}from'react/jsx-runtime';function z(e={}){let{threshold:t=.1,rootMargin:r="50px",enabled:o=true}=e,[a,i]=useState(!o),c=useRef(null);return useEffect(()=>{if(!o){i(true);return}if(typeof window>"u"||!("IntersectionObserver"in window)){i(true);return}let m=c.current;if(!m)return;let n=new IntersectionObserver(([s])=>{s.isIntersecting&&(i(true),n.disconnect());},{threshold:t,rootMargin:r});return n.observe(m),()=>{n.disconnect();}},[o,t,r]),{isVisible:a,elementRef:c}}var b=null;function I(){return b!==null?Promise.resolve(b):typeof window>"u"?(b=false,Promise.resolve(false)):new Promise(e=>{let t=new Image;t.onload=()=>{b=t.width>0&&t.height>0,e(b);},t.onerror=()=>{b=false,e(false);},t.src="data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA";})}function h(e){return M(e)?e.replace(/\.(jpe?g|png)(\?.*)?$/i,".webp$2"):e}function M(e){return /^https?:\/\//.test(e)}var O=null;function v(e){let{src:t,lazy:r=true,webp:o=true,quality:a=80,fallbackSrc:i,onOptimizedLoad:c,onOptimizedError:m}=e,[n,s]=useState(t),[d,l]=useState(true),[g,p]=useState(null),{isVisible:w,elementRef:y}=z({enabled:r}),u=useRef(false);return useEffect(()=>r&&!w?void 0:(u.current=false,(async()=>{l(true),p(null);try{O===null&&(O=await I());let f=t;if(o&&O&&R(t)&&(f=h(t)),R(f)&&typeof a=="number"){let A=f.includes("?")?"&":"?";f+=`${A}q=${a}`;}await V(f),u.current||(s(f),l(!1),c?.());}catch(f){u.current||(i?(s(i),l(false)):(p(f),l(false),m?.()));}})(),()=>{u.current=true;}),[t,w,r,o,a,i]),{src:n,isLoading:d,error:g,elementRef:y,isVisible:w}}function R(e){return /^https?:\/\//.test(e)}function V(e){return new Promise((t,r)=>{let o=new Image;o.onload=()=>t(),o.onerror=()=>r(new Error(`Failed to load image: ${e}`)),o.src=e;})}var U=({src:e,lazy:t=true,webp:r=true,quality:o=80,placeholderSrc:a,fallbackSrc:i,showLoadingIndicator:c=true,className:m="",alt:n,onLoad:s,onError:d,...l})=>{let{src:g,isLoading:p,error:w,elementRef:y}=v({src:e,lazy:t,webp:r,quality:o,fallbackSrc:i,onOptimizedLoad:()=>{},onOptimizedError:()=>{}}),u=n||e.split("/").pop()?.replace(/[-_]/g," ")||"image";return p&&c&&a?jsx("img",{ref:y,src:a,alt:`Loading ${u}`,className:`${m} media-optimizer-loading`,"aria-busy":"true",...l}):w&&i?jsx("img",{ref:y,src:i,alt:`Fallback for ${u}`,className:`${m} media-optimizer-error`,onLoad:s,onError:d,...l}):jsx("img",{ref:y,src:g,alt:u,className:`${m} media-optimizer-loaded`,loading:t?void 0:"eager",decoding:"async",onLoad:s,onError:d,...l})};var H=({src:e,poster:t,lazy:r=true,webm:o=true,mp4:a=true,className:i="",autoPlay:c,muted:m,...n})=>{let{isVisible:s,elementRef:d}=z({enabled:r}),l=c?true:m,g=C(e,{webm:o,mp4:a});return r&&!s?jsx("div",{className:`${i} media-optimizer-video-placeholder`,style:{background:t?`url(${t}) center / cover no-repeat`:"#f0f0f0",width:n.width||"100%",height:n.height||"300px",borderRadius:"4px"},"aria-label":"Video loading"}):jsxs("video",{ref:d,className:`${i} media-optimizer-video`,poster:t,preload:"metadata",playsInline:true,autoPlay:c,muted:l,...n,children:[g.map(p=>jsx("source",{src:p.src,type:p.type},p.src)),"Your browser does not support the video tag."]})};function C(e,t){let r=[],o=e.split("?")[0].toLowerCase(),a=o.endsWith(".mp4"),i=o.endsWith(".webm");return a?(t.webm&&r.push({src:e.replace(/\.mp4(\?.*)?$/,".webm$1"),type:"video/webm"}),t.mp4&&r.push({src:e,type:"video/mp4"})):i?r.push({src:e,type:"video/webm"}):r.push({src:e,type:"video/mp4"}),r}async function k(e,t={}){if(typeof window>"u")throw new Error("compressImage can only run in the browser");let{quality:r=.8,maxWidth:o=1920,maxHeight:a=1080,mimeType:i="image/jpeg"}=t,c=URL.createObjectURL(e);try{let m=await B(c),{width:n,height:s}=m;n>o&&(s=s*o/n,n=o),s>a&&(n=n*a/s,s=a);let d=document.createElement("canvas");d.width=n,d.height=s;let l=d.getContext("2d");if(!l)throw new Error("Canvas context not available");l.drawImage(m,0,0,n,s);let g=await q(d,i,r);return new File([g],D(e.name,i),{type:i,lastModified:Date.now()})}finally{URL.revokeObjectURL(c);}}function j(e,t){return `${((e-t)/e*100).toFixed(1)}% smaller`}function B(e){return new Promise((t,r)=>{let o=new Image;o.onload=()=>t(o),o.onerror=()=>r(new Error("Failed to load image")),o.src=e;})}function q(e,t,r){return new Promise((o,a)=>{e.toBlob(i=>{i?o(i):a(new Error("Image compression failed"));},t,r);})}function D(e,t){let r=t==="image/png"?"png":t==="image/webp"?"webp":"jpg";return e.replace(/\.\w+$/,`.${r}`)}
|
|
2
|
+
export{U as OptimizedImage,H as OptimizedVideo,j as calculateSizeReduction,k as compressImage,h as convertToWebP,I as supportsWebP,z as useLazyLoad,v as useOptimizedImage};//# sourceMappingURL=index.mjs.map
|
|
3
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useLazyLoad.ts","../src/utils/webpConverter.ts","../src/hooks/useOptimizedImage.ts","../src/components/OptimizedImage.tsx","../src/components/OptimizedVideo.tsx","../src/utils/compress.ts"],"names":["useLazyLoad","options","threshold","rootMargin","enabled","isVisible","setIsVisible","useState","elementRef","useRef","useEffect","node","observer","entry","cachedWebPSupport","supportsWebP","resolve","img","convertToWebP","src","isRemoteUrl","url","useOptimizedImage","lazy","webp","quality","fallbackSrc","onOptimizedLoad","onOptimizedError","optimizedSrc","setOptimizedSrc","isLoading","setIsLoading","error","setError","isCancelled","finalSrc","separator","preloadImage","err","reject","OptimizedImage","placeholderSrc","showLoadingIndicator","className","alt","onLoad","onError","imgProps","resolvedAlt","jsx","OptimizedVideo","poster","webm","mp4","autoPlay","muted","videoProps","resolvedMuted","sources","buildVideoSources","jsxs","source","cleanSrc","isMp4","isWebm","compressImage","file","maxWidth","maxHeight","mimeType","objectUrl","loadImage","width","height","canvas","ctx","blob","canvasToBlob","replaceExtension","calculateSizeReduction","originalSize","newSize","type","filename","ext"],"mappings":"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","file":"index.mjs","sourcesContent":["// src/hooks/useLazyLoad.ts\r\nimport { useState, useEffect, useRef, RefObject } from 'react';\r\n\r\ninterface UseLazyLoadOptions {\r\n threshold?: number;\r\n rootMargin?: string;\r\n enabled?: boolean;\r\n}\r\n\r\n/**\r\n * useLazyLoad\r\n * - Observes element visibility using IntersectionObserver\r\n * - Safe for SSR\r\n * - Stable ref ownership\r\n */\r\nexport function useLazyLoad<T extends HTMLElement = HTMLElement>(\r\n options: UseLazyLoadOptions = {}\r\n) {\r\n const {\r\n threshold = 0.1,\r\n rootMargin = '50px',\r\n enabled = true,\r\n } = options;\r\n\r\n const [isVisible, setIsVisible] = useState(!enabled);\r\n const elementRef = useRef<T | null>(null);\r\n\r\n useEffect(() => {\r\n if (!enabled) {\r\n setIsVisible(true);\r\n return;\r\n }\r\n\r\n // SSR / legacy browser safety\r\n if (typeof window === 'undefined' || !('IntersectionObserver' in window)) {\r\n setIsVisible(true);\r\n return;\r\n }\r\n\r\n const node = elementRef.current;\r\n if (!node) return;\r\n\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting) {\r\n setIsVisible(true);\r\n observer.disconnect();\r\n }\r\n },\r\n { threshold, rootMargin }\r\n );\r\n\r\n observer.observe(node);\r\n\r\n return () => {\r\n observer.disconnect();\r\n };\r\n }, [enabled, threshold, rootMargin]);\r\n\r\n return {\r\n isVisible,\r\n elementRef: elementRef as RefObject<T>,\r\n };\r\n}\r\n","// src/utils/webpConverter.ts\r\n\r\nlet cachedWebPSupport: boolean | null = null;\r\n\r\n/**\r\n * Detects WebP support (cached)\r\n * Safe for SSR\r\n */\r\nexport function supportsWebP(): Promise<boolean> {\r\n if (cachedWebPSupport !== null) {\r\n return Promise.resolve(cachedWebPSupport);\r\n }\r\n\r\n if (typeof window === 'undefined') {\r\n cachedWebPSupport = false;\r\n return Promise.resolve(false);\r\n }\r\n\r\n return new Promise((resolve) => {\r\n const img = new Image();\r\n img.onload = () => {\r\n cachedWebPSupport = img.width > 0 && img.height > 0;\r\n resolve(cachedWebPSupport);\r\n };\r\n img.onerror = () => {\r\n cachedWebPSupport = false;\r\n resolve(false);\r\n };\r\n img.src =\r\n 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';\r\n });\r\n}\r\n\r\n/**\r\n * Converts image URL to .webp by rewriting extension\r\n * NOTE: Only works if server/CDN supports WebP\r\n */\r\nexport function convertToWebP(src: string): string {\r\n if (!isRemoteUrl(src)) return src;\r\n\r\n return src.replace(/\\.(jpe?g|png)(\\?.*)?$/i, '.webp$2');\r\n}\r\n\r\n/**\r\n * Returns optimal image format (simple heuristic)\r\n */\r\nexport function getOptimalFormat(): 'webp' | 'jpeg' | 'png' {\r\n return 'webp';\r\n}\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction isRemoteUrl(url: string) {\r\n return /^https?:\\/\\//.test(url);\r\n}\r\n","import { useState, useEffect, useRef } from 'react';\r\nimport { useLazyLoad } from './useLazyLoad';\r\nimport { supportsWebP, convertToWebP } from '../utils/webpConverter';\r\n\r\n/**\r\n * Internal hook options\r\n * NOTE:\r\n * - This hook does NOT accept React event handlers\r\n * - It exposes simple lifecycle callbacks instead\r\n */\r\ninterface UseOptimizedImageOptions {\r\n src: string;\r\n lazy?: boolean;\r\n webp?: boolean;\r\n quality?: number;\r\n fallbackSrc?: string;\r\n onOptimizedLoad?: () => void;\r\n onOptimizedError?: () => void;\r\n}\r\n\r\nlet cachedWebPSupport: boolean | null = null;\r\n\r\nexport function useOptimizedImage(options: UseOptimizedImageOptions) {\r\n const {\r\n src,\r\n lazy = true,\r\n webp = true,\r\n quality = 80,\r\n fallbackSrc,\r\n onOptimizedLoad,\r\n onOptimizedError,\r\n } = options;\r\n\r\n const [optimizedSrc, setOptimizedSrc] = useState(src);\r\n const [isLoading, setIsLoading] = useState(true);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const { isVisible, elementRef } =\r\n useLazyLoad<HTMLImageElement>({ enabled: lazy });\r\n\r\n const isCancelled = useRef(false);\r\n\r\n useEffect(() => {\r\n if (lazy && !isVisible) return;\r\n\r\n isCancelled.current = false;\r\n\r\n const optimizeImage = async () => {\r\n setIsLoading(true);\r\n setError(null);\r\n\r\n try {\r\n // Cache WebP support (run once per app)\r\n if (cachedWebPSupport === null) {\r\n cachedWebPSupport = await supportsWebP();\r\n }\r\n\r\n let finalSrc = src;\r\n\r\n // Convert to WebP if supported and remote\r\n if (webp && cachedWebPSupport && isRemoteUrl(src)) {\r\n finalSrc = convertToWebP(src);\r\n }\r\n\r\n // Append quality only for remote URLs\r\n if (isRemoteUrl(finalSrc) && typeof quality === 'number') {\r\n const separator = finalSrc.includes('?') ? '&' : '?';\r\n finalSrc += `${separator}q=${quality}`;\r\n }\r\n\r\n // Preload image\r\n await preloadImage(finalSrc);\r\n\r\n if (!isCancelled.current) {\r\n setOptimizedSrc(finalSrc);\r\n setIsLoading(false);\r\n onOptimizedLoad?.();\r\n }\r\n } catch (err) {\r\n if (!isCancelled.current) {\r\n if (fallbackSrc) {\r\n setOptimizedSrc(fallbackSrc);\r\n setIsLoading(false);\r\n } else {\r\n setError(err as Error);\r\n setIsLoading(false);\r\n onOptimizedError?.();\r\n }\r\n }\r\n }\r\n };\r\n\r\n optimizeImage();\r\n\r\n return () => {\r\n isCancelled.current = true;\r\n };\r\n }, [src, isVisible, lazy, webp, quality, fallbackSrc]);\r\n\r\n return {\r\n src: optimizedSrc,\r\n isLoading,\r\n error,\r\n elementRef,\r\n isVisible,\r\n };\r\n}\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction isRemoteUrl(url: string) {\r\n return /^https?:\\/\\//.test(url);\r\n}\r\n\r\nfunction preloadImage(src: string) {\r\n return new Promise<void>((resolve, reject) => {\r\n const img = new Image();\r\n img.onload = () => resolve();\r\n img.onerror = () => reject(new Error(`Failed to load image: ${src}`));\r\n img.src = src;\r\n });\r\n}\r\n","import React from 'react';\r\nimport { useOptimizedImage } from '../hooks/useOptimizedImage';\r\n\r\ninterface OptimizedImageProps\r\n extends React.ImgHTMLAttributes<HTMLImageElement> {\r\n src: string;\r\n lazy?: boolean;\r\n webp?: boolean;\r\n quality?: number;\r\n placeholderSrc?: string;\r\n fallbackSrc?: string;\r\n showLoadingIndicator?: boolean;\r\n}\r\n\r\nexport const OptimizedImage: React.FC<OptimizedImageProps> = ({\r\n src,\r\n lazy = true,\r\n webp = true,\r\n quality = 80,\r\n placeholderSrc,\r\n fallbackSrc,\r\n showLoadingIndicator = true,\r\n className = '',\r\n alt,\r\n onLoad, \r\n onError,\r\n ...imgProps\r\n}) => {\r\n const {\r\n src: optimizedSrc,\r\n isLoading,\r\n error,\r\n elementRef,\r\n } = useOptimizedImage({\r\n src,\r\n lazy,\r\n webp,\r\n quality,\r\n fallbackSrc,\r\n // 👇 internal lifecycle hooks (NOT React events)\r\n onOptimizedLoad: () => {\r\n // optional: internal tracking / analytics\r\n },\r\n onOptimizedError: () => {\r\n // optional: internal error handling\r\n },\r\n });\r\n\r\n const resolvedAlt =\r\n alt ||\r\n src.split('/').pop()?.replace(/[-_]/g, ' ') ||\r\n 'image';\r\n\r\n /* ---------------- Loading / Placeholder ---------------- */\r\n\r\n if (isLoading && showLoadingIndicator && placeholderSrc) {\r\n return (\r\n <img\r\n ref={elementRef}\r\n src={placeholderSrc}\r\n alt={`Loading ${resolvedAlt}`}\r\n className={`${className} media-optimizer-loading`}\r\n aria-busy=\"true\"\r\n {...imgProps}\r\n />\r\n );\r\n }\r\n\r\n /* ---------------- Error fallback ---------------- */\r\n\r\n if (error && fallbackSrc) {\r\n return (\r\n <img\r\n ref={elementRef}\r\n src={fallbackSrc}\r\n alt={`Fallback for ${resolvedAlt}`}\r\n className={`${className} media-optimizer-error`}\r\n onLoad={onLoad}\r\n onError={onError}\r\n {...imgProps}\r\n />\r\n );\r\n }\r\n\r\n /* ---------------- Optimized image ---------------- */\r\n\r\n return (\r\n <img\r\n ref={elementRef}\r\n src={optimizedSrc}\r\n alt={resolvedAlt}\r\n className={`${className} media-optimizer-loaded`}\r\n loading={lazy ? undefined : 'eager'}\r\n decoding=\"async\"\r\n onLoad={onLoad} // React DOM event\r\n onError={onError} // React DOM event\r\n {...imgProps}\r\n />\r\n );\r\n};\r\n\r\n/* ---------------- Optional default styles ---------------- */\r\n\r\nexport const OptimizedImageStyles = `\r\n.media-optimizer-loading {\r\n opacity: 0.6;\r\n}\r\n\r\n.media-optimizer-loaded {\r\n animation: fadeIn 0.4s ease;\r\n}\r\n\r\n@keyframes fadeIn {\r\n from { opacity: 0.6; }\r\n to { opacity: 1; }\r\n}\r\n`;\r\n","// src/components/OptimizedVideo.tsx\r\nimport React from 'react';\r\nimport { useLazyLoad } from '../hooks/useLazyLoad';\r\n\r\ninterface OptimizedVideoProps\r\n extends React.VideoHTMLAttributes<HTMLVideoElement> {\r\n src: string;\r\n poster?: string;\r\n lazy?: boolean;\r\n webm?: boolean;\r\n mp4?: boolean;\r\n}\r\n\r\nexport const OptimizedVideo: React.FC<OptimizedVideoProps> = ({\r\n src,\r\n poster,\r\n lazy = true,\r\n webm = true,\r\n mp4 = true,\r\n className = '',\r\n autoPlay,\r\n muted,\r\n ...videoProps\r\n}) => {\r\n const { isVisible, elementRef } = useLazyLoad<HTMLVideoElement>({\r\n enabled: lazy,\r\n });\r\n\r\n const resolvedMuted = autoPlay ? true : muted;\r\n\r\n const sources = buildVideoSources(src, { webm, mp4 });\r\n\r\n // Placeholder (lazy)\r\n if (lazy && !isVisible) {\r\n return (\r\n <div\r\n className={`${className} media-optimizer-video-placeholder`}\r\n style={{\r\n background: poster\r\n ? `url(${poster}) center / cover no-repeat`\r\n : '#f0f0f0',\r\n width: videoProps.width || '100%',\r\n height: videoProps.height || '300px',\r\n borderRadius: '4px',\r\n }}\r\n aria-label=\"Video loading\"\r\n />\r\n );\r\n }\r\n\r\n return (\r\n <video\r\n ref={elementRef}\r\n className={`${className} media-optimizer-video`}\r\n poster={poster}\r\n preload=\"metadata\"\r\n playsInline\r\n autoPlay={autoPlay}\r\n muted={resolvedMuted}\r\n {...videoProps}\r\n >\r\n {sources.map((source) => (\r\n <source key={source.src} src={source.src} type={source.type} />\r\n ))}\r\n Your browser does not support the video tag.\r\n </video>\r\n );\r\n};\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction buildVideoSources(\r\n src: string,\r\n options: { webm: boolean; mp4: boolean }\r\n) {\r\n const sources: { src: string; type: string }[] = [];\r\n const cleanSrc = src.split('?')[0].toLowerCase();\r\n\r\n const isMp4 = cleanSrc.endsWith('.mp4');\r\n const isWebm = cleanSrc.endsWith('.webm');\r\n\r\n if (isMp4) {\r\n if (options.webm) {\r\n sources.push({\r\n src: src.replace(/\\.mp4(\\?.*)?$/, '.webm$1'),\r\n type: 'video/webm',\r\n });\r\n }\r\n\r\n if (options.mp4) {\r\n sources.push({\r\n src,\r\n type: 'video/mp4',\r\n });\r\n }\r\n } else if (isWebm) {\r\n sources.push({\r\n src,\r\n type: 'video/webm',\r\n });\r\n } else {\r\n // Unknown format fallback\r\n sources.push({\r\n src,\r\n type: 'video/mp4',\r\n });\r\n }\r\n\r\n return sources;\r\n}\r\n","// src/utils/compress.ts\r\n\r\ninterface CompressionOptions {\r\n quality?: number;\r\n maxWidth?: number;\r\n maxHeight?: number;\r\n mimeType?: 'image/jpeg' | 'image/png' | 'image/webp';\r\n}\r\n\r\n/**\r\n * Compress image using Canvas\r\n * Browser-only utility\r\n */\r\nexport async function compressImage(\r\n file: File,\r\n options: CompressionOptions = {}\r\n): Promise<File> {\r\n if (typeof window === 'undefined') {\r\n throw new Error('compressImage can only run in the browser');\r\n }\r\n\r\n const {\r\n quality = 0.8,\r\n maxWidth = 1920,\r\n maxHeight = 1080,\r\n mimeType = 'image/jpeg',\r\n } = options;\r\n\r\n const objectUrl = URL.createObjectURL(file);\r\n\r\n try {\r\n const img = await loadImage(objectUrl);\r\n\r\n let { width, height } = img;\r\n\r\n // Resize proportionally\r\n if (width > maxWidth) {\r\n height = (height * maxWidth) / width;\r\n width = maxWidth;\r\n }\r\n\r\n if (height > maxHeight) {\r\n width = (width * maxHeight) / height;\r\n height = maxHeight;\r\n }\r\n\r\n const canvas = document.createElement('canvas');\r\n canvas.width = width;\r\n canvas.height = height;\r\n\r\n const ctx = canvas.getContext('2d');\r\n if (!ctx) {\r\n throw new Error('Canvas context not available');\r\n }\r\n\r\n ctx.drawImage(img, 0, 0, width, height);\r\n\r\n const blob = await canvasToBlob(canvas, mimeType, quality);\r\n\r\n return new File([blob], replaceExtension(file.name, mimeType), {\r\n type: mimeType,\r\n lastModified: Date.now(),\r\n });\r\n } finally {\r\n URL.revokeObjectURL(objectUrl);\r\n }\r\n}\r\n\r\n/**\r\n * Calculate compression reduction\r\n */\r\nexport function calculateSizeReduction(\r\n originalSize: number,\r\n newSize: number\r\n): string {\r\n const reduction = ((originalSize - newSize) / originalSize) * 100;\r\n return `${reduction.toFixed(1)}% smaller`;\r\n}\r\n\r\n/* ---------------- helpers ---------------- */\r\n\r\nfunction loadImage(src: string): Promise<HTMLImageElement> {\r\n return new Promise((resolve, reject) => {\r\n const img = new Image();\r\n img.onload = () => resolve(img);\r\n img.onerror = () => reject(new Error('Failed to load image'));\r\n img.src = src;\r\n });\r\n}\r\n\r\nfunction canvasToBlob(\r\n canvas: HTMLCanvasElement,\r\n type: string,\r\n quality: number\r\n): Promise<Blob> {\r\n return new Promise((resolve, reject) => {\r\n canvas.toBlob((blob) => {\r\n if (blob) resolve(blob);\r\n else reject(new Error('Image compression failed'));\r\n }, type, quality);\r\n });\r\n}\r\n\r\nfunction replaceExtension(\r\n filename: string,\r\n mimeType: string\r\n): string {\r\n const ext =\r\n mimeType === 'image/png'\r\n ? 'png'\r\n : mimeType === 'image/webp'\r\n ? 'webp'\r\n : 'jpg';\r\n\r\n return filename.replace(/\\.\\w+$/, `.${ext}`);\r\n}\r\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-media-optimizer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drop-in React component for auto-optimized images & media with lazy loading, WebP conversion, and performance optimization",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.esm.js",
|
|
16
|
+
"require": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --external react,react-dom",
|
|
24
|
+
"type-check": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"test": "vitest",
|
|
27
|
+
"lint": "eslint src --ext ts,tsx"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"react",
|
|
31
|
+
"image",
|
|
32
|
+
"optimization",
|
|
33
|
+
"performance",
|
|
34
|
+
"lazy-load",
|
|
35
|
+
"webp",
|
|
36
|
+
"media",
|
|
37
|
+
"video",
|
|
38
|
+
"compression",
|
|
39
|
+
"seo",
|
|
40
|
+
"web-vitals",
|
|
41
|
+
"nextjs",
|
|
42
|
+
"gatsby"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": ">=16.8.0",
|
|
46
|
+
"react-dom": ">=16.8.0"
|
|
47
|
+
},
|
|
48
|
+
"author": "yared abebe",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/yaredabebe/react-media-optimizer.git"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/yaredabebe/react-media-optimizer/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/yaredabebe/react-media-optimizer#readme",
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"react": ">=16.8.0",
|
|
60
|
+
"react-dom": ">=16.8.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^25.0.9",
|
|
64
|
+
"@types/react": "^19.2.8",
|
|
65
|
+
"@types/react-dom": "^19.2.3",
|
|
66
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
67
|
+
"tsup": "^8.5.1",
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"vite": "^7.3.1"
|
|
70
|
+
}
|
|
71
|
+
}
|