@rozaqi02/reusable-dashboard 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [1.0.0] - 2026-04-15
6
+
7
+ ### Added
8
+
9
+ 1. Initial reusable dashboard module release.
10
+ 2. Reusable dashboard presentation components (`stat`, `chart`, `table`, `layout`).
11
+ 3. Widget configuration layer for travel and dummy UMKM domain.
12
+ 4. Data adapter and Supabase data source with live update subscription.
13
+ 5. Hook orchestration for filters, snapshot refresh, and realtime update.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmad Abror Rozaqi Fatoni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,571 @@
1
+ # @rozaqi02/reusable-dashboard
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.
6
+
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)](#)
10
+
11
+ ---
12
+
13
+ ## 📦 Installation
14
+
15
+ ```bash
16
+ npm install @rozaqi02/reusable-dashboard
17
+ ```
18
+
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)).
36
+
37
+ ---
38
+
39
+ ## 🚀 Quick Start
40
+
41
+ ### Step 1: Setup Supabase Client
42
+
43
+ Buat file `src/lib/supabaseClient.js`:
44
+
45
+ ```javascript
46
+ import { createClient } from '@supabase/supabase-js';
47
+
48
+ 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
52
+ );
53
+ ```
54
+
55
+ Buat file `.env` di root project:
56
+
57
+ ```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
65
+ ```
66
+
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**:
79
+
80
+ ```jsx
81
+ import React, { useMemo } from "react";
82
+ import { supabase } from "./lib/supabaseClient";
83
+ import {
84
+ ReusableDashboardView,
85
+ tokoSepatuWidgetConfig,
86
+ createTokoSepatuSupabaseSource,
87
+ adaptTokoSepatuData,
88
+ createEmptyTokoSepatuData,
89
+ useReusableDashboard,
90
+ } from "@rozaqi02/reusable-dashboard";
91
+
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
+ };
139
+
140
+ export default function Dashboard() {
141
+ const state = useReusableDashboard({
142
+ config: tokoSepatuWidgetConfig,
143
+ dataSource: source,
144
+ adapter: adaptTokoSepatuData,
145
+ createEmptyState: createEmptyTokoSepatuData,
146
+ languageCode: "id",
147
+ dateLocale: "id-ID",
148
+ labels,
149
+ });
150
+
151
+ return (
152
+ <ReusableDashboardView
153
+ config={tokoSepatuWidgetConfig}
154
+ labels={labels}
155
+ loading={state.loading}
156
+ error={state.error}
157
+ filters={state.filters}
158
+ onFilterChange={state.updateFilter}
159
+ onResetFilters={state.resetFilters}
160
+ onRefresh={state.refresh}
161
+ data={state.data}
162
+ dateLocale="id-ID"
163
+ liveUpdateEnabled={state.liveUpdateEnabled}
164
+ />
165
+ );
166
+ }
167
+ ```
168
+
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
173
+
174
+ ```bash
175
+ npm run dev # Vite
176
+ # atau
177
+ npm start # Create React App
178
+ ```
179
+
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.
226
+
227
+ ---
228
+
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();
334
+
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
+ ```
375
+
376
+ ---
377
+
378
+ ## 🌐 Label & i18n
379
+
380
+ ### Tanpa i18n (plain object)
381
+
382
+ Buat objek label secara manual (lihat contoh di Quick Start).
383
+
384
+ ### Dengan react-i18next
385
+
386
+ ```javascript
387
+ import { useTranslation } from "react-i18next";
388
+ import { createDashboardLabels } from "@rozaqi02/reusable-dashboard";
389
+
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
+ }
399
+ ```
400
+
401
+ ---
402
+
403
+ ## 📖 API Reference
404
+
405
+ ### Configs
406
+
407
+ | Export | Deskripsi |
408
+ |--------|-----------|
409
+ | `cidikaWidgetConfig` | Config dashboard travel agency |
410
+ | `tokoSepatuWidgetConfig` | Config dashboard toko online |
411
+ | `dummyUmkmWidgetConfig` | Config dashboard bisnis generik |
412
+
413
+ ### Data Sources
414
+
415
+ | Export | Parameter | Deskripsi |
416
+ |--------|-----------|-----------|
417
+ | `createCidikaSupabaseSource(supabase)` | Supabase client | Data source Cidika Travel |
418
+ | `createTokoSepatuSupabaseSource(supabase)` | Supabase client | Data source Toko Sepatu |
419
+
420
+ ### Data Adapters
421
+
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 |
430
+
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
+ }
467
+ ```
468
+
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
+ ---
521
+
522
+ ## 🛠️ Development
523
+
524
+ ```bash
525
+ # Clone & install
526
+ git clone <repo-url>
527
+ cd reusable-dashboard-umkm
528
+ npm install
529
+
530
+ # Build production
531
+ npm run build
532
+
533
+ # Run tests
534
+ npm test
535
+
536
+ # Test via npm link
537
+ npm link
538
+ cd /path/to/your-project
539
+ npm link @rozaqi02/reusable-dashboard
540
+ ```
541
+
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
+ ---
548
+
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
+ ---
570
+
571
+ **Made with ❤️ for UMKM Indonesia**