@skalfa/skalfa-app 1.0.0

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.
Files changed (112) hide show
  1. package/.env.example +44 -0
  2. package/README.md +28 -0
  3. package/app/auth/edit/page.tsx +65 -0
  4. package/app/auth/login/page.tsx +63 -0
  5. package/app/auth/me/page.tsx +58 -0
  6. package/app/auth/register/page.tsx +69 -0
  7. package/app/auth/verify/page.tsx +53 -0
  8. package/app/dashboard/layout.tsx +47 -0
  9. package/app/dashboard/page.tsx +9 -0
  10. package/app/dashboard/user/page.tsx +77 -0
  11. package/app/index.ts +14 -0
  12. package/app/layout.tsx +38 -0
  13. package/app/page.tsx +13 -0
  14. package/barrels.json +6 -0
  15. package/blueprints/starter.blueprint.json +103 -0
  16. package/components/base.components/accordion/Accordion.component.tsx +82 -0
  17. package/components/base.components/breadcrumb/Breadcrumb.component.tsx +80 -0
  18. package/components/base.components/button/Button.component.tsx +91 -0
  19. package/components/base.components/button/IconButton.component.tsx +88 -0
  20. package/components/base.components/button/button.decorate.ts +82 -0
  21. package/components/base.components/card/AlertCard.component.tsx +69 -0
  22. package/components/base.components/card/Card.component.tsx +25 -0
  23. package/components/base.components/card/DashboardCard.component.tsx +44 -0
  24. package/components/base.components/card/GalleryCard.component.tsx +50 -0
  25. package/components/base.components/card/ProductCard.component.tsx +65 -0
  26. package/components/base.components/card/ProfileCard.component.tsx +71 -0
  27. package/components/base.components/carousel/Carousel.component.tsx +113 -0
  28. package/components/base.components/chip/Chip.component.tsx +39 -0
  29. package/components/base.components/document/DocumentViewer.component.tsx +164 -0
  30. package/components/base.components/document/ExportExcel.component.tsx +340 -0
  31. package/components/base.components/document/ImportExcel.component.tsx +315 -0
  32. package/components/base.components/document/PrintTable.component.tsx +204 -0
  33. package/components/base.components/document/RenderPDF.component.tsx +416 -0
  34. package/components/base.components/index.ts +85 -0
  35. package/components/base.components/input/Checkbox.component.tsx +109 -0
  36. package/components/base.components/input/Input.component.tsx +332 -0
  37. package/components/base.components/input/InputCheckbox.component.tsx +174 -0
  38. package/components/base.components/input/InputCurrency.component.tsx +163 -0
  39. package/components/base.components/input/InputDate.component.tsx +352 -0
  40. package/components/base.components/input/InputDatetime.component.tsx +260 -0
  41. package/components/base.components/input/InputDocument.component.tsx +352 -0
  42. package/components/base.components/input/InputImage.component.tsx +533 -0
  43. package/components/base.components/input/InputMap.component.tsx +318 -0
  44. package/components/base.components/input/InputNumber.component.tsx +192 -0
  45. package/components/base.components/input/InputOtp.component.tsx +169 -0
  46. package/components/base.components/input/InputPassword.component.tsx +236 -0
  47. package/components/base.components/input/InputRadio.component.tsx +175 -0
  48. package/components/base.components/input/InputTime.component.tsx +276 -0
  49. package/components/base.components/input/InputValues.component.tsx +68 -0
  50. package/components/base.components/input/Radio.component.tsx +102 -0
  51. package/components/base.components/input/Select.component.tsx +541 -0
  52. package/components/base.components/modal/BottomSheet.component.tsx +246 -0
  53. package/components/base.components/modal/FloatingPage.component.tsx +104 -0
  54. package/components/base.components/modal/Modal.component.tsx +96 -0
  55. package/components/base.components/modal/ModalConfirm.component.tsx +218 -0
  56. package/components/base.components/modal/Toast.component.tsx +126 -0
  57. package/components/base.components/nav/Bottombar.component.tsx +116 -0
  58. package/components/base.components/nav/Footer.component.tsx +144 -0
  59. package/components/base.components/nav/Headbar.component.tsx +104 -0
  60. package/components/base.components/nav/Navbar.component.tsx +100 -0
  61. package/components/base.components/nav/Sidebar.component.tsx +301 -0
  62. package/components/base.components/nav/Tabbar.component.tsx +60 -0
  63. package/components/base.components/nav/Wizard.component.tsx +73 -0
  64. package/components/base.components/supervision/FormSupervision.component.tsx +434 -0
  65. package/components/base.components/supervision/TableSupervision.component.tsx +697 -0
  66. package/components/base.components/table/ControlBar.component.tsx +497 -0
  67. package/components/base.components/table/FilterComponent.tsx +518 -0
  68. package/components/base.components/table/Pagination.component.tsx +159 -0
  69. package/components/base.components/table/Table.component.tsx +469 -0
  70. package/components/base.components/typography/TypographyArticle.component.tsx +26 -0
  71. package/components/base.components/typography/TypographyColumn.component.tsx +20 -0
  72. package/components/base.components/typography/TypographyContent.component.tsx +20 -0
  73. package/components/base.components/typography/TypographyTips.component.tsx +20 -0
  74. package/components/base.components/wrap/Draggable.component.tsx +303 -0
  75. package/components/base.components/wrap/IDBProvider.tsx +12 -0
  76. package/components/base.components/wrap/Image.component.tsx +10 -0
  77. package/components/base.components/wrap/OutsideClick.component.tsx +48 -0
  78. package/components/base.components/wrap/ScrollContainer.component.tsx +104 -0
  79. package/components/base.components/wrap/ShortcutProvider.tsx +57 -0
  80. package/components/base.components/wrap/Swipe.component.tsx +93 -0
  81. package/components/construct.components/example.tsx +1 -0
  82. package/components/construct.components/index.ts +5 -0
  83. package/components/index.ts +3 -0
  84. package/components/structure.components/example.tsx +1 -0
  85. package/components/structure.components/index.ts +5 -0
  86. package/contexts/AppProvider.tsx +12 -0
  87. package/contexts/Auth.context.tsx +64 -0
  88. package/contexts/Toggle.context.tsx +44 -0
  89. package/contexts/index.ts +7 -0
  90. package/eslint.config.mjs +34 -0
  91. package/langs/index.ts +1 -0
  92. package/langs/validation.langs.ts +17 -0
  93. package/next.config.ts +17 -0
  94. package/package.json +43 -0
  95. package/postcss.config.mjs +12 -0
  96. package/public/204.svg +19 -0
  97. package/public/500.svg +39 -0
  98. package/public/images/avatar.jpg +0 -0
  99. package/public/images/example.png +0 -0
  100. package/schema/idb/app.schema.ts +9 -0
  101. package/schema/index.ts +5 -0
  102. package/styles/globals.css +231 -0
  103. package/styles/tailwind.safelist +69 -0
  104. package/tailwind.config.ts +10 -0
  105. package/tsconfig.json +35 -0
  106. package/utils/commands/barrels.ts +28 -0
  107. package/utils/commands/blueprint.ts +421 -0
  108. package/utils/commands/light.ts +21 -0
  109. package/utils/commands/logger.ts +42 -0
  110. package/utils/commands/stubs/table-blueprint.stub +13 -0
  111. package/utils/commands/use-pdf.ts +29 -0
  112. package/utils/index.ts +3 -0
@@ -0,0 +1,434 @@
1
+ "use client"
2
+
3
+ import React, { ReactNode, useEffect, useRef, useState } from "react";
4
+ import { faSave, faQuestionCircle, faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
5
+ import { ApiType, cn, pcn, FormErrorType, FormRegisterType, FormValueType, useForm, ValidationRules, DBSchema } from "@utils";
6
+ import {
7
+ InputCheckboxComponent,
8
+ InputComponent,
9
+ InputCurrencyComponent,
10
+ InputDateComponent,
11
+ InputNumberComponent,
12
+ InputOtpComponent,
13
+ InputPasswordComponent,
14
+ InputRadioComponent,
15
+ SelectComponent,
16
+ ButtonComponent,
17
+ ModalConfirmComponent,
18
+ ToastComponent,
19
+ InputProps,
20
+ InputCheckboxProps,
21
+ InputCurrencyProps,
22
+ InputDateProps,
23
+ InputNumberProps,
24
+ InputRadioProps,
25
+ SelectProps,
26
+ InputPasswordProps,
27
+ InputOtpProps,
28
+ InputTimeProps,
29
+ InputImageProps,
30
+ InputDateTimeProps,
31
+ InputDatetimeComponent,
32
+ InputTimeComponent,
33
+ IconButtonComponent,
34
+ InputImageComponent,
35
+ InputMapComponent,
36
+ InputMapProps,
37
+ } from "@components";
38
+
39
+
40
+
41
+ type CT = "base" | "title" | "submit";
42
+
43
+ type formCustomConstructionProps = ({
44
+ formControl,
45
+ values,
46
+ setValues,
47
+ setRegister,
48
+ errors,
49
+ setErrors,
50
+ }: {
51
+ formControl : (name: string) => {
52
+ register: (regName: string, regValidations?: ValidationRules | undefined) => void;
53
+ unregister: (regName: string) => void;
54
+ onChange: (e: any) => void;
55
+ value: any;
56
+ invalid: any;
57
+ };
58
+ values : { name: string; value?: any }[];
59
+ setValues : (values: FormValueType[]) => void;
60
+ errors : FormErrorType[];
61
+ setErrors : (errors: FormErrorType[]) => void;
62
+ setRegister : (registers: FormRegisterType) => void;
63
+ prefixName ?: string;
64
+ }) => ReactNode;
65
+
66
+ type ClusterConstruction = {
67
+ name : string;
68
+ label : string;
69
+ tip : string;
70
+ fields : FormType[];
71
+ wrap : boolean;
72
+ min ?: number;
73
+
74
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
75
+ className : string;
76
+ };
77
+
78
+ type ConstructionMap = {
79
+ default : InputProps;
80
+ check : InputCheckboxProps;
81
+ currency : InputCurrencyProps;
82
+ date : InputDateProps;
83
+ datetime : InputDateTimeProps;
84
+ time : InputTimeProps;
85
+ image : InputImageProps;
86
+ cluster : ClusterConstruction;
87
+ number : InputNumberProps;
88
+ radio : InputRadioProps;
89
+ select : SelectProps;
90
+ "enter-password" : InputPasswordProps;
91
+ otp : InputOtpProps;
92
+ map : InputMapProps;
93
+ custom : formCustomConstructionProps;
94
+ };
95
+
96
+ type TypeKeys = keyof ConstructionMap;
97
+
98
+ export type WatchContext = {
99
+ values : Record<string, any>
100
+ self : string
101
+ prev : WatchAction
102
+ }
103
+
104
+ export type WatchAction = {
105
+ disabled ?: boolean
106
+ hidden ?: boolean
107
+ value ?: any
108
+ required ?: boolean
109
+ readonly ?: boolean
110
+ reset ?: boolean
111
+ }
112
+
113
+ export interface FormType<T extends TypeKeys = keyof ConstructionMap> {
114
+ col ?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | string;
115
+ className ?: string;
116
+ construction ?: ConstructionMap[T];
117
+ type ?: T;
118
+ onHide ?: (values: any) => boolean;
119
+ watch ?: (ctx: WatchContext) => WatchAction | undefined;
120
+ }
121
+
122
+ export interface formSupervisionProps {
123
+ title ?: string;
124
+ fields : FormType[];
125
+ confirmation ?: boolean;
126
+ defaultValue ?: object | null;
127
+ payload ?: (values: any) => Promise<object> | object;
128
+ submitControl : (ApiType & { idb?: never }) | { idb: { store: string, schema?: DBSchema }};
129
+ footerControl ?: ({ loading }: { loading: boolean }) => ReactNode;
130
+ onSuccess ?: (data: any) => void;
131
+ onError ?: (code: number) => void;
132
+ className ?: string;
133
+ }
134
+
135
+
136
+
137
+ export function FormSupervisionComponent({
138
+ title,
139
+ fields,
140
+ submitControl,
141
+ confirmation,
142
+ defaultValue,
143
+ onSuccess,
144
+ onError,
145
+ footerControl,
146
+ payload,
147
+ className = "",
148
+ }: formSupervisionProps) {
149
+ const [modal, setModal] = useState<boolean | "success" | "failed">(false);
150
+ const [fresh, setFresh] = useState<boolean>(true);
151
+ const [mapGroups, setMapGroups] = useState<Record<string, number[]>>({});
152
+ const [watchState, setWatchState] = useState<Record<string, WatchAction>>({});
153
+ const watchRef = useRef<Record<string, WatchAction>>({});
154
+
155
+ const { formControl, setRegister, unregister, unregisterPrefix, values, setValues, errors, setErrors, setDefaultValues, submit, loading, confirm } = useForm({
156
+ ...submitControl,
157
+ payload,
158
+ confirmation,
159
+ onSuccess: (data: any) => {
160
+ onSuccess?.(data);
161
+ setModal("success");
162
+ resetFresh();
163
+ },
164
+ onFailed: (code: number) => {
165
+ onError?.(code);
166
+ if (code == 422) confirm.onClose();
167
+ else setModal("failed");
168
+ },
169
+ });
170
+
171
+ const resetFresh = () => {
172
+ setFresh(false);
173
+ setTimeout(() => setFresh(true), 300);
174
+ };
175
+
176
+ useEffect(() => {
177
+ resetFresh();
178
+ }, [fields]);
179
+
180
+ useEffect(() => {
181
+ if (defaultValue) setDefaultValues(defaultValue);
182
+ else {
183
+ setDefaultValues(null);
184
+ resetFresh();
185
+ }
186
+ }, [defaultValue]);
187
+
188
+ // ==============================>
189
+ // ## Watch: collect watchers from fields
190
+ // ==============================>
191
+ const collectWatchers = (fieldList: FormType[], prefix?: string): { name: string, watch: NonNullable<FormType['watch']>, construction: any }[] => {
192
+ const result: { name: string, watch: NonNullable<FormType['watch']>, construction: any }[] = [];
193
+
194
+ for (const f of fieldList) {
195
+ const inputType = f.type || "default";
196
+ const name = prefix ? `${prefix}.${f.construction?.name}` : f.construction?.name || "";
197
+
198
+ if (inputType === "cluster") {
199
+ const cluster = f.construction as ClusterConstruction;
200
+ const groupKey = prefix ? `${prefix}.${cluster.name}` : cluster.name;
201
+ const group = mapGroups[groupKey] || [0];
202
+
203
+ for (const gIndex of group) {
204
+ result.push(...collectWatchers(cluster.fields, `${cluster.name}[${gIndex}]`));
205
+ }
206
+ } else if (f.watch) {
207
+ result.push({ name, watch: f.watch, construction: f.construction });
208
+ }
209
+ }
210
+
211
+ return result;
212
+ };
213
+
214
+
215
+ // ==============================>
216
+ // ## Watch: execute watchers on value change
217
+ // ==============================>
218
+ useEffect(() => {
219
+ const watchers = collectWatchers(fields);
220
+ if (watchers.length === 0) {
221
+ if (Object.keys(watchRef.current).length > 0) {
222
+ watchRef.current = {};
223
+ setWatchState({});
224
+ }
225
+ return;
226
+ }
227
+
228
+ const valMap = values.reduce<Record<string, any>>((acc, v) => { acc[v.name] = v.value; return acc; }, {});
229
+
230
+ const nextState : Record<string, WatchAction> = {};
231
+ const valueUpdates : FormValueType[] = [];
232
+
233
+ for (const { name, watch, construction } of watchers) {
234
+ const prev = watchRef.current[name] || {};
235
+ const action = watch({ values: valMap, self: name, prev });
236
+
237
+ if (!action) continue;
238
+
239
+ nextState[name] = action;
240
+
241
+ if (action.hidden && !prev.hidden) unregister(name);
242
+
243
+ if (action.required !== prev.required) {
244
+ const baseValidations = Array.isArray(construction?.validations) ? [...construction.validations] : [];
245
+ const newValidations = action.required ? (baseValidations.includes("required") ? baseValidations : [...baseValidations, "required"]) : baseValidations.filter((v: string) => v !== "required");
246
+
247
+ setRegister({ name, validations: newValidations });
248
+ }
249
+
250
+ if (action.reset) {
251
+ const cur = valMap[name];
252
+
253
+ if (cur != null && cur !== "") valueUpdates.push({ name, value: "" });
254
+ } else if (action.value !== undefined && action.value !== valMap[name]) {
255
+ valueUpdates.push({ name, value: action.value });
256
+ }
257
+ }
258
+
259
+ if (JSON.stringify(watchRef.current) !== JSON.stringify(nextState)) {
260
+ watchRef.current = nextState;
261
+ setWatchState(nextState);
262
+ }
263
+
264
+ if (valueUpdates.length > 0) {
265
+ const merged = [...values];
266
+
267
+ for (const upd of valueUpdates) {
268
+ const idx = merged.findIndex(v => v.name === upd.name);
269
+ if (idx >= 0) merged[idx] = upd;
270
+ else merged.push(upd);
271
+ }
272
+
273
+ setValues(merged);
274
+ }
275
+ }, [values, fields, mapGroups]);
276
+
277
+
278
+ const generateColClass = (col: string | number) => String(col).split(" ").map((c) => (c.includes(":") ? `${c.replace(":", ":col-span-")}` : `col-span-${c}`)).join(" ");
279
+
280
+ const inputMap: Record<TypeKeys, React.FC<any>> = {
281
+ default : InputComponent,
282
+ check : InputCheckboxComponent,
283
+ currency : InputCurrencyComponent,
284
+ date : InputDateComponent,
285
+ datetime : InputDatetimeComponent,
286
+ time : InputTimeComponent,
287
+ number : InputNumberComponent,
288
+ radio : InputRadioComponent,
289
+ select : SelectComponent,
290
+ "enter-password" : InputPasswordComponent,
291
+ otp : InputOtpComponent,
292
+ image : InputImageComponent,
293
+ map : InputMapComponent,
294
+ cluster : () => null,
295
+ custom : () => null,
296
+ };
297
+
298
+ const renderInput = (form: FormType, key: number, prefix?: string) => {
299
+ const inputType = form.type || "default";
300
+ const name = prefix ? `${prefix}.${form.construction?.name}` : form.construction?.name || "input_name";
301
+
302
+ if (form?.onHide?.(values)) return null;
303
+
304
+ const ws = watchState[name];
305
+ if (ws?.hidden) return null;
306
+
307
+ if (inputType === "cluster") {
308
+ const { name: mapName, fields: innerForms, label, tip, wrap, className, min = 0 } = form.construction as ClusterConstruction;
309
+
310
+ const groupKey = prefix ? `${prefix}.${mapName}` : mapName;
311
+ const group = mapGroups[groupKey] || Array.from({ length: Math.max(min, 1) }, (_, i) => i);
312
+
313
+ const addGroup = () => setMapGroups((prev) => ({ ...prev, [groupKey]: [...group, group.length > 0 ? Math.max(...group) + 1 : 0] }));
314
+
315
+ const removeGroup = (gIndex: number) => {
316
+ setMapGroups((prev) => ({ ...prev, [groupKey]: group.filter((g) => g !== gIndex) }));
317
+
318
+ unregisterPrefix(`${groupKey}[${gIndex}]`);
319
+ };
320
+
321
+ return (
322
+ <div key={key} className={cn("flex flex-col gap-4", generateColClass(form.col || "12"))}>
323
+ {group.map((gIndex) => (
324
+ <div key={gIndex} className={cn("relative pr-8", wrap && "p-4 rounded border", className)}>
325
+ {label && <p className="input-label">{label} {gIndex + 1}</p>}
326
+ {tip && <small className={cn("input-tip")}>{tip}</small>}
327
+ {(label || tip) && <div className="mb-2"></div>}
328
+
329
+ <div className="w-full grid grid-cols-12 gap-4">
330
+ {innerForms.map((inner, i) => renderInput(inner, i, `${mapName}[${gIndex}]`))}
331
+ </div>
332
+
333
+ {group.length > min && (
334
+ <IconButtonComponent
335
+ icon={faTimes}
336
+ paint="danger"
337
+ variant="outline"
338
+ size="xs"
339
+ className={cn("absolute top-10 right-2 translate-x-[50%] -translate-y-[50%]", wrap && "translate-x-0 -translate-y-0 top-1 right-1")}
340
+ onClick={() => removeGroup(gIndex)}
341
+ />
342
+ )}
343
+ </div>
344
+ ))}
345
+
346
+ <div>
347
+ <ButtonComponent
348
+ icon={faPlus}
349
+ label={`Tambah ${label || mapName}`}
350
+ variant="outline"
351
+ size="sm"
352
+ onClick={addGroup}
353
+ />
354
+ </div>
355
+ </div>
356
+ );
357
+ }
358
+
359
+ if (inputType === "custom") {
360
+ const customRender = form.construction as formCustomConstructionProps;
361
+ return (
362
+ <div key={key} className={cn(form.className, generateColClass(form.col || "12"))}>
363
+ {customRender?.({ formControl, values, setValues, errors, setErrors, setRegister, prefixName: prefix })}
364
+ </div>
365
+ );
366
+ }
367
+
368
+ const Component = inputMap[inputType] || InputComponent;
369
+ return (
370
+ <div key={key} className={cn(form.className, generateColClass(form.col || "12"))}>
371
+ <Component
372
+ {...(form.construction as any)}
373
+ {...formControl(name)}
374
+ disabled={ws?.disabled}
375
+ readOnly={ws?.readonly}
376
+ />
377
+ </div>
378
+ );
379
+ };
380
+
381
+ return (
382
+ <>
383
+ {title && <h4 className={cn("text-lg font-semibold mb-4", pcn<CT>(className, "title"))}>{title}</h4>}
384
+
385
+ <form className={cn("grid grid-cols-12 gap-4", pcn<CT>(className, "base"))} onSubmit={submit}>
386
+ {fresh && fields.map((f, i) => renderInput(f, i))}
387
+
388
+ <div className="col-span-12">
389
+ {footerControl?.({ loading }) || (
390
+ <div className="flex justify-end mt-4">
391
+ <ButtonComponent
392
+ type="submit"
393
+ label="Simpan"
394
+ icon={faSave}
395
+ loading={loading}
396
+ className={pcn<CT>(className, "submit")}
397
+ />
398
+ </div>
399
+ )}
400
+ </div>
401
+ </form>
402
+
403
+ <ModalConfirmComponent
404
+ show={confirm.show}
405
+ onClose={() => confirm.onClose()}
406
+ icon={faQuestionCircle}
407
+ title="Yakin"
408
+ submitControl={{ onSubmit: () => confirm?.onConfirm(), paint: "primary" }}
409
+ >
410
+ <p className="px-2 pb-2 text-sm text-center">Yakin semua data sudah benar?</p>
411
+ </ModalConfirmComponent>
412
+
413
+ <ToastComponent
414
+ show={modal == "failed"}
415
+ onClose={() => setModal(false)}
416
+ title="Gagal"
417
+ className="!border-danger header::text-danger"
418
+ >
419
+ <p className="px-3 pb-2 text-sm">
420
+ Data gagal disimpan, cek data dan koneksi internet lalu coba kembali!
421
+ </p>
422
+ </ToastComponent>
423
+
424
+ <ToastComponent
425
+ show={modal == "success"}
426
+ onClose={() => setModal(false)}
427
+ title="Berhasil"
428
+ className="!border-success header::text-success"
429
+ >
430
+ <p className="px-3 pb-2 text-sm">Data berhasil disimpan!</p>
431
+ </ToastComponent>
432
+ </>
433
+ );
434
+ }