@herowcode/utils 1.0.2 → 1.1.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 +236 -81
- package/dist/files/compress-image.d.ts +10 -0
- package/dist/files/compress-image.d.ts.map +1 -0
- package/dist/files/compress-image.esm.js +57 -0
- package/dist/files/compress-image.js +57 -0
- package/dist/files/compress-image.js.map +1 -0
- package/dist/files/download-url.d.ts +2 -0
- package/dist/files/download-url.d.ts.map +1 -0
- package/dist/files/download-url.esm.js +24 -0
- package/dist/files/download-url.js +24 -0
- package/dist/files/download-url.js.map +1 -0
- package/dist/files/format-bytes.d.ts +2 -0
- package/dist/files/format-bytes.d.ts.map +1 -0
- package/dist/files/format-bytes.esm.js +13 -0
- package/dist/files/format-bytes.js +13 -0
- package/dist/files/format-bytes.js.map +1 -0
- package/dist/files/index.d.ts +4 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.esm.js +3 -0
- package/dist/files/index.js +3 -0
- package/dist/files/index.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +6 -10
- package/dist/index.js +6 -10
- package/dist/index.js.map +1 -1
- package/dist/string/format-hms-to-seconds.d.ts +2 -0
- package/dist/string/format-hms-to-seconds.d.ts.map +1 -0
- package/dist/string/format-hms-to-seconds.esm.js +25 -0
- package/dist/string/format-hms-to-seconds.js +25 -0
- package/dist/string/format-hms-to-seconds.js.map +1 -0
- package/dist/string/format-seconds-to-fragment.d.ts +3 -0
- package/dist/string/format-seconds-to-fragment.d.ts.map +1 -0
- package/dist/string/format-seconds-to-fragment.esm.js +15 -0
- package/dist/string/format-seconds-to-fragment.js +15 -0
- package/dist/string/format-seconds-to-fragment.js.map +1 -0
- package/dist/string/format-seconds-to-hms.d.ts +2 -0
- package/dist/string/format-seconds-to-hms.d.ts.map +1 -0
- package/dist/string/format-seconds-to-hms.esm.js +13 -0
- package/dist/string/format-seconds-to-hms.js +13 -0
- package/dist/string/format-seconds-to-hms.js.map +1 -0
- package/dist/string/format-string-to-time.d.ts +2 -0
- package/dist/string/format-string-to-time.d.ts.map +1 -0
- package/dist/string/format-string-to-time.esm.js +10 -0
- package/dist/string/format-string-to-time.js +10 -0
- package/dist/string/format-string-to-time.js.map +1 -0
- package/dist/string/index.d.ts +4 -0
- package/dist/string/index.d.ts.map +1 -1
- package/dist/string/index.esm.js +4 -0
- package/dist/string/index.js +4 -0
- package/dist/string/index.js.map +1 -1
- package/dist/youtube/extract-youtube-video-id.d.ts +2 -0
- package/dist/youtube/extract-youtube-video-id.d.ts.map +1 -0
- package/dist/youtube/extract-youtube-video-id.esm.js +26 -0
- package/dist/youtube/extract-youtube-video-id.js +26 -0
- package/dist/youtube/extract-youtube-video-id.js.map +1 -0
- package/dist/youtube/generate-youtube-url.d.ts +20 -0
- package/dist/youtube/generate-youtube-url.d.ts.map +1 -0
- package/dist/youtube/generate-youtube-url.esm.js +81 -0
- package/dist/youtube/generate-youtube-url.js +81 -0
- package/dist/youtube/generate-youtube-url.js.map +1 -0
- package/dist/youtube/index.d.ts +5 -0
- package/dist/youtube/index.d.ts.map +1 -0
- package/dist/youtube/index.esm.js +4 -0
- package/dist/youtube/index.js +4 -0
- package/dist/youtube/index.js.map +1 -0
- package/dist/youtube/use-get-video-duration.d.ts +7 -0
- package/dist/youtube/use-get-video-duration.d.ts.map +1 -0
- package/dist/youtube/use-get-video-duration.esm.js +150 -0
- package/dist/youtube/use-get-video-duration.js +150 -0
- package/dist/youtube/use-get-video-duration.js.map +1 -0
- package/dist/youtube/validate-youtube-link.d.ts +2 -0
- package/dist/youtube/validate-youtube-link.d.ts.map +1 -0
- package/dist/youtube/validate-youtube-link.esm.js +40 -0
- package/dist/youtube/validate-youtube-link.js +40 -0
- package/dist/youtube/validate-youtube-link.js.map +1 -0
- package/package.json +33 -17
- package/dist/number.d.ts +0 -8
- package/dist/number.d.ts.map +0 -1
- package/dist/number.esm.js +0 -15
- package/dist/number.js +0 -15
- package/dist/number.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @herowcode/utils
|
|
2
2
|
|
|
3
|
-
A lightweight collection of utility functions for everyday JavaScript/TypeScript development. Built with dayjs for powerful date manipulation.
|
|
3
|
+
A lightweight collection of utility functions for everyday JavaScript/TypeScript development. Built with dayjs for powerful date manipulation and React hooks for YouTube integration.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ A lightweight collection of utility functions for everyday JavaScript/TypeScript
|
|
|
10
10
|
- 📱 **Universal** - Works in Node.js and browsers
|
|
11
11
|
- 🎯 **Tree-shakable** - Only import what you need
|
|
12
12
|
- 📂 **Scoped exports** - Import from specific modules
|
|
13
|
+
- 🎥 **YouTube utilities** - Extract video IDs, generate URLs, and get video durations
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
@@ -23,7 +24,7 @@ yarn add @herowcode/utils
|
|
|
23
24
|
|
|
24
25
|
### Import everything:
|
|
25
26
|
```typescript
|
|
26
|
-
import { formatDate, capitalize, debounce } from '@herowcode/utils';
|
|
27
|
+
import { formatDate, capitalize, debounce, extractYouTubeId } from '@herowcode/utils';
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
### Import by scope:
|
|
@@ -32,6 +33,7 @@ import { formatDate, addDays } from '@herowcode/utils/date';
|
|
|
32
33
|
import { capitalize, camelCase } from '@herowcode/utils/string';
|
|
33
34
|
import { randomInt } from '@herowcode/utils/number';
|
|
34
35
|
import { debounce, throttle } from '@herowcode/utils/function';
|
|
36
|
+
import { extractYouTubeId, generateYoutubeURL } from '@herowcode/utils/youtube';
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
### Examples:
|
|
@@ -47,137 +49,297 @@ console.log(kebabCase('helloWorld')); // "hello-world"
|
|
|
47
49
|
|
|
48
50
|
// Function utilities
|
|
49
51
|
const debouncedFn = debounce(() => console.log('Called!'), 300);
|
|
52
|
+
|
|
53
|
+
// YouTube utilities
|
|
54
|
+
const videoId = extractYouTubeId('https://youtu.be/dQw4w9WgXcQ'); // "dQw4w9WgXcQ"
|
|
55
|
+
const embedUrl = generateYoutubeURL({
|
|
56
|
+
videoURL: 'https://youtu.be/abc123',
|
|
57
|
+
embed: true,
|
|
58
|
+
autoplay: true
|
|
59
|
+
}); // "https://www.youtube.com/embed/abc123?autoplay=1"
|
|
50
60
|
```
|
|
51
61
|
|
|
52
62
|
## API Reference
|
|
53
63
|
|
|
64
|
+
### Array Utilities
|
|
65
|
+
|
|
66
|
+
#### `shuffle<T>(array: T[]): T[]`
|
|
67
|
+
Returns a new array with the elements shuffled in random order.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
shuffle([1, 2, 3, 4]); // e.g., [3, 1, 4, 2]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### `unique<T>(array: T[]): T[]`
|
|
74
|
+
Removes duplicate values from an array, preserving the first occurrence.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
unique([1, 2, 2, 3, 1]); // [1, 2, 3]
|
|
78
|
+
```
|
|
79
|
+
|
|
54
80
|
### Date Utilities
|
|
55
81
|
|
|
56
|
-
#### `formatDate(date: Date,
|
|
82
|
+
#### `formatDate(date: Date | string | number, locale?: string, opts?: Intl.DateTimeFormatOptions): string`
|
|
83
|
+
Formats a date using the specified locale and options.
|
|
57
84
|
|
|
58
|
-
|
|
85
|
+
```typescript
|
|
86
|
+
formatDate(new Date('2023-12-25'), 'en-US'); // "December 25, 2023"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `fixTimezoneOffset(utcDate: Date | string): Dayjs`
|
|
90
|
+
Adjusts a UTC date string or Date object for the local timezone offset.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
fixTimezoneOffset('2025-09-08T12:00:00Z');
|
|
94
|
+
```
|
|
59
95
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- `YY` - 2-digit year
|
|
63
|
-
- `MM` - 2-digit month (01-12)
|
|
64
|
-
- `DD` - 2-digit day (01-31)
|
|
65
|
-
- `HH` - 2-digit hours (00-23)
|
|
66
|
-
- `mm` - 2-digit minutes (00-59)
|
|
67
|
-
- `ss` - 2-digit seconds (00-59)
|
|
96
|
+
#### `getCurrentDateInUTC(): Dayjs`
|
|
97
|
+
Returns the current date/time as a Dayjs object in UTC.
|
|
68
98
|
|
|
69
99
|
```typescript
|
|
70
|
-
|
|
71
|
-
formatDate(new Date('2023-12-25'), 'DD/MM/YYYY'); // "25/12/2023"
|
|
72
|
-
formatDate(new Date('2023-12-25T10:30:45'), 'DD/MM/YYYY HH:mm:ss'); // "25/12/2023 10:30:45"
|
|
100
|
+
getCurrentDateInUTC();
|
|
73
101
|
```
|
|
74
102
|
|
|
75
|
-
#### `
|
|
103
|
+
#### `getDateInUTC(date: Date): Dayjs`
|
|
104
|
+
Converts a Date to a Dayjs object in UTC.
|
|
76
105
|
|
|
77
|
-
|
|
106
|
+
```typescript
|
|
107
|
+
getDateInUTC(new Date());
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### `parseTimeSpent(initialDate: string | Date, finalDate: string | Date, locale?: string): string`
|
|
111
|
+
Returns a human-readable string describing the time difference between two dates, localized.
|
|
78
112
|
|
|
79
113
|
```typescript
|
|
80
|
-
|
|
81
|
-
addDays(date, 5); // 2023-12-30
|
|
82
|
-
addDays(date, -5); // 2023-12-20
|
|
114
|
+
parseTimeSpent('2020-01-01', '2022-04-16', 'en-US'); // "2 years, 3 months, and 15 days"
|
|
83
115
|
```
|
|
84
116
|
|
|
85
|
-
|
|
117
|
+
### Files Utilities
|
|
86
118
|
|
|
87
|
-
|
|
119
|
+
#### `compressImage({ file, maxWidth, maxHeight, quality, allowedFileTypes }): Promise<File>`
|
|
120
|
+
Compresses an image file to WebP format, optionally resizing and restricting file types.
|
|
88
121
|
|
|
89
122
|
```typescript
|
|
90
|
-
|
|
91
|
-
const date2 = new Date('2023-12-25');
|
|
92
|
-
diffInDays(date1, date2); // 5
|
|
93
|
-
diffInDays(date2, date1); // -5
|
|
123
|
+
await compressImage({ file, maxWidth: 800, maxHeight: 600, quality: 0.8 });
|
|
94
124
|
```
|
|
95
125
|
|
|
96
|
-
#### `
|
|
126
|
+
#### `downloadUrl(url: string): Promise<boolean>`
|
|
127
|
+
Downloads a file from a URL in the browser, returning true if successful.
|
|
97
128
|
|
|
98
|
-
|
|
129
|
+
```typescript
|
|
130
|
+
await downloadUrl('https://example.com/file.pdf');
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `formatBytes(bytes: number): string`
|
|
134
|
+
Formats a byte count as a human-readable string (e.g., "1.23 MB").
|
|
99
135
|
|
|
100
136
|
```typescript
|
|
101
|
-
|
|
102
|
-
const date2 = new Date('2023-12-25');
|
|
103
|
-
isBefore(date1, date2); // true
|
|
104
|
-
isBefore(date2, date1); // false
|
|
137
|
+
formatBytes(1234567); // "1.18 MB"
|
|
105
138
|
```
|
|
106
139
|
|
|
107
|
-
|
|
140
|
+
### Function Utilities
|
|
141
|
+
|
|
142
|
+
#### `debounce<T>(fn: T, delay: number): (...args: Parameters<T>) => void`
|
|
143
|
+
Creates a debounced function that delays invoking `fn` until after `delay` ms have elapsed since the last call.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const debounced = debounce(() => { /* ... */ }, 300);
|
|
147
|
+
```
|
|
108
148
|
|
|
109
|
-
|
|
149
|
+
#### `throttle<T>(fn: T, delay: number): (...args: Parameters<T>) => void`
|
|
150
|
+
Creates a throttled function that only invokes `fn` at most once per `delay` ms.
|
|
110
151
|
|
|
111
152
|
```typescript
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
const throttled = throttle(() => { /* ... */ }, 100);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `tryCatch<T, E = Error, D = null>(fn: Promise<T> | (() => Promise<T> | T), defaultData?: D): Promise<{ data: T | D; error: E | null }>`
|
|
157
|
+
Executes a function or promise and returns an object with `data` or `error`.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const result = await tryCatch(() => fetchData());
|
|
161
|
+
if (result.error) { /* handle error */ }
|
|
116
162
|
```
|
|
117
163
|
|
|
118
164
|
### String Utilities
|
|
119
165
|
|
|
166
|
+
#### `camelCase(str: string): string`
|
|
167
|
+
Converts a string to camelCase.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
camelCase('hello world'); // "helloWorld"
|
|
171
|
+
```
|
|
172
|
+
|
|
120
173
|
#### `capitalize(str: string): string`
|
|
174
|
+
Capitalizes the first letter and lowercases the rest.
|
|
121
175
|
|
|
122
|
-
|
|
176
|
+
```typescript
|
|
177
|
+
capitalize('hELLO'); // "Hello"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### `formatHMSToSeconds(val?: number | string): number | null`
|
|
181
|
+
Converts HMS time format or numeric strings to seconds. Supports formats like "90", "01:30", "1:02:03".
|
|
123
182
|
|
|
124
183
|
```typescript
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
184
|
+
formatHMSToSeconds("1:30"); // 90
|
|
185
|
+
formatHMSToSeconds("1:02:03"); // 3723
|
|
186
|
+
formatHMSToSeconds(120); // 120
|
|
128
187
|
```
|
|
129
188
|
|
|
130
|
-
#### `
|
|
189
|
+
#### `formatSecondsToFragment(secs: number): string`
|
|
190
|
+
Converts seconds to YouTube-style fragment format (e.g., "1h2m3s").
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
formatSecondsToFragment(3723); // "1h2m3s"
|
|
194
|
+
formatSecondsToFragment(90); // "1m30s"
|
|
195
|
+
formatSecondsToFragment(42); // "42s"
|
|
196
|
+
```
|
|
131
197
|
|
|
132
|
-
|
|
198
|
+
#### `formatSecondsToHMS(totalSeconds: number): string`
|
|
199
|
+
Formats a number of seconds into an HH:MM:SS string, rounding and clamping negatives to zero.
|
|
133
200
|
|
|
134
201
|
```typescript
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
camelCase('hello_world_test'); // "helloWorldTest"
|
|
138
|
-
camelCase('The Quick Brown Fox'); // "theQuickBrownFox"
|
|
202
|
+
formatSecondsToHMS(3661); // "01:01:01"
|
|
203
|
+
formatSecondsToHMS(5); // "00:05"
|
|
139
204
|
```
|
|
140
205
|
|
|
141
|
-
|
|
206
|
+
#### `formatStringToTime(str: string): string`
|
|
207
|
+
Parses a numeric time string (or a string containing digits) into MM:SS or HH:MM:SS format. Non-digits are removed before formatting. Short inputs are zero-padded.
|
|
142
208
|
|
|
143
|
-
|
|
209
|
+
```typescript
|
|
210
|
+
formatStringToTime('123'); // "01:23"
|
|
211
|
+
formatStringToTime('12345'); // "01:23:45"
|
|
212
|
+
formatStringToTime(' 12:34 '); // "12:34"
|
|
213
|
+
```
|
|
144
214
|
|
|
145
|
-
|
|
215
|
+
#### `kebabCase(str: string): string`
|
|
216
|
+
Converts a string to kebab-case.
|
|
146
217
|
|
|
147
218
|
```typescript
|
|
148
|
-
|
|
149
|
-
randomInt(0, 100); // Random integer between 0 and 100
|
|
150
|
-
randomInt(-5, 5); // Random integer between -5 and 5
|
|
219
|
+
kebabCase('Hello World'); // "hello-world"
|
|
151
220
|
```
|
|
152
221
|
|
|
153
|
-
|
|
222
|
+
#### `removeHtmlTags(input: string): string`
|
|
223
|
+
Removes all HTML tags from a string.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
removeHtmlTags('<p>Hello</p>'); // "Hello"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### `slugify(text: string): string`
|
|
230
|
+
Converts a string to a URL-friendly slug.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
slugify('Hello World!'); // "hello-world"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `snakeCase(str: string): string`
|
|
237
|
+
Converts a string to snake_case.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
snakeCase('Hello World'); // "hello_world"
|
|
241
|
+
```
|
|
154
242
|
|
|
155
|
-
#### `
|
|
243
|
+
#### `toSentenceCase(str: string): string`
|
|
244
|
+
Converts a string to sentence case.
|
|
156
245
|
|
|
157
|
-
|
|
246
|
+
```typescript
|
|
247
|
+
toSentenceCase('helloWorld'); // "Hello world"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### `truncate(str: string, length: number, suffix = "..."): string`
|
|
251
|
+
Truncates a string to a specified length, appending a suffix if truncated.
|
|
158
252
|
|
|
159
253
|
```typescript
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}, 300);
|
|
254
|
+
truncate('Hello world', 5); // "He..."
|
|
255
|
+
```
|
|
163
256
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
257
|
+
### YouTube Utilities
|
|
258
|
+
|
|
259
|
+
#### `extractYouTubeId(urlString: string | null): string | null`
|
|
260
|
+
Extracts the video ID from various YouTube URL formats.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
extractYouTubeId('https://youtu.be/dQw4w9WgXcQ'); // "dQw4w9WgXcQ"
|
|
264
|
+
extractYouTubeId('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); // "dQw4w9WgXcQ"
|
|
265
|
+
extractYouTubeId('https://www.youtube.com/embed/dQw4w9WgXcQ'); // "dQw4w9WgXcQ"
|
|
266
|
+
extractYouTubeId('invalid-url'); // null
|
|
168
267
|
```
|
|
169
268
|
|
|
170
|
-
#### `
|
|
269
|
+
#### `generateYoutubeURL(options: TCreateYoutubeLinkOptions): string | null`
|
|
270
|
+
Generates YouTube URLs with various options for watch, embed, or short formats.
|
|
171
271
|
|
|
172
|
-
|
|
272
|
+
```typescript
|
|
273
|
+
// Basic watch URL
|
|
274
|
+
generateYoutubeURL({ videoURL: 'https://youtu.be/abc123' });
|
|
275
|
+
// "https://www.youtube.com/watch?v=abc123"
|
|
276
|
+
|
|
277
|
+
// Embed URL with autoplay
|
|
278
|
+
generateYoutubeURL({
|
|
279
|
+
videoURL: 'https://youtu.be/abc123',
|
|
280
|
+
embed: true,
|
|
281
|
+
autoplay: true
|
|
282
|
+
});
|
|
283
|
+
// "https://www.youtube.com/embed/abc123?autoplay=1"
|
|
284
|
+
|
|
285
|
+
// Short URL with timestamp
|
|
286
|
+
generateYoutubeURL({
|
|
287
|
+
videoURL: 'https://youtu.be/abc123',
|
|
288
|
+
short: true,
|
|
289
|
+
start: "1:30"
|
|
290
|
+
});
|
|
291
|
+
// "https://youtu.be/abc123?t=90"
|
|
292
|
+
|
|
293
|
+
// URL with fragment timestamp
|
|
294
|
+
generateYoutubeURL({
|
|
295
|
+
videoURL: 'https://youtu.be/abc123',
|
|
296
|
+
start: "1:30",
|
|
297
|
+
useFragment: true
|
|
298
|
+
});
|
|
299
|
+
// "https://www.youtube.com/watch?v=abc123#t=1m30s"
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Options:**
|
|
303
|
+
- `videoURL` (required): YouTube URL to process
|
|
304
|
+
- `start`/`end`: Start/end times as seconds (number) or HMS strings ("90", "01:30", "1:02:03")
|
|
305
|
+
- `embed`: Generate embed URL format
|
|
306
|
+
- `short`: Generate youtu.be short URL format
|
|
307
|
+
- `useFragment`: Use #t=1m2s style fragment for timestamps
|
|
308
|
+
- `autoplay`, `controls`, `rel`, `loop`, `mute`, `modestbranding`: Player options
|
|
309
|
+
- `origin`, `playlist`: Additional parameters
|
|
310
|
+
- `params`: Custom query parameters
|
|
311
|
+
|
|
312
|
+
#### `useGetYoutubeVideoDuration(): (videoUrl: string) => Promise<string | null>`
|
|
313
|
+
React hook that returns a function to get YouTube video duration using the YouTube IFrame API.
|
|
173
314
|
|
|
174
315
|
```typescript
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
316
|
+
import { useGetYoutubeVideoDuration } from '@herowcode/utils/youtube';
|
|
317
|
+
|
|
318
|
+
function VideoComponent() {
|
|
319
|
+
const getVideoDuration = useGetYoutubeVideoDuration();
|
|
320
|
+
|
|
321
|
+
const handleGetDuration = async () => {
|
|
322
|
+
const duration = await getVideoDuration('https://youtu.be/dQw4w9WgXcQ');
|
|
323
|
+
console.log(duration); // "03:32" or null if failed
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return <button onClick={handleGetDuration}>Get Duration</button>;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Features:**
|
|
331
|
+
- Automatically loads YouTube IFrame API if not present
|
|
332
|
+
- Creates offscreen iframe for duration detection
|
|
333
|
+
- Handles retry logic for videos that don't immediately report duration
|
|
334
|
+
- 10-second timeout with automatic cleanup
|
|
335
|
+
- Returns formatted duration string (HH:MM:SS) or null on failure
|
|
336
|
+
|
|
337
|
+
#### `validateYoutubeLink(videoUrl: string): Promise<boolean>`
|
|
338
|
+
Checks whether a YouTube video exists by probing thumbnails and falling back to the oEmbed endpoint. Returns `true` for found/public videos and `false` otherwise.
|
|
178
339
|
|
|
179
|
-
|
|
180
|
-
|
|
340
|
+
```typescript
|
|
341
|
+
const ok = await validateYoutubeLink('https://youtu.be/dQw4w9WgXcQ');
|
|
342
|
+
// true | false
|
|
181
343
|
```
|
|
182
344
|
|
|
183
345
|
## Browser Support
|
|
@@ -187,6 +349,8 @@ This library supports all modern browsers and Node.js environments. It uses ES20
|
|
|
187
349
|
- Node.js 10+
|
|
188
350
|
- Modern browsers (Chrome 63+, Firefox 58+, Safari 12+, Edge 79+)
|
|
189
351
|
|
|
352
|
+
The YouTube utilities require a browser environment with DOM support.
|
|
353
|
+
|
|
190
354
|
## Development
|
|
191
355
|
|
|
192
356
|
```bash
|
|
@@ -212,13 +376,4 @@ MIT © [HerowCode](https://github.com/herowcode)
|
|
|
212
376
|
|
|
213
377
|
## Contributing
|
|
214
378
|
|
|
215
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
216
|
-
|
|
217
|
-
## Changelog
|
|
218
|
-
|
|
219
|
-
### 1.0.0
|
|
220
|
-
- Initial release
|
|
221
|
-
- Date utilities: `formatDate`, `addDays`, `diffInDays`, `isBefore`, `isAfter`
|
|
222
|
-
- String utilities: `capitalize`, `camelCase`
|
|
223
|
-
- Number utilities: `randomInt`
|
|
224
|
-
- Function utilities: `debounce`, `throttle`
|
|
379
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface ICompressImageParams {
|
|
2
|
+
file: File;
|
|
3
|
+
maxWidth?: number;
|
|
4
|
+
maxHeight?: number;
|
|
5
|
+
quality?: number;
|
|
6
|
+
allowedFileTypes?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function compressImage({ file, maxWidth, maxHeight, quality, allowedFileTypes, }: ICompressImageParams): Promise<File>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=compress-image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compress-image.d.ts","sourceRoot":"","sources":["../../src/files/compress-image.ts"],"names":[],"mappings":"AAAA,UAAU,oBAAoB;IAC5B,IAAI,EAAE,IAAI,CAAA;IACV,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC5B;AAYD,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,QAAmC,EACnC,SAAoC,EACpC,OAAW,EACX,gBAAyE,GAC1E,EAAE,oBAAoB,iBAiEtB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function convertToWebp(filename) {
|
|
2
|
+
const lastDotIndex = filename.lastIndexOf(".");
|
|
3
|
+
if (lastDotIndex === -1) {
|
|
4
|
+
return `${filename}.webp`;
|
|
5
|
+
}
|
|
6
|
+
return `${filename.substring(0, lastDotIndex)}.webp`;
|
|
7
|
+
}
|
|
8
|
+
export function compressImage({ file, maxWidth = Number.POSITIVE_INFINITY, maxHeight = Number.POSITIVE_INFINITY, quality = 1, allowedFileTypes = ["image/jpg", "image/jpeg", "image/png", "image/webp"], }) {
|
|
9
|
+
if (!allowedFileTypes.includes(file.type)) {
|
|
10
|
+
throw new Error("Image format not supported");
|
|
11
|
+
}
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const reader = new FileReader();
|
|
14
|
+
reader.onload = (event) => {
|
|
15
|
+
var _a;
|
|
16
|
+
const compressed = new Image();
|
|
17
|
+
compressed.onload = () => {
|
|
18
|
+
const canvas = document.createElement("canvas");
|
|
19
|
+
let width = compressed.width;
|
|
20
|
+
let height = compressed.height;
|
|
21
|
+
if (width > height) {
|
|
22
|
+
if (width > maxWidth) {
|
|
23
|
+
height *= maxWidth / width;
|
|
24
|
+
width = maxWidth;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (height > maxHeight) {
|
|
29
|
+
width *= maxHeight / height;
|
|
30
|
+
height = maxHeight;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
canvas.width = width;
|
|
34
|
+
canvas.height = height;
|
|
35
|
+
const context = canvas.getContext("2d");
|
|
36
|
+
if (!context) {
|
|
37
|
+
reject(new Error("Failed to get canvas context"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
context.drawImage(compressed, 0, 0, width, height);
|
|
41
|
+
canvas.toBlob((blob) => {
|
|
42
|
+
if (!blob) {
|
|
43
|
+
reject(new Error("Failed to compress image."));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const compressedFile = new File([blob], convertToWebp(file.name), {
|
|
47
|
+
type: "image/webp",
|
|
48
|
+
lastModified: Date.now(),
|
|
49
|
+
});
|
|
50
|
+
resolve(compressedFile);
|
|
51
|
+
}, "image/webp", quality);
|
|
52
|
+
};
|
|
53
|
+
compressed.src = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
54
|
+
};
|
|
55
|
+
reader.readAsDataURL(file);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function convertToWebp(filename) {
|
|
2
|
+
const lastDotIndex = filename.lastIndexOf(".");
|
|
3
|
+
if (lastDotIndex === -1) {
|
|
4
|
+
return `${filename}.webp`;
|
|
5
|
+
}
|
|
6
|
+
return `${filename.substring(0, lastDotIndex)}.webp`;
|
|
7
|
+
}
|
|
8
|
+
export function compressImage({ file, maxWidth = Number.POSITIVE_INFINITY, maxHeight = Number.POSITIVE_INFINITY, quality = 1, allowedFileTypes = ["image/jpg", "image/jpeg", "image/png", "image/webp"], }) {
|
|
9
|
+
if (!allowedFileTypes.includes(file.type)) {
|
|
10
|
+
throw new Error("Image format not supported");
|
|
11
|
+
}
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const reader = new FileReader();
|
|
14
|
+
reader.onload = (event) => {
|
|
15
|
+
var _a;
|
|
16
|
+
const compressed = new Image();
|
|
17
|
+
compressed.onload = () => {
|
|
18
|
+
const canvas = document.createElement("canvas");
|
|
19
|
+
let width = compressed.width;
|
|
20
|
+
let height = compressed.height;
|
|
21
|
+
if (width > height) {
|
|
22
|
+
if (width > maxWidth) {
|
|
23
|
+
height *= maxWidth / width;
|
|
24
|
+
width = maxWidth;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (height > maxHeight) {
|
|
29
|
+
width *= maxHeight / height;
|
|
30
|
+
height = maxHeight;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
canvas.width = width;
|
|
34
|
+
canvas.height = height;
|
|
35
|
+
const context = canvas.getContext("2d");
|
|
36
|
+
if (!context) {
|
|
37
|
+
reject(new Error("Failed to get canvas context"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
context.drawImage(compressed, 0, 0, width, height);
|
|
41
|
+
canvas.toBlob((blob) => {
|
|
42
|
+
if (!blob) {
|
|
43
|
+
reject(new Error("Failed to compress image."));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const compressedFile = new File([blob], convertToWebp(file.name), {
|
|
47
|
+
type: "image/webp",
|
|
48
|
+
lastModified: Date.now(),
|
|
49
|
+
});
|
|
50
|
+
resolve(compressedFile);
|
|
51
|
+
}, "image/webp", quality);
|
|
52
|
+
};
|
|
53
|
+
compressed.src = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
54
|
+
};
|
|
55
|
+
reader.readAsDataURL(file);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compress-image.js","sourceRoot":"","sources":["../../src/files/compress-image.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-url.d.ts","sourceRoot":"","sources":["../../src/files/download-url.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,OAAO,CAyB9D,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const downloadUrl = async (url) => {
|
|
2
|
+
try {
|
|
3
|
+
const urlObj = new URL(url);
|
|
4
|
+
const pathname = urlObj.pathname;
|
|
5
|
+
const segments = pathname.split("/").filter((segment) => segment.length > 0);
|
|
6
|
+
const filename = segments.length > 0 ? segments[segments.length - 1] : null;
|
|
7
|
+
if (!filename || !filename.includes(".")) {
|
|
8
|
+
throw new Error("URL does not contain a valid filename");
|
|
9
|
+
}
|
|
10
|
+
const response = await fetch(url, { mode: "cors" });
|
|
11
|
+
const blob = await response.blob();
|
|
12
|
+
const link = document.createElement("a");
|
|
13
|
+
link.href = window.URL.createObjectURL(blob);
|
|
14
|
+
link.download = filename;
|
|
15
|
+
document.body.appendChild(link);
|
|
16
|
+
link.click();
|
|
17
|
+
document.body.removeChild(link);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error("Error downloading the file", error);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const downloadUrl = async (url) => {
|
|
2
|
+
try {
|
|
3
|
+
const urlObj = new URL(url);
|
|
4
|
+
const pathname = urlObj.pathname;
|
|
5
|
+
const segments = pathname.split("/").filter((segment) => segment.length > 0);
|
|
6
|
+
const filename = segments.length > 0 ? segments[segments.length - 1] : null;
|
|
7
|
+
if (!filename || !filename.includes(".")) {
|
|
8
|
+
throw new Error("URL does not contain a valid filename");
|
|
9
|
+
}
|
|
10
|
+
const response = await fetch(url, { mode: "cors" });
|
|
11
|
+
const blob = await response.blob();
|
|
12
|
+
const link = document.createElement("a");
|
|
13
|
+
link.href = window.URL.createObjectURL(blob);
|
|
14
|
+
link.download = filename;
|
|
15
|
+
document.body.appendChild(link);
|
|
16
|
+
link.click();
|
|
17
|
+
document.body.removeChild(link);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error("Error downloading the file", error);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-url.js","sourceRoot":"","sources":["../../src/files/download-url.ts"],"names":[],"mappings":";;;AAAO,MAAM,WAAW,GAAG,KAAK,EAAE,GAAW,EAAoB,EAAE;IACjE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;QAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAC5E,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAE3E,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;QAC1D,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;QACnD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAElC,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;QACxC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;QAC5C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAA;QACZ,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QAC/B,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAA;QAClD,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AAzBY,QAAA,WAAW,eAyBvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-bytes.d.ts","sourceRoot":"","sources":["../../src/files/format-bytes.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAejD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatBytes(bytes) {
|
|
2
|
+
if (bytes < 0) {
|
|
3
|
+
throw new Error("Size in bytes cannot be negative");
|
|
4
|
+
}
|
|
5
|
+
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
6
|
+
let size = bytes;
|
|
7
|
+
let index = 0;
|
|
8
|
+
while (size >= 1024 && index < units.length - 1) {
|
|
9
|
+
size /= 1024;
|
|
10
|
+
index++;
|
|
11
|
+
}
|
|
12
|
+
return `${size.toFixed(2)} ${units[index]}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatBytes(bytes) {
|
|
2
|
+
if (bytes < 0) {
|
|
3
|
+
throw new Error("Size in bytes cannot be negative");
|
|
4
|
+
}
|
|
5
|
+
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
6
|
+
let size = bytes;
|
|
7
|
+
let index = 0;
|
|
8
|
+
while (size >= 1024 && index < units.length - 1) {
|
|
9
|
+
size /= 1024;
|
|
10
|
+
index++;
|
|
11
|
+
}
|
|
12
|
+
return `${size.toFixed(2)} ${units[index]}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-bytes.js","sourceRoot":"","sources":["../../src/files/format-bytes.ts"],"names":[],"mappings":";;AAAA,kCAeC;AAfD,SAAgB,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACrD,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IACjD,IAAI,IAAI,GAAG,KAAK,CAAA;IAChB,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,OAAO,IAAI,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,IAAI,IAAI,IAAI,CAAA;QACZ,KAAK,EAAE,CAAA;IACT,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAA;AAC7C,CAAC"}
|