@skalfa/skalfa-app 1.0.2 → 1.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.
Files changed (99) hide show
  1. package/.env.example +8 -16
  2. package/app/auth/edit/page.tsx +1 -1
  3. package/app/auth/login/page.tsx +1 -1
  4. package/app/auth/me/page.tsx +1 -1
  5. package/app/auth/register/page.tsx +1 -1
  6. package/app/auth/verify/page.tsx +1 -1
  7. package/app/dashboard/layout.tsx +2 -2
  8. package/app/dashboard/page.tsx +1 -1
  9. package/app/index.ts +1 -0
  10. package/app/layout.tsx +2 -4
  11. package/app/page.tsx +2 -2
  12. package/bun.lock +39 -24
  13. package/components/index.ts +1 -3
  14. package/package.json +8 -7
  15. package/styles/components.css +1392 -0
  16. package/styles/globals.css +40 -175
  17. package/styles/utilities.css +37 -0
  18. package/tsconfig.json +4 -2
  19. package/utils/commands/skalfa.ts +3 -0
  20. package/blueprints/starter.blueprint.json +0 -103
  21. package/components/base.components/accordion/Accordion.component.tsx +0 -82
  22. package/components/base.components/breadcrumb/Breadcrumb.component.tsx +0 -80
  23. package/components/base.components/button/Button.component.tsx +0 -91
  24. package/components/base.components/button/IconButton.component.tsx +0 -88
  25. package/components/base.components/button/button.decorate.ts +0 -82
  26. package/components/base.components/card/AlertCard.component.tsx +0 -69
  27. package/components/base.components/card/Card.component.tsx +0 -25
  28. package/components/base.components/card/DashboardCard.component.tsx +0 -44
  29. package/components/base.components/card/GalleryCard.component.tsx +0 -50
  30. package/components/base.components/card/ProductCard.component.tsx +0 -65
  31. package/components/base.components/card/ProfileCard.component.tsx +0 -71
  32. package/components/base.components/carousel/Carousel.component.tsx +0 -113
  33. package/components/base.components/chip/Chip.component.tsx +0 -39
  34. package/components/base.components/document/DocumentViewer.component.tsx +0 -164
  35. package/components/base.components/document/ExportExcel.component.tsx +0 -340
  36. package/components/base.components/document/ImportExcel.component.tsx +0 -315
  37. package/components/base.components/document/PrintTable.component.tsx +0 -204
  38. package/components/base.components/document/RenderPDF.component.tsx +0 -416
  39. package/components/base.components/index.ts +0 -85
  40. package/components/base.components/input/Checkbox.component.tsx +0 -109
  41. package/components/base.components/input/Input.component.tsx +0 -332
  42. package/components/base.components/input/InputCheckbox.component.tsx +0 -174
  43. package/components/base.components/input/InputCurrency.component.tsx +0 -163
  44. package/components/base.components/input/InputDate.component.tsx +0 -352
  45. package/components/base.components/input/InputDatetime.component.tsx +0 -260
  46. package/components/base.components/input/InputDocument.component.tsx +0 -352
  47. package/components/base.components/input/InputImage.component.tsx +0 -533
  48. package/components/base.components/input/InputMap.component.tsx +0 -318
  49. package/components/base.components/input/InputNumber.component.tsx +0 -192
  50. package/components/base.components/input/InputOtp.component.tsx +0 -169
  51. package/components/base.components/input/InputPassword.component.tsx +0 -236
  52. package/components/base.components/input/InputRadio.component.tsx +0 -175
  53. package/components/base.components/input/InputTime.component.tsx +0 -276
  54. package/components/base.components/input/InputValues.component.tsx +0 -68
  55. package/components/base.components/input/Radio.component.tsx +0 -102
  56. package/components/base.components/input/Select.component.tsx +0 -541
  57. package/components/base.components/modal/BottomSheet.component.tsx +0 -246
  58. package/components/base.components/modal/FloatingPage.component.tsx +0 -104
  59. package/components/base.components/modal/Modal.component.tsx +0 -96
  60. package/components/base.components/modal/ModalConfirm.component.tsx +0 -218
  61. package/components/base.components/modal/Toast.component.tsx +0 -126
  62. package/components/base.components/nav/Bottombar.component.tsx +0 -116
  63. package/components/base.components/nav/Footer.component.tsx +0 -144
  64. package/components/base.components/nav/Headbar.component.tsx +0 -104
  65. package/components/base.components/nav/Navbar.component.tsx +0 -100
  66. package/components/base.components/nav/Sidebar.component.tsx +0 -301
  67. package/components/base.components/nav/Tabbar.component.tsx +0 -60
  68. package/components/base.components/nav/Wizard.component.tsx +0 -73
  69. package/components/base.components/supervision/FormSupervision.component.tsx +0 -434
  70. package/components/base.components/supervision/TableSupervision.component.tsx +0 -697
  71. package/components/base.components/table/ControlBar.component.tsx +0 -497
  72. package/components/base.components/table/FilterComponent.tsx +0 -518
  73. package/components/base.components/table/Pagination.component.tsx +0 -159
  74. package/components/base.components/table/Table.component.tsx +0 -469
  75. package/components/base.components/typography/TypographyArticle.component.tsx +0 -26
  76. package/components/base.components/typography/TypographyColumn.component.tsx +0 -20
  77. package/components/base.components/typography/TypographyContent.component.tsx +0 -20
  78. package/components/base.components/typography/TypographyTips.component.tsx +0 -20
  79. package/components/base.components/wrap/Draggable.component.tsx +0 -303
  80. package/components/base.components/wrap/IDBProvider.tsx +0 -12
  81. package/components/base.components/wrap/Image.component.tsx +0 -10
  82. package/components/base.components/wrap/OutsideClick.component.tsx +0 -48
  83. package/components/base.components/wrap/ScrollContainer.component.tsx +0 -104
  84. package/components/base.components/wrap/ShortcutProvider.tsx +0 -57
  85. package/components/base.components/wrap/Swipe.component.tsx +0 -93
  86. package/components/construct.components/example.tsx +0 -1
  87. package/components/construct.components/index.ts +0 -5
  88. package/components/structure.components/example.tsx +0 -1
  89. package/components/structure.components/index.ts +0 -5
  90. package/langs/index.ts +0 -1
  91. package/langs/validation.langs.ts +0 -17
  92. package/schema/idb/app.schema.ts +0 -9
  93. package/schema/index.ts +0 -5
  94. package/utils/commands/barrels.ts +0 -28
  95. package/utils/commands/blueprint.ts +0 -421
  96. package/utils/commands/light.ts +0 -21
  97. package/utils/commands/logger.ts +0 -42
  98. package/utils/commands/stubs/table-blueprint.stub +0 -13
  99. package/utils/commands/use-pdf.ts +0 -29
@@ -1,164 +0,0 @@
1
- "use client"
2
-
3
- import { useEffect, useMemo, useRef, useState } from "react";
4
- import Image from "next/image";
5
- import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
- import { faFilePdf, faFileImage, faFileWord, faFileExcel, faFile } from "@fortawesome/free-solid-svg-icons";
7
- import { cn } from "@/utils";
8
-
9
-
10
-
11
- type Props = {
12
- file ?: File | string | null;
13
- width ?: number;
14
- height ?: number;
15
- mode ?: "full" | "thumb";
16
- };
17
-
18
-
19
-
20
- export const DocumentViewerIcon = (ext: string) => {
21
- switch (ext) {
22
- case "jpg":
23
- case "jpeg":
24
- case "png":
25
- case "webp":
26
- return faFileImage;
27
- case "pdf":
28
- return faFilePdf;
29
- case "doc":
30
- case "docx":
31
- return faFileWord;
32
- case "xls":
33
- case "xlsx":
34
- return faFileExcel;
35
- default:
36
- return faFile;
37
- }
38
- };
39
-
40
-
41
-
42
- export function DocumentViewerComponent({ file, width, height, mode = "full" }: Props) {
43
- const [fileUrl, setFileUrl] = useState<string | null>(null);
44
- const canvasRef = useRef<HTMLCanvasElement>(null);
45
-
46
- useEffect(() => {
47
- if (!file) {
48
- setFileUrl(null);
49
- return;
50
- }
51
-
52
- if (typeof file === "string") {
53
- setFileUrl(file);
54
- return;
55
- }
56
-
57
- const objectUrl = URL.createObjectURL(file);
58
- setFileUrl(objectUrl);
59
- return () => URL.revokeObjectURL(objectUrl);
60
- }, [file]);
61
-
62
- const extension = useMemo(() => {
63
- if (!file) return null;
64
- if (typeof file === "string") {
65
- const clean = file.split("?")[0].split("#")[0];
66
- return clean.split(".").pop()?.toLowerCase() || null;
67
- }
68
- return file.name.split(".").pop()?.toLowerCase() || null;
69
- }, [file]);
70
-
71
- const isImage = ["jpg", "jpeg", "png", "webp"].includes(extension || "");
72
- const isPdf = extension === "pdf";
73
-
74
- const renderTaskRef = useRef<any>(null);
75
-
76
- useEffect(() => {
77
- if (!file || !isPdf || !fileUrl || !canvasRef.current) return;
78
-
79
- let cancelled = false;
80
-
81
- const renderPdf = async () => {
82
- const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
83
- pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";
84
-
85
- if (renderTaskRef.current) {
86
- try {
87
- await renderTaskRef.current.cancel();
88
- } catch {}
89
- renderTaskRef.current = null;
90
- }
91
-
92
- let loadingTask;
93
-
94
- if (typeof file === "string") {
95
- loadingTask = pdfjs.getDocument(fileUrl);
96
- } else {
97
- const buf = await file.arrayBuffer();
98
- const uint8 = new Uint8Array(buf);
99
- loadingTask = pdfjs.getDocument({ data: uint8 });
100
- }
101
-
102
- const pdf = await loadingTask.promise;
103
- if (cancelled) return;
104
-
105
- const page = await pdf.getPage(1);
106
- if (cancelled) return;
107
-
108
- const scale = mode === "thumb" ? 0.35 : 1;
109
- const viewport = page.getViewport({ scale });
110
-
111
- const canvas = canvasRef.current;
112
- if (!canvas) return;
113
-
114
- const ctx = canvas.getContext("2d");
115
- if (!ctx) return;
116
-
117
- canvas.width = viewport.width;
118
- canvas.height = viewport.height;
119
-
120
- const renderTask = page.render({ canvasContext: ctx, viewport, canvas });;
121
- renderTaskRef.current = renderTask;
122
-
123
- await renderTask.promise;
124
- renderTaskRef.current = null;
125
- };
126
-
127
- renderPdf();
128
-
129
- return () => {
130
- cancelled = true;
131
- if (renderTaskRef.current) {
132
- try {
133
- renderTaskRef.current.cancel();
134
- } catch {}
135
- renderTaskRef.current = null;
136
- }
137
- };
138
- }, [isPdf, fileUrl, mode]);
139
-
140
- if (!fileUrl) return null;
141
-
142
-
143
- return (
144
- <div
145
- style={{
146
- width: width ? width : "100%",
147
- height: height ? height : "100%",
148
- overflow: "hidden",
149
- position: "relative",
150
- }}
151
- >
152
- {isImage && <Image src={fileUrl} alt="document" fill className={mode === "thumb" ? "object-cover" : "object-contain"}/>}
153
-
154
- {isPdf && <canvas ref={canvasRef} className={cn("block w-full h-full", mode === "thumb" ? "object-cover" : "object-contain")} />}
155
-
156
- {!isImage && !isPdf && (
157
- <div className={cn("w-full h-full flex flex-col items-center justify-center opacity-50 gap-4")}>
158
- <FontAwesomeIcon icon={DocumentViewerIcon(extension || "")} className={mode !== "thumb" ? "text-3xl" : "text-lg"} />
159
- {mode !== "thumb" && file instanceof File && (<p className="text-center text-sm">{file.name}</p>)}
160
- </div>
161
- )}
162
- </div>
163
- );
164
- }
@@ -1,340 +0,0 @@
1
- "use client";
2
-
3
- import ExcelJS from "exceljs";
4
- import { useEffect, useMemo, useState } from "react";
5
- import { faArrowLeft, faArrowRight, faEdit, faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
6
- import { api, FetchControlType, useTable } from "@utils";
7
- import { TableComponent, ButtonComponent, IconButtonComponent, SelectComponent, ModalComponent } from "@components";
8
- import { useToggleContext } from "@contexts";
9
- import { FilterComponent } from "../table/FilterComponent";
10
-
11
-
12
-
13
- type ExportColumn = {
14
- selector : string;
15
- label : string;
16
- source : string | null;
17
- };
18
-
19
- export type ExportExcelColumnControlType = {
20
- label : string;
21
- selector : string;
22
- status ?: "default" | "optional" | "hidden";
23
- };
24
-
25
- type ExportExcelProps = {
26
- fetchControl : FetchControlType;
27
- columnControl ?: ExportExcelColumnControlType[];
28
- filename ?: string;
29
- };
30
-
31
-
32
-
33
- const ADD_COLUMN_KEY = "__add_column__";
34
-
35
- function numberToExcelColumn(index: number): string {
36
- let column = "";
37
- let n = index;
38
-
39
- while (n >= 0) {
40
- column = String.fromCharCode((n % 26) + 65) + column;
41
- n = Math.floor(n / 26) - 1;
42
- }
43
-
44
- return column;
45
- }
46
-
47
-
48
-
49
- export function ExportExcel({ fetchControl, columnControl, filename }: ExportExcelProps) {
50
- const { toggle, setToggle } = useToggleContext()
51
-
52
- const { data, tableControl, setParam, params } = useTable(fetchControl, undefined, undefined, false);
53
-
54
- const fields = useMemo(() => {
55
- if (columnControl?.length) {
56
- return columnControl.filter(c => c.status !== "hidden").map(c => c.selector);
57
- }
58
-
59
- if (!data?.data?.[0]) return [];
60
- return Object.keys(data.data[0]);
61
- }, [data, columnControl]);
62
-
63
- const [columns, setColumns] = useState<ExportColumn[]>([]);
64
-
65
-
66
- useEffect(() => {
67
- if (!data?.data?.[0]) return;
68
-
69
- setColumns(prev => {
70
- if (prev.length) return prev;
71
-
72
- const baseFields = columnControl?.length ? columnControl.filter(c => c.status === "default" || c.status === undefined).map(c => c.selector) : Object.keys(data.data[0]);
73
-
74
- return baseFields.map((field, i) => {
75
- const label = numberToExcelColumn(i);
76
-
77
- return {
78
- selector : label,
79
- label : label,
80
- source : field,
81
- };
82
- });
83
- });
84
- }, [data, columnControl]);
85
-
86
-
87
- const addColumn = (selector: string, source: string) => {
88
- setColumns(prev => [...prev, { selector, label: selector, source }]);
89
- };
90
-
91
- const removeColumn = (selector: string) => {
92
- setColumns(prev => prev.filter(c => c.selector !== selector).map((c, i) => {
93
- const label = numberToExcelColumn(i);
94
- return { ...c, selector: label, label };
95
- }));
96
- };
97
-
98
- const moveColumn = (selector: string, dir: "left" | "right") => {
99
- setColumns(prev => {
100
- const idx = prev.findIndex(c => c.selector === selector);
101
- if (idx === -1) return prev;
102
-
103
- const target = dir === "left" ? idx - 1 : idx + 1;
104
- if (target < 0 || target >= prev.length) return prev;
105
-
106
- const next = [...prev];
107
- [next[idx], next[target]] = [next[target], next[idx]];
108
-
109
- return next.map((c, i) => {
110
- const label = numberToExcelColumn(i);
111
- return { ...c, selector: label, label };
112
- });
113
- });
114
- };
115
-
116
- const setSource = (selector: string, source: string | null) => setColumns(prev => prev.map(c => (c.selector === selector ? { ...c, source } : c)));
117
-
118
- const getColumnLabel = (source: string | null) => {
119
- if (!source) return "";
120
-
121
- const found = columnControl?.find(c => c.selector === source);
122
-
123
- return found?.label ?? source;
124
- };
125
-
126
-
127
- const tableColumns = useMemo(() => {
128
- return [
129
- ...columns?.map((c => ({
130
- ...c,
131
- label: <div className="w-full text-center">{c.label}</div>
132
- }))),
133
- { selector: ADD_COLUMN_KEY, label: "" } as any,
134
- ];
135
- }, [columns]);
136
-
137
- const tableData = useMemo(() => {
138
- if (!data?.data || !columns.length) return [];
139
-
140
- const controlRow: Record<string, any> = {};
141
-
142
- columns.forEach((col, iCol) => {
143
- controlRow[col.selector] = (
144
- <div className="flex justify-between">
145
- <p className="font-semibold">{getColumnLabel(col.source)}</p>
146
-
147
- <div className="flex gap-1">
148
- {iCol > 0 && (
149
- <IconButtonComponent
150
- icon={faArrowLeft}
151
- size="xs"
152
- variant="outline"
153
- className="!text-foreground"
154
- onClick={() => moveColumn(col.selector, "left")}
155
- />
156
- )}
157
-
158
- {iCol < (columns.length - 1) && (
159
- <IconButtonComponent
160
- icon={faArrowRight}
161
- size="xs"
162
- variant="outline"
163
- className="!text-foreground"
164
- onClick={() => moveColumn(col.selector, "right")}
165
- />
166
- )}
167
-
168
- <IconButtonComponent
169
- icon={faEdit}
170
- size="xs"
171
- paint="warning"
172
- variant="outline"
173
- disabled={columns.length <= 1}
174
- onClick={() => setToggle("MODAL_FIELD_EXPORT", {selector: col.selector, value: col.source})}
175
- />
176
-
177
- <IconButtonComponent
178
- icon={faTimes}
179
- size="xs"
180
- paint="danger"
181
- variant="outline"
182
- disabled={columns.length <= 1}
183
- onClick={() => removeColumn(col.selector)}
184
- />
185
- </div>
186
-
187
-
188
- </div>
189
- );
190
- });
191
-
192
- controlRow[ADD_COLUMN_KEY] = (
193
- <div className="flex justify-center">
194
- <ButtonComponent
195
- icon={faPlus}
196
- label="Tambah Kolom"
197
- size="xs"
198
- variant="outline"
199
- onClick={() => {
200
- const idx = columns.length;
201
- const label = numberToExcelColumn(idx);
202
- setToggle("MODAL_FIELD_EXPORT", { selector: label, value: "", add: true })
203
- }}
204
- />
205
- </div>
206
- );
207
-
208
- const excelRows = data.data.map((item: Record<string, any>) => {
209
- const row: Record<string, any> = {};
210
-
211
- row[ADD_COLUMN_KEY] = "-"
212
- columns.forEach(col => {
213
- row[col.selector] = col.source ? <span className="line-clamp-1">{item[col.source] || "-"}</span> : "-";
214
- });
215
-
216
- return row;
217
- });
218
-
219
- return [controlRow, ...excelRows];
220
- }, [data, columns, fields]);
221
-
222
-
223
- const handleDownloadExcel = async () => {
224
- const record = await api({...fetchControl, params: {...fetchControl.params, ...params, page: 1, paginate: 9999}});
225
- if (!record?.data?.data?.length || !columns.length) return;
226
-
227
- const workbook = new ExcelJS.Workbook();
228
- const worksheet = workbook.addWorksheet("Export");
229
-
230
- worksheet.columns = columns.map(col => ({
231
- header : getColumnLabel(col.source),
232
- key : col.selector,
233
- width : 20,
234
- }));
235
-
236
- worksheet.getRow(1).eachCell(cell => {
237
- cell.font = { bold: true };
238
- cell.alignment = { horizontal: "center", vertical: "middle" };
239
- });
240
-
241
- record.data.data.forEach((item: Record<string, any>) => {
242
- const row: Record<string, any> = {};
243
-
244
- columns.forEach(col => {
245
- row[col.selector] = col.source ? item[col.source] : "";
246
- });
247
-
248
- worksheet.addRow(row);
249
- });
250
-
251
- const buffer = await workbook.xlsx.writeBuffer();
252
- const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
253
-
254
- const link = document.createElement("a");
255
- link.href = URL.createObjectURL(blob);
256
- link.download = filename ? filename + ".xlsx" : "export.xlsx";
257
- link.click();
258
-
259
- URL.revokeObjectURL(link.href);
260
- };
261
-
262
-
263
-
264
- return (
265
- <>
266
- <div className="p-4">
267
- <FilterComponent
268
- columns={columnControl?.length
269
- ? columnControl.filter(c => c.status !== "hidden").map(c => ({
270
- label: c.label,
271
- selector: c.selector,
272
- type: "text"
273
- }))
274
- : (data?.data?.[0] ? Object.keys(data.data[0]).map((c) => ({
275
- label: c,
276
- selector: c,
277
- type: "text"
278
- })) : [])
279
- }
280
- onChange={(filters) => setParam('filter', filters)}
281
- value={params?.filter || []}
282
- />
283
- </div>
284
-
285
- <TableComponent
286
- controlBar={false}
287
- columns={tableColumns}
288
- data={tableData}
289
- {...tableControl}
290
- pagination={false}
291
- className="p-4 bg-background row::bg-transparent row::border-0 row::gap-0 row::!hover:bg-background column::p-2 column::border head-column::p-2 head-column::border"
292
- />
293
-
294
- <div className="px-4 mt-8">
295
- <ButtonComponent label="Download Excel" block onClick={() => handleDownloadExcel()} />
296
- </div>
297
-
298
- <ModalComponent
299
- show={!!toggle["MODAL_FIELD_EXPORT"]}
300
- onClose={() => setToggle("MODAL_FIELD_EXPORT", false)}
301
- title="Pilih Kolom"
302
- footer={
303
- <div className="flex justify-end">
304
- <ButtonComponent
305
- label="Terapkan"
306
- onClick={() => {
307
- if(!!(toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value) {
308
- if((toggle["MODAL_FIELD_EXPORT"] as { add: boolean })?.add) {
309
- addColumn((toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector, (toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value)
310
- } else {
311
- setSource((toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector, (toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value)
312
- }
313
- }
314
- setToggle("MODAL_FIELD_EXPORT", false)
315
- }}
316
- />
317
- </div>
318
- }
319
- >
320
- <div className="p-4">
321
- <SelectComponent
322
- name={`column_${(toggle["MODAL_FIELD_EXPORT"] as { selector: string })?.selector}`}
323
- placeholder="Pilih kolom data..."
324
- value={(toggle["MODAL_FIELD_EXPORT"] as { value: string })?.value ?? ""}
325
- onChange={e => setToggle("MODAL_FIELD_EXPORT", {...(toggle["MODAL_FIELD_EXPORT"] as object), value: e})}
326
- options={fields.map((f: string) => {
327
- const col = columnControl?.find(c => c.selector === f);
328
- if (col?.status === "hidden") return null;
329
-
330
- return {
331
- label: col?.label || f,
332
- value: f,
333
- };
334
- }).filter(Boolean) as any}
335
- />
336
- </div>
337
- </ModalComponent>
338
- </>
339
- );
340
- }