kmod-cli 1.0.10
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 +53 -0
- package/bin/gen-components.js +68 -0
- package/bin/index.js +153 -0
- package/component-templates/components/access-denied.tsx +130 -0
- package/component-templates/components/breadcumb.tsx +42 -0
- package/component-templates/components/count-down.tsx +94 -0
- package/component-templates/components/count-input.tsx +221 -0
- package/component-templates/components/date-range-calendar/button.tsx +61 -0
- package/component-templates/components/date-range-calendar/calendar.tsx +132 -0
- package/component-templates/components/date-range-calendar/date-input.tsx +259 -0
- package/component-templates/components/date-range-calendar/date-range-picker.tsx +594 -0
- package/component-templates/components/date-range-calendar/label.tsx +31 -0
- package/component-templates/components/date-range-calendar/popover.tsx +32 -0
- package/component-templates/components/date-range-calendar/select.tsx +125 -0
- package/component-templates/components/date-range-calendar/switch.tsx +30 -0
- package/component-templates/components/datetime-picker/button.tsx +61 -0
- package/component-templates/components/datetime-picker/calendar.tsx +156 -0
- package/component-templates/components/datetime-picker/datetime-picker.tsx +75 -0
- package/component-templates/components/datetime-picker/input.tsx +20 -0
- package/component-templates/components/datetime-picker/label.tsx +18 -0
- package/component-templates/components/datetime-picker/period-input.tsx +62 -0
- package/component-templates/components/datetime-picker/popover.tsx +32 -0
- package/component-templates/components/datetime-picker/select.tsx +125 -0
- package/component-templates/components/datetime-picker/time-picker-input.tsx +131 -0
- package/component-templates/components/datetime-picker/time-picker-utils.tsx +204 -0
- package/component-templates/components/datetime-picker/time-picker.tsx +59 -0
- package/component-templates/components/gradient-outline.tsx +233 -0
- package/component-templates/components/gradient-svg.tsx +157 -0
- package/component-templates/components/grid-layout.tsx +69 -0
- package/component-templates/components/hydrate-guard.tsx +40 -0
- package/component-templates/components/image.tsx +92 -0
- package/component-templates/components/loader-slash-gradient.tsx +85 -0
- package/component-templates/components/masonry-gallery.tsx +221 -0
- package/component-templates/components/modal.tsx +110 -0
- package/component-templates/components/multi-select.tsx +447 -0
- package/component-templates/components/non-hydration.tsx +27 -0
- package/component-templates/components/portal.tsx +34 -0
- package/component-templates/components/segments-circle.tsx +235 -0
- package/component-templates/components/single-select.tsx +248 -0
- package/component-templates/components/stroke-circle.tsx +57 -0
- package/component-templates/components/table/column-table.tsx +15 -0
- package/component-templates/components/table/data-table.tsx +339 -0
- package/component-templates/components/table/readme.tsx +95 -0
- package/component-templates/components/table/table.tsx +60 -0
- package/component-templates/components/text-hover-effect.tsx +120 -0
- package/component-templates/components/timout-loader.tsx +52 -0
- package/component-templates/components/toast.tsx +994 -0
- package/component-templates/configs/config.ts +33 -0
- package/component-templates/configs/feature-config.tsx +432 -0
- package/component-templates/configs/keys.ts +7 -0
- package/component-templates/core/api-service.ts +202 -0
- package/component-templates/core/calculate.ts +18 -0
- package/component-templates/core/idb.ts +166 -0
- package/component-templates/core/storage.ts +213 -0
- package/component-templates/hooks/count-down.ts +38 -0
- package/component-templates/hooks/fade-on-scroll.ts +52 -0
- package/component-templates/hooks/safe-action.ts +59 -0
- package/component-templates/hooks/spam-guard.ts +31 -0
- package/component-templates/lib/utils.ts +6 -0
- package/component-templates/providers/feature-guard.tsx +432 -0
- package/component-templates/queries/query.tsx +775 -0
- package/component-templates/utils/colors/color-by-text.ts +307 -0
- package/component-templates/utils/colors/stripe-effect.ts +100 -0
- package/component-templates/utils/hash/hash-aes.ts +35 -0
- package/components.json +348 -0
- package/package.json +60 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DBSchema,
|
|
3
|
+
deleteDB,
|
|
4
|
+
IDBPDatabase,
|
|
5
|
+
openDB,
|
|
6
|
+
} from 'idb';
|
|
7
|
+
|
|
8
|
+
// =======================
|
|
9
|
+
// DB Schema định nghĩa
|
|
10
|
+
// =======================
|
|
11
|
+
export type AppDBSchema = DBSchema & {
|
|
12
|
+
items: {
|
|
13
|
+
key: string;
|
|
14
|
+
value: any;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// =======================
|
|
19
|
+
// Core Init
|
|
20
|
+
// =======================
|
|
21
|
+
export const initDB = async (
|
|
22
|
+
name: string = "AppDB",
|
|
23
|
+
version: number = 1
|
|
24
|
+
): Promise<IDBPDatabase<AppDBSchema>> => {
|
|
25
|
+
return await openDB<AppDBSchema>(name, version, {
|
|
26
|
+
upgrade(db) {
|
|
27
|
+
if (!db.objectStoreNames.contains("items")) {
|
|
28
|
+
db.createObjectStore("items", { keyPath: "id" });
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// =======================
|
|
35
|
+
// CRUD helpers
|
|
36
|
+
// =======================
|
|
37
|
+
export const db = {
|
|
38
|
+
add: async (data: { id: string; [key: string]: any }, dbName?: string) => {
|
|
39
|
+
try {
|
|
40
|
+
if (!data?.id) throw new Error("id is required");
|
|
41
|
+
const db = await initDB(dbName);
|
|
42
|
+
await db.put("items", data);
|
|
43
|
+
return { success: true, message: "Item added" };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return { success: false, message: String(err) };
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
get: async (id: string, dbName?: string) => {
|
|
50
|
+
try {
|
|
51
|
+
if (!id) throw new Error("id is required");
|
|
52
|
+
const db = await initDB(dbName);
|
|
53
|
+
const item = await db.get("items", id);
|
|
54
|
+
return item
|
|
55
|
+
? { success: true, data: item }
|
|
56
|
+
: { success: false, message: "Item not found" };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { success: false, message: String(err) };
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
getAll: async (dbName?: string) => {
|
|
63
|
+
try {
|
|
64
|
+
const db = await initDB(dbName);
|
|
65
|
+
const items = await db.getAll("items");
|
|
66
|
+
return { success: true, data: items };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return { success: false, message: String(err) };
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
update: async (id: string, updatedData: any, dbName?: string) => {
|
|
73
|
+
try {
|
|
74
|
+
const db = await initDB(dbName);
|
|
75
|
+
const item = await db.get("items", id);
|
|
76
|
+
if (!item) return { success: false, message: "Item not found" };
|
|
77
|
+
const newItem = { ...item, ...updatedData };
|
|
78
|
+
await db.put("items", newItem);
|
|
79
|
+
return { success: true, message: "Item updated" };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { success: false, message: String(err) };
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
delete: async (id: string, dbName?: string) => {
|
|
86
|
+
try {
|
|
87
|
+
const db = await initDB(dbName);
|
|
88
|
+
await db.delete("items", id);
|
|
89
|
+
return { success: true, message: "Item deleted" };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { success: false, message: String(err) };
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
clear: async (dbName?: string) => {
|
|
96
|
+
try {
|
|
97
|
+
const db = await initDB(dbName);
|
|
98
|
+
await db.clear("items");
|
|
99
|
+
return { success: true, message: "All items cleared" };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return { success: false, message: String(err) };
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// =======================
|
|
107
|
+
// DB Management helpers
|
|
108
|
+
// =======================
|
|
109
|
+
export const dbManager = {
|
|
110
|
+
createDB: async (name: string, version: number = 1) => {
|
|
111
|
+
try {
|
|
112
|
+
await openDB(name, version, {
|
|
113
|
+
upgrade(db) {
|
|
114
|
+
if (!db.objectStoreNames.contains("items")) {
|
|
115
|
+
db.createObjectStore("items", { keyPath: "id" });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
return { success: true, message: `DB ${name} created` };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return { success: false, message: String(err) };
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
deleteDB: async (name: string) => {
|
|
126
|
+
try {
|
|
127
|
+
await deleteDB(name);
|
|
128
|
+
return { success: true, message: `DB ${name} deleted` };
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return { success: false, message: String(err) };
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
listDBs: async () => {
|
|
135
|
+
try {
|
|
136
|
+
if ("databases" in indexedDB) {
|
|
137
|
+
const dbs = await (indexedDB as any).databases();
|
|
138
|
+
return { success: true, data: dbs };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
message: "listDBs not supported in this browser",
|
|
143
|
+
};
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return { success: false, message: String(err) };
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
clearAllDBs: async () => {
|
|
150
|
+
try {
|
|
151
|
+
if ("databases" in indexedDB) {
|
|
152
|
+
const dbs = await (indexedDB as any).databases();
|
|
153
|
+
for (const db of dbs) {
|
|
154
|
+
if (db.name) await deleteDB(db.name);
|
|
155
|
+
}
|
|
156
|
+
return { success: true, message: "All DBs cleared" };
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
message: "clearAllDBs not supported in this browser",
|
|
161
|
+
};
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return { success: false, message: String(err) };
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export type CookieSet = {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string | object;
|
|
4
|
+
days?: number | "infinity";
|
|
5
|
+
path?: string;
|
|
6
|
+
sameSite?: "Lax" | "Strict" | "None";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SessionSet<T> = {
|
|
10
|
+
key: string;
|
|
11
|
+
value: T;
|
|
12
|
+
expiresMs?: number | boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SessionGet<T> = {
|
|
16
|
+
key: string;
|
|
17
|
+
expiredCheck?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type SessionGetHas = {
|
|
20
|
+
key: string;
|
|
21
|
+
expiredCheck?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class Storage {
|
|
25
|
+
private static prefix = "app_";
|
|
26
|
+
|
|
27
|
+
// ------------------- Set Prefix -------------------
|
|
28
|
+
static setPrefix(prefix: string) {
|
|
29
|
+
this.prefix = prefix;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ------------------- LocalStorage -------------------
|
|
33
|
+
static setItem<T>({
|
|
34
|
+
key,
|
|
35
|
+
value,
|
|
36
|
+
expiresMs = false,
|
|
37
|
+
}: {
|
|
38
|
+
key: string;
|
|
39
|
+
value: T;
|
|
40
|
+
expiresMs?: number | boolean;
|
|
41
|
+
}) {
|
|
42
|
+
const item =
|
|
43
|
+
typeof expiresMs === "number"
|
|
44
|
+
? {
|
|
45
|
+
value,
|
|
46
|
+
expires: expiresMs ? Date.now() + expiresMs : null,
|
|
47
|
+
}
|
|
48
|
+
: value;
|
|
49
|
+
localStorage.setItem(this.prefix + key, JSON.stringify(item));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static getItem<T>({
|
|
53
|
+
key,
|
|
54
|
+
expiredCheck = false,
|
|
55
|
+
}: {
|
|
56
|
+
key: string;
|
|
57
|
+
expiredCheck?: boolean;
|
|
58
|
+
}): T | null {
|
|
59
|
+
const itemStr = localStorage.getItem(this.prefix + key);
|
|
60
|
+
if (!itemStr) return null;
|
|
61
|
+
try {
|
|
62
|
+
const item = JSON.parse(itemStr);
|
|
63
|
+
if (expiredCheck === false) return item.value;
|
|
64
|
+
if (item.expires && Date.now() > item.expires) {
|
|
65
|
+
localStorage.removeItem(this.prefix + key);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return item.value;
|
|
69
|
+
} catch {
|
|
70
|
+
localStorage.removeItem(this.prefix + key);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static removeItem(key: string) {
|
|
76
|
+
localStorage.removeItem(this.prefix + key);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static clearStorage() {
|
|
80
|
+
Object.keys(localStorage)
|
|
81
|
+
.filter((key) => key.startsWith(this.prefix))
|
|
82
|
+
.forEach((key) => localStorage.removeItem(key));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static hasItem({
|
|
86
|
+
key,
|
|
87
|
+
expiredCheck = false,
|
|
88
|
+
}: {
|
|
89
|
+
key: string;
|
|
90
|
+
expiredCheck?: boolean;
|
|
91
|
+
}): boolean {
|
|
92
|
+
return this.getItem({ key, expiredCheck }) !== null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ------------------- Cookies -------------------
|
|
96
|
+
/**
|
|
97
|
+
* Sets a cookie.
|
|
98
|
+
* @param name - The name of the cookie.
|
|
99
|
+
* @param value - The value of the cookie.
|
|
100
|
+
* @param days - The number of days until the cookie expires.
|
|
101
|
+
* @param path - The path of the cookie.
|
|
102
|
+
* @param sameSite - The SameSite attribute of the cookie. Defaults to "Lax".
|
|
103
|
+
*/
|
|
104
|
+
static setCookie({
|
|
105
|
+
name,
|
|
106
|
+
value,
|
|
107
|
+
days,
|
|
108
|
+
path = "/",
|
|
109
|
+
sameSite = "Lax",
|
|
110
|
+
}: CookieSet) {
|
|
111
|
+
let val = typeof value === "object" ? JSON.stringify(value) : value;
|
|
112
|
+
let cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(
|
|
113
|
+
val
|
|
114
|
+
)}; path=${path}; SameSite=${sameSite}`;
|
|
115
|
+
if (days === "infinity") cookieStr += "; max-age=3153600000";
|
|
116
|
+
if (days && days !== "infinity") {
|
|
117
|
+
const expires = new Date();
|
|
118
|
+
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
|
119
|
+
cookieStr += `; expires=${expires.toUTCString()}`;
|
|
120
|
+
}
|
|
121
|
+
if (location.protocol === "https:") cookieStr += "; Secure";
|
|
122
|
+
document.cookie = cookieStr;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Retrieves a cookie value by name.
|
|
127
|
+
* @param name The name of the cookie.
|
|
128
|
+
* @returns The value of the cookie, or undefined if it does not exist.
|
|
129
|
+
* The value is parsed as JSON if it is a valid JSON string; otherwise it is
|
|
130
|
+
* returned as a string.
|
|
131
|
+
*/
|
|
132
|
+
static getCookie<T = string>(name: string): T | undefined {
|
|
133
|
+
const nameEQ = encodeURIComponent(name) + "=";
|
|
134
|
+
const ca = document.cookie.split(";").map((c) => c.trim());
|
|
135
|
+
for (let c of ca) {
|
|
136
|
+
if (c.indexOf(nameEQ) === 0) {
|
|
137
|
+
const val = decodeURIComponent(c.substring(nameEQ.length));
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(val) as T;
|
|
140
|
+
} catch {
|
|
141
|
+
return val as unknown as T;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Removes a cookie by name.
|
|
150
|
+
* @param name The name of the cookie to remove.
|
|
151
|
+
* @param path The path of the cookie to remove; defaults to "/".
|
|
152
|
+
*/
|
|
153
|
+
static removeCookie(name: string, path = "/") {
|
|
154
|
+
document.cookie = `${encodeURIComponent(
|
|
155
|
+
name
|
|
156
|
+
)}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Checks if a cookie with the given name exists.
|
|
161
|
+
* @param name The name of the cookie to check.
|
|
162
|
+
* @returns true if the cookie exists, false otherwise.
|
|
163
|
+
*/
|
|
164
|
+
static cookieExists(name: string): boolean {
|
|
165
|
+
return this.getCookie(name) !== undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//------------------- Session Storage -------------------
|
|
169
|
+
static setSessionItem<T>({ key, value, expiresMs = false }: SessionSet<T>) {
|
|
170
|
+
const item =
|
|
171
|
+
typeof expiresMs === "number"
|
|
172
|
+
? {
|
|
173
|
+
value,
|
|
174
|
+
expires: expiresMs ? Date.now() + expiresMs : null,
|
|
175
|
+
}
|
|
176
|
+
: value;
|
|
177
|
+
sessionStorage.setItem(this.prefix + key, JSON.stringify(item));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static getSessionItem<T>({
|
|
181
|
+
key,
|
|
182
|
+
expiredCheck = false,
|
|
183
|
+
}: SessionGet<T>): T | null {
|
|
184
|
+
const itemStr = sessionStorage.getItem(this.prefix + key);
|
|
185
|
+
if (!itemStr) return null;
|
|
186
|
+
try {
|
|
187
|
+
const item = JSON.parse(itemStr);
|
|
188
|
+
if (expiredCheck === false) return item.value;
|
|
189
|
+
if (item.expires && Date.now() > item.expires) {
|
|
190
|
+
sessionStorage.removeItem(this.prefix + key);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return item.value;
|
|
194
|
+
} catch {
|
|
195
|
+
sessionStorage.removeItem(this.prefix + key);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static removeSessionItem(key: string) {
|
|
201
|
+
sessionStorage.removeItem(this.prefix + key);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
static clearSession() {
|
|
205
|
+
Object.keys(sessionStorage)
|
|
206
|
+
.filter((key) => key.startsWith(this.prefix))
|
|
207
|
+
.forEach((key) => sessionStorage.removeItem(key));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static hasSessionItem({ key, expiredCheck = false }: SessionGetHas): boolean {
|
|
211
|
+
return this.getSessionItem({ key, expiredCheck }) !== null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// countdownStore.ts
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
|
|
4
|
+
export type CountdownState = {
|
|
5
|
+
timeLeft: number;
|
|
6
|
+
isRunning: boolean;
|
|
7
|
+
freezing: boolean;
|
|
8
|
+
setTime: (time: number) => void;
|
|
9
|
+
decrement: () => void;
|
|
10
|
+
reset: () => void;
|
|
11
|
+
start: () => void;
|
|
12
|
+
stop: () => void;
|
|
13
|
+
freeze: () => void;
|
|
14
|
+
defrost: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useCountdownStore = create<CountdownState>((set) => ({
|
|
18
|
+
timeLeft: 0,
|
|
19
|
+
isRunning: false,
|
|
20
|
+
freezing: false,
|
|
21
|
+
|
|
22
|
+
setTime: (time) => set({ timeLeft: time }),
|
|
23
|
+
|
|
24
|
+
decrement: () =>
|
|
25
|
+
set((state) =>
|
|
26
|
+
state.timeLeft > 0 ? { timeLeft: state.timeLeft - 1 } : state
|
|
27
|
+
),
|
|
28
|
+
|
|
29
|
+
reset: () => set({ timeLeft: 0, isRunning: false }),
|
|
30
|
+
|
|
31
|
+
start: () => set({ isRunning: true }),
|
|
32
|
+
|
|
33
|
+
stop: () => set({ isRunning: false }),
|
|
34
|
+
|
|
35
|
+
freeze: () => set({ freezing: true }),
|
|
36
|
+
|
|
37
|
+
defrost: () => set({ freezing: false }),
|
|
38
|
+
}));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
} from 'react';
|
|
5
|
+
|
|
6
|
+
export type FadeOnScrollProps = {
|
|
7
|
+
ref: React.RefObject<HTMLElement>;
|
|
8
|
+
threshold?: number; // Intersection Observer threshold
|
|
9
|
+
repeat?: boolean; // If true, will toggle visibility on entering/exiting viewport
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* useFadeUpOnScroll - Hook to track if element is visible in viewport
|
|
14
|
+
* @param {React.RefObject<HTMLElement>} ref - Ref to the element to observe
|
|
15
|
+
* @param {number} [threshold=0.5] - IntersectionObserver threshold
|
|
16
|
+
* @param {boolean} [repeat=false] - If true, will toggle visibility on entering/exiting viewport
|
|
17
|
+
* @return {boolean} - true if element is visible in viewport
|
|
18
|
+
*/
|
|
19
|
+
export const useFadeUpOnScroll = ({ ref, threshold = 0.5, repeat = false }: FadeOnScrollProps): boolean => {
|
|
20
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const observer = new IntersectionObserver(
|
|
24
|
+
(entries) => {
|
|
25
|
+
entries.forEach((entry) => {
|
|
26
|
+
if (repeat) {
|
|
27
|
+
// If repeat: toggle in the state in/out Viewport
|
|
28
|
+
setIsVisible(entry.isIntersecting);
|
|
29
|
+
} else {
|
|
30
|
+
// If not repeat: just set true once
|
|
31
|
+
if (entry.isIntersecting) {
|
|
32
|
+
setIsVisible((prev) => prev || true);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
{ threshold }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const node = ref.current;
|
|
41
|
+
if (node) observer.observe(node);
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
if (node) observer.unobserve(node);
|
|
45
|
+
observer.disconnect();
|
|
46
|
+
};
|
|
47
|
+
}, [threshold, ref, repeat]);
|
|
48
|
+
|
|
49
|
+
return isVisible;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default useFadeUpOnScroll;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useRef,
|
|
4
|
+
} from 'react';
|
|
5
|
+
|
|
6
|
+
import { debounce as lodashDebounce } from 'lodash';
|
|
7
|
+
|
|
8
|
+
export type Options = {
|
|
9
|
+
loading: boolean;
|
|
10
|
+
enableDebounce?: boolean;
|
|
11
|
+
debounceMs?: number;
|
|
12
|
+
preventDoubleCall?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wraps an action function to prevent double calls, and debounce action calls
|
|
17
|
+
* if needed.
|
|
18
|
+
*
|
|
19
|
+
* @param {() => Promise<void> | void} action
|
|
20
|
+
* @param {{
|
|
21
|
+
* loading: boolean;
|
|
22
|
+
* enableDebounce?: boolean;
|
|
23
|
+
* debounceMs?: number;
|
|
24
|
+
* preventDoubleCall?: boolean;
|
|
25
|
+
* }} options
|
|
26
|
+
*
|
|
27
|
+
* @returns {(args: any[]) => Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export const useSafeAction = (action: () => Promise<void> | void,
|
|
31
|
+
{
|
|
32
|
+
loading,
|
|
33
|
+
enableDebounce = false,
|
|
34
|
+
debounceMs = 300,
|
|
35
|
+
preventDoubleCall = true,
|
|
36
|
+
}: Options
|
|
37
|
+
): (args: any[]) => Promise<void> => {
|
|
38
|
+
const isHandling = useRef(false);
|
|
39
|
+
|
|
40
|
+
const wrapped = useCallback(async () => {
|
|
41
|
+
if (loading || (preventDoubleCall && isHandling.current)) return;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
isHandling.current = true;
|
|
45
|
+
await action();
|
|
46
|
+
} finally {
|
|
47
|
+
isHandling.current = false;
|
|
48
|
+
}
|
|
49
|
+
}, [action, loading, preventDoubleCall]);
|
|
50
|
+
|
|
51
|
+
if (enableDebounce) {
|
|
52
|
+
return useCallback(
|
|
53
|
+
lodashDebounce(wrapped, debounceMs, { leading: true, trailing: false }),
|
|
54
|
+
[wrapped, debounceMs]
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return wrapped;
|
|
59
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export type SpamGuardProps = {
|
|
4
|
+
limit?: number;
|
|
5
|
+
timeFrame?: number;
|
|
6
|
+
onSpam?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useSpamGuard = ({
|
|
10
|
+
limit = 1,
|
|
11
|
+
timeFrame = 600,
|
|
12
|
+
onSpam,
|
|
13
|
+
}: SpamGuardProps = {}): () => boolean => {
|
|
14
|
+
const actionsRef = useRef<number[]>([]);
|
|
15
|
+
|
|
16
|
+
const checkSpam = () => {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const updatedActions = actionsRef.current.filter((t) => now - t < timeFrame);
|
|
19
|
+
|
|
20
|
+
if (updatedActions.length >= limit) {
|
|
21
|
+
onSpam?.();
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
updatedActions.push(now);
|
|
26
|
+
actionsRef.current = updatedActions;
|
|
27
|
+
return true;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return checkSpam;
|
|
31
|
+
}
|