@dub/utils 0.0.2 → 0.0.4

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/src/index.ts DELETED
@@ -1,540 +0,0 @@
1
- import slugify from "@sindresorhus/slugify";
2
- import { clsx, type ClassValue } from "clsx";
3
- import ms from "ms";
4
- import { customAlphabet } from "nanoid";
5
- import { Metadata } from "next";
6
- import { NextRouter } from "next/router";
7
- import { twMerge } from "tailwind-merge";
8
- import {
9
- HOME_DOMAIN,
10
- SECOND_LEVEL_DOMAINS,
11
- SPECIAL_APEX_DOMAINS,
12
- ccTLDs,
13
- } from "./constants";
14
-
15
- export * from "./constants";
16
-
17
- export function cn(...inputs: ClassValue[]) {
18
- return twMerge(clsx(inputs));
19
- }
20
-
21
- export function constructMetadata({
22
- title = "Dub - Link Management for Modern Marketing Teams",
23
- description = "Dub is an open-source link management tool for modern marketing teams to create, share, and track short links.",
24
- image = "https://dub.co/_static/thumbnail.png",
25
- icons = "/favicon.ico",
26
- noIndex = false,
27
- }: {
28
- title?: string;
29
- description?: string;
30
- image?: string;
31
- icons?: string;
32
- noIndex?: boolean;
33
- } = {}): Metadata {
34
- return {
35
- title,
36
- description,
37
- openGraph: {
38
- title,
39
- description,
40
- images: [
41
- {
42
- url: image,
43
- },
44
- ],
45
- },
46
- twitter: {
47
- card: "summary_large_image",
48
- title,
49
- description,
50
- images: [image],
51
- creator: "@dubdotco",
52
- },
53
- icons,
54
- metadataBase: new URL(HOME_DOMAIN),
55
- themeColor: "#FFF",
56
- ...(noIndex && {
57
- robots: {
58
- index: false,
59
- follow: false,
60
- },
61
- }),
62
- };
63
- }
64
-
65
- export const nanoid = customAlphabet(
66
- "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
67
- 7,
68
- ); // 7-character random string
69
-
70
- interface SWRError extends Error {
71
- status: number;
72
- }
73
-
74
- export async function fetcher<JSON = any>(
75
- input: RequestInfo,
76
- init?: RequestInit,
77
- ): Promise<JSON> {
78
- const res = await fetch(input, init);
79
-
80
- if (!res.ok) {
81
- const error = await res.text();
82
- const err = new Error(error) as SWRError;
83
- err.status = res.status;
84
- throw err;
85
- }
86
-
87
- return res.json();
88
- }
89
-
90
- export function nFormatter(
91
- num?: number,
92
- opts: { digits?: number; full?: boolean } = {
93
- digits: 1,
94
- },
95
- ) {
96
- if (!num) return "0";
97
- if (opts.full) {
98
- return Intl.NumberFormat("en-US").format(num);
99
- }
100
- const lookup = [
101
- { value: 1, symbol: "" },
102
- { value: 1e3, symbol: "K" },
103
- { value: 1e6, symbol: "M" },
104
- { value: 1e9, symbol: "G" },
105
- { value: 1e12, symbol: "T" },
106
- { value: 1e15, symbol: "P" },
107
- { value: 1e18, symbol: "E" },
108
- ];
109
- const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
110
- var item = lookup
111
- .slice()
112
- .reverse()
113
- .find(function (item) {
114
- return num >= item.value;
115
- });
116
- return item
117
- ? (num / item.value).toFixed(opts.digits).replace(rx, "$1") + item.symbol
118
- : "0";
119
- }
120
-
121
- export function capitalize(str: string) {
122
- if (!str || typeof str !== "string") return str;
123
- return str.charAt(0).toUpperCase() + str.slice(1);
124
- }
125
-
126
- export const chunk = <T>(array: T[], chunk_size: number): T[][] => {
127
- return array.reduce((resultArray, item, index) => {
128
- const chunkIndex = Math.floor(index / chunk_size);
129
-
130
- if (!resultArray[chunkIndex]) {
131
- resultArray[chunkIndex] = []; // start a new chunk
132
- }
133
-
134
- resultArray[chunkIndex].push(item);
135
-
136
- return resultArray;
137
- }, [] as T[][]);
138
- };
139
-
140
- export function linkConstructor({
141
- key,
142
- domain = "dub.sh",
143
- localhost,
144
- pretty,
145
- noDomain,
146
- }: {
147
- key: string;
148
- domain?: string;
149
- localhost?: boolean;
150
- pretty?: boolean;
151
- noDomain?: boolean;
152
- }) {
153
- const link = `${
154
- localhost ? "http://home.localhost:8888" : `https://${domain}`
155
- }${key !== "_root" ? `/${key}` : ""}`;
156
-
157
- if (noDomain) return `/${key}`;
158
- return pretty ? link.replace(/^https?:\/\//, "") : link;
159
- }
160
-
161
- export const timeAgo = (
162
- timestamp: Date | null,
163
- {
164
- withAgo,
165
- }: {
166
- withAgo?: boolean;
167
- } = {},
168
- ): string => {
169
- if (!timestamp) return "Never";
170
- const diff = Date.now() - new Date(timestamp).getTime();
171
- if (diff < 1000) {
172
- // less than 1 second
173
- return "Just now";
174
- } else if (diff > 82800000) {
175
- // more than 23 hours – similar to how Twitter displays timestamps
176
- return new Date(timestamp).toLocaleDateString("en-US", {
177
- month: "short",
178
- day: "numeric",
179
- year:
180
- new Date(timestamp).getFullYear() !== new Date().getFullYear()
181
- ? "numeric"
182
- : undefined,
183
- });
184
- }
185
- return `${ms(diff)}${withAgo ? " ago" : ""}`;
186
- };
187
-
188
- export const getDateTimeLocal = (timestamp?: Date): string => {
189
- const d = timestamp ? new Date(timestamp) : new Date();
190
- if (d.toString() === "Invalid Date") return "";
191
- return new Date(d.getTime() - d.getTimezoneOffset() * 60000)
192
- .toISOString()
193
- .split(":")
194
- .slice(0, 2)
195
- .join(":");
196
- };
197
-
198
- export const formatDate = (dateString: string) => {
199
- return new Date(`${dateString}T00:00:00Z`).toLocaleDateString("en-US", {
200
- day: "numeric",
201
- month: "long",
202
- year: "numeric",
203
- timeZone: "UTC",
204
- });
205
- };
206
-
207
- export const getFirstAndLastDay = (day: number) => {
208
- const today = new Date();
209
- const currentDay = today.getDate();
210
- const currentMonth = today.getMonth();
211
- const currentYear = today.getFullYear();
212
- if (currentDay >= day) {
213
- // if the current day is greater than target day, it means that we just passed it
214
- return {
215
- firstDay: new Date(currentYear, currentMonth, day),
216
- lastDay: new Date(currentYear, currentMonth + 1, day - 1),
217
- };
218
- } else {
219
- // if the current day is less than target day, it means that we haven't passed it yet
220
- const lastYear = currentMonth === 0 ? currentYear - 1 : currentYear; // if the current month is January, we need to go back a year
221
- const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1; // if the current month is January, we need to go back to December
222
- return {
223
- firstDay: new Date(lastYear, lastMonth, day),
224
- lastDay: new Date(currentYear, currentMonth, day - 1),
225
- };
226
- }
227
- };
228
-
229
- // Function to get the last day of the current month
230
- export const getLastDayOfMonth = () => {
231
- const today = new Date();
232
- const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0); // This will give the last day of the current month
233
- return lastDay.getDate();
234
- };
235
-
236
- // Adjust the billingCycleStart based on the number of days in the current month
237
- export const getAdjustedBillingCycleStart = (billingCycleStart: number) => {
238
- const lastDay = getLastDayOfMonth();
239
- if (billingCycleStart > lastDay) {
240
- return lastDay;
241
- } else {
242
- return billingCycleStart;
243
- }
244
- };
245
-
246
- export const generateDomainFromName = (name: string) => {
247
- const normalizedName = slugify(name, { separator: "" });
248
- if (normalizedName.length < 3) {
249
- return "";
250
- }
251
- if (ccTLDs.has(normalizedName.slice(-2))) {
252
- return `${normalizedName.slice(0, -2)}.${normalizedName.slice(-2)}`;
253
- }
254
- // remove vowels
255
- const devowel = normalizedName.replace(/[aeiou]/g, "");
256
- if (devowel.length >= 3 && ccTLDs.has(devowel.slice(-2))) {
257
- return `${devowel.slice(0, -2)}.${devowel.slice(-2)}`;
258
- }
259
-
260
- const shortestString = [normalizedName, devowel].reduce((a, b) =>
261
- a.length < b.length ? a : b,
262
- );
263
-
264
- return `${shortestString}.to`;
265
- };
266
-
267
- // courtesy of ChatGPT: https://sharegpt.com/c/pUYXtRs
268
- export const validDomainRegex = new RegExp(
269
- /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/,
270
- );
271
-
272
- export const validKeyRegex = new RegExp(/^[0-9A-Za-z\u0080-\uFFFF\/\-]*$/u);
273
-
274
- export const validSlugRegex = new RegExp(/^[a-zA-Z0-9\-]+$/);
275
-
276
- export const getSubdomain = (name: string, apexName: string) => {
277
- if (name === apexName) return null;
278
- return name.slice(0, name.length - apexName.length - 1);
279
- };
280
-
281
- export const getApexDomain = (url: string) => {
282
- let domain;
283
- try {
284
- // replace any custom scheme (e.g. notion://) with https://
285
- // use the URL constructor to get the hostname
286
- domain = new URL(url.replace(/^[a-zA-Z]+:\/\//, "https://")).hostname;
287
- } catch (e) {
288
- return "";
289
- }
290
- if (domain === "youtu.be") return "youtube.com";
291
- if (domain === "raw.githubusercontent.com") return "github.com";
292
- if (domain.endsWith(".vercel.app")) return "vercel.app";
293
-
294
- const parts = domain.split(".");
295
- if (parts.length > 2) {
296
- if (
297
- // if this is a second-level TLD (e.g. co.uk, .com.ua, .org.tt), we need to return the last 3 parts
298
- (SECOND_LEVEL_DOMAINS.has(parts[parts.length - 2]) &&
299
- ccTLDs.has(parts[parts.length - 1])) ||
300
- // if it's a special subdomain for website builders (e.g. weathergpt.vercel.app/)
301
- SPECIAL_APEX_DOMAINS.has(parts.slice(-2).join("."))
302
- ) {
303
- return parts.slice(-3).join(".");
304
- }
305
- // otherwise, it's a subdomain (e.g. dub.vercel.app), so we return the last 2 parts
306
- return parts.slice(-2).join(".");
307
- }
308
- // if it's a normal domain (e.g. dub.co), we return the domain
309
- return domain;
310
- };
311
-
312
- export const isValidUrl = (url: string) => {
313
- try {
314
- new URL(url);
315
- return true;
316
- } catch (e) {
317
- return false;
318
- }
319
- };
320
-
321
- export const getUrlFromString = (str: string) => {
322
- if (isValidUrl(str)) return str;
323
- try {
324
- if (str.includes(".") && !str.includes(" ")) {
325
- return new URL(`https://${str}`).toString();
326
- }
327
- } catch (e) {
328
- return null;
329
- }
330
- };
331
-
332
- export const getDomainWithoutWWW = (url: string) => {
333
- if (isValidUrl(url)) {
334
- return new URL(url).hostname.replace(/^www\./, "");
335
- }
336
- try {
337
- if (url.includes(".") && !url.includes(" ")) {
338
- return new URL(`https://${url}`).hostname.replace(/^www\./, "");
339
- }
340
- } catch (e) {
341
- return null;
342
- }
343
- };
344
-
345
- export const getQueryString = (
346
- router: NextRouter,
347
- opts?: Record<string, string>,
348
- ) => {
349
- const queryString = new URLSearchParams({
350
- ...(router.query as Record<string, string>),
351
- ...opts,
352
- }).toString();
353
- return `${queryString ? "?" : ""}${queryString}`;
354
- };
355
-
356
- export const setQueryString = ({
357
- router,
358
- param,
359
- value,
360
- }: {
361
- router: NextRouter;
362
- param: string;
363
- value: string;
364
- }) => {
365
- if (param !== "page") delete router.query.page;
366
- let newQuery;
367
- if (value.length > 0) {
368
- newQuery = {
369
- ...router.query,
370
- [param]: value,
371
- };
372
- } else {
373
- delete router.query[param];
374
- newQuery = { ...router.query };
375
- }
376
- // here, we omit the slug from the query string as well
377
- const { slug, ...finalQuery } = newQuery;
378
- router.replace({
379
- pathname: `/${router.query.slug || "links"}`,
380
- query: finalQuery,
381
- });
382
- };
383
-
384
- export const truncate = (str: string | null, length: number) => {
385
- if (!str || str.length <= length) return str;
386
- return `${str.slice(0, length - 3)}...`;
387
- };
388
-
389
- export const getParamsFromURL = (url: string) => {
390
- if (!url) return {};
391
- try {
392
- const params = new URL(url).searchParams;
393
- const paramsObj: Record<string, string> = {};
394
- for (const [key, value] of params.entries()) {
395
- if (value && value !== "") {
396
- paramsObj[key] = value;
397
- }
398
- }
399
- return paramsObj;
400
- } catch (e) {
401
- return {};
402
- }
403
- };
404
-
405
- export const constructURLFromUTMParams = (
406
- url: string,
407
- utmParams: Record<string, string>,
408
- ) => {
409
- if (!url) return "";
410
- try {
411
- const newURL = new URL(url);
412
- for (const [key, value] of Object.entries(utmParams)) {
413
- if (value === "") {
414
- newURL.searchParams.delete(key);
415
- } else {
416
- newURL.searchParams.set(key, value);
417
- }
418
- }
419
- return newURL.toString();
420
- } catch (e) {
421
- return "";
422
- }
423
- };
424
-
425
- export const paramsMetadata = [
426
- { display: "Referral (ref)", key: "ref", examples: "twitter, facebook" },
427
- { display: "UTM Source", key: "utm_source", examples: "twitter, facebook" },
428
- { display: "UTM Medium", key: "utm_medium", examples: "social, email" },
429
- { display: "UTM Campaign", key: "utm_campaign", examples: "summer_sale" },
430
- { display: "UTM Term", key: "utm_term", examples: "blue_shoes" },
431
- { display: "UTM Content", key: "utm_content", examples: "logolink" },
432
- ];
433
-
434
- export const getUrlWithoutUTMParams = (url: string) => {
435
- try {
436
- const newURL = new URL(url);
437
- paramsMetadata.forEach((param) => newURL.searchParams.delete(param.key));
438
- return newURL.toString();
439
- } catch (e) {
440
- return url;
441
- }
442
- };
443
-
444
- export async function generateMD5Hash(message: string) {
445
- // Convert the message string to a Uint8Array
446
- const encoder = new TextEncoder();
447
- const data = encoder.encode(message);
448
-
449
- // Generate the hash using the SubtleCrypto interface
450
- const hashBuffer = await crypto.subtle.digest("MD5", data);
451
-
452
- // Convert the hash to a hexadecimal string
453
- const hashArray = Array.from(new Uint8Array(hashBuffer));
454
- const hashHex = hashArray
455
- .map((byte) => byte.toString(16).padStart(2, "0"))
456
- .join("");
457
-
458
- return hashHex;
459
- }
460
-
461
- const logTypeToEnv = {
462
- cron: process.env.DUB_SLACK_HOOK_CRON,
463
- links: process.env.DUB_SLACK_HOOK_LINKS,
464
- };
465
-
466
- export const log = async ({
467
- message,
468
- type,
469
- mention = false,
470
- }: {
471
- message: string;
472
- type: "cron" | "links";
473
- mention?: boolean;
474
- }) => {
475
- if (
476
- process.env.NODE_ENV === "development" ||
477
- !process.env.DUB_SLACK_HOOK_CRON ||
478
- !process.env.DUB_SLACK_HOOK_LINKS
479
- )
480
- console.log(message);
481
- /* Log a message to the console */
482
- const HOOK = logTypeToEnv[type];
483
- if (!HOOK) return;
484
- try {
485
- return await fetch(HOOK, {
486
- method: "POST",
487
- headers: {
488
- "Content-Type": "application/json",
489
- },
490
- body: JSON.stringify({
491
- blocks: [
492
- {
493
- type: "section",
494
- text: {
495
- type: "mrkdwn",
496
- text: `${mention ? "<@U0404G6J3NJ> " : ""}${message}`,
497
- },
498
- },
499
- ],
500
- }),
501
- });
502
- } catch (e) {
503
- console.log(`Failed to log to Dub Slack. Error: ${e}`);
504
- }
505
- };
506
-
507
- type DeepEqual = (
508
- obj1: Record<string, any>,
509
- obj2: Record<string, any>,
510
- ) => boolean;
511
-
512
- export const deepEqual: DeepEqual = (obj1, obj2) => {
513
- if (obj1 === obj2) {
514
- return true;
515
- }
516
-
517
- if (
518
- typeof obj1 !== "object" ||
519
- typeof obj2 !== "object" ||
520
- obj1 === null ||
521
- obj2 === null
522
- ) {
523
- return false;
524
- }
525
-
526
- const keys1 = Object.keys(obj1);
527
- const keys2 = Object.keys(obj2);
528
-
529
- if (keys1.length !== keys2.length) {
530
- return false;
531
- }
532
-
533
- for (const key of keys1) {
534
- if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
535
- return false;
536
- }
537
- }
538
-
539
- return true;
540
- };
package/tsconfig.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "tsconfig/react-library.json",
3
- "include": ["."],
4
- "exclude": ["dist", "build", "node_modules"]
5
- }
package/tsup.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig, Options } from "tsup";
2
-
3
- export default defineConfig((options: Options) => ({
4
- entry: ["src/**/*.ts"],
5
- format: ["esm"],
6
- dts: true,
7
- minify: true,
8
- external: ["react"],
9
- ...options,
10
- }));