@rozaqi02/reusable-dashboard 1.0.0 → 1.1.1

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 CHANGED
@@ -1,148 +1,68 @@
1
1
  # @rozaqi02/reusable-dashboard
2
2
 
3
- Modul dashboard admin reusable untuk UMKM berbasis React + Atomic Design + Supabase.
4
- Mendukung berbagai domain bisnis (travel, toko online, dll.) tanpa mengubah komponen inti
5
- cukup definisikan widget configuration dan data adapter yang sesuai.
3
+ Modul dashboard admin reusable untuk UMKM. Dibangun dengan React + Atomic Design + Supabase.
4
+ Mendukung berbagai domain bisnis tanpa mengubah komponen inti.
6
5
 
7
- [![version](https://img.shields.io/badge/version-1.0.0-blue)](./CHANGELOG.md)
8
- [![license](https://img.shields.io/badge/license-Proprietary-red)](./LICENSE)
9
- [![build](https://img.shields.io/badge/build-passing-brightgreen)](#)
6
+ [![version](https://img.shields.io/badge/version-1.1.0-blue)](./CHANGELOG.md)
7
+ [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
10
8
 
11
9
  ---
12
10
 
13
- ## 📦 Installation
11
+ ## Install
14
12
 
15
13
  ```bash
16
14
  npm install @rozaqi02/reusable-dashboard
15
+ npm install recharts @supabase/supabase-js
17
16
  ```
18
17
 
19
- ## Prerequisites
20
-
21
- - **Node.js** >= 16.x
22
- - **React** >= 18.0.0
23
- - **Supabase** account dan project yang sudah dibuat
24
- - **Tailwind CSS** >= 3.0 dikonfigurasi pada project konsumen
25
-
26
- ## 🔧 Peer Dependencies
27
-
28
- Install peer dependencies yang dibutuhkan:
29
-
30
- ```bash
31
- npm install react react-dom recharts @supabase/supabase-js lucide-react prop-types tailwindcss
32
- ```
33
-
34
- > **Tailwind CSS wajib** karena komponen menggunakan utility classes Tailwind untuk styling.
35
- > Pastikan Tailwind sudah dikonfigurasi di project kamu (lihat [panduan Tailwind](https://tailwindcss.com/docs/installation)).
18
+ Hanya 2 perintah. CSS sudah otomatis terbundle — tidak perlu konfigurasi Tailwind.
36
19
 
37
20
  ---
38
21
 
39
- ## 🚀 Quick Start
40
-
41
- ### Step 1: Setup Supabase Client
22
+ ## Quick Start (5 menit)
42
23
 
43
- Buat file `src/lib/supabaseClient.js`:
24
+ ### 1. Buat Supabase client
44
25
 
45
- ```javascript
26
+ ```js
27
+ // src/lib/supabaseClient.js
46
28
  import { createClient } from '@supabase/supabase-js';
47
-
48
29
  export const supabase = createClient(
49
- import.meta.env.VITE_SUPABASE_URL, // Vite
50
- import.meta.env.VITE_SUPABASE_ANON_KEY // Vite
51
- // atau process.env.REACT_APP_SUPABASE_URL jika pakai Create React App
30
+ process.env.REACT_APP_SUPABASE_URL,
31
+ process.env.REACT_APP_SUPABASE_ANON_KEY
52
32
  );
53
33
  ```
54
34
 
55
- Buat file `.env` di root project:
56
-
57
35
  ```env
58
- # Vite
59
- VITE_SUPABASE_URL=https://your-project.supabase.co
60
- VITE_SUPABASE_ANON_KEY=your-anon-key
61
-
62
- # Create React App
63
- # REACT_APP_SUPABASE_URL=https://your-project.supabase.co
64
- # REACT_APP_SUPABASE_ANON_KEY=your-anon-key
36
+ # .env
37
+ REACT_APP_SUPABASE_URL=https://xxx.supabase.co
38
+ REACT_APP_SUPABASE_ANON_KEY=eyJ...
65
39
  ```
66
40
 
67
- ### Step 2: Setup Database Schema
68
-
69
- Jalankan SQL schema di Supabase SQL Editor sesuai domain bisnis:
70
-
71
- | Domain | File SQL |
72
- |--------|----------|
73
- | Cidika Travel | `examples/cidika-travel-example/supabase-schema.sql` |
74
- | Toko Sepatu | `examples/toko-sepatu-example/supabase-schema.sql` |
75
-
76
- ### Step 3: Implementasi Dashboard
77
-
78
- Contoh menggunakan preset **Toko Sepatu**:
41
+ ### 2. Pakai di halaman dashboard
79
42
 
80
43
  ```jsx
81
44
  import React, { useMemo } from "react";
82
45
  import { supabase } from "./lib/supabaseClient";
83
46
  import {
84
47
  ReusableDashboardView,
85
- tokoSepatuWidgetConfig,
86
- createTokoSepatuSupabaseSource,
87
- adaptTokoSepatuData,
88
- createEmptyTokoSepatuData,
48
+ cidikaWidgetConfig,
49
+ createCidikaSupabaseSource,
50
+ adaptCidikaDashboardData,
51
+ createEmptyDashboardData,
52
+ createDashboardLabels,
89
53
  useReusableDashboard,
90
54
  } from "@rozaqi02/reusable-dashboard";
91
55
 
92
- // Inisialisasi data source satu kali di luar komponen
93
- const source = createTokoSepatuSupabaseSource(supabase);
94
-
95
- // Label minimal tanpa i18n — override semua key yang diperlukan
96
- const labels = {
97
- title: "Dashboard Toko Sepatu",
98
- refresh: "Refresh",
99
- liveUpdate: "Live",
100
- loadFailed: "Gagal memuat data.",
101
- retry: "Coba Lagi",
102
- confirmedOnly: "Confirmed",
103
- pendingOnly: "Pending",
104
- allStatus: "Semua Status",
105
- showPendingOverlay: "Tampilkan pending",
106
- allAudience: "Semua",
107
- customDate: "Kustom",
108
- reset: "Reset",
109
- topSort: "Urutkan",
110
- sortBookings: "Qty",
111
- sortRevenue: "Revenue",
112
- sortDesc: "Turun",
113
- sortAsc: "Naik",
114
- confirmedBookings: "Total Pesanan",
115
- confirmedRevenue: "Total Pendapatan",
116
- avgRevenue: "Rata-rata / Pesanan",
117
- conversionRate: "Conversion Rate",
118
- totalProducts: "Total Produk",
119
- dailyTrends: "Tren Harian",
120
- statusDistribution: "Distribusi Status",
121
- topPackages: "Produk Terlaris",
122
- recentBookings: "Pesanan Terbaru",
123
- date: "Tanggal",
124
- customer: "Pelanggan",
125
- package: "Produk",
126
- total: "Total",
127
- status: "Status",
128
- noRecentBookings: "Belum ada pesanan",
129
- bookingsMetric: "Pesanan",
130
- revenueMetric: "Pendapatan",
131
- pendingMetric: "Pesanan (Pending)",
132
- confirmedBookingMetric: "Pesanan (Confirmed)",
133
- confirmedRevenueMetric: "Pendapatan (Confirmed)",
134
- unknownAudience: "Unknown",
135
- dayLabel: (count) => `${count} hari`,
136
- formatStatusLabel: (s) => ({ confirmed: "Confirmed", pending: "Pending", cancelled: "Cancelled" })[s] || s,
137
- formatAudienceLabel: (v) => v || "Unknown",
138
- };
56
+ const source = createCidikaSupabaseSource(supabase);
139
57
 
140
58
  export default function Dashboard() {
59
+ const labels = useMemo(() => createDashboardLabels((key) => key), []);
60
+
141
61
  const state = useReusableDashboard({
142
- config: tokoSepatuWidgetConfig,
62
+ config: cidikaWidgetConfig,
143
63
  dataSource: source,
144
- adapter: adaptTokoSepatuData,
145
- createEmptyState: createEmptyTokoSepatuData,
64
+ adapter: adaptCidikaDashboardData,
65
+ createEmptyState: createEmptyDashboardData,
146
66
  languageCode: "id",
147
67
  dateLocale: "id-ID",
148
68
  labels,
@@ -150,7 +70,7 @@ export default function Dashboard() {
150
70
 
151
71
  return (
152
72
  <ReusableDashboardView
153
- config={tokoSepatuWidgetConfig}
73
+ config={cidikaWidgetConfig}
154
74
  labels={labels}
155
75
  loading={state.loading}
156
76
  error={state.error}
@@ -166,406 +86,77 @@ export default function Dashboard() {
166
86
  }
167
87
  ```
168
88
 
169
- > **Menggunakan react-i18next?** Gunakan `createDashboardLabels(t)` dari export modul
170
- > sebagai pengganti objek labels di atas. Lihat [bagian i18n](#-label--i18n).
171
-
172
- ### Step 4: Jalankan Aplikasi
89
+ ### 3. Jalankan
173
90
 
174
91
  ```bash
175
- npm run dev # Vite
176
- # atau
177
- npm start # Create React App
92
+ npm start
178
93
  ```
179
94
 
180
- ---
181
-
182
- ## 📚 Available Presets
183
-
184
- ### 1. Cidika Travel (Travel Agency)
185
-
186
- ```javascript
187
- import {
188
- cidikaWidgetConfig,
189
- createCidikaSupabaseSource,
190
- adaptCidikaDashboardData,
191
- createEmptyDashboardData,
192
- } from "@rozaqi02/reusable-dashboard";
193
-
194
- const source = createCidikaSupabaseSource(supabase);
195
- ```
196
-
197
- Tabel Supabase yang dibutuhkan: `bookings`, `packages`, `package_locales`, `page_sections`
198
-
199
- ### 2. Toko Sepatu (Online Shop)
200
-
201
- ```javascript
202
- import {
203
- tokoSepatuWidgetConfig,
204
- createTokoSepatuSupabaseSource,
205
- adaptTokoSepatuData,
206
- createEmptyTokoSepatuData,
207
- } from "@rozaqi02/reusable-dashboard";
208
-
209
- const source = createTokoSepatuSupabaseSource(supabase);
210
- ```
211
-
212
- Tabel Supabase yang dibutuhkan: `orders`, `order_items`, `products`, `customers`
213
-
214
- ### 3. Dummy UMKM (Generic)
215
-
216
- ```javascript
217
- import {
218
- dummyUmkmWidgetConfig,
219
- adaptDummyUmkmData,
220
- createEmptyDummyUmkmData,
221
- } from "@rozaqi02/reusable-dashboard";
222
- ```
223
-
224
- > Dummy UMKM tidak memiliki data source khusus.
225
- > Gunakan data source Toko Sepatu atau buat sendiri.
95
+ Selesai.
226
96
 
227
97
  ---
228
98
 
229
- ## 🎨 Customization
230
-
231
- ### Custom Widget Configuration
232
-
233
- Struktur konfigurasi yang benar (sesuai kontrak API modul):
234
-
235
- ```javascript
236
- export const myConfig = {
237
- id: "my-business.dashboard",
238
- defaultFilters: {
239
- statusScope: "all", // "confirmed" | "pending" | "all"
240
- daysPreset: 30, // preset hari (7 | 30 | 90 | 0 untuk kustom)
241
- sortPkgBy: "revenue", // "bookings" | "revenue"
242
- sortPkgDir: "desc", // "asc" | "desc"
243
- includePendingOverlay: false,
244
- audience: "",
245
- },
246
- widgets: {
247
- stats: [
248
- {
249
- id: "totalOrders",
250
- label: "confirmedBookings", // key pada objek labels
251
- icon: "TrendingUp", // nama ikon dari Lucide React
252
- valueKey: "bookingsConfirm", // key pada data.stats dari adapter
253
- format: "number", // "number" | "currency" | "percent"
254
- },
255
- {
256
- id: "totalRevenue",
257
- label: "confirmedRevenue",
258
- icon: "DollarSign",
259
- valueKey: "revenueConfirm",
260
- format: "currency",
261
- },
262
- ],
263
- charts: [
264
- {
265
- id: "dailyTrends",
266
- type: "dailyArea", // "dailyArea" | "statusPie" | "audiencePie" | "topPackagesBar"
267
- label: "dailyTrends",
268
- icon: "BarChart3",
269
- },
270
- {
271
- id: "statusDist",
272
- type: "statusPie",
273
- label: "statusDistribution",
274
- icon: "PieChart",
275
- },
276
- ],
277
- table: {
278
- id: "recentOrders",
279
- label: "recentBookings",
280
- icon: "Calendar",
281
- emptyLabel: "noRecentBookings",
282
- columns: [
283
- { id: "date", label: "date", accessor: "createdAt", type: "date" },
284
- { id: "customer", label: "customer", accessor: "customerName" },
285
- { id: "total", label: "total", accessor: "totalIDR", type: "currency" },
286
- {
287
- id: "status", label: "status", accessor: "statusLabel",
288
- type: "statusBadge", statusAccessor: "status",
289
- },
290
- ],
291
- },
292
- },
293
- };
294
- ```
295
-
296
- ### Custom Data Source
297
-
298
- Kontrak yang harus diimplementasikan:
299
-
300
- ```javascript
301
- export function createMySupabaseSource(supabase) {
302
- return {
303
- // Wajib: mengambil snapshot data dashboard
304
- async fetchDashboardSnapshot({ fromISO, toISO, audience, statusScope, languageCode }) {
305
- const { data: orders, error } = await supabase
306
- .from("orders")
307
- .select("id, created_at, total_amount, status, customer_id")
308
- .gte("created_at", fromISO)
309
- .lte("created_at", toISO);
310
-
311
- if (error) throw new Error("Failed to load orders.");
312
- return { bookings: orders, recent: [], packageLocales: [], staticCounts: {} };
313
- },
314
-
315
- // Opsional: subscribe ke perubahan realtime (untuk live update)
316
- subscribeLiveUpdate(onEvent) {
317
- const channel = supabase
318
- .channel("my-dashboard-live")
319
- .on("postgres_changes", { event: "*", schema: "public", table: "orders" }, onEvent)
320
- .subscribe();
321
- return () => { supabase.removeChannel(channel); };
322
- },
323
- };
324
- }
325
- ```
326
-
327
- ### Custom Data Adapter
328
-
329
- Adapter menerima parameter objek, bukan positional argument:
330
-
331
- ```javascript
332
- export function adaptMyBusinessData({ raw, filters, range, dateLocale, languageCode, labels }) {
333
- if (!raw) return createMyEmptyData();
99
+ ## Preset yang tersedia
334
100
 
335
- const orders = raw.bookings || [];
336
- let totalConfirmed = 0;
337
- let totalRevenue = 0;
338
-
339
- orders.forEach((row) => {
340
- if (row.status === "confirmed") {
341
- totalConfirmed += 1;
342
- totalRevenue += Number(row.total_amount) || 0;
343
- }
344
- });
345
-
346
- return {
347
- stats: {
348
- bookingsConfirm: totalConfirmed,
349
- revenueConfirm: totalRevenue,
350
- avgRevenue: totalConfirmed > 0 ? Math.round(totalRevenue / totalConfirmed) : 0,
351
- conversionRate: 0,
352
- packages: 0,
353
- sections: 0,
354
- },
355
- charts: {
356
- dailyTrends: [],
357
- statusDistribution: [],
358
- audienceDistribution: [],
359
- topPackages: [],
360
- },
361
- table: { recentBookings: [] },
362
- };
363
- }
364
-
365
- export function createMyEmptyData() {
366
- return {
367
- stats: { bookingsConfirm: 0, revenueConfirm: 0, avgRevenue: 0,
368
- conversionRate: 0, packages: 0, sections: 0 },
369
- charts: { dailyTrends: [], statusDistribution: [],
370
- audienceDistribution: [], topPackages: [] },
371
- table: { recentBookings: [] },
372
- };
373
- }
374
- ```
101
+ | Preset | Import | Tabel Supabase yang dibutuhkan |
102
+ |--------|--------|-------------------------------|
103
+ | **Cidika Travel** | `cidikaWidgetConfig`, `createCidikaSupabaseSource`, `adaptCidikaDashboardData` | `bookings`, `packages`, `package_locales`, `page_sections` |
104
+ | **Toko Sepatu** | `tokoSepatuWidgetConfig`, `createTokoSepatuSupabaseSource`, `adaptTokoSepatuData` | `orders`, `order_items`, `products`, `customers` |
105
+ | **Dummy UMKM** | `dummyUmkmWidgetConfig`, `adaptDummyUmkmData` | (gunakan salah satu source di atas) |
375
106
 
376
107
  ---
377
108
 
378
- ## 🌐 Label & i18n
379
-
380
- ### Tanpa i18n (plain object)
381
-
382
- Buat objek label secara manual (lihat contoh di Quick Start).
109
+ ## Pakai dengan i18n (react-i18next)
383
110
 
384
- ### Dengan react-i18next
385
-
386
- ```javascript
111
+ ```jsx
387
112
  import { useTranslation } from "react-i18next";
388
113
  import { createDashboardLabels } from "@rozaqi02/reusable-dashboard";
389
114
 
390
- function Dashboard() {
391
- const { t, i18n } = useTranslation();
392
- const languageCode = (i18n.language || "id").slice(0, 2);
393
- const dateLocale = languageCode === "id" ? "id-ID" : "en-US";
394
-
395
- // createDashboardLabels menerima fungsi t dari react-i18next
396
- const labels = useMemo(() => createDashboardLabels(t), [t]);
397
- // ...
398
- }
115
+ const { t } = useTranslation();
116
+ const labels = useMemo(() => createDashboardLabels(t), [t]);
399
117
  ```
400
118
 
401
119
  ---
402
120
 
403
- ## 📖 API Reference
404
-
405
- ### Configs
121
+ ## API Ringkas
406
122
 
407
- | Export | Deskripsi |
408
- |--------|-----------|
409
- | `cidikaWidgetConfig` | Config dashboard travel agency |
410
- | `tokoSepatuWidgetConfig` | Config dashboard toko online |
411
- | `dummyUmkmWidgetConfig` | Config dashboard bisnis generik |
123
+ ```js
124
+ // Config
125
+ import { cidikaWidgetConfig, tokoSepatuWidgetConfig, dummyUmkmWidgetConfig }
412
126
 
413
- ### Data Sources
127
+ // Data source
128
+ import { createCidikaSupabaseSource, createTokoSepatuSupabaseSource }
414
129
 
415
- | Export | Parameter | Deskripsi |
416
- |--------|-----------|-----------|
417
- | `createCidikaSupabaseSource(supabase)` | Supabase client | Data source Cidika Travel |
418
- | `createTokoSepatuSupabaseSource(supabase)` | Supabase client | Data source Toko Sepatu |
130
+ // Adapter
131
+ import { adaptCidikaDashboardData, createEmptyDashboardData }
132
+ import { adaptTokoSepatuData, createEmptyTokoSepatuData }
133
+ import { adaptDummyUmkmData, createEmptyDummyUmkmData }
419
134
 
420
- ### Data Adapters
135
+ // Hook
136
+ import { useReusableDashboard, useRealtimeUpdate }
421
137
 
422
- | Export | Deskripsi |
423
- |--------|-----------|
424
- | `adaptCidikaDashboardData({ raw, filters, range, dateLocale, languageCode, labels })` | Adapter Cidika Travel |
425
- | `createEmptyDashboardData()` | Empty state Cidika Travel |
426
- | `adaptTokoSepatuData({ raw, filters, range, dateLocale, languageCode, labels })` | Adapter Toko Sepatu |
427
- | `createEmptyTokoSepatuData()` | Empty state Toko Sepatu |
428
- | `adaptDummyUmkmData({ raw, filters, range, dateLocale, languageCode, labels })` | Adapter Dummy UMKM |
429
- | `createEmptyDummyUmkmData()` | Empty state Dummy UMKM |
138
+ // View
139
+ import { ReusableDashboardView }
430
140
 
431
- ### Hooks
432
-
433
- | Hook | Deskripsi |
434
- |------|-----------|
435
- | `useReusableDashboard(options)` | Hook utama orkestrasi dashboard |
436
- | `useRealtimeUpdate(options)` | Hook subscription Supabase Realtime |
437
-
438
- **useReusableDashboard options:**
439
-
440
- ```typescript
441
- {
442
- config: object, // Widget configuration
443
- dataSource: object, // Objek dengan fetchDashboardSnapshot + subscribeLiveUpdate
444
- adapter: Function, // ({ raw, filters, range, dateLocale, languageCode, labels }) => data
445
- createEmptyState: Function, // () => data kosong
446
- languageCode: string, // "id" | "en"
447
- dateLocale: string, // "id-ID" | "en-US"
448
- labels: object, // Objek label teks UI
449
- }
450
- ```
451
-
452
- **Return value useReusableDashboard:**
453
-
454
- ```typescript
455
- {
456
- data: object, // Data dashboard terproses
457
- loading: boolean, // Status loading
458
- error: string, // Pesan error (kosong jika tidak ada)
459
- filters: object, // State filter aktif
460
- updateFilter: (field, value) => void, // Update satu filter
461
- resetFilters: () => void, // Reset semua filter ke default
462
- refresh: ({ silent?: boolean }) => void, // Muat ulang data
463
- range: { fromISO, toISO, daysWindow }, // Rentang tanggal aktif
464
- liveUpdateEnabled: boolean, // Apakah realtime subscription aktif
465
- lastUpdatedAt: Date | null, // Waktu terakhir data diperbarui
466
- }
141
+ // Utils
142
+ import { createDashboardLabels, createDefaultFilters, resolveDateRange }
143
+ import { formatIDR, formatDate, shortId, formatYYYYMMDD }
467
144
  ```
468
145
 
469
- ### Components
470
-
471
- #### Atoms
472
- | Komponen | Props Utama | Deskripsi |
473
- |----------|-------------|-----------|
474
- | `Button` | `variant` (primary\|secondary\|ghost), `size` (sm\|md\|lg), `onClick`, `disabled` | Tombol aksi |
475
- | `Input` | `type`, `value`, `onChange`, `placeholder`, `label`, `disabled` | Field teks |
476
- | `Icon` | `name` (nama Lucide), `size`, `className` | Ikon SVG via registry |
477
- | `Typography` | `variant` (h1\|h2\|h3\|subheading\|body\|caption\|metric) | Teks hierarki |
478
- | `Badge` | `status` (confirmed\|pending\|cancelled\|success\|info\|default) | Label status |
479
- | `SkeletonLoader` | `className` | Placeholder loading |
480
-
481
- #### Molecules
482
- | Komponen | Props Utama | Deskripsi |
483
- |----------|-------------|-----------|
484
- | `StatCard` | `label`, `value`, `icon`, `format` (number\|currency\|percent), `trend` (up\|down) | Kartu metrik |
485
- | `SearchBar` | `value`, `onSearch`, `placeholder` | Input pencarian |
486
- | `DateRangeFilter` | `value`, `onChange` | Filter tanggal |
487
- | `ChartHeader` | `title`, `icon`, `actions` | Header grafik |
488
-
489
- #### Organisms
490
- | Komponen | Props Utama | Deskripsi |
491
- |----------|-------------|-----------|
492
- | `DataTable` | `columns`, `data`, `labels`, `dateLocale`, `searchable`, `sortable`, `pageSize`, `emptyLabel` | Tabel interaktif |
493
- | `ChartCard` | `widget`, `labels`, `loading`, `filters`, `chartData` | Kartu grafik |
494
- | `FilterPanel` | `filters`, `labels`, `onFilterChange`, `onResetFilters` | Panel filter |
495
- | `SidebarNavigation` | `items`, `activeId`, `onSelect` | Sidebar nav |
496
- | `TopbarHeader` | `title`, `onRefresh`, `liveUpdateEnabled` | Header atas |
497
-
498
- #### Templates & Pages
499
- | Komponen | Deskripsi |
500
- |----------|-----------|
501
- | `DashboardLayout` | Layout dengan slot sidebar dan main content |
502
- | `ReusableDashboardView` | Halaman dashboard lengkap (gunakan ini untuk integrasi cepat) |
503
-
504
- ### Utility Functions
505
-
506
- | Function | Signature | Deskripsi |
507
- |----------|-----------|-----------|
508
- | `createDashboardLabels(t)` | `t: Function` | Buat objek label dari fungsi i18n |
509
- | `createDefaultFilters(base?)` | `base?: object` | Buat filter default |
510
- | `resolveDateRange({ daysPreset, dateFrom, dateTo })` | — | Resolve range ke `{ fromISO, toISO, daysWindow }` |
511
- | `formatYYYYMMDD(date)` | `date: Date` | Format tanggal ke `YYYY-MM-DD` |
512
- | `formatIDR(value)` | `value: number` | Format angka ke Rupiah (`1.500.000`) |
513
- | `formatDate(value, locale)` | `value: string, locale: string` | Format tanggal dengan locale |
514
- | `shortId(value, length?)` | `value: string, length?: number` | Potong string (default 8 karakter) |
515
- | `toNumber(value)` | `value: any` | Konversi ke angka finite, fallback 0 |
516
- | `buildDayBuckets(daysWindow, fromISO, dateLocale)` | — | Bangun array bucket harian untuk chart |
517
- | `sortMapEntries(map, direction)` | `direction: "asc"\|"desc"` | Urutkan entri Map berdasarkan nilai |
518
- | `resolveIcon(name)` | `name: string` | Resolve nama string ke komponen Lucide |
519
-
520
146
  ---
521
147
 
522
- ## 🛠️ Development
148
+ ## Development
523
149
 
524
150
  ```bash
525
- # Clone & install
526
- git clone <repo-url>
151
+ git clone <repo>
527
152
  cd reusable-dashboard-umkm
528
153
  npm install
529
-
530
- # Build production
531
- npm run build
532
-
533
- # Run tests
154
+ npm run build # output ke dist/
534
155
  npm test
535
-
536
- # Test via npm link
537
- npm link
538
- cd /path/to/your-project
539
- npm link @rozaqi02/reusable-dashboard
540
156
  ```
541
157
 
542
- **Output build** (`dist/`):
543
- - `dist/index.js` — ESM (untuk Vite, modern bundler)
544
- - `dist/index.cjs` — CommonJS (untuk CRA, Node.js)
545
- - `dist/index.js.map` + `dist/index.cjs.map` — Source maps
546
-
547
158
  ---
548
159
 
549
- ## 📄 Examples
550
-
551
- | Folder | Domain | Konten |
552
- |--------|--------|--------|
553
- | `examples/cidika-travel-example/` | Travel agency | `AdminDashboard.jsx`, schema SQL |
554
- | `examples/toko-sepatu-example/` | Toko online | `TokoSepatuDashboard.jsx`, schema SQL, seed data |
555
- | `examples/dummy-umkm-example/` | UMKM generik | `DummyUmkmDashboard.jsx` |
556
-
557
- ---
558
-
559
- ## 🔒 License
560
-
561
- Proprietary — hanya untuk penggunaan oleh pihak yang berwenang.
562
-
563
- ---
564
-
565
- ## 📞 Support
566
-
567
- - Email: abrorrozaqi@gmail.com
568
-
569
- ---
160
+ ## License
570
161
 
571
- **Made with ❤️ for UMKM Indonesia**
162
+ MIT Ahmad Abror Rozaqi Fatoni