@dub/utils 0.0.3 → 0.0.5
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 +11 -0
- package/package.json +14 -8
- package/.turbo/turbo-build.log +0 -24
- package/dist/chunk-2U2IYZHN.mjs +0 -1
- package/dist/chunk-35O76FBU.mjs +0 -2
- package/dist/chunk-64C57Q4W.mjs +0 -2
- package/dist/chunk-64KMNIJU.mjs +0 -2
- package/dist/chunk-6MWSVUBA.mjs +0 -1
- package/dist/chunk-6YUAHP47.mjs +0 -1
- package/dist/chunk-BXDPGIU7.mjs +0 -1
- package/dist/chunk-D7V6QEC3.mjs +0 -2
- package/dist/chunk-G2EXSXOB.mjs +0 -1
- package/dist/chunk-GIGD5PB3.mjs +0 -1
- package/dist/chunk-GMO2NK2C.mjs +0 -1
- package/dist/chunk-H4LU5LOY.mjs +0 -2
- package/dist/chunk-H53DALDZ.mjs +0 -1
- package/dist/chunk-LEHV3NWH.mjs +0 -2
- package/dist/chunk-LXDQ4KMN.mjs +0 -0
- package/dist/chunk-M5FLGH3T.mjs +0 -1
- package/dist/chunk-MQNZDHLT.mjs +0 -2
- package/dist/chunk-OH6TH3J6.mjs +0 -1
- package/dist/chunk-ONIHRKH7.mjs +0 -1
- package/dist/chunk-TN4P6NPQ.mjs +0 -1
- package/dist/chunk-U4XQUV65.mjs +0 -1
- package/dist/chunk-UBCCNLBS.mjs +0 -1
- package/dist/chunk-UE5AMEFG.mjs +0 -2
- package/dist/chunk-UNXRAJQG.mjs +0 -1
- package/dist/chunk-UYE4FVX2.mjs +0 -2
- package/dist/chunk-V4HIRCCV.mjs +0 -1
- package/dist/chunk-W5IYXLXA.mjs +0 -1
- package/dist/chunk-XACKIZEQ.mjs +0 -2
- package/dist/chunk-Y2N4Y4WZ.mjs +0 -1
- package/dist/chunk-YUMUNL66.mjs +0 -1
- package/dist/chunk-ZGU5QGQR.mjs +0 -1
- package/dist/constants/content.d.ts +0 -10
- package/dist/constants/content.mjs +0 -1
- package/dist/hooks/index.d.ts +0 -6
- package/dist/hooks/index.mjs +0 -2
- package/dist/hooks/use-current-anchor.d.ts +0 -3
- package/dist/hooks/use-current-anchor.mjs +0 -2
- package/dist/hooks/use-intersection-observer.d.ts +0 -8
- package/dist/hooks/use-intersection-observer.mjs +0 -2
- package/dist/hooks/use-local-storage.d.ts +0 -3
- package/dist/hooks/use-local-storage.mjs +0 -2
- package/dist/hooks/use-media-query.d.ts +0 -10
- package/dist/hooks/use-media-query.mjs +0 -2
- package/dist/hooks/use-scroll.d.ts +0 -3
- package/dist/hooks/use-scroll.mjs +0 -2
- package/dist/utils.d.ts +0 -72
- package/dist/utils.mjs +0 -1
- package/src/constants/cctlds.ts +0 -276
- package/src/constants/countries.ts +0 -254
- package/src/constants/index.ts +0 -211
- package/src/index.ts +0 -540
- package/tsconfig.json +0 -5
- package/tsup.config.ts +0 -10
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