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,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
createFeatureDefaults,
|
|
4
|
+
FeatureConfig,
|
|
5
|
+
FlagMap,
|
|
6
|
+
} from './feature-config';
|
|
7
|
+
import { FeatureKey } from './keys';
|
|
8
|
+
|
|
9
|
+
const disableFlags: FlagMap<FeatureKey> = {
|
|
10
|
+
[FeatureKey.HomeCreateProduct]: true,
|
|
11
|
+
[FeatureKey.HomeCreateCustomer]: true,
|
|
12
|
+
[FeatureKey.HomeCreateOrder]: false,
|
|
13
|
+
[FeatureKey.HomeCreateInvoice]: true,
|
|
14
|
+
[FeatureKey.HomeCreatePayment]: true,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const defaultFlags = createFeatureDefaults(FeatureKey);
|
|
20
|
+
|
|
21
|
+
export const {
|
|
22
|
+
Feature,
|
|
23
|
+
FeatureLock,
|
|
24
|
+
FeatureProvider,
|
|
25
|
+
useFeature,
|
|
26
|
+
useFlags,
|
|
27
|
+
refresh,
|
|
28
|
+
setFlags,
|
|
29
|
+
sources,
|
|
30
|
+
} = FeatureConfig({
|
|
31
|
+
keys: { ...FeatureKey },
|
|
32
|
+
initialFlags: {...defaultFlags, ...disableFlags},
|
|
33
|
+
});
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// A reusable, library-style Feature Flags system for React/TypeScript
|
|
2
|
+
// ---------------------------------------------------------------
|
|
3
|
+
// Quick start:
|
|
4
|
+
//
|
|
5
|
+
// import { FeatureConfig } from "./feature-config";
|
|
6
|
+
// import { FeatureKey } from "./feature-keys";
|
|
7
|
+
//
|
|
8
|
+
// export const {
|
|
9
|
+
// FeatureProvider,
|
|
10
|
+
// Feature,
|
|
11
|
+
// FeatureLock,
|
|
12
|
+
// useFeature,
|
|
13
|
+
// useFlags,
|
|
14
|
+
// refresh,
|
|
15
|
+
// setFlags,
|
|
16
|
+
// sources,
|
|
17
|
+
// } = FeatureConfig({
|
|
18
|
+
// keys: FeatureKey, // enum or keys array
|
|
19
|
+
// sources: [
|
|
20
|
+
// sources.local({
|
|
21
|
+
// flags: {
|
|
22
|
+
// [FeatureKey.NewDashboard]: false,
|
|
23
|
+
// [FeatureKey.BetaButton]: true,
|
|
24
|
+
// },
|
|
25
|
+
// priority: 0,
|
|
26
|
+
// }),
|
|
27
|
+
// sources.storage({ storageKey: "feature_flags", priority: 5 }),
|
|
28
|
+
// sources.remote({
|
|
29
|
+
// priority: 10, // remote override local
|
|
30
|
+
// fetch: async () => {
|
|
31
|
+
// const res = await fetch("/api/feature-flags");
|
|
32
|
+
// return (await res.json()) as Record<string, boolean>;
|
|
33
|
+
// },
|
|
34
|
+
// transform: (raw) => raw, // optional map
|
|
35
|
+
// }),
|
|
36
|
+
// ],
|
|
37
|
+
// strategy: "last-wins", // "any-true" | "all-true" | custom reducer
|
|
38
|
+
// strict: "warn", // "error" | "warn" | "silent"
|
|
39
|
+
// });
|
|
40
|
+
//
|
|
41
|
+
// // App root
|
|
42
|
+
// <FeatureProvider>{children}</FeatureProvider>
|
|
43
|
+
//
|
|
44
|
+
// // Usage
|
|
45
|
+
// <Feature feature={FeatureKey.NewDashboard}>New UI</Feature>
|
|
46
|
+
// <FeatureLock feature={FeatureKey.BetaButton}>Beta locked</FeatureLock>
|
|
47
|
+
// ---------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
"use client";
|
|
50
|
+
import {
|
|
51
|
+
createContext,
|
|
52
|
+
ReactNode,
|
|
53
|
+
useContext,
|
|
54
|
+
useEffect,
|
|
55
|
+
useMemo,
|
|
56
|
+
useState,
|
|
57
|
+
} from 'react';
|
|
58
|
+
|
|
59
|
+
// -----------------------------
|
|
60
|
+
// Types
|
|
61
|
+
// -----------------------------
|
|
62
|
+
export type EnumLike = Record<string, string | number> | readonly string[];
|
|
63
|
+
|
|
64
|
+
type InferKeys<E extends EnumLike> = E extends readonly string[]
|
|
65
|
+
? E[number]
|
|
66
|
+
: E extends Record<string, infer V>
|
|
67
|
+
? Extract<V, string> | Extract<V, number> extends infer U
|
|
68
|
+
? Extract<U, string> // we only accept string keys for flags
|
|
69
|
+
: never
|
|
70
|
+
: never;
|
|
71
|
+
|
|
72
|
+
export type FlagMap<K extends string> = Partial<Record<K, boolean>>;
|
|
73
|
+
|
|
74
|
+
export type FeatureSource<K extends string> = {
|
|
75
|
+
name?: string;
|
|
76
|
+
priority?: number; // higher runs later if last-wins
|
|
77
|
+
load: (ctx: {
|
|
78
|
+
abortSignal?: AbortSignal;
|
|
79
|
+
}) => Promise<FlagMap<K>> | FlagMap<K>;
|
|
80
|
+
save?: (flags: FlagMap<K>) => Promise<void> | void; // optional, for writable stores
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type MergeStrategy<K extends string> =
|
|
84
|
+
| "last-wins"
|
|
85
|
+
| "any-true"
|
|
86
|
+
| "all-true"
|
|
87
|
+
| ((entries: { name: string; flags: FlagMap<K> }[]) => FlagMap<K>);
|
|
88
|
+
|
|
89
|
+
export type StrictMode = "error" | "warn" | "silent";
|
|
90
|
+
|
|
91
|
+
export interface FeatureOptions<
|
|
92
|
+
E extends EnumLike,
|
|
93
|
+
K extends string = InferKeys<E> & string
|
|
94
|
+
> {
|
|
95
|
+
keys: E; // enum object or readonly string[]
|
|
96
|
+
sources?: FeatureSource<K>[];
|
|
97
|
+
strategy?: MergeStrategy<K>;
|
|
98
|
+
strict?: StrictMode;
|
|
99
|
+
initialFlags?: FlagMap<K>; // SSR hydration or preloaded
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// -----------------------------
|
|
103
|
+
// Helpers
|
|
104
|
+
// -----------------------------
|
|
105
|
+
function normalizeEnum<E extends EnumLike>(e: E): string[] {
|
|
106
|
+
if (Array.isArray(e)) return e as string[];
|
|
107
|
+
// TS enum has both keys and reverse mapping for numeric enums –
|
|
108
|
+
// we're interested in string values only.
|
|
109
|
+
return Object.values(e).filter((v): v is string => typeof v === "string");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function filterUnknownKeys<K extends string>(
|
|
113
|
+
keys: readonly string[],
|
|
114
|
+
flags: FlagMap<K>,
|
|
115
|
+
strict: StrictMode,
|
|
116
|
+
sourceName: string
|
|
117
|
+
): FlagMap<K> {
|
|
118
|
+
const allowed = new Set(keys);
|
|
119
|
+
const out: FlagMap<K> = {};
|
|
120
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
121
|
+
if (allowed.has(k)) {
|
|
122
|
+
(out as any)[k] = !!v;
|
|
123
|
+
} else if (strict !== "silent") {
|
|
124
|
+
const msg = `[FeatureConfig] Unknown key "${k}" from ${sourceName}`;
|
|
125
|
+
strict === "error" ? console.error(msg) : console.warn(msg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function mergeFlags<K extends string>(
|
|
132
|
+
entries: { name: string; flags: FlagMap<K> }[],
|
|
133
|
+
strategy: MergeStrategy<K>
|
|
134
|
+
): FlagMap<K> {
|
|
135
|
+
if (typeof strategy === "function") return strategy(entries);
|
|
136
|
+
|
|
137
|
+
if (strategy === "any-true") {
|
|
138
|
+
const out: FlagMap<K> = {};
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
for (const [k, v] of Object.entries(e.flags)) {
|
|
141
|
+
(out as any)[k] = Boolean((out as any)[k]) || Boolean(v);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (strategy === "all-true") {
|
|
148
|
+
// Start with union of keys, then AND them across sources
|
|
149
|
+
const out: Record<string, boolean> = {};
|
|
150
|
+
const allKeys = new Set<string>();
|
|
151
|
+
entries.forEach((e) => Object.keys(e.flags).forEach((k) => allKeys.add(k)));
|
|
152
|
+
allKeys.forEach((k) => {
|
|
153
|
+
out[k] = entries.every((e) => e.flags[k as K] === true);
|
|
154
|
+
});
|
|
155
|
+
return out as FlagMap<K>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// default: last-wins (based on order of entries)
|
|
159
|
+
const out: FlagMap<K> = {};
|
|
160
|
+
for (const e of entries) Object.assign(out, e.flags);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// -----------------------------
|
|
165
|
+
// Built-in Sources (adapters)
|
|
166
|
+
// -----------------------------
|
|
167
|
+
function localSource<K extends string>(opts: {
|
|
168
|
+
flags: FlagMap<K>;
|
|
169
|
+
priority?: number;
|
|
170
|
+
name?: string;
|
|
171
|
+
}): FeatureSource<K> {
|
|
172
|
+
const { flags, priority = 0, name = "local" } = opts;
|
|
173
|
+
return {
|
|
174
|
+
name,
|
|
175
|
+
priority,
|
|
176
|
+
load: () => flags,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function storageSource<K extends string>(opts: {
|
|
181
|
+
storageKey: string;
|
|
182
|
+
priority?: number;
|
|
183
|
+
name?: string;
|
|
184
|
+
storage?: Storage | null; // default localStorage if available
|
|
185
|
+
}): FeatureSource<K> {
|
|
186
|
+
const { storageKey, priority = 0, name = "storage", storage } = opts;
|
|
187
|
+
return {
|
|
188
|
+
name,
|
|
189
|
+
priority,
|
|
190
|
+
load: () => {
|
|
191
|
+
try {
|
|
192
|
+
const s =
|
|
193
|
+
storage ??
|
|
194
|
+
(typeof window !== "undefined" ? window.localStorage : null);
|
|
195
|
+
const raw = s?.getItem(storageKey);
|
|
196
|
+
return raw ? (JSON.parse(raw) as FlagMap<K>) : {};
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.warn(`[FeatureConfig] storageSource load error`, e);
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function remoteSource<K extends string>(opts: {
|
|
206
|
+
fetch: (ctx: {
|
|
207
|
+
abortSignal?: AbortSignal;
|
|
208
|
+
}) => Promise<Record<string, boolean>>;
|
|
209
|
+
transform?: (raw: Record<string, boolean>) => FlagMap<K>;
|
|
210
|
+
priority?: number;
|
|
211
|
+
name?: string;
|
|
212
|
+
}): FeatureSource<K> {
|
|
213
|
+
const { fetch, transform, priority = 10, name = "remote" } = opts;
|
|
214
|
+
return {
|
|
215
|
+
name,
|
|
216
|
+
priority,
|
|
217
|
+
load: async ({ abortSignal }) => {
|
|
218
|
+
const raw = await fetch({ abortSignal });
|
|
219
|
+
return transform ? transform(raw) : (raw as FlagMap<K>);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// -----------------------------
|
|
225
|
+
// Factory – FeatureConfig
|
|
226
|
+
// -----------------------------
|
|
227
|
+
export function FeatureConfig<
|
|
228
|
+
E extends EnumLike,
|
|
229
|
+
K extends string = InferKeys<E> & string
|
|
230
|
+
>(options: FeatureOptions<E, K>) {
|
|
231
|
+
const allKeys = normalizeEnum(options.keys);
|
|
232
|
+
const strict: StrictMode = options.strict ?? "warn";
|
|
233
|
+
const strategy: MergeStrategy<K> = options.strategy ?? "last-wins";
|
|
234
|
+
|
|
235
|
+
// Context shape
|
|
236
|
+
type Ctx = {
|
|
237
|
+
flags: FlagMap<K>;
|
|
238
|
+
loading: boolean;
|
|
239
|
+
refresh: () => Promise<void>;
|
|
240
|
+
setFlags: (patch: FlagMap<K> | ((prev: FlagMap<K>) => FlagMap<K>)) => void;
|
|
241
|
+
isEnabled: (k: K) => boolean;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const FeatureContext = createContext<Ctx | null>(null);
|
|
245
|
+
|
|
246
|
+
function FeatureProvider({ children }: { children: ReactNode }) {
|
|
247
|
+
const [flags, _setFlags] = useState<FlagMap<K>>(options.initialFlags ?? {});
|
|
248
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
249
|
+
const sourcesSorted = useMemo(
|
|
250
|
+
() =>
|
|
251
|
+
(options.sources ?? [])
|
|
252
|
+
.map((s, i) => ({
|
|
253
|
+
i,
|
|
254
|
+
s,
|
|
255
|
+
priority: s.priority ?? 0,
|
|
256
|
+
name: s.name ?? `source_${i}`,
|
|
257
|
+
}))
|
|
258
|
+
.sort((a, b) => a.priority - b.priority),
|
|
259
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
260
|
+
[]
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const safeSetFlags = (
|
|
264
|
+
next: FlagMap<K> | ((prev: FlagMap<K>) => FlagMap<K>)
|
|
265
|
+
) => {
|
|
266
|
+
_setFlags((prev) => {
|
|
267
|
+
const nextFlags =
|
|
268
|
+
typeof next === "function" ? (next as any)(prev) : next;
|
|
269
|
+
return nextFlags;
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const refresh = async (): Promise<void> => {
|
|
274
|
+
if (!sourcesSorted.length) {
|
|
275
|
+
// even without sources, enforce key filtering on initial flags
|
|
276
|
+
setLoading(false);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
setLoading(true);
|
|
280
|
+
const ctrl = new AbortController();
|
|
281
|
+
const collected: { name: string; flags: FlagMap<K> }[] = [];
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
for (const { s, name } of sourcesSorted) {
|
|
285
|
+
try {
|
|
286
|
+
const data = await s.load({ abortSignal: ctrl.signal });
|
|
287
|
+
const filtered = filterUnknownKeys<K>(allKeys, data, strict, name);
|
|
288
|
+
collected.push({ name, flags: filtered });
|
|
289
|
+
} catch (e) {
|
|
290
|
+
console.warn(`[FeatureConfig] Source "${name}" load failed`, e);
|
|
291
|
+
collected.push({ name, flags: {} });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const merged = mergeFlags<K>(collected, strategy);
|
|
295
|
+
// also filter initial flags/options
|
|
296
|
+
const initialFiltered = filterUnknownKeys<K>(
|
|
297
|
+
allKeys,
|
|
298
|
+
flags,
|
|
299
|
+
strict,
|
|
300
|
+
"initial"
|
|
301
|
+
);
|
|
302
|
+
_setFlags({ ...initialFiltered, ...merged });
|
|
303
|
+
} finally {
|
|
304
|
+
setLoading(false);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
// On mount: validate initial flags & then fetch from sources
|
|
310
|
+
const initialFiltered = filterUnknownKeys<K>(
|
|
311
|
+
allKeys,
|
|
312
|
+
options.initialFlags ?? {},
|
|
313
|
+
strict,
|
|
314
|
+
"initial"
|
|
315
|
+
);
|
|
316
|
+
if (Object.keys(initialFiltered).length) _setFlags(initialFiltered);
|
|
317
|
+
// Fetch async sources
|
|
318
|
+
refresh();
|
|
319
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
322
|
+
const isEnabled = (k: K) => Boolean(flags[k]);
|
|
323
|
+
|
|
324
|
+
const ctx: Ctx = {
|
|
325
|
+
flags,
|
|
326
|
+
loading,
|
|
327
|
+
refresh,
|
|
328
|
+
setFlags: safeSetFlags,
|
|
329
|
+
isEnabled,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<FeatureContext.Provider value={ctx}>{children}</FeatureContext.Provider>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Hooks & Components
|
|
338
|
+
function useFlags() {
|
|
339
|
+
const ctx = useContext(FeatureContext);
|
|
340
|
+
if (!ctx) throw new Error("useFlags must be used inside FeatureProvider");
|
|
341
|
+
return ctx;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function useFeature(key: K) {
|
|
345
|
+
const { isEnabled, loading } = useFlags();
|
|
346
|
+
return { enabled: isEnabled(key), loading };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
type GateProps = {
|
|
350
|
+
feature: K;
|
|
351
|
+
fallback?: ReactNode;
|
|
352
|
+
loadingFallback?: ReactNode;
|
|
353
|
+
children: ReactNode;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
function Feature({ feature, fallback = null, loadingFallback = null, children }: GateProps) {
|
|
357
|
+
const { enabled, loading } = useFeature(feature);
|
|
358
|
+
if (loading) return <>{loadingFallback}</>;
|
|
359
|
+
return <>{enabled ? children : fallback}</>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function FeatureLock({ feature, fallback = null, loadingFallback = null, children }: GateProps) {
|
|
363
|
+
const { enabled, loading } = useFeature(feature);
|
|
364
|
+
if (loading) return <>{loadingFallback}</>;
|
|
365
|
+
return <>{enabled ? fallback : children}</>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// public helpers (instance-scoped)
|
|
369
|
+
const api = {
|
|
370
|
+
FeatureProvider,
|
|
371
|
+
useFeature,
|
|
372
|
+
useFlags,
|
|
373
|
+
Feature,
|
|
374
|
+
FeatureLock,
|
|
375
|
+
refresh: () => {
|
|
376
|
+
const ctx = (FeatureContext as any)._currentValue as ReturnType<
|
|
377
|
+
typeof useFlags
|
|
378
|
+
> | null;
|
|
379
|
+
// Note: in strict React this isn't guaranteed across roots; prefer calling useFlags() in components.
|
|
380
|
+
if (ctx?.refresh) return ctx.refresh();
|
|
381
|
+
return Promise.resolve();
|
|
382
|
+
},
|
|
383
|
+
setFlags: (patch: FlagMap<K> | ((prev: FlagMap<K>) => FlagMap<K>)) => {
|
|
384
|
+
const ctx = (FeatureContext as any)._currentValue as ReturnType<
|
|
385
|
+
typeof useFlags
|
|
386
|
+
> | null;
|
|
387
|
+
if (ctx?.setFlags) ctx.setFlags(patch);
|
|
388
|
+
},
|
|
389
|
+
sources: {
|
|
390
|
+
local: localSource<K>,
|
|
391
|
+
storage: storageSource<K>,
|
|
392
|
+
remote: remoteSource<K>,
|
|
393
|
+
},
|
|
394
|
+
} as const;
|
|
395
|
+
|
|
396
|
+
return api;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// feature-config-builder
|
|
400
|
+
/**
|
|
401
|
+
* automatically create feature defaults from enum
|
|
402
|
+
* @param keysEnum - enum of feature keys
|
|
403
|
+
* @param manualDefaults - manual overrides for defaults
|
|
404
|
+
* @param globalDefault - global default for all features (default: true)
|
|
405
|
+
* @returns - feature defaults object
|
|
406
|
+
*/
|
|
407
|
+
export function createFeatureDefaults<
|
|
408
|
+
T extends Record<string, string> | Record<string, number>
|
|
409
|
+
>(
|
|
410
|
+
keysEnum: T,
|
|
411
|
+
manualDefaults: Partial<Record<T[keyof T], boolean>> = {},
|
|
412
|
+
globalDefault: boolean = true
|
|
413
|
+
): Record<T[keyof T], boolean> {
|
|
414
|
+
const keys = Object.values(keysEnum) as T[keyof T][];
|
|
415
|
+
const defaults: Record<T[keyof T], boolean> = {} as any;
|
|
416
|
+
|
|
417
|
+
for (const key of keys) {
|
|
418
|
+
defaults[key] = globalDefault;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// merge manual override
|
|
422
|
+
return { ...defaults, ...manualDefaults };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// -----------------------------
|
|
426
|
+
// Example enum (you can delete this from your build and place in a separate file)
|
|
427
|
+
// -----------------------------
|
|
428
|
+
// export enum FeatureKey {
|
|
429
|
+
// NewDashboard = "newDashboard",
|
|
430
|
+
// BetaButton = "betaButton",
|
|
431
|
+
// AiAssistant = "aiAssistant",
|
|
432
|
+
// }
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// const response = await apiHandler.get('/users');
|
|
2
|
+
// const response = await apiHandler.post('/users', { name: 'John Doe' });
|
|
3
|
+
// const response = await apiHandler.put('/users/1', { name: 'Jane Doe' });
|
|
4
|
+
// const response = await apiHandler.delete('/users/1');
|
|
5
|
+
|
|
6
|
+
import axios, {
|
|
7
|
+
AxiosInstance,
|
|
8
|
+
AxiosRequestConfig,
|
|
9
|
+
AxiosResponse,
|
|
10
|
+
} from 'axios';
|
|
11
|
+
|
|
12
|
+
/** Generic API response */
|
|
13
|
+
export interface ApiResponse<T> {
|
|
14
|
+
data: T;
|
|
15
|
+
message: string;
|
|
16
|
+
status: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Custom error class */
|
|
20
|
+
export class ApiError extends Error {
|
|
21
|
+
data: any;
|
|
22
|
+
status: number;
|
|
23
|
+
|
|
24
|
+
constructor(message: string, data: any, status: number) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "ApiError";
|
|
27
|
+
this.data = data;
|
|
28
|
+
this.status = status;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ApiServiceOptions = {
|
|
33
|
+
baseURL?: string;
|
|
34
|
+
/** Callback trả về token runtime, có thể từ localStorage, cookie, context, etc. */
|
|
35
|
+
getToken?: () => string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ApiServices {
|
|
39
|
+
private axiosInstance: AxiosInstance;
|
|
40
|
+
private getToken?: () => string | null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Constructor
|
|
44
|
+
*
|
|
45
|
+
* @param {ApiServiceOptions} [options] Options
|
|
46
|
+
* @param {string} [options.baseURL] Base URL of API
|
|
47
|
+
* @param {() => string | null} [options.getToken] Callback trả về token runtime
|
|
48
|
+
*/
|
|
49
|
+
constructor(options?: ApiServiceOptions) {
|
|
50
|
+
const baseURL =
|
|
51
|
+
options?.baseURL ||
|
|
52
|
+
(typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL : undefined) ||
|
|
53
|
+
"/api";
|
|
54
|
+
|
|
55
|
+
this.getToken = options?.getToken;
|
|
56
|
+
|
|
57
|
+
this.axiosInstance = axios.create({ baseURL });
|
|
58
|
+
this.initializeInterceptors();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize interceptors
|
|
63
|
+
*
|
|
64
|
+
* @private
|
|
65
|
+
* @memberof ApiServices
|
|
66
|
+
*/
|
|
67
|
+
private initializeInterceptors() {
|
|
68
|
+
this.axiosInstance.interceptors.request.use((config) => {
|
|
69
|
+
const token = this.getToken?.();
|
|
70
|
+
if (token) {
|
|
71
|
+
if (!config.headers) config.headers = { ...{} } as typeof config.headers;
|
|
72
|
+
config.headers["Authorization"] = `Bearer ${token}`;
|
|
73
|
+
}
|
|
74
|
+
return config;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Send a request to API
|
|
80
|
+
*
|
|
81
|
+
* @param {("get" | "post" | "put" | "delete")} method Request method
|
|
82
|
+
* @param {string} url Request URL
|
|
83
|
+
* @param {any} [data] Data to send in request body
|
|
84
|
+
* @param {AxiosRequestConfig} [config] Configuration for axios
|
|
85
|
+
* @returns {Promise<ApiResponse<T>>} Promise resolves to ApiResponse
|
|
86
|
+
* @throws {ApiError} If request fails
|
|
87
|
+
*
|
|
88
|
+
* @private
|
|
89
|
+
* @memberof ApiServices
|
|
90
|
+
*/
|
|
91
|
+
private async request<T>(
|
|
92
|
+
method: "get" | "post" | "put" | "delete" | "patch",
|
|
93
|
+
url: string,
|
|
94
|
+
data?: any,
|
|
95
|
+
config?: AxiosRequestConfig
|
|
96
|
+
): Promise<ApiResponse<T>> {
|
|
97
|
+
try {
|
|
98
|
+
const response: AxiosResponse<T> =
|
|
99
|
+
method === "get"
|
|
100
|
+
? await this.axiosInstance.get(url, config)
|
|
101
|
+
: method === "post"
|
|
102
|
+
? await this.axiosInstance.post(url, data, config)
|
|
103
|
+
: method === "put"
|
|
104
|
+
? await this.axiosInstance.put(url, data, config)
|
|
105
|
+
: await this.axiosInstance.delete(url, config);
|
|
106
|
+
|
|
107
|
+
return { data: response.data, message: "Success", status: response.status };
|
|
108
|
+
} catch (error: any) {
|
|
109
|
+
if (error.response) {
|
|
110
|
+
throw new ApiError(error.response.data?.message || "Server Error", error.response.data, error.response.status);
|
|
111
|
+
} else if (error.request) {
|
|
112
|
+
throw new ApiError("No response from server", null, 0);
|
|
113
|
+
} else {
|
|
114
|
+
throw new ApiError(error.message, null, -1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Send a GET request to API
|
|
121
|
+
*
|
|
122
|
+
* @param {string} url Request URL
|
|
123
|
+
* @param {AxiosRequestConfig} [config] Configuration for axios
|
|
124
|
+
* @returns {Promise<ApiResponse<T>>} Promise resolves to ApiResponse
|
|
125
|
+
* @throws {ApiError} If request fails
|
|
126
|
+
*
|
|
127
|
+
* @public
|
|
128
|
+
* @memberof ApiServices
|
|
129
|
+
*/
|
|
130
|
+
get<T>(url: string, config?: AxiosRequestConfig) {
|
|
131
|
+
return this.request<T>("get", url, undefined, config);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Send a POST request to API
|
|
136
|
+
*
|
|
137
|
+
* @param {string} url Request URL
|
|
138
|
+
* @param {any} data Request body
|
|
139
|
+
* @param {AxiosRequestConfig} [config] Configuration for axios
|
|
140
|
+
* @returns {Promise<ApiResponse<T>>} Promise resolves to ApiResponse
|
|
141
|
+
* @throws {ApiError} If request fails
|
|
142
|
+
*
|
|
143
|
+
* @public
|
|
144
|
+
* @memberof ApiServices
|
|
145
|
+
*/
|
|
146
|
+
post<T>(url: string, data: any, config?: AxiosRequestConfig) {
|
|
147
|
+
return this.request<T>("post", url, data, config);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Send a PUT request to API
|
|
152
|
+
*
|
|
153
|
+
* @param {string} url Request URL
|
|
154
|
+
* @param {any} data Request body
|
|
155
|
+
* @param {AxiosRequestConfig} [config] Configuration for axios
|
|
156
|
+
* @returns {Promise<ApiResponse<T>>} Promise resolves to ApiResponse
|
|
157
|
+
* @throws {ApiError} If request fails
|
|
158
|
+
*
|
|
159
|
+
* @public
|
|
160
|
+
* @memberof ApiServices
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
put<T>(url: string, data: any, config?: AxiosRequestConfig) {
|
|
164
|
+
return this.request<T>("put", url, data, config);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Send a PATCH request to API
|
|
169
|
+
*
|
|
170
|
+
* @param {string} url Request URL
|
|
171
|
+
* @param {any} data Request body
|
|
172
|
+
* @param {AxiosRequestConfig} [config] Configuration for axios
|
|
173
|
+
* @returns {Promise<ApiResponse<T>>} Promise resolves to ApiResponse
|
|
174
|
+
* @throws {ApiError} If request fails
|
|
175
|
+
*
|
|
176
|
+
* @public
|
|
177
|
+
* @memberof ApiServices
|
|
178
|
+
*/
|
|
179
|
+
patch<T>(url: string, data: any, config?: AxiosRequestConfig) {
|
|
180
|
+
return this.request<T>("patch", url, data, config);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Send a DELETE request to API
|
|
185
|
+
*
|
|
186
|
+
* @param {string} url Request URL
|
|
187
|
+
* @param {AxiosRequestConfig} [config] Configuration for axios
|
|
188
|
+
* @returns {Promise<ApiResponse<T>>} Promise resolves to ApiResponse
|
|
189
|
+
* @throws {ApiError} If request fails
|
|
190
|
+
*
|
|
191
|
+
* @public
|
|
192
|
+
* @memberof ApiServices
|
|
193
|
+
*/
|
|
194
|
+
delete<T>(url: string, config?: AxiosRequestConfig) {
|
|
195
|
+
return this.request<T>("delete", url, undefined, config);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Default singleton handler, uses token from localStorage */
|
|
200
|
+
export const apiHandler = new ApiServices({
|
|
201
|
+
getToken: () => (typeof window !== "undefined" ? localStorage.getItem("token") : null),
|
|
202
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type SegmentsNumberProps = {
|
|
2
|
+
percent?: number;
|
|
3
|
+
segmentsCount?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Calculate the number of segments required for a progress circle.
|
|
7
|
+
* @param {{percent?: number; segmentsCount?: number;}} [props] - Optional props for calculation.
|
|
8
|
+
* @param {number} [props.percent=90] - The percentage of circle to be taken up by the progress.
|
|
9
|
+
* @param {number} [props.segmentsCount=10] - The number of segments to use in the circle.
|
|
10
|
+
* @returns {number} The number of segments required.
|
|
11
|
+
*/
|
|
12
|
+
export const segmentsNumber = ({percent, segmentsCount}: SegmentsNumberProps): number => {
|
|
13
|
+
if(!percent || !segmentsCount) {
|
|
14
|
+
return 0
|
|
15
|
+
}
|
|
16
|
+
const segments = (segmentsCount * (percent / 100) * segmentsCount).toFixed(0)
|
|
17
|
+
return Number(segments);
|
|
18
|
+
}
|