This commit is contained in:
alealien666 2025-02-26 03:04:33 +07:00
parent 2870e2b7b9
commit d1e06e99aa
110 changed files with 4491 additions and 112 deletions

View File

@ -10,6 +10,7 @@
use Inertia\Inertia;
use App\Services\cekDenda;
use App\Services\GenerateMonthlyBill;
use Illuminate\Support\Facades\DB;
use Exception;
class PaymentController extends Controller
@ -53,16 +54,90 @@ public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $gene
]);
}
public function manualPayment(Request $request, $id)
public function manualPayment(Request $request, $paymentId)
{
$request->validate([
''
], [
'amount.required' => 'wajib mengisi nominal pembayaran',
]);
// $request->validate([
// 'range' => 'required|integer|min:1',
// ], [
// 'range.required' => 'Jumlah bulan pembayaran harus diisi.',
// 'range.integer' => 'Jumlah bulan pembayaran harus berupa angka.',
// 'range.min' => 'Minimal pembayaran adalah 1 bulan.',
// ]);
try {
DB::beginTransaction();
$range = (int) $request->input('range');
$payment = Payment::find($paymentId);
$unpaidDetails = DetailPayment::where('payment_id', $paymentId)
->where('status', 'unpaid')
->orderBy('payment_year', 'asc')
->orderBy('payment_month', 'asc')
->get();
$jumlahUnpaid = $unpaidDetails->count();
$totalAmount = 0;
if ($jumlahUnpaid >= $range) {
foreach ($unpaidDetails->take($range) as $detail) {
$total = $detail->amount + $detail->penalty;
$detail->update([
'status' => 'paid'
]);
$totalAmount += $total;
}
} else {
foreach ($unpaidDetails as $detail) {
$total = $detail->amount + $detail->penalty;
$detail->update([
'status' => 'paid'
]);
$totalAmount += $total;
}
$sisa = $range - $jumlahUnpaid;
$latestUnpaid = $unpaidDetails->last();
$bulanTerakhir = $latestUnpaid ? $latestUnpaid->payment_month : now()->month;
$tahunTerakhir = $latestUnpaid ? $latestUnpaid->payment_year : now()->year;
$typeId = $latestUnpaid ? $latestUnpaid->type_id : PaymentType::first()->id;
$nominal = PaymentType::where('id', $typeId)->value('nominal');
for ($i = 1; $i <= $sisa; $i++) {
$bulanTerakhir++;
if ($bulanTerakhir > 12) {
$bulanTerakhir = 1;
$tahunTerakhir++;
}
$penalty = 0;
$totalAmount += $nominal + $penalty;
DetailPayment::create([
'payment_id' => $paymentId,
'payment_month' => $bulanTerakhir,
'payment_year' => $tahunTerakhir,
'amount' => $nominal,
'penalty' => $penalty,
'status' => 'paid',
'type_id' => $typeId
]);
}
}
$payment->update([
'amount_payment' => $totalAmount
]);
DB::commit();
// return redirect()->back()->with('success', 'Berhasil Melakukan Pembayaran');
// return $request->all();
return $payment;
} catch (Exception $e) {
DB::rollBack();
// return redirect()->back()->with('error', $e->getMessage());
return $e->getMessage();
}
}
}

370
package-lock.json generated
View File

@ -5,7 +5,13 @@
"packages": {
"": {
"dependencies": {
"@inertiajs/inertia": "^0.11.1"
"@heroicons/react": "^2.2.0",
"@inertiajs/inertia": "^0.11.1",
"@reduxjs/toolkit": "^2.6.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
"theme-change": "^2.5.0",
"web-vitals": "^4.2.4"
},
"devDependencies": {
"@headlessui/react": "^1.4.2",
@ -63,30 +69,30 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
"integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
"version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz",
"integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
"integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.5",
"@babel/generator": "^7.26.9",
"@babel/helper-compilation-targets": "^7.26.5",
"@babel/helper-module-transforms": "^7.26.0",
"@babel/helpers": "^7.26.7",
"@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
"@babel/traverse": "^7.26.7",
"@babel/types": "^7.26.7",
"@babel/helpers": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/template": "^7.26.9",
"@babel/traverse": "^7.26.9",
"@babel/types": "^7.26.9",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@ -102,13 +108,13 @@
}
},
"node_modules/@babel/generator": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
"integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.26.5",
"@babel/types": "^7.26.5",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@ -200,25 +206,25 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
"dev": true,
"dependencies": {
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.7"
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
"dev": true,
"dependencies": {
"@babel/types": "^7.26.7"
"@babel/types": "^7.26.9"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -258,30 +264,30 @@
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
"integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.5",
"@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.7",
"@babel/generator": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@ -290,9 +296,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@ -671,6 +677,14 @@
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
"peerDependencies": {
"react": ">= 16 || ^19.0.0-rc"
}
},
"node_modules/@inertiajs/core": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-1.3.0.tgz",
@ -824,6 +838,29 @@
"node": ">=14"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz",
"integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==",
"dependencies": {
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@ -837,12 +874,12 @@
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
"integrity": "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==",
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz",
"integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==",
"dev": true,
"dependencies": {
"@tanstack/virtual-core": "3.11.3"
"@tanstack/virtual-core": "3.13.2"
},
"funding": {
"type": "github",
@ -854,9 +891,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz",
"integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==",
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz",
"integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==",
"dev": true,
"funding": {
"type": "github",
@ -904,6 +941,16 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
},
"node_modules/@vitejs/plugin-react": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
@ -1098,9 +1145,9 @@
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -1134,9 +1181,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001696",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
"integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==",
"version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true,
"funding": [
{
@ -1240,6 +1287,14 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -1370,9 +1425,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.5.88",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz",
"integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==",
"version": "1.5.104",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.104.tgz",
"integrity": "sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==",
"dev": true
},
"node_modules/emoji-regex": {
@ -1408,6 +1463,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@ -1489,9 +1559,9 @@
"dev": true
},
"node_modules/fastq": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
@ -1529,12 +1599,12 @@
}
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
@ -1545,13 +1615,14 @@
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
@ -1603,16 +1674,16 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
@ -1700,6 +1771,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -1711,6 +1797,15 @@
"node": ">= 0.4"
}
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -1810,8 +1905,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/jsesc": {
"version": "3.1.0",
@ -1882,7 +1976,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -2067,9 +2160,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"engines": {
"node": ">= 0.4"
},
@ -2157,9 +2250,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
@ -2343,7 +2436,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -2355,7 +2447,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@ -2364,6 +2455,28 @@
"react": "^18.3.1"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@ -2373,6 +2486,44 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.2.0.tgz",
"integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.2.0.tgz",
"integrity": "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q==",
"dependencies": {
"react-router": "7.2.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -2394,6 +2545,24 @@
"node": ">=8.10.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -2467,7 +2636,6 @@
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@ -2481,6 +2649,11 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2758,6 +2931,11 @@
"node": ">=14.0.0"
}
},
"node_modules/theme-change": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/theme-change/-/theme-change-2.5.0.tgz",
"integrity": "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ=="
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -2797,6 +2975,11 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
},
"node_modules/update-browserslist-db": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
@ -2827,6 +3010,14 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2898,6 +3089,11 @@
"picomatch": "^2.3.1"
}
},
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -20,6 +20,12 @@
"vite": "^4.0.0"
},
"dependencies": {
"@inertiajs/inertia": "^0.11.1"
"@heroicons/react": "^2.2.0",
"@inertiajs/inertia": "^0.11.1",
"@reduxjs/toolkit": "^2.6.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
"theme-change": "^2.5.0",
"web-vitals": "^4.2.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

BIN
public/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/assets/intro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
public/assets/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/assets/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/assets/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "DashWind",
"name": "DashWind",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/assets/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

14
public/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Test</title>
</head>
<body>
<div id="root">Loading...</div>
</body>
</html>

View File

@ -0,0 +1,44 @@
const moment = require("moment");
module.exports = Object.freeze({
CALENDAR_INITIAL_EVENTS : [
{title : "Product call", theme : "GREEN", startTime : moment().add(-12, 'd').startOf('day'), endTime : moment().add(-12, 'd').endOf('day')},
{title : "Meeting with tech team", theme : "PINK", startTime : moment().add(-8, 'd').startOf('day'), endTime : moment().add(-8, 'd').endOf('day')},
{title : "Meeting with Cristina", theme : "PURPLE", startTime : moment().add(-2, 'd').startOf('day'), endTime : moment().add(-2, 'd').endOf('day')},
{title : "Meeting with Alex", theme : "BLUE", startTime : moment().startOf('day'), endTime : moment().endOf('day')},
{title : "Product Call", theme : "GREEN", startTime : moment().startOf('day'), endTime : moment().endOf('day')},
{title : "Client Meeting", theme : "PURPLE", startTime : moment().startOf('day'), endTime : moment().endOf('day')},
{title : "Client Meeting", theme : "ORANGE", startTime : moment().add(3, 'd').startOf('day'), endTime : moment().add(3, 'd').endOf('day')},
{title : "Product meeting", theme : "PINK", startTime : moment().add(5, 'd').startOf('day'), endTime : moment().add(5, 'd').endOf('day')},
{title : "Sales Meeting", theme : "GREEN", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Product Meeting", theme : "ORANGE", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Marketing Meeting", theme : "PINK", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Client Meeting", theme : "GREEN", startTime : moment().add(8, 'd').startOf('day'), endTime : moment().add(8, 'd').endOf('day')},
{title : "Sales meeting", theme : "BLUE", startTime : moment().add(12, 'd').startOf('day'), endTime : moment().add(12, 'd').endOf('day')},
{title : "Client meeting", theme : "PURPLE", startTime : moment().add(16, 'd').startOf('day'), endTime : moment().add(16, 'd').endOf('day')},
],
RECENT_TRANSACTIONS : [
{name : "Alex", avatar : "https://reqres.in/img/faces/1-image.jpg", email : "alex@dashwind.com", location : "Paris", amount : 100, date : moment().endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "ereena@dashwind.com", location : "London", amount : 190, date : moment().add(-1, 'd').endOf('day')},
{name : "John", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "jhon@dashwind.com", location : "Canada", amount : 112, date : moment().add(-1, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/4-image.jpg", email : "matrix@dashwind.com", location : "Peru", amount : 111, date : moment().add(-1, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/5-image.jpg", email : "virat@dashwind.com", location : "London", amount : 190, date : moment().add(-2, 'd').endOf('day')},
{name : "Miya", avatar : "https://reqres.in/img/faces/6-image.jpg", email : "miya@dashwind.com", location : "Paris", amount : 230, date : moment().add(-2, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Canada", amount : 331, date : moment().add(-2, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/1-image.jpg", email : "matrix@dashwind.com", location : "London", amount : 581, date : moment().add(-2, 'd').endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "ereena@dashwind.com", location : "Tokyo", amount : 151, date : moment().add(-2, 'd').endOf('day')},
{name : "John", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "jhon@dashwind.com", location : "Paris", amount : 91, date : moment().add(-2, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Canada", amount : 161, date : moment().add(-3, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/4-image.jpg", email : "matrix@dashwind.com", location : "US", amount : 121, date : moment().add(-3, 'd').endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/6-image.jpg", email : "jhon@dashwind.com", location : "Tokyo", amount : 713, date : moment().add(-3, 'd').endOf('day')},
{name : "John", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "ereena@dashwind.com", location : "London", amount : 217, date : moment().add(-3, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Paris", amount : 117, date : moment().add(-3, 'd').endOf('day')},
{name : "Miya", avatar : "https://reqres.in/img/faces/7-image.jpg", email : "jhon@dashwind.com", location : "Canada", amount : 612, date : moment().add(-3, 'd').endOf('day')},
{name : "Matrix", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "matrix@dashwind.com", location : "London", amount : 631, date : moment().add(-3, 'd').endOf('day')},
{name : "Virat", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "ereena@dashwind.com", location : "Tokyo", amount : 151, date : moment().add(-3, 'd').endOf('day')},
{name : "Ereena", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "virat@dashwind.com", location : "Paris", amount : 617, date : moment().add(-3, 'd').endOf('day')},
]
});

View File

@ -0,0 +1,15 @@
export const MODAL_BODY_TYPES = Object.freeze({
USER_DETAIL: "USER_DETAIL",
LEAD_ADD_NEW: "LEAD_ADD_NEW",
CONFIRMATION: "CONFIRMATION",
DEFAULT: "",
});
export const RIGHT_DRAWER_TYPES = Object.freeze({
NOTIFICATION: "NOTIFICATION",
CALENDAR_EVENTS: "CALENDAR_EVENTS",
});
export const CONFIRMATION_MODAL_CLOSE_TYPES = Object.freeze({
LEAD_DELETE: "LEAD_DELETE",
});

38
resources/js/App.css Normal file
View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,37 @@
import axios from "axios"
const checkAuth = () => {
/* Getting token value stored in localstorage, if token is not present we will open login page
for all internal dashboard routes */
const TOKEN = localStorage.getItem("token")
const PUBLIC_ROUTES = ["login", "forgot-password", "register", "documentation"]
const isPublicPage = PUBLIC_ROUTES.some(r => window.location.href.includes(r))
if (!TOKEN && !isPublicPage) {
window.location.href = '/login'
return;
} else {
axios.defaults.headers.common['Authorization'] = `Bearer ${TOKEN}`
axios.interceptors.request.use(function (config) {
// UPDATE: Add this code to show global loading indicator
document.body.classList.add('loading-indicator');
return config
}, function (error) {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
// UPDATE: Add this code to hide global loading indicator
document.body.classList.remove('loading-indicator');
return response;
}, function (error) {
document.body.classList.remove('loading-indicator');
return Promise.reject(error);
});
return TOKEN
}
}
export default checkAuth

View File

@ -0,0 +1,27 @@
import axios from "axios"
const initializeApp = () => {
// Setting base URL for all API request via axios
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL;
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
// dev code
} else {
// Prod build code
// Removing console.log from prod
console.log = () => { };
// init analytics here
}
}
export default initializeApp

View File

@ -0,0 +1,16 @@
import { configureStore } from '@reduxjs/toolkit'
import headerSlice from '../Components/features/common/headerSlice'
import modalSlice from '../Components/features/common/modalSlice'
import rightDrawerSlice from '../Components/features/common/rightDrawerSlice'
import leadsSlice from '../Components/features/leads/leadSlice'
const combinedReducer = {
header: headerSlice,
rightDrawer: rightDrawerSlice,
modal: modalSlice,
lead: leadsSlice
}
export default configureStore({
reducer: combinedReducer
})

View File

@ -0,0 +1,146 @@
import { useEffect, useState } from "react";
import ChevronLeftIcon from "@heroicons/react/24/solid/ChevronLeftIcon";
import ChevronRightIcon from "@heroicons/react/24/solid/ChevronRightIcon";
import moment from "moment";
import { CALENDAR_EVENT_STYLE } from "./util";
const THEME_BG = CALENDAR_EVENT_STYLE
function CalendarView({calendarEvents, addNewEvent, openDayDetail}){
const today = moment().startOf('day')
const weekdays = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
const colStartClasses = [
"",
"col-start-2",
"col-start-3",
"col-start-4",
"col-start-5",
"col-start-6",
"col-start-7",
];
const [firstDayOfMonth, setFirstDayOfMonth] = useState(moment().startOf('month'))
const [events, setEvents] = useState([])
const [currMonth, setCurrMonth] = useState(() => moment(today).format("MMM-yyyy"));
useEffect(() => {
setEvents(calendarEvents)
}, [calendarEvents])
const allDaysInMonth = ()=> {
let start = moment(firstDayOfMonth).startOf('week')
let end = moment(moment(firstDayOfMonth).endOf('month')).endOf('week')
var days = [];
var day = start;
while (day <= end) {
days.push(day.toDate());
day = day.clone().add(1, 'd');
}
return days
}
const getEventsForCurrentDate = (date) => {
let filteredEvents = events.filter((e) => {return moment(date).isSame(moment(e.startTime), 'day') } )
if(filteredEvents.length > 2){
let originalLength = filteredEvents.length
filteredEvents = filteredEvents.slice(0, 2)
filteredEvents.push({title : `${originalLength - 2} more`, theme : "MORE"})
}
return filteredEvents
}
const openAllEventsDetail = (date, theme) => {
if(theme != "MORE")return 1
let filteredEvents = events.filter((e) => {return moment(date).isSame(moment(e.startTime), 'day') } ).map((e) => {return {title : e.title, theme : e.theme}})
openDayDetail({filteredEvents, title : moment(date).format("D MMM YYYY")})
}
const isToday = (date) => {
return moment(date).isSame(moment(), 'day');
}
const isDifferentMonth = (date) => {
return moment(date).month() != moment(firstDayOfMonth).month()
}
const getPrevMonth = (event) => {
const firstDayOfPrevMonth = moment(firstDayOfMonth).add(-1, 'M').startOf('month');
setFirstDayOfMonth(firstDayOfPrevMonth)
setCurrMonth(moment(firstDayOfPrevMonth).format("MMM-yyyy"));
};
const getCurrentMonth = (event) => {
const firstDayOfCurrMonth = moment().startOf('month');
setFirstDayOfMonth(firstDayOfCurrMonth)
setCurrMonth(moment(firstDayOfCurrMonth).format("MMM-yyyy"));
};
const getNextMonth = (event) => {
const firstDayOfNextMonth = moment(firstDayOfMonth).add(1, 'M').startOf('month');
setFirstDayOfMonth(firstDayOfNextMonth)
setCurrMonth(moment(firstDayOfNextMonth).format("MMM-yyyy"));
};
return(
<>
<div className="w-full bg-base-100 p-4 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex justify-normal gap-2 sm:gap-4">
<p className="font-semibold text-xl w-48">
{moment(firstDayOfMonth).format("MMMM yyyy").toString()}<span className="text-xs ml-2 ">Beta</span>
</p>
<button className="btn btn-square btn-sm btn-ghost" onClick={getPrevMonth}><ChevronLeftIcon
className="w-5 h-5"
/></button>
<button className="btn btn-sm btn-ghost normal-case" onClick={getCurrentMonth}>
Current Month</button>
<button className="btn btn-square btn-sm btn-ghost" onClick={getNextMonth}><ChevronRightIcon
className="w-5 h-5"
/></button>
</div>
<div>
<button className="btn btn-sm btn-ghost btn-outline normal-case" onClick={addNewEvent}>Add New Event</button>
</div>
</div>
<div className="my-4 divider" />
<div className="grid grid-cols-7 gap-6 sm:gap-12 place-items-center">
{weekdays.map((day, key) => {
return (
<div className="text-xs capitalize" key={key}>
{day}
</div>
);
})}
</div>
<div className="grid grid-cols-7 mt-1 place-items-center">
{allDaysInMonth().map((day, idx) => {
return (
<div key={idx} className={colStartClasses[moment(day).day().toString()] + " border border-solid w-full h-28 "}>
<p className={`inline-block flex items-center justify-center h-8 w-8 rounded-full mx-1 mt-1 text-sm cursor-pointer hover:bg-base-300 ${isToday(day) && " bg-blue-100 dark:bg-blue-400 dark:hover:bg-base-300 dark:text-white"} ${isDifferentMonth(day) && " text-slate-400 dark:text-slate-600"}`} onClick={() => addNewEvent(day)}> { moment(day).format("D") }</p>
{
getEventsForCurrentDate(day).map((e, k) => {
return <p key={k} onClick={() => openAllEventsDetail(day, e.theme)} className={`text-xs px-2 mt-1 truncate ${THEME_BG[e.theme] || ""}`}>{e.title}</p>
})
}
</div>
);
})}
</div>
</div>
</>
)
}
export default CalendarView

View File

@ -0,0 +1,10 @@
const CALENDAR_EVENT_STYLE = Object.freeze({
BLUE: "bg-blue-200 dark:bg-blue-600 dark:text-blue-100",
GREEN: "bg-green-200 dark:bg-green-600 dark:text-green-100",
PURPLE: "bg-purple-200 dark:bg-purple-600 dark:text-purple-100",
ORANGE: "bg-orange-200 dark:bg-orange-600 dark:text-orange-100",
PINK: "bg-pink-200 dark:bg-pink-600 dark:text-pink-100",
MORE: "hover:underline cursor-pointer font-medium"
});
export { CALENDAR_EVENT_STYLE };

View File

@ -0,0 +1,30 @@
import Subtitle from "../Typography/Subtitle"
function TitleCard({title, children, topMargin, TopSideButtons}){
return(
<div className={"card w-full p-6 bg-base-100 shadow-xl " + (topMargin || "mt-6")}>
{/* Title for Card */}
<Subtitle styleClass={TopSideButtons ? "inline-block" : ""}>
{title}
{/* Top side button, show only if present */}
{
TopSideButtons && <div className="inline-block float-right">{TopSideButtons}</div>
}
</Subtitle>
<div className="divider mt-2"></div>
{/** Card Body */}
<div className='h-full w-full pb-6 bg-base-100'>
{children}
</div>
</div>
)
}
export default TitleCard

View File

@ -0,0 +1,83 @@
import { themeChange } from 'theme-change'
import React, { useEffect, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import BellIcon from '@heroicons/react/24/outline/BellIcon'
import Bars3Icon from '@heroicons/react/24/outline/Bars3Icon'
import MoonIcon from '@heroicons/react/24/outline/MoonIcon'
import SunIcon from '@heroicons/react/24/outline/SunIcon'
import { openRightDrawer } from './features/common/rightDrawerSlice'
import { RIGHT_DRAWER_TYPES } from '../../../public/utils/globalConstantUtil'
import { Link } from 'react-router-dom'
function Header() {
const dispatch = useDispatch()
const { noOfNotifications, pageTitle } = useSelector(state => state.header)
// State untuk menyimpan tema saat ini
const [theme, setTheme] = useState(localStorage.getItem("theme") ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light")
);
useEffect(() => {
themeChange(false);
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}, [theme]); // Setiap kali state theme berubah, update data-theme
// Fungsi untuk mengganti tema
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === "dark" ? "light" : "dark");
}
const openNotification = () => {
dispatch(openRightDrawer({ header: "Notifications", bodyType: RIGHT_DRAWER_TYPES.NOTIFICATION }))
}
function logoutUser() {
localStorage.clear();
window.location.href = '/'
}
return (
<div className="navbar sticky top-0 bg-base-100 z-10 shadow-md">
<div className="flex-1">
<label htmlFor="left-sidebar-drawer" className="btn btn-primary drawer-button lg:hidden">
<Bars3Icon className="h-5 inline-block w-5" />
</label>
<h1 className="text-2xl font-semibold ml-2">{pageTitle}</h1>
</div>
<div className="flex-none">
{/* Light and dark theme selection toggle */}
<button onClick={toggleTheme} className="btn btn-ghost">
{theme === "dark" ? <SunIcon className="w-6 h-6" /> : <MoonIcon className="w-6 h-6" />}
</button>
{/* Notification icon */}
<button className="btn btn-ghost ml-4 btn-circle" onClick={openNotification}>
<div className="indicator">
<BellIcon className="h-6 w-6" />
{noOfNotifications > 0 && <span className="indicator-item badge badge-secondary badge-sm">{noOfNotifications}</span>}
</div>
</button>
{/* Profile icon */}
<div className="dropdown dropdown-end ml-4">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<img src="https://placeimg.com/80/80/people" alt="profile" />
</div>
</label>
<ul tabIndex={0} className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
<li><Link to={'/app/settings-profile'}>Profile Settings</Link></li>
<li><Link to={'/app/settings-billing'}>Bill History</Link></li>
<div className="divider mt-0 mb-0"></div>
<li><a onClick={logoutUser}>Logout</a></li>
</ul>
</div>
</div>
</div>
)
}
export default Header;

View File

@ -0,0 +1,24 @@
import { useState } from "react"
function InputText({labelTitle, labelStyle, type, containerStyle, defaultValue, placeholder, updateFormValue, updateType}){
const [value, setValue] = useState(defaultValue)
const updateInputValue = (val) => {
setValue(val)
updateFormValue({updateType, value : val})
}
return(
<div className={`form-control w-full ${containerStyle}`}>
<label className="label">
<span className={"label-text text-base-content " + labelStyle}>{labelTitle}</span>
</label>
<input type={type || "text"} value={value} placeholder={placeholder || ""} onChange={(e) => updateInputValue(e.target.value)}className="input input-bordered w-full " />
</div>
)
}
export default InputText

View File

@ -0,0 +1,22 @@
import React, { useEffect } from 'react'
function SearchBar({searchText, styleClass, placeholderText, setSearchText}) {
const updateSearchInput = (value) => {
setSearchText(value)
}
return (
<div className={"inline-block " + styleClass}>
<div className="input-group relative flex flex-wrap items-stretch w-full ">
<input type="search" value={searchText} placeholder={placeholderText || "Search"} onChange={(e) => updateSearchInput(e.target.value)} className="input input-sm input-bordered w-full max-w-xs" />
</div>
</div>
)
}
export default SearchBar

View File

@ -0,0 +1,41 @@
import axios from 'axios'
import capitalize from 'capitalize-the-first-letter'
import React, { useState, useEffect } from 'react'
import InformationCircleIcon from '@heroicons/react/24/outline/InformationCircleIcon'
function SelectBox(props){
const {labelTitle, labelDescription, defaultValue, containerStyle, placeholder, labelStyle, options, updateType, updateFormValue} = props
const [value, setValue] = useState(defaultValue || "")
const updateValue = (newValue) =>{
updateFormValue({updateType, value : newValue})
setValue(newValue)
}
return (
<div className={`inline-block ${containerStyle}`}>
<label className={`label ${labelStyle}`}>
<div className="label-text">{labelTitle}
{labelDescription && <div className="tooltip tooltip-right" data-tip={labelDescription}><InformationCircleIcon className='w-4 h-4'/></div>}
</div>
</label>
<select className="select select-bordered w-full" value={value} onChange={(e) => updateValue(e.target.value)}>
<option disabled value="PLACEHOLDER">{placeholder}</option>
{
options.map((o, k) => {
return <option value={o.value || o.name} key={k}>{o.name}</option>
})
}
</select>
</div>
)
}
export default SelectBox

View File

@ -0,0 +1,24 @@
import { useState } from "react"
function TextAreaInput({labelTitle, labelStyle, type, containerStyle, defaultValue, placeholder, updateFormValue, updateType}){
const [value, setValue] = useState(defaultValue)
const updateInputValue = (val) => {
setValue(val)
updateFormValue({updateType, value : val})
}
return(
<div className={`form-control w-full ${containerStyle}`}>
<label className="label">
<span className={"label-text text-base-content " + labelStyle}>{labelTitle}</span>
</label>
<textarea value={value} className="textarea textarea-bordered w-full" placeholder={placeholder || ""} onChange={(e) => updateInputValue(e.target.value)}></textarea>
</div>
)
}
export default TextAreaInput

View File

@ -0,0 +1,24 @@
import { useState } from "react"
function ToogleInput({labelTitle, labelStyle, type, containerStyle, defaultValue, placeholder, updateFormValue, updateType}){
const [value, setValue] = useState(defaultValue)
const updateToogleValue = () => {
setValue(!value)
updateFormValue({updateType, value : !value})
}
return(
<div className={`form-control w-full ${containerStyle}`}>
<label className="label cursor-pointer">
<span className={"label-text text-base-content " + labelStyle}>{labelTitle}</span>
<input type="checkbox" className="toggle" checked={value} onChange={(e) => updateToogleValue()}/>
</label>
</div>
)
}
export default ToogleInput

View File

@ -0,0 +1,47 @@
import PageContent from "./PageContent"
import LeftSidebar from "./LeftSidebar"
import { useSelector, useDispatch } from 'react-redux'
import RightSidebar from './RightSidebar'
import { useEffect } from "react"
import { removeNotificationMessage } from "@/Components/features/common/headerSlice"
// import { NotificationContainer, NotificationManager } from 'react-notifications';
import 'react-notifications/lib/notifications.css';
import ModalLayout from "./ModalLayout"
function Layout() {
const dispatch = useDispatch()
const { newNotificationMessage, newNotificationStatus } = useSelector(state => state.header)
useEffect(() => {
if (newNotificationMessage !== "") {
if (newNotificationStatus === 1) NotificationManager.success(newNotificationMessage, 'Success')
if (newNotificationStatus === 0) NotificationManager.error(newNotificationMessage, 'Error')
dispatch(removeNotificationMessage())
}
}, [newNotificationMessage])
return (
<>
{ /* Left drawer - containing page content and side bar (always open) */}
<div className="drawer lg:drawer-open">
<input id="left-sidebar-drawer" type="checkbox" className="drawer-toggle" />
<PageContent />
<LeftSidebar />
</div>
{ /* Right drawer - containing secondary content like notifications list etc.. */}
<RightSidebar />
{/** Notification layout container */}
{/* <NotificationContainer /> */}
{/* Modal layout container */}
<ModalLayout />
</>
)
}
export default Layout

View File

@ -0,0 +1,58 @@
import routes from '../Routes/sidebar'
import { NavLink, Routes, Link, useLocation } from 'react-router-dom'
import SidebarSubmenu from './SidebarSubmenu';
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'
import { useDispatch } from 'react-redux';
function LeftSidebar() {
const location = useLocation();
const dispatch = useDispatch()
console.log('anu')
const close = (e) => {
document.getElementById('left-sidebar-drawer').click()
}
return (
<div className="drawer-side z-30">
<label htmlFor="left-sidebar-drawer" className="drawer-overlay"></label>
<ul className="menu pt-2 w-80 bg-base-100 min-h-full text-base-content">
<button className="btn btn-ghost bg-base-300 btn-circle z-50 top-0 right-0 mt-4 mr-2 absolute lg:hidden" onClick={() => close()}>
<XMarkIcon className="h-5 inline-block w-5" />
</button>
<li className="mb-2 font-semibold text-xl">
<Link to={'/app/welcome'}><img className="mask mask-squircle w-10" src="/logo192.png" alt="DashWind Logo" />DashWind</Link> </li>
{
routes.map((route, k) => {
return (
<li className="" key={k}>
{
route.submenu ?
<SidebarSubmenu {...route} /> :
(<NavLink
end
to={route.path}
className={({ isActive }) => `${isActive ? 'font-semibold bg-base-200 ' : 'font-normal'}`} >
{route.icon} {route.name}
{
location.pathname === route.path ? (<span className="absolute inset-y-0 left-0 w-1 rounded-tr-md rounded-br-md bg-primary "
aria-hidden="true"></span>) : null
}
</NavLink>)
}
</li>
)
})
}
</ul>
</div>
)
}
export default LeftSidebar

View File

@ -8,6 +8,7 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
const [paymentDetails, setPaymentDetails] = useState({});
useEffect(() => {
// console.log(initialData)
setFormData(initialData || {});
setSelectedPayments([]);
setPaymentDetails({});
@ -59,7 +60,6 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
});
};
const handleRemovePayment = (paymentType) => {
setSelectedPayments(selectedPayments.filter((p) => p !== paymentType));
const newDetails = { ...paymentDetails };
@ -79,6 +79,14 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
formDataObj.append("payments", JSON.stringify(paymentDetails));
const url = initialData ? `/update${tableName}/${initialData.id}` : `/add${tableName}`;
if (!initialData?.id) {
// console.error("Error: initialData.id tidak ditemukan");
return;
}
// console.log("URL yang dikirim:", url);
// console.log("Data yang dikirim:", formDataObj);
Inertia.post(url, formDataObj, {
forceFormData: true,
onError: (errors) => setErrors(errors),
@ -173,7 +181,7 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
min="1"
value={paymentDetails[paymentType].range}
onChange={(e) => handleRangeChange(paymentType, parseInt(e.target.value))}
className="grow border-none focus:ring-0"
className="grow border-none focus:ring-0" name="range"
/>
</label>
</div>
@ -221,6 +229,6 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
);
};
export default ModalInput;
export default ModalInput

View File

@ -0,0 +1,46 @@
import { useEffect } from 'react'
import { MODAL_BODY_TYPES } from '../../../public/utils/globalConstantUtil'
import { useSelector, useDispatch } from 'react-redux'
import { closeModal } from '@/Components/features/common/modalSlice'
import AddLeadModalBody from '@/Components/features/leads/components/AddLeadModalBody'
import ConfirmationModalBody from '@/Components/features/common/components/ConfirmationModalBody'
function ModalLayout() {
const { isOpen, bodyType, size, extraObject, title } = useSelector(state => state.modal)
const dispatch = useDispatch()
const close = (e) => {
dispatch(closeModal(e))
}
return (
<>
{/* The button to open modal */}
{/* Put this part before </body> tag */}
<div className={`modal ${isOpen ? "modal-open" : ""}`}>
<div className={`modal-box ${size === 'lg' ? 'max-w-5xl' : ''}`}>
<button className="btn btn-sm btn-circle absolute right-2 top-2" onClick={() => close()}></button>
<h3 className="font-semibold text-2xl pb-6 text-center">{title}</h3>
{/* Loading modal body according to different modal type */}
{
{
[MODAL_BODY_TYPES.LEAD_ADD_NEW]: <AddLeadModalBody closeModal={close} extraObject={extraObject} />,
[MODAL_BODY_TYPES.CONFIRMATION]: <ConfirmationModalBody extraObject={extraObject} closeModal={close} />,
[MODAL_BODY_TYPES.DEFAULT]: <div></div>
}[bodyType]
}
</div>
</div>
</>
)
}
export default ModalLayout

View File

@ -0,0 +1,55 @@
import Header from "./Header"
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
import routes from '@/Routes'
import { Suspense, lazy } from 'react'
import SuspenseContent from "./SuspenseContent"
import { useSelector } from 'react-redux'
import { useEffect, useRef } from "react"
// const Page404 = lazy(() => import('@/Pages/protected/404'))
function PageContent() {
const mainContentRef = useRef(null);
const { pageTitle } = useSelector(state => state.header)
// Scroll back to top on new page load
useEffect(() => {
mainContentRef.current.scroll({
top: 0,
behavior: "smooth"
});
}, [pageTitle])
return (
<div className="drawer-content flex flex-col ">
<Header />
<main className="flex-1 overflow-y-auto md:pt-4 pt-4 px-6 bg-base-200" ref={mainContentRef}>
<Suspense fallback={<SuspenseContent />}>
<Routes>
{
routes.map((route, key) => {
return (
<Route
key={key}
exact={true}
path={`${route.path}`}
element={<route.component />}
/>
)
})
}
{/* Redirecting unknown url to 404 page */}
{/* <Route path="*" element={<Page404 />} /> */}
</Routes>
</Suspense>
<div className="h-16"></div>
</main>
</div>
)
}
export default PageContent

View File

@ -0,0 +1,60 @@
import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon'
import { useDispatch, useSelector } from 'react-redux'
import NotificationBodyRightDrawer from '@/Components/features/common/components/NotificationBodyRightDrawer'
import { closeRightDrawer } from '@/Components/features/common/rightDrawerSlice'
import { RIGHT_DRAWER_TYPES } from '../.../../../../public/utils/globalConstantUtil'
import CalendarEventsBodyRightDrawer from '@/Components/features/calendar/CalendarEventsBodyRightDrawer'
function RightSidebar() {
const { isOpen, bodyType, extraObject, header } = useSelector(state => state.rightDrawer)
const dispatch = useDispatch()
const close = (e) => {
dispatch(closeRightDrawer(e))
}
return (
<div className={" fixed overflow-hidden z-20 bg-gray-900 bg-opacity-25 inset-0 transform ease-in-out " + (isOpen ? " transition-opacity opacity-100 duration-500 translate-x-0 " : " transition-all delay-500 opacity-0 translate-x-full ")}>
<section className={"w-80 md:w-96 right-0 absolute bg-base-100 h-full shadow-xl delay-400 duration-500 ease-in-out transition-all transform " + (isOpen ? " translate-x-0 " : " translate-x-full ")}>
<div className="relative pb-5 flex flex-col h-full">
{/* Header */}
<div className="navbar flex pl-4 pr-4 shadow-md ">
<button className="float-left btn btn-circle btn-outline btn-sm" onClick={() => close()}>
<XMarkIcon className="h-5 w-5" />
</button>
<span className="ml-2 font-bold text-xl">{header}</span>
</div>
{/* ------------------ Content Start ------------------ */}
<div className="overflow-y-scroll pl-4 pr-4">
<div className="flex flex-col w-full">
{/* Loading drawer body according to different drawer type */}
{
{
[RIGHT_DRAWER_TYPES.NOTIFICATION]: <NotificationBodyRightDrawer {...extraObject} closeRightDrawer={close} />,
[RIGHT_DRAWER_TYPES.CALENDAR_EVENTS]: <CalendarEventsBodyRightDrawer {...extraObject} closeRightDrawer={close} />,
[RIGHT_DRAWER_TYPES.DEFAULT]: <div></div>
}[bodyType]
}
</div>
</div>
{/* ------------------ Content End ------------------ */}
</div>
</section>
<section className=" w-screen h-full cursor-pointer " onClick={() => close()} ></section>
</div>
)
}
export default RightSidebar

View File

@ -0,0 +1,49 @@
import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon'
import {useEffect, useState} from 'react'
import { Link, useLocation } from 'react-router-dom'
function SidebarSubmenu({submenu, name, icon}){
const location = useLocation()
const [isExpanded, setIsExpanded] = useState(false)
/** Open Submenu list if path found in routes, this is for directly loading submenu routes first time */
useEffect(() => {
if(submenu.filter(m => {return m.path === location.pathname})[0])setIsExpanded(true)
}, [])
return (
<div className='flex flex-col'>
{/** Route header */}
<div className='w-full block' onClick={() => setIsExpanded(!isExpanded)}>
{icon} {name}
<ChevronDownIcon className={'w-5 h-5 mt-1 float-right delay-400 duration-500 transition-all ' + (isExpanded ? 'rotate-180' : '')}/>
</div>
{/** Submenu list */}
<div className={` w-full `+ (isExpanded ? "" : "hidden")}>
<ul className={`menu menu-compact`}>
{
submenu.map((m, k) => {
return(
<li key={k}>
<Link to={m.path}>
{m.icon} {m.name}
{
location.pathname == m.path ? (<span className="absolute mt-1 mb-1 inset-y-0 left-0 w-1 rounded-tr-md rounded-br-md bg-primary "
aria-hidden="true"></span>) : null
}
</Link>
</li>
)
})
}
</ul>
</div>
</div>
)
}
export default SidebarSubmenu

View File

@ -0,0 +1,9 @@
function SuspenseContent(){
return(
<div className="w-full h-screen text-gray-300 dark:text-gray-200 bg-base-100">
Loading...
</div>
)
}
export default SuspenseContent

View File

@ -0,0 +1,7 @@
function ErrorText({styleClass, children}){
return(
<p className={`text-center text-error ${styleClass}`}>{children}</p>
)
}
export default ErrorText

View File

@ -0,0 +1,7 @@
function HelperText({className, children}){
return(
<div className={`text-slate-400 ${className}`}>{children}</div>
)
}
export default HelperText

View File

@ -0,0 +1,7 @@
function Subtitle({styleClass, children}){
return(
<div className={`text-xl font-semibold ${styleClass}`}>{children}</div>
)
}
export default Subtitle

View File

@ -0,0 +1,7 @@
function Title({className, children}){
return(
<p className={`text-2xl font-bold ${className}`}>{children}</p>
)
}
export default Title

View File

@ -0,0 +1,19 @@
import { CALENDAR_EVENT_STYLE } from "@/Components/CalendarView/util"
const THEME_BG = CALENDAR_EVENT_STYLE
function CalendarEventsBodyRightDrawer({ filteredEvents }) {
return (
<>
{
filteredEvents.map((e, k) => {
return <div key={k} className={`grid mt-3 card rounded-box p-3 ${THEME_BG[e.theme] || ""}`}>
{e.title}
</div>
})
}
</>
)
}
export default CalendarEventsBodyRightDrawer

View File

@ -0,0 +1,45 @@
import { useState } from 'react'
import CalendarView from '../../components/CalendarView'
import moment from 'moment'
import { CALENDAR_INITIAL_EVENTS } from '../../utils/dummyData'
import { useDispatch } from 'react-redux'
import { openRightDrawer } from '../common/rightDrawerSlice'
import { RIGHT_DRAWER_TYPES } from '../../utils/globalConstantUtil'
import { showNotification } from '../common/headerSlice'
const INITIAL_EVENTS = CALENDAR_INITIAL_EVENTS
function Calendar(){
const dispatch = useDispatch()
const [events, setEvents] = useState(INITIAL_EVENTS)
// Add your own Add Event handler, like opening modal or random event addition
// Format - {title :"", theme: "", startTime : "", endTime : ""}, typescript version comming soon :)
const addNewEvent = (date) => {
let randomEvent = INITIAL_EVENTS[Math.floor(Math.random() * 10)]
let newEventObj = {title : randomEvent.title, theme : randomEvent.theme, startTime : moment(date).startOf('day'), endTime : moment(date).endOf('day')}
setEvents([...events, newEventObj])
dispatch(showNotification({message : "New Event Added!", status : 1}))
}
// Open all events of current day in sidebar
const openDayDetail = ({filteredEvents, title}) => {
dispatch(openRightDrawer({header : title, bodyType : RIGHT_DRAWER_TYPES.CALENDAR_EVENTS, extraObject : {filteredEvents}}))
}
return(
<>
<CalendarView
calendarEvents={events}
addNewEvent={addNewEvent}
openDayDetail={openDayDetail}
/>
</>
)
}
export default Calendar

View File

@ -0,0 +1,53 @@
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
function BarChart(){
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
}
},
};
const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels,
datasets: [
{
label: 'Store 1',
data: labels.map(() => { return Math.random() * 1000 + 500 }),
backgroundColor: 'rgba(255, 99, 132, 1)',
},
{
label: 'Store 2',
data: labels.map(() => { return Math.random() * 1000 + 500 }),
backgroundColor: 'rgba(53, 162, 235, 1)',
},
],
};
return(
<TitleCard title={"No of Orders"} topMargin="mt-2">
<Bar options={options} data={data} />
</TitleCard>
)
}
export default BarChart

View File

@ -0,0 +1,66 @@
import {
Chart as ChartJS,
Filler,
ArcElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Doughnut } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
import Subtitle from '../../../components/Typography/Subtitle';
ChartJS.register(ArcElement, Tooltip, Legend,
Tooltip,
Filler,
Legend);
function DoughnutChart(){
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
},
};
const labels = ['Electronics', 'Home Applicances', 'Beauty', 'Furniture', 'Watches', 'Apparel'];
const data = {
labels,
datasets: [
{
label: '# of Orders',
data: [122, 219, 30, 51, 82, 13],
backgroundColor: [
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)',
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
],
borderWidth: 1,
}
],
};
return(
<TitleCard title={"Orders by Category"}>
<Doughnut options={options} data={data} />
</TitleCard>
)
}
export default DoughnutChart

View File

@ -0,0 +1,62 @@
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend
);
function LineChart(){
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
},
};
const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels,
datasets: [
{
fill: true,
label: 'MAU',
data: labels.map(() => { return Math.random() * 100 + 500 }),
borderColor: 'rgb(53, 162, 235)',
backgroundColor: 'rgba(53, 162, 235, 0.5)',
},
],
};
return(
<TitleCard title={"Montly Active Users (in k)"} >
<Line data={data} options={options}/>
</TitleCard>
)
}
export default LineChart

View File

@ -0,0 +1,66 @@
import {
Chart as ChartJS,
Filler,
ArcElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Pie } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
import Subtitle from '../../../components/Typography/Subtitle';
ChartJS.register(ArcElement, Tooltip, Legend,
Tooltip,
Filler,
Legend);
function PieChart(){
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
},
};
const labels = ['India', 'Middle East', 'Europe', 'US', 'Latin America', 'Asia(non-india)'];
const data = {
labels,
datasets: [
{
label: '# of Orders',
data: [122, 219, 30, 51, 82, 13],
backgroundColor: [
'rgba(255, 99, 255, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 255, 0.8)',
'rgba(75, 192, 255, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 255, 0.8)',
],
borderColor: [
'rgba(255, 99, 255, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 255, 1)',
'rgba(75, 192, 255, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 255, 1)',
],
borderWidth: 1,
}
],
};
return(
<TitleCard title={"Orders by country"}>
<Pie options={options} data={data} />
</TitleCard>
)
}
export default PieChart

View File

@ -0,0 +1,55 @@
import {
Chart as ChartJS,
Filler,
ArcElement,
Tooltip,
Legend,
} from 'chart.js';
import { Scatter } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
ChartJS.register(ArcElement, Tooltip, Legend,
Tooltip,
Filler,
Legend);
function ScatterChart(){
const options = {
scales: {
y: {
beginAtZero: true,
},
},
};
const data = {
datasets: [
{
label: 'Orders > 1k',
data: Array.from({ length: 100 }, () => ({
x: Math.random() * 11,
y: Math.random() * 31,
})),
backgroundColor: 'rgba(255, 99, 132, 1)',
},
{
label: 'Orders > 2K',
data: Array.from({ length: 100 }, () => ({
x: Math.random() * 12,
y: Math.random() * 12,
})),
backgroundColor: 'rgba(0, 0, 255, 1)',
},
],
};
return(
<TitleCard title={"No of Orders by month (in k)"}>
<Scatter options={options} data={data} />
</TitleCard>
)
}
export default ScatterChart

View File

@ -0,0 +1,61 @@
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
function StackBarChart(){
const options = {
responsive: true,
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
},
},
};
const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels,
datasets: [
{
label: 'Store 1',
data: labels.map(() => { return Math.random() * 1000 + 500 }),
backgroundColor: 'rgba(255, 99, 132, 1)',
},
{
label: 'Store 2',
data: labels.map(() => { return Math.random() * 1000 + 500 }),
backgroundColor: 'rgba(53, 162, 235, 1)',
},
{
label: 'Store 3',
data: labels.map(() => { return Math.random() * 1000 + 500 }),
backgroundColor: 'rgba(235, 162, 235, 1)',
},
],
};
return(
<TitleCard title={"Sales"} topMargin="mt-2">
<Bar options={options} data={data} />
</TitleCard>
)
}
export default StackBarChart

View File

@ -0,0 +1,58 @@
import LineChart from './components/LineChart'
import BarChart from './components/BarChart'
import DoughnutChart from './components/DoughnutChart'
import PieChart from './components/PieChart'
import ScatterChart from './components/ScatterChart'
import StackBarChart from './components/StackBarChart'
import Datepicker from "react-tailwindcss-datepicker";
import { useState } from 'react'
function Charts(){
const [dateValue, setDateValue] = useState({
startDate: new Date(),
endDate: new Date()
});
const handleDatePickerValueChange = (newValue) => {
console.log("newValue:", newValue);
setDateValue(newValue);
}
return(
<>
<Datepicker
containerClassName="w-72"
value={dateValue}
theme={"light"}
inputClassName="input input-bordered w-72"
popoverDirection={"down"}
toggleClassName="invisible"
onChange={handleDatePickerValueChange}
showShortcuts={true}
primaryColor={"white"}
/>
{/** ---------------------- Different charts ------------------------- */}
<div className="grid lg:grid-cols-2 mt-0 grid-cols-1 gap-6">
<StackBarChart />
<BarChart />
</div>
<div className="grid lg:grid-cols-2 mt-4 grid-cols-1 gap-6">
<DoughnutChart />
<PieChart />
</div>
<div className="grid lg:grid-cols-2 mt-4 grid-cols-1 gap-6">
<ScatterChart />
<LineChart />
</div>
</>
)
}
export default Charts

View File

@ -0,0 +1,40 @@
import { useDispatch, useSelector } from 'react-redux'
import axios from 'axios'
import { CONFIRMATION_MODAL_CLOSE_TYPES } from '../../../../../../public/utils/globalConstantUtil'
import { deleteLead } from '../../leads/leadSlice'
import { showNotification } from '../headerSlice'
function ConfirmationModalBody({ extraObject, closeModal }) {
const dispatch = useDispatch()
const { message, type, _id, index } = extraObject
const proceedWithYes = async () => {
if (type === CONFIRMATION_MODAL_CLOSE_TYPES.LEAD_DELETE) {
// positive response, call api or dispatch redux function
dispatch(deleteLead({ index }))
dispatch(showNotification({ message: "Lead Deleted!", status: 1 }))
}
closeModal()
}
return (
<>
<p className=' text-xl mt-8 text-center'>
{message}
</p>
<div className="modal-action mt-12">
<button className="btn btn-outline " onClick={() => closeModal()}>Cancel</button>
<button className="btn btn-primary w-36" onClick={() => proceedWithYes()}>Yes</button>
</div>
</>
)
}
export default ConfirmationModalBody

View File

@ -0,0 +1,15 @@
function NotificationBodyRightDrawer(){
return(
<>
{
[...Array(15)].map((_, i) => {
return <div key={i} className={"grid mt-3 card bg-base-200 rounded-box p-3" + (i < 5 ? " bg-blue-100" : "")}>
{i % 2 === 0 ? `Your sales has increased by 30% yesterday` : `Total likes for instagram post - New launch this week, has crossed 100k `}
</div>
})
}
</>
)
}
export default NotificationBodyRightDrawer

View File

@ -0,0 +1,30 @@
import { createSlice } from '@reduxjs/toolkit'
export const headerSlice = createSlice({
name: 'header',
initialState: {
pageTitle: "Home", // current page title state management
noOfNotifications : 15, // no of unread notifications
newNotificationMessage : "", // message of notification to be shown
newNotificationStatus : 1, // to check the notification type - success/ error/ info
},
reducers: {
setPageTitle: (state, action) => {
state.pageTitle = action.payload.title
},
removeNotificationMessage: (state, action) => {
state.newNotificationMessage = ""
},
showNotification: (state, action) => {
state.newNotificationMessage = action.payload.message
state.newNotificationStatus = action.payload.status
},
}
})
export const { setPageTitle, removeNotificationMessage, showNotification } = headerSlice.actions
export default headerSlice.reducer

View File

@ -0,0 +1,35 @@
import { createSlice } from '@reduxjs/toolkit'
export const modalSlice = createSlice({
name: 'modal',
initialState: {
title: "", // current title state management
isOpen : false, // modal state management for opening closing
bodyType : "", // modal content management
size : "", // modal content management
extraObject : {},
},
reducers: {
openModal: (state, action) => {
const {title, bodyType, extraObject, size} = action.payload
state.isOpen = true
state.bodyType = bodyType
state.title = title
state.size = size || 'md'
state.extraObject = extraObject
},
closeModal: (state, action) => {
state.isOpen = false
state.bodyType = ""
state.title = ""
state.extraObject = {}
},
}
})
export const { openModal, closeModal } = modalSlice.actions
export default modalSlice.reducer

View File

@ -0,0 +1,33 @@
import { createSlice } from '@reduxjs/toolkit'
export const rightDrawerSlice = createSlice({
name: 'rightDrawer',
initialState: {
header: "", // current title state management
isOpen : false, // right drawer state management for opening closing
bodyType : "", // right drawer content management
extraObject : {},
},
reducers: {
openRightDrawer: (state, action) => {
const {header, bodyType, extraObject} = action.payload
state.isOpen = true
state.bodyType = bodyType
state.header = header
state.extraObject = extraObject
},
closeRightDrawer: (state, action) => {
state.isOpen = false
state.bodyType = ""
state.header = ""
state.extraObject = {}
},
}
})
export const { openRightDrawer, closeRightDrawer } = rightDrawerSlice.actions
export default rightDrawerSlice.reducer

View File

@ -0,0 +1,25 @@
function AmountStats({}){
return(
<div className="stats bg-base-100 shadow">
<div className="stat">
<div className="stat-title">Amount to be Collected</div>
<div className="stat-value">$25,600</div>
<div className="stat-actions">
<button className="btn btn-xs">View Users</button>
</div>
</div>
<div className="stat">
<div className="stat-title">Cash in hand</div>
<div className="stat-value">$5,600</div>
<div className="stat-actions">
<button className="btn btn-xs">View Members</button>
</div>
</div>
</div>
)
}
export default AmountStats

View File

@ -0,0 +1,53 @@
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
function BarChart(){
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
}
},
};
const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels,
datasets: [
{
label: 'Store 1',
data: labels.map(() => { return Math.random() * 1000 + 500 }),
backgroundColor: 'rgba(255, 99, 132, 1)',
},
{
label: 'Store 2',
data: labels.map(() => { return Math.random() * 1000 + 500 }),
backgroundColor: 'rgba(53, 162, 235, 1)',
},
],
};
return(
<TitleCard title={"Revenue"}>
<Bar options={options} data={data} />
</TitleCard>
)
}
export default BarChart

View File

@ -0,0 +1,23 @@
function DashboardStats({title, icon, value, description, colorIndex}){
const COLORS = ["primary", "primary"]
const getDescStyle = () => {
if(description.includes("↗︎"))return "font-bold text-green-700 dark:text-green-300"
else if(description.includes("↙"))return "font-bold text-rose-500 dark:text-red-400"
else return ""
}
return(
<div className="stats shadow">
<div className="stat">
<div className={`stat-figure dark:text-slate-300 text-${COLORS[colorIndex%2]}`}>{icon}</div>
<div className="stat-title dark:text-slate-300">{title}</div>
<div className={`stat-value dark:text-slate-300 text-${COLORS[colorIndex%2]}`}>{value}</div>
<div className={"stat-desc " + getDescStyle()}>{description}</div>
</div>
</div>
)
}
export default DashboardStats

View File

@ -0,0 +1,75 @@
import SelectBox from "../../../components/Input/SelectBox"
import ArrowDownTrayIcon from '@heroicons/react/24/outline/ArrowDownTrayIcon'
import ShareIcon from '@heroicons/react/24/outline/ShareIcon'
import EnvelopeIcon from '@heroicons/react/24/outline/EnvelopeIcon'
import EllipsisVerticalIcon from '@heroicons/react/24/outline/EllipsisVerticalIcon'
import ArrowPathIcon from '@heroicons/react/24/outline/ArrowPathIcon'
import { useState } from "react"
import Datepicker from "react-tailwindcss-datepicker";
const periodOptions = [
{name : "Today", value : "TODAY"},
{name : "Yesterday", value : "YESTERDAY"},
{name : "This Week", value : "THIS_WEEK"},
{name : "Last Week", value : "LAST_WEEK"},
{name : "This Month", value : "THIS_MONTH"},
{name : "Last Month", value : "LAST_MONTH"},
]
function DashboardTopBar({updateDashboardPeriod}){
const [dateValue, setDateValue] = useState({
startDate: new Date(),
endDate: new Date()
});
const handleDatePickerValueChange = (newValue) => {
console.log("newValue:", newValue);
setDateValue(newValue);
updateDashboardPeriod(newValue)
}
return(
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="">
<Datepicker
containerClassName="w-72 "
value={dateValue}
theme={"light"}
inputClassName="input input-bordered w-72"
popoverDirection={"down"}
toggleClassName="invisible"
onChange={handleDatePickerValueChange}
showShortcuts={true}
primaryColor={"white"}
/>
{/* <SelectBox
options={periodOptions}
labelTitle="Period"
placeholder="Select date range"
containerStyle="w-72"
labelStyle="hidden"
defaultValue="TODAY"
updateFormValue={updateSelectBoxValue}
/> */}
</div>
<div className="text-right ">
<button className="btn btn-ghost btn-sm normal-case"><ArrowPathIcon className="w-4 mr-2"/>Refresh Data</button>
<button className="btn btn-ghost btn-sm normal-case ml-2"><ShareIcon className="w-4 mr-2"/>Share</button>
<div className="dropdown dropdown-bottom dropdown-end ml-2">
<label tabIndex={0} className="btn btn-ghost btn-sm normal-case btn-square "><EllipsisVerticalIcon className="w-5"/></label>
<ul tabIndex={0} className="dropdown-content menu menu-compact p-2 shadow bg-base-100 rounded-box w-52">
<li><a><EnvelopeIcon className="w-4"/>Email Digests</a></li>
<li><a><ArrowDownTrayIcon className="w-4"/>Download</a></li>
</ul>
</div>
</div>
</div>
)
}
export default DashboardTopBar

View File

@ -0,0 +1,66 @@
import {
Chart as ChartJS,
Filler,
ArcElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Doughnut } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
import Subtitle from '../../../components/Typography/Subtitle';
ChartJS.register(ArcElement, Tooltip, Legend,
Tooltip,
Filler,
Legend);
function DoughnutChart(){
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
},
};
const labels = ['Electronics', 'Home Applicances', 'Beauty', 'Furniture', 'Watches', 'Apparel'];
const data = {
labels,
datasets: [
{
label: '# of Orders',
data: [122, 219, 30, 51, 82, 13],
backgroundColor: [
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)',
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
],
borderWidth: 1,
}
],
};
return(
<TitleCard title={"Orders by Category"}>
<Doughnut options={options} data={data} />
</TitleCard>
)
}
export default DoughnutChart

View File

@ -0,0 +1,62 @@
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import TitleCard from '../../../components/Cards/TitleCard';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend
);
function LineChart(){
const options = {
responsive: true,
plugins: {
legend: {
position: 'top',
},
},
};
const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
const data = {
labels,
datasets: [
{
fill: true,
label: 'MAU',
data: labels.map(() => { return Math.random() * 100 + 500 }),
borderColor: 'rgb(53, 162, 235)',
backgroundColor: 'rgba(53, 162, 235, 0.5)',
},
],
};
return(
<TitleCard title={"Montly Active Users (in K)"}>
<Line data={data} options={options}/>
</TitleCard>
)
}
export default LineChart

View File

@ -0,0 +1,30 @@
import HeartIcon from '@heroicons/react/24/outline/HeartIcon'
import BoltIcon from '@heroicons/react/24/outline/BoltIcon'
function PageStats({}){
return(
<div className="stats bg-base-100 shadow">
<div className="stat">
<div className="stat-figure invisible md:visible">
<HeartIcon className='w-8 h-8'/>
</div>
<div className="stat-title">Total Likes</div>
<div className="stat-value">25.6K</div>
<div className="stat-desc">21% more than last month</div>
</div>
<div className="stat">
<div className="stat-figure invisible md:visible">
<BoltIcon className='w-8 h-8'/>
</div>
<div className="stat-title">Page Views</div>
<div className="stat-value">2.6M</div>
<div className="stat-desc">14% more than last month</div>
</div>
</div>
)
}
export default PageStats

View File

@ -0,0 +1,45 @@
import TitleCard from "../../../components/Cards/TitleCard"
const userSourceData = [
{source : "Facebook Ads", count : "26,345", conversionPercent : 10.2},
{source : "Google Ads", count : "21,341", conversionPercent : 11.7},
{source : "Instagram Ads", count : "34,379", conversionPercent : 12.4},
{source : "Affiliates", count : "12,359", conversionPercent : 20.9},
{source : "Organic", count : "10,345", conversionPercent : 10.3},
]
function UserChannels(){
return(
<TitleCard title={"User Signup Source"}>
{/** Table Data */}
<div className="overflow-x-auto">
<table className="table w-full">
<thead>
<tr>
<th></th>
<th className="normal-case">Source</th>
<th className="normal-case">No of Users</th>
<th className="normal-case">Conversion</th>
</tr>
</thead>
<tbody>
{
userSourceData.map((u, k) => {
return(
<tr key={k}>
<th>{k+1}</th>
<td>{u.source}</td>
<td>{u.count}</td>
<td>{`${u.conversionPercent}%`}</td>
</tr>
)
})
}
</tbody>
</table>
</div>
</TitleCard>
)
}
export default UserChannels

View File

@ -0,0 +1,78 @@
import DashboardStats from './components/DashboardStats'
import AmountStats from './components/AmountStats'
import PageStats from './components/PageStats'
import UserGroupIcon from '@heroicons/react/24/outline/UserGroupIcon'
import UsersIcon from '@heroicons/react/24/outline/UsersIcon'
import CircleStackIcon from '@heroicons/react/24/outline/CircleStackIcon'
import CreditCardIcon from '@heroicons/react/24/outline/CreditCardIcon'
import UserChannels from './components/UserChannels'
import LineChart from './components/LineChart'
import BarChart from './components/BarChart'
import DashboardTopBar from './components/DashboardTopBar'
import { useDispatch } from 'react-redux'
import {showNotification} from '../common/headerSlice'
import DoughnutChart from './components/DoughnutChart'
import { useState } from 'react'
const statsData = [
{title : "New Users", value : "34.7k", icon : <UserGroupIcon className='w-8 h-8'/>, description : "↗︎ 2300 (22%)"},
{title : "Total Sales", value : "$34,545", icon : <CreditCardIcon className='w-8 h-8'/>, description : "Current month"},
{title : "Pending Leads", value : "450", icon : <CircleStackIcon className='w-8 h-8'/>, description : "50 in hot leads"},
{title : "Active Users", value : "5.6k", icon : <UsersIcon className='w-8 h-8'/>, description : "↙ 300 (18%)"},
]
function Dashboard(){
const dispatch = useDispatch()
const updateDashboardPeriod = (newRange) => {
// Dashboard range changed, write code to refresh your values
dispatch(showNotification({message : `Period updated to ${newRange.startDate} to ${newRange.endDate}`, status : 1}))
}
return(
<>
{/** ---------------------- Select Period Content ------------------------- */}
<DashboardTopBar updateDashboardPeriod={updateDashboardPeriod}/>
{/** ---------------------- Different stats content 1 ------------------------- */}
<div className="grid lg:grid-cols-4 mt-2 md:grid-cols-2 grid-cols-1 gap-6">
{
statsData.map((d, k) => {
return (
<DashboardStats key={k} {...d} colorIndex={k}/>
)
})
}
</div>
{/** ---------------------- Different charts ------------------------- */}
<div className="grid lg:grid-cols-2 mt-4 grid-cols-1 gap-6">
<LineChart />
<BarChart />
</div>
{/** ---------------------- Different stats content 2 ------------------------- */}
<div className="grid lg:grid-cols-2 mt-10 grid-cols-1 gap-6">
<AmountStats />
<PageStats />
</div>
{/** ---------------------- User source channels table ------------------------- */}
<div className="grid lg:grid-cols-2 mt-4 grid-cols-1 gap-6">
<UserChannels />
<DoughnutChart />
</div>
</>
)
}
export default Dashboard

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from "react"
import { useDispatch } from "react-redux"
import TitleCard from "../../components/Cards/TitleCard"
import { setPageTitle, showNotification } from "../common/headerSlice"
import DocComponentsNav from "./components/DocComponentsNav"
import ReadMe from "./components/GettingStartedContent"
import DocComponentsContent from "./components/DocComponentsContent"
import FeaturesNav from "./components/FeaturesNav"
import FeaturesContent from "./components/FeaturesContent"
function DocComponents(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Documentation"}))
}, [])
return(
<>
<div className="bg-base-100 flex overflow-hidden rounded-lg" style={{height : "82vh"}}>
<div className="flex-none p-4">
<DocComponentsNav activeIndex={1}/>
</div>
<div className="grow pt-16 overflow-y-scroll">
<DocComponentsContent />
</div>
</div>
</>
)
}
export default DocComponents

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from "react"
import { useDispatch } from "react-redux"
import TitleCard from "../../components/Cards/TitleCard"
import { setPageTitle, showNotification } from "../common/headerSlice"
import GettingStartedNav from "./components/GettingStartedNav"
import ReadMe from "./components/GettingStartedContent"
import GettingStartedContent from "./components/GettingStartedContent"
import FeaturesNav from "./components/FeaturesNav"
import FeaturesContent from "./components/FeaturesContent"
function Features(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Documentation"}))
}, [])
return(
<>
<div className="bg-base-100 flex overflow-hidden rounded-lg" style={{height : "82vh"}}>
<div className="flex-none p-4">
<FeaturesNav activeIndex={1}/>
</div>
<div className="grow pt-16 overflow-y-scroll">
<FeaturesContent />
</div>
</div>
</>
)
}
export default Features

View File

@ -0,0 +1,37 @@
import { useEffect, useState } from "react"
import { useDispatch } from "react-redux"
import TitleCard from "../../components/Cards/TitleCard"
import { setPageTitle, showNotification } from "../common/headerSlice"
import GettingStartedNav from "./components/GettingStartedNav"
import ReadMe from "./components/GettingStartedContent"
import GettingStartedContent from "./components/GettingStartedContent"
function GettingStarted(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Documentation"}))
}, [])
return(
<>
<div className="bg-base-100 flex overflow-hidden rounded-lg" style={{height : "82vh"}}>
<div className="flex-none p-4">
<GettingStartedNav activeIndex={1}/>
</div>
<div className="grow pt-16 overflow-y-scroll">
<GettingStartedContent />
</div>
</div>
</>
)
}
export default GettingStarted

View File

@ -0,0 +1,99 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import InputText from '../../../components/Input/InputText'
import Title from '../../../components/Typography/Title'
import Subtitle from '../../../components/Typography/Subtitle'
import ErrorText from '../../../components/Typography/ErrorText'
import HelperText from '../../../components/Typography/HelperText'
import { setPageTitle, showNotification } from '../../common/headerSlice'
import TitleCard from '../../../components/Cards/TitleCard'
function DocComponentsContent(){
const dispatch = useDispatch()
const updateFormValue = () => {
// Dummy function for input text component
}
return(
<>
<article className="prose">
<h1 className="" >Components</h1>
We have added some global components that are used commonly inside the project.
{/* Typography*/}
<h2 id="component1">Typography</h2>
<div>
These components are present under <span className="badge mt-0 mb-0 badge-ghost">/components/Typography</span> folder. It accepts styleClass as props which can be used to pass additional className for style. It has following components which you can import and use it -
<div className="mockup-code mt-4">
<pre className='my-0 py-0'><code>{'import Title from "../components/Typography/Title"\n <Title>Your Title here</Title>'}</code></pre>
</div>
<ul>
<li><span className='font-bold'>Title</span> - Use this component to show title
<Title>Title Example</Title>
</li>
<li><span className='font-bold'>Subtitle</span> - Component that shows text smaller than title
<Subtitle styleClass="mt-4 mb-6">Subtitle Example</Subtitle>
</li>
<li><span className='font-bold'>ErrorText</span> - Used for showing error messages
<ErrorText styleClass="mt-2">Error Text Example</ErrorText>
</li>
<li><span className='font-bold'>HelperText</span> - Used for showing secondary message
<HelperText styleClass="">Helper Text Example</HelperText></li>
</ul>
</div>
{/* Form Input*/}
<h2 id="component2">Form Input</h2>
<p>
Many times we have to use form input like text, select one or toogle and in every file we have to handle its state management, here we have added global form component that can be used in any file and state variables can be managed by passing props to it. It is present in <span className="badge mt-0 mb-0 badge-ghost">/components/Input</span> folder.
</p>
Ex-
<div className="mockup-code mt-4">
<pre className='my-0 py-0'><code>{'const INITIAL_LEAD_OBJ = {\n first_name : "", \n last_name : "", \n email : "" \n } \n const [leadObj, setLeadObj] = useState(INITIAL_LEAD_OBJ) \n const updateFormValue = ({updateType, value}) => {\n setErrorMessage("") \n setLeadObj({...leadObj, [updateType] : value})\n }\n\n<InputText type="text" defaultValue={leadObj.first_name} \n updateType="first_name" containerStyle="mt-4" \n labelTitle="First Name" updateFormValue={updateFormValue}/>'}</code></pre>
</div>
<InputText type="text" defaultValue={"input value"} updateType="first_name" containerStyle="mt-3" labelTitle="Label Title" updateFormValue={updateFormValue}/>
<p> This example is from add new lead modal, here we are importing component for creating text input and passing some props to handle its content and state variable. Description of props are as follows - </p>
<ul>
<li><span className='font-bold'>type</span> - Input type value like number, date, time etc.. </li>
<li><span className='font-bold'>updateType</span> - This is used to update state variable in parent component</li>
<li><span className='font-bold'>containerStyle</span> - Style class for container of input, which include label as well</li>
<li><span className='font-bold'>labelTitle</span> - Title of the label</li>
<li><span className='font-bold'>updateFormValue</span> - Function of parent component to update state variable</li>
</ul>
{/* Cards */}
<h2 id="component3">Cards</h2>
<p>
<a href="https://daisyui.com/components/card/" target="_blank">Daisy UI</a> already have many cards layout, on top of that we have added one card component that accept title props and shows children inside its body. Also there is a divider between title and body of card. On more provision has been added to add buttons on top left side of card using TopSideButtons props (check leads page).
</p>
Ex -
<div className="mockup-code mt-4">
<pre className='my-0 py-0'><code>{'<TitleCard title={"Card Title"}> <h1>Card Body</h1></TitleCard>'}</code></pre>
</div>
<div className='p-8 bg-base-300 rounded-lg mt-4'>
<TitleCard title={"Card Title"}> <h1>Card Body</h1></TitleCard>
</div>
<div className='h-24'></div>
</article>
</>
)
}
export default DocComponentsContent

View File

@ -0,0 +1,35 @@
import { useState } from "react"
function DocComponentsNav({activeIndex}){
const SECTION_NAVS = [
{name : "Typography", isActive : activeIndex === 1 ? true : false},
{name : "Form Input", isActive : false},
{name : "Cards", isActive : false},
]
const [navs, setNavs] = useState(SECTION_NAVS)
const scrollToSection = (currentIndex) => {
setNavs(navs.map((n, k) => {
if(k === currentIndex)return {...n, isActive : true}
else return {...n, isActive : false}
}))
document.getElementById('component'+(currentIndex+1)).scrollIntoView({behavior: 'smooth' })
}
return(
<ul className="menu w-56 mt-10 text-sm">
<li className="menu-title"><span className="">Components</span></li>
{
navs.map((n, k) => {
return(
<li key={k} onClick={() => scrollToSection(k)} className={n.isActive ? "bordered" : ""}><a>{n.name}</a></li>
)
})
}
</ul>
)
}
export default DocComponentsNav

View File

@ -0,0 +1,147 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Subtitle from '../../../components/Typography/Subtitle'
import { setPageTitle, showNotification } from '../../common/headerSlice'
function FeaturesContent(){
const dispatch = useDispatch()
return(
<>
<article className="prose">
<h1 className="">Features</h1>
{/* Authentication*/}
<h2 id="feature1">Authentication</h2>
<p>
JWT based Authentication logic is present in <span className="badge mt-0 mb-0 badge-ghost">/app/auth.js</span>. In the file you can see we are adding bearer token in header for every request. Every routes under <span className="badge mt-0 mb-0 badge-ghost">/routes/</span> folder will need authentication. For public routes like login, register you will have to add routes in <span className="badge mt-0 mb-0 badge-ghost">App.js</span> file and also include the path in PUBLIC_ROUTES variable under <span className="badge mt-0 mb-0 badge-ghost">/app/auth.js</span> file so that auto redirect to login page is not triggered.
</p>
{/* Left Sidebar*/}
<h2 id="feature2">Left Sidebar</h2>
<p>
This is main internal navigation (for pages that will come after login only), all sidebar menu items with their icons are present in <span className="badge mt-0 mb-0 badge-ghost">/routes/sidebar.js</span> file, while path and page components mapping are respectively present in <span className="badge mt-0 mb-0 badge-ghost">/routes/index.js</span> file.
</p>
{/* Add New Page*/}
<h2 id="feature3">Add New Page</h2>
<p>All <span className='font-semibold'>public routes</span> are present in <span className="badge mt-0 mb-0 badge-ghost">App.js</span> file. Steps to add new public page -
</p>
<ul className='mt-0'>
<li>Create Page inside <span className="badge mt-0 mb-0 badge-ghost">/pages</span> folder</li>
<li>Go to <span className="badge mt-0 mb-0 badge-ghost">App.js</span> and import the component and add its path</li>
<li>Add your new route path in <span className="badge mt-0 mb-0 badge-ghost">/app/auth.js</span> file under PUBLIC_ROUTES variable, this will allow the page to open without login.</li>
</ul>
<p className='mt-4'>All <span className='font-semibold'>protected routes</span> are present in <span className="badge mt-0 mb-0 badge-ghost">/routes/sidebar.js</span> file</p>
<ul className='mt-0'>
<li>Create your page inside <span className="badge mt-0 mb-0 badge-ghost">/pages/protected</span> folder</li>
<li>Add your new routes in <span className="badge mt-0 mb-0 badge-ghost">/routes/sidebar.js</span>, this will show your new page in sidebar</li>
<li>Import your new routes component and map its path in <span className="badge mt-0 mb-0 badge-ghost">/routes/index.js</span></li>
</ul>
{/* Right Sidebar*/}
<h2 id="feature4">Right Sidebar</h2>
<div>
This is used for showing long list contents like notifications, settings etc.. We are using redux to show and hide and it is single component and can be called from any file with dispatch method.
To add new content follow following steps:
<ul>
<li>Create new component file containing main body of your content</li>
<li>Create new variable in <span className="badge mt-0 mb-0 badge-ghost">/utils/globalConstantUtils.js</span> file under RIGHT_DRAWER_TYPES variable</li>
<li>Now include the file mapped with the new variable in <span className="badge mt-0 mb-0 badge-ghost">/containers/RightSidebar.js</span> file using switch. <br />
For ex- If you new component name is <span className="badge mt-0 mb-0 badge-ghost">TestRightSideBar.js</span> and variable name is TEST_RIGHT_SIDEBAR, then add following code inside switch code block
<br />
<div className="mockup-code mt-4">
<pre className='my-0 py-0'><code>{`[RIGHT_DRAWER_TYPES.TEST_RIGHT_SIDEBAR] : \n<TestRightSideBar {...extraObject} closeRightDrawer={close}/>`}</code></pre>
</div>
<span className='text-sm mt-1 italic'>Here extraObject have variables that is passed from parent component while calling openRightDrawer method</span>
</li>
<li>Now the last step, call dispatch method as follows
<div className="mockup-code mt-1">
<pre className='my-0 py-0'><code>{'import { useDispatch } from "react-redux"\n const dispatch = useDispatch()\n dispatch(openRightDrawer({header : "Test Right Drawer", \n bodyType : RIGHT_DRAWER_TYPES.TEST_RIGHT_SIDEBAR}))'}</code></pre>
</div>
</li>
</ul>
</div>
{/* Themes*/}
<h2 id="feature5">Themes</h2>
<p>
By default we have added light and dark theme and Daisy UI comes with a number of themes, which you can use with no extra effort, you just have to include it in <span className="badge mt-0 mb-0 badge-ghost">tailwind.config.js</span> file, you can add themes like cupcake, corporate, reto etc... Also we can configure themes colors in config file, for more documentation on themes checkout <a href="https://daisyui.com/docs/themes/" target="_blank">Daisy UI documentation.</a>
</p>
{/* Modal*/}
<h2 id="feature6">Modal</h2>
<div>
With global modal functionality you dont have to create seperate modal for each page. We are using redux to show and hide and it is a single component and can be called from any file with dispatch method.
Code for showing modal is present in modalSlice and layout container component. To show modal just call openModal() function of modalSlice using dispatch.
<br />
To add new modal in any page follow following steps:
<ul>
<li>Create new component file containing main body of your modal content</li>
<li>Create new variable in <span className="badge mt-0 mb-0 badge-ghost">/utils/globalConstantUtils.js</span> file under MODAL_BODY_TYPES variable</li>
<li>Now include the file mapped with the new variable in <span className="badge mt-0 mb-0 badge-ghost">/containers/ModalLayout.js</span> file using switch. <br />
For ex- If you new component name is <span className="badge mt-0 mb-0 badge-ghost">TestModal.js</span> and variable name is TEST_MODAL, then add following code inside switch code block
<br />
<div className="mockup-code mt-4">
<pre className='my-0 py-0'><code>{`[RIGHT_DRAWER_TYPES.TEST_MODAL] : \n<TestModal closeModal={close} extraObject={extraObject}/>`}</code></pre>
</div>
<span className='text-sm mt-1 italic'>Here extraObject have variables that is passed from parent component while calling openModal method</span>
</li>
<li>Now the last step, call dispatch method as follows
<div className="mockup-code mt-1">
<pre className='my-0 py-0'><code>{'import { useDispatch } from "react-redux"\n const dispatch = useDispatch()\n dispatch(openModal({title : "Test Modal Title", \n bodyType : MODAL_BODY_TYPES.TEST_MODAL}))'}</code></pre>
</div>
</li>
</ul>
</div>
{/* Notification*/}
<h2 id="feature7">Notification</h2>
<p>Many times we have to show notification to user be it on successfull form submission or any api success. And requirement can come to show notification from any page, so global notification handling is needed.</p>
<p className='mt-4'>Code for showing notification is present in headerSlice and layout container component. To show notification just call <span className='badge badge-ghost'>showNotification()</span> function of headerSlice using dispatch. To show success message notification pass status as 1 and for showing error message pass status as 0.</p>
<div className="mockup-code mb-4">
<pre className='my-0 py-0'><code>{'import { useDispatch } from "react-redux"\n const dispatch = useDispatch()\n dispatch(showNotification({message : "Message here", status : 1}))'}</code></pre>
</div>
<p>Click on this button to check</p>
<button className='btn btn-success' onClick={() => dispatch(showNotification({message : "Your message has been sent!", status : 1}))}>Success</button>
<button className='btn btn-error ml-4' onClick={() => dispatch(showNotification({message : "Something went wrong!", status : 0}))}>Error</button>
<div className='h-24'></div>
</article>
</>
)
}
export default FeaturesContent

View File

@ -0,0 +1,39 @@
import { useState } from "react"
function FeaturesNav({activeIndex}){
const SECTION_NAVS = [
{name : "Authentication", isActive : activeIndex === 1 ? true : false},
{name : "Sidebar", isActive : false},
{name : "Add New Page", isActive : false},
{name : "Right sidebar", isActive : false},
{name : "Themes", isActive : false},
{name : "Modal", isActive : false},
{name : "Notification", isActive : false},
]
const [navs, setNavs] = useState(SECTION_NAVS)
const scrollToSection = (currentIndex) => {
setNavs(navs.map((n, k) => {
if(k === currentIndex)return {...n, isActive : true}
else return {...n, isActive : false}
}))
document.getElementById('feature'+(currentIndex+1)).scrollIntoView({behavior: 'smooth' })
}
return(
<ul className="menu w-56 mt-10 text-sm">
<li className="menu-title"><span className="">Features</span></li>
{
navs.map((n, k) => {
return(
<li key={k} onClick={() => scrollToSection(k)} className={n.isActive ? "bordered" : ""}><a>{n.name}</a></li>
)
})
}
</ul>
)
}
export default FeaturesNav

View File

@ -0,0 +1,173 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Subtitle from '../../../components/Typography/Subtitle'
import { setPageTitle } from '../../common/headerSlice'
function GettingStartedContent(){
const dispatch = useDispatch()
return(
<>
<article className="prose">
<h1 className="">Getting Started</h1>
{/* Introduction */}
<h2 className="" id="getstarted1">Introduction</h2>
<p>A free dashboard template using <span className='font-bold'>Daisy UI</span> and react js. With the help of Dasisy UI, it comes with <span className='font-bold'>fully customizable and themable CSS</span> and power of Tailwind CSS utility classes. We have also added <span className='font-bold'>redux toolkit</span> and configured it for API calls and state management.</p>
<p>User authentication has been implemented using JWT token method (ofcourse you need backend API for generating and verifying token). This template can be used to start your next SaaS project or build new internal tools in your company.</p>
<h4> Core libraries used - </h4>
<ul>
<li><a href="https://reactjs.org/" target="_blank">React JS v18.2.0</a></li>
<li><a href="https://reactrouter.com/en/main" target="_blank">React Router v6.4.3</a></li>
<li><a href="https://tailwindcss.com/" target="_blank">Tailwind CSS v3.3.6</a></li>
<li><a href="https://daisyui.com/" target="_blank">Daisy UI v4.4.19</a></li>
<li><a href="https://heroicons.com/" target="_blank">HeroIcons v2.0.13</a></li>
<li><a href="https://redux-toolkit.js.org/" target="_blank">Redux toolkit v1.9.0</a></li>
<li><a href="https://react-chartjs-2.js.org/" target="_blank">React ChartJS 2 v5.0.1</a></li>
</ul>
<h4>Major features - </h4>
<p className=''>Almost all major UI components are available in Daisy UI library. Apart from this logic has been added for following - </p>
<ul>
<li> <span className='font-bold'>Light/dark</span> mode toggle</li>
<li> Token based user authentication</li>
<li> <span className='font-bold'>Submenu support</span> in sidebar</li>
<li> Store management using <span className='font-bold'>redux toolkit</span></li>
<li> <span className='font-bold'>Daisy UI</span> components</li>
<li> <span className='font-bold'>Right and left sidebar</span>, Universal loader, notifications and other components</li>
<li> React <span className='font-bold'>chart js 2</span> examples</li>
</ul>
{/* How to Use */}
<h2 id="getstarted2">How to use?</h2>
<p>
Just clone the repo from github and then run following command (Make sure you have node js installed )<br/>
<a href="https://github.com/srobbin01/daisyui-admin-dashboard-template" className='text-sm text-blue-500' target="_blank">Repo Link</a>
<br />
<code> npm install </code><br />
<code>npm start</code>
</p>
{/* Tailwind CSS*/}
<h2 id="getstarted3">Tailwind CSS</h2>
<p>
Tailwind CSS is a utility-first CSS framework with predefined classes that you can use to build and design the UI directly in the JSX. We have also included Daisy UI Component, that is based on tailwind CSS.
</p>
{/* Daisy UI */}
<h2 id="getstarted4">Daisy UI</h2>
<p><a href="https://daisyui.com/" target="_blank" className='text-xl btn-link'>Daisy UI</a>, a popular free and opensource tailwind component library has been used for this template. It has a rich collection of components, layouts and is fully customizable and themeable.</p>
<p>Apart from this it also helps in making HTML code more cleaner as we don't have to include all utility classes of tailwind to make the UI. Check components <a href="https://daisyui.com/components/button/" target="_blank" className='btn-link'>documentation here</a>. For Ex- </p>
<div className='text-center'>
<h2 className='text-xl font-bold mb-0.5'>Creating a button</h2>
</div>
<div className="">
<div className='text-center'>
<p className='text-center font-semibold'> using only utility classes of tailwind</p>
<div className="mockup-code text-justify mb-4">
<pre className='my-0 py-0'><code>{'<a className="inline-block px-4 py-3 \n text-sm font-semibold text-center \n text-white uppercase transition duration-200 \n ease-in-out bg-indigo-600 \n rounded-md cursor-pointer \n hover:bg-indigo-700">Button</a>'}</code></pre>
</div>
<button className="inline-block px-4 py-3 text-sm font-semibold text-center text-white uppercase transition duration-200 ease-in-out bg-indigo-600 rounded-md cursor-pointer hover:bg-indigo-700">Button</button>
</div>
<div className="divider"></div>
<div className='grid w-full flex-grow'>
<p className='text-center font-semibold'>using daisyUI component classes</p>
<div className="mockup-code mb-4">
<pre className='my-0 py-0'><code>{'<a className="btn btn-primary">\nButton</a>'}</code></pre>
</div>
<button className="btn btn-primary">Button</button>
</div>
</div>
{/* Chart JS */}
<h2 id="getstarted5">Chart JS</h2>
<p>
Chart JS library has rich components of different charts available. It is based on <a href="https://www.chartjs.org/" target="_blank" alt=""> Chart.js</a> library, the most popular charting library. We have added this library and added couple of examples in seperate page.
</p>
{/* Redux Toolkit */}
<h2 id="getstarted6">Redux Toolkit</h2>
<p>
The Redux Toolkit package helps in writing redux logic easily. It was originally created to help address three common concerns about Redux:
<li>Configuring a Redux store is too complicated</li>
<li>I have to add a lot of packages to get Redux to do anything useful</li>
<li>Redux requires too much boilerplate code"</li>
This library has been configured and used for showing notifications, modals and loading data from API in leads page.
</p>
{/* Hero Icons */}
<h2 id="getstarted7">Hero Icons</h2>
<p><a href="https://heroicons.com/" target="_blank" className='text-xl btn-link'>HeroIcons</a> library has been used for all the icons in this templates. It has a rich collection of SVG icons, and is made by the makers of Tailwind CSS.</p>
<p className='mt-4'>Each icon can be imported individually as a React component, check <a href="https://github.com/tailwindlabs/heroicons" target="_blank" className='btn-link'>documentation</a></p>
<pre><code>{"import BeakerIcon from '@heroicons/react/24/solid/BeakerIcon'"}</code></pre>
<p>Use as follows in your component</p>
<pre><code>{"<BeakerIcon className='h-6 w-6'/>"}</code></pre>
<div className="divider "></div>
<div className="alert mt-4 alert-warning shadow-lg">
<div><span>Note: Importing all icons in single line will increase your build time</span></div>
</div>
<p>Don't import like this (will load all icons and increase build time)</p>
<pre><code>{"import {BeakerIcon, BellIcon } from '@heroicons/react/24/solid'"}</code></pre>
<p>Instead import as follows</p>
<pre><code>{"import BeakerIcon from '@heroicons/react/24/solid/BeakerIcon'"}<br />
{"import BellIcon from '@heroicons/react/24/solid/BellIcon'"}</code></pre>
<div className="badge badge-secondary">This is better way for importing icons</div>
{/* Project Structure */}
<h2 id="getstarted8">Project Structure</h2>
<h4>Folders - </h4>
<ul className='mt-0'>
<li>app - store management, auth and libraries settings are present</li>
<li>components - this include all common components to be used in project</li>
<li>containers - components related to layout like sidebar, page layout, header etc..</li>
<li>features - main folder where all page logic resides, there will be folder for each page and additional folder inside that to group different functionalities like components, modals etc... Redux slice file will also present inside page specific folder.</li>
<li>pages - this contain one single file related to one page, if you want to divide page into different components file, use features folder and create seperate folder related to that page</li>
<li>routes - all settings related to routes</li>
</ul>
<h4>Files - </h4>
<ul className='mt-0'>
<li>App.js - Main file containing different routes and components </li>
<li>index.css - Additional global css if required</li>
<li>index.js - Entry point of project</li>
<li>package.json - All dependencies and npm scripts</li>
<li>tailwind.config.js - Tailwind CSS configuration file, add theme customization and new themes in this file</li>
</ul>
<div className='h-24'></div>
</article>
</>
)
}
export default GettingStartedContent

View File

@ -0,0 +1,40 @@
import { useState } from "react"
function GettingStartedNav({activeIndex}){
const SECTION_NAVS = [
{name : "Introduction", isActive : activeIndex === 1 ? true : false},
{name : "How to Use", isActive : false},
{name : "Tailwind CSS", isActive : false},
{name : "Daisy UI", isActive : false},
{name : "Chart JS", isActive : false},
{name : "Redux Toolkit", isActive : false},
{name : "Hero Icons", isActive : false},
{name : "Project Structure", isActive : false},
]
const [navs, setNavs] = useState(SECTION_NAVS)
const scrollToSection = (currentIndex) => {
setNavs(navs.map((n, k) => {
if(k === currentIndex)return {...n, isActive : true}
else return {...n, isActive : false}
}))
document.getElementById('getstarted'+(currentIndex+1)).scrollIntoView({behavior: 'smooth' })
}
return(
<ul className="menu w-56 mt-10 text-sm">
<li className="menu-title"><span className="">Getting Started</span></li>
{
navs.map((n, k) => {
return(
<li key={k} onClick={() => scrollToSection(k)} className={n.isActive ? "bordered" : ""}><a>{n.name}</a></li>
)
})
}
</ul>
)
}
export default GettingStartedNav

View File

@ -0,0 +1,60 @@
import { useState } from "react"
import { useDispatch } from "react-redux"
import TitleCard from "../../components/Cards/TitleCard"
import { showNotification } from "../common/headerSlice"
const INITIAL_INTEGRATION_LIST = [
{name : "Slack", icon : "https://cdn-icons-png.flaticon.com/512/2111/2111615.png", isActive : true, description : "Slack is an instant messaging program designed by Slack Technologies and owned by Salesforce."},
{name : "Facebook", icon : "https://cdn-icons-png.flaticon.com/512/124/124010.png", isActive : false, description : "Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook."},
{name : "Linkedin", icon : "https://cdn-icons-png.flaticon.com/512/174/174857.png", isActive : true, description : "LinkedIn is a business and employment-focused social media platform that works through websites and mobile apps."},
{name : "Google Ads", icon : "https://cdn-icons-png.flaticon.com/512/2301/2301145.png", isActive : false, description : "Google Ads is an online advertising platform developed by Google, where advertisers bid to display brief advertisements, service offerings"},
{name : "Gmail", icon : "https://cdn-icons-png.flaticon.com/512/5968/5968534.png", isActive : false, description : "Gmail is a free email service provided by Google. As of 2019, it had 1.5 billion active users worldwide."},
{name : "Salesforce", icon : "https://cdn-icons-png.flaticon.com/512/5968/5968880.png", isActive : false, description : "It provides customer relationship management software and applications focused on sales, customer service, marketing automation."},
{name : "Hubspot", icon : "https://cdn-icons-png.flaticon.com/512/5968/5968872.png", isActive : false, description : "American developer and marketer of software products for inbound marketing, sales, and customer service."},
]
function Integration(){
const dispatch = useDispatch()
const [integrationList, setIntegrationList] = useState(INITIAL_INTEGRATION_LIST)
const updateIntegrationStatus = (index) => {
let integration = integrationList[index]
setIntegrationList(integrationList.map((i, k) => {
if(k===index)return {...i, isActive : !i.isActive}
return i
}))
dispatch(showNotification({message : `${integration.name} ${integration.isActive ? "disabled" : "enabled"}` , status : 1}))
}
return(
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{
integrationList.map((i, k) => {
return(
<TitleCard key={k} title={i.name} topMargin={"mt-2"}>
<p className="flex">
<img alt="icon" src={i.icon} className="w-12 h-12 inline-block mr-4" />
{i.description}
</p>
<div className="mt-6 text-right">
<input type="checkbox" className="toggle toggle-success toggle-lg" checked={i.isActive} onChange={() => updateIntegrationStatus(k)}/>
</div>
</TitleCard>
)
})
}
</div>
</>
)
}
export default Integration

View File

@ -0,0 +1,62 @@
import { useState } from "react"
import { useDispatch } from "react-redux"
import InputText from '@/Components/Input/InputText'
import ErrorText from '@/Components/Typography/ErrorText'
import { showNotification } from '@/Components/features/common/headerSlice'
import { addNewLead } from "../leadSlice"
const INITIAL_LEAD_OBJ = {
first_name: "",
last_name: "",
email: ""
}
function AddLeadModalBody({ closeModal }) {
const dispatch = useDispatch()
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState("")
const [leadObj, setLeadObj] = useState(INITIAL_LEAD_OBJ)
const saveNewLead = () => {
if (leadObj.first_name.trim() === "") return setErrorMessage("First Name is required!")
else if (leadObj.email.trim() === "") return setErrorMessage("Email id is required!")
else {
let newLeadObj = {
"id": 7,
"email": leadObj.email,
"first_name": leadObj.first_name,
"last_name": leadObj.last_name,
"avatar": "https://reqres.in/img/faces/1-image.jpg"
}
dispatch(addNewLead({ newLeadObj }))
dispatch(showNotification({ message: "New Lead Added!", status: 1 }))
closeModal()
}
}
const updateFormValue = ({ updateType, value }) => {
setErrorMessage("")
setLeadObj({ ...leadObj, [updateType]: value })
}
return (
<>
<InputText type="text" defaultValue={leadObj.first_name} updateType="first_name" containerStyle="mt-4" labelTitle="First Name" updateFormValue={updateFormValue} />
<InputText type="text" defaultValue={leadObj.last_name} updateType="last_name" containerStyle="mt-4" labelTitle="Last Name" updateFormValue={updateFormValue} />
<InputText type="email" defaultValue={leadObj.email} updateType="email" containerStyle="mt-4" labelTitle="Email Id" updateFormValue={updateFormValue} />
<ErrorText styleClass="mt-16">{errorMessage}</ErrorText>
<div className="modal-action">
<button className="btn btn-ghost" onClick={() => closeModal()}>Cancel</button>
<button className="btn btn-primary px-6" onClick={() => saveNewLead()}>Save</button>
</div>
</>
)
}
export default AddLeadModalBody

View File

@ -0,0 +1,104 @@
import moment from "moment"
import { useEffect } from "react"
import { useDispatch, useSelector } from "react-redux"
import TitleCard from "../../components/Cards/TitleCard"
import { openModal } from "../common/modalSlice"
import { deleteLead, getLeadsContent } from "./leadSlice"
import { CONFIRMATION_MODAL_CLOSE_TYPES, MODAL_BODY_TYPES } from '../../utils/globalConstantUtil'
import TrashIcon from '@heroicons/react/24/outline/TrashIcon'
import { showNotification } from '../common/headerSlice'
const TopSideButtons = () => {
const dispatch = useDispatch()
const openAddNewLeadModal = () => {
dispatch(openModal({title : "Add New Lead", bodyType : MODAL_BODY_TYPES.LEAD_ADD_NEW}))
}
return(
<div className="inline-block float-right">
<button className="btn px-6 btn-sm normal-case btn-primary" onClick={() => openAddNewLeadModal()}>Add New</button>
</div>
)
}
function Leads(){
const {leads } = useSelector(state => state.lead)
const dispatch = useDispatch()
useEffect(() => {
dispatch(getLeadsContent())
}, [])
const getDummyStatus = (index) => {
if(index % 5 === 0)return <div className="badge">Not Interested</div>
else if(index % 5 === 1)return <div className="badge badge-primary">In Progress</div>
else if(index % 5 === 2)return <div className="badge badge-secondary">Sold</div>
else if(index % 5 === 3)return <div className="badge badge-accent">Need Followup</div>
else return <div className="badge badge-ghost">Open</div>
}
const deleteCurrentLead = (index) => {
dispatch(openModal({title : "Confirmation", bodyType : MODAL_BODY_TYPES.CONFIRMATION,
extraObject : { message : `Are you sure you want to delete this lead?`, type : CONFIRMATION_MODAL_CLOSE_TYPES.LEAD_DELETE, index}}))
}
return(
<>
<TitleCard title="Current Leads" topMargin="mt-2" TopSideButtons={<TopSideButtons />}>
{/* Leads List in table format loaded from slice after api call */}
<div className="overflow-x-auto w-full">
<table className="table w-full">
<thead>
<tr>
<th>Name</th>
<th>Email Id</th>
<th>Created At</th>
<th>Status</th>
<th>Assigned To</th>
<th></th>
</tr>
</thead>
<tbody>
{
leads.map((l, k) => {
return(
<tr key={k}>
<td>
<div className="flex items-center space-x-3">
<div className="avatar">
<div className="mask mask-squircle w-12 h-12">
<img src={l.avatar} alt="Avatar" />
</div>
</div>
<div>
<div className="font-bold">{l.first_name}</div>
<div className="text-sm opacity-50">{l.last_name}</div>
</div>
</div>
</td>
<td>{l.email}</td>
<td>{moment(new Date()).add(-5*(k+2), 'days').format("DD MMM YY")}</td>
<td>{getDummyStatus(k)}</td>
<td>{l.last_name}</td>
<td><button className="btn btn-square btn-ghost" onClick={() => deleteCurrentLead(k)}><TrashIcon className="w-5"/></button></td>
</tr>
)
})
}
</tbody>
</table>
</div>
</TitleCard>
</>
)
}
export default Leads

View File

@ -0,0 +1,49 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
export const getLeadsContent = createAsyncThunk('/leads/content', async () => {
const response = await axios.get('/api/users?page=2', {})
return response.data;
})
export const leadsSlice = createSlice({
name: 'leads',
initialState: {
isLoading: false,
leads : []
},
reducers: {
addNewLead: (state, action) => {
let {newLeadObj} = action.payload
state.leads = [...state.leads, newLeadObj]
},
deleteLead: (state, action) => {
let {index} = action.payload
state.leads.splice(index, 1)
}
},
extraReducers: (builder) => {
builder
.addCase(getLeadsContent.pending, (state) => {
state.isLoading = true;
})
.addCase(getLeadsContent.fulfilled, (state, action) => {
state.leads = action.payload.data;
state.isLoading = false;
})
.addCase(getLeadsContent.rejected, (state) => {
state.isLoading = false;
});
}
})
export const { addNewLead, deleteLead } = leadsSlice.actions
export default leadsSlice.reducer

View File

@ -0,0 +1,82 @@
import moment from "moment"
import { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux"
import TitleCard from "../../../components/Cards/TitleCard"
import { showNotification } from '../../common/headerSlice'
const BILLS = [
{invoiceNo : "#4567", amount : "23,989", description : "Product usages", status : "Pending", generatedOn : moment(new Date()).add(-30*1, 'days').format("DD MMM YYYY"), paidOn : "-"},
{invoiceNo : "#4523", amount : "34,989", description : "Product usages", status : "Pending", generatedOn : moment(new Date()).add(-30*2, 'days').format("DD MMM YYYY"), paidOn : "-"},
{invoiceNo : "#4453", amount : "39,989", description : "Product usages", status : "Paid", generatedOn : moment(new Date()).add(-30*3, 'days').format("DD MMM YYYY"), paidOn : moment(new Date()).add(-24*2, 'days').format("DD MMM YYYY")},
{invoiceNo : "#4359", amount : "28,927", description : "Product usages", status : "Paid", generatedOn : moment(new Date()).add(-30*4, 'days').format("DD MMM YYYY"), paidOn : moment(new Date()).add(-24*3, 'days').format("DD MMM YYYY")},
{invoiceNo : "#3359", amount : "28,927", description : "Product usages", status : "Paid", generatedOn : moment(new Date()).add(-30*5, 'days').format("DD MMM YYYY"), paidOn : moment(new Date()).add(-24*4, 'days').format("DD MMM YYYY")},
{invoiceNo : "#3367", amount : "28,927", description : "Product usages", status : "Paid", generatedOn : moment(new Date()).add(-30*6, 'days').format("DD MMM YYYY"), paidOn : moment(new Date()).add(-24*5, 'days').format("DD MMM YYYY")},
{invoiceNo : "#3359", amount : "28,927", description : "Product usages", status : "Paid", generatedOn : moment(new Date()).add(-30*7, 'days').format("DD MMM YYYY"), paidOn : moment(new Date()).add(-24*6, 'days').format("DD MMM YYYY")},
{invoiceNo : "#2359", amount : "28,927", description : "Product usages", status : "Paid", generatedOn : moment(new Date()).add(-30*8, 'days').format("DD MMM YYYY"), paidOn : moment(new Date()).add(-24*7, 'days').format("DD MMM YYYY")},
]
function Billing(){
const [bills, setBills] = useState(BILLS)
const getPaymentStatus = (status) => {
if(status === "Paid")return <div className="badge badge-success">{status}</div>
if(status === "Pending")return <div className="badge badge-primary">{status}</div>
else return <div className="badge badge-ghost">{status}</div>
}
return(
<>
<TitleCard title="Billing History" topMargin="mt-2">
{/* Invoice list in table format loaded constant */}
<div className="overflow-x-auto w-full">
<table className="table w-full">
<thead>
<tr>
<th>Invoice No</th>
<th>Invoice Generated On</th>
<th>Description</th>
<th>Amount</th>
<th>Status</th>
<th>Invoice Paid On</th>
</tr>
</thead>
<tbody>
{
bills.map((l, k) => {
return(
<tr key={k}>
<td>{l.invoiceNo}</td>
<td>{l.generatedOn}</td>
<td>{l.description}</td>
<td>${l.amount}</td>
<td>{getPaymentStatus(l.status)}</td>
<td>{l.paidOn}</td>
</tr>
)
})
}
</tbody>
</table>
</div>
</TitleCard>
</>
)
}
export default Billing

View File

@ -0,0 +1,51 @@
import moment from "moment"
import { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux"
import TitleCard from "../../../components/Cards/TitleCard"
import { showNotification } from '../../common/headerSlice'
import InputText from '../../../components/Input/InputText'
import TextAreaInput from '../../../components/Input/TextAreaInput'
import ToogleInput from '../../../components/Input/ToogleInput'
function ProfileSettings(){
const dispatch = useDispatch()
// Call API to update profile settings changes
const updateProfile = () => {
dispatch(showNotification({message : "Profile Updated", status : 1}))
}
const updateFormValue = ({updateType, value}) => {
console.log(updateType)
}
return(
<>
<TitleCard title="Profile Settings" topMargin="mt-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<InputText labelTitle="Name" defaultValue="Alex" updateFormValue={updateFormValue}/>
<InputText labelTitle="Email Id" defaultValue="alex@dashwind.com" updateFormValue={updateFormValue}/>
<InputText labelTitle="Title" defaultValue="UI/UX Designer" updateFormValue={updateFormValue}/>
<InputText labelTitle="Place" defaultValue="California" updateFormValue={updateFormValue}/>
<TextAreaInput labelTitle="About" defaultValue="Doing what I love, part time traveller" updateFormValue={updateFormValue}/>
</div>
<div className="divider" ></div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<InputText labelTitle="Language" defaultValue="English" updateFormValue={updateFormValue}/>
<InputText labelTitle="Timezone" defaultValue="IST" updateFormValue={updateFormValue}/>
<ToogleInput updateType="syncData" labelTitle="Sync Data" defaultValue={true} updateFormValue={updateFormValue}/>
</div>
<div className="mt-16"><button className="btn btn-primary float-right" onClick={() => updateProfile()}>Update</button></div>
</TitleCard>
</>
)
}
export default ProfileSettings

View File

@ -0,0 +1,97 @@
import moment from "moment"
import { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux"
import TitleCard from "../../../components/Cards/TitleCard"
import { showNotification } from '../../common/headerSlice'
const TopSideButtons = () => {
const dispatch = useDispatch()
const addNewTeamMember = () => {
dispatch(showNotification({message : "Add New Member clicked", status : 1}))
}
return(
<div className="inline-block float-right">
<button className="btn px-6 btn-sm normal-case btn-primary" onClick={() => addNewTeamMember()}>Invite New</button>
</div>
)
}
const TEAM_MEMBERS = [
{name : "Alex", avatar : "https://reqres.in/img/faces/1-image.jpg", email : "alex@dashwind.com", role : "Owner", joinedOn : moment(new Date()).add(-5*1, 'days').format("DD MMM YYYY"), lastActive : "5 hr ago"},
{name : "Ereena", avatar : "https://reqres.in/img/faces/2-image.jpg", email : "ereena@dashwind.com", role : "Admin", joinedOn : moment(new Date()).add(-5*2, 'days').format("DD MMM YYYY"), lastActive : "15 min ago"},
{name : "John", avatar : "https://reqres.in/img/faces/3-image.jpg", email : "jhon@dashwind.com", role : "Admin", joinedOn : moment(new Date()).add(-5*3, 'days').format("DD MMM YYYY"), lastActive : "20 hr ago"},
{name : "Matrix", avatar : "https://reqres.in/img/faces/4-image.jpg", email : "matrix@dashwind.com", role : "Manager", joinedOn : moment(new Date()).add(-5*4, 'days').format("DD MMM YYYY"), lastActive : "1 hr ago"},
{name : "Virat", avatar : "https://reqres.in/img/faces/5-image.jpg", email : "virat@dashwind.com", role : "Support", joinedOn : moment(new Date()).add(-5*5, 'days').format("DD MMM YYYY"), lastActive : "40 min ago"},
{name : "Miya", avatar : "https://reqres.in/img/faces/6-image.jpg", email : "miya@dashwind.com", role : "Support", joinedOn : moment(new Date()).add(-5*7, 'days').format("DD MMM YYYY"), lastActive : "5 hr ago"},
]
function Team(){
const [members, setMembers] = useState(TEAM_MEMBERS)
const getRoleComponent = (role) => {
if(role === "Admin")return <div className="badge badge-secondary">{role}</div>
if(role === "Manager")return <div className="badge">{role}</div>
if(role === "Owner")return <div className="badge badge-primary">{role}</div>
if(role === "Support")return <div className="badge badge-accent">{role}</div>
else return <div className="badge badge-ghost">{role}</div>
}
return(
<>
<TitleCard title="Active Members" topMargin="mt-2" TopSideButtons={<TopSideButtons />}>
{/* Team Member list in table format loaded constant */}
<div className="overflow-x-auto w-full">
<table className="table w-full">
<thead>
<tr>
<th>Name</th>
<th>Email Id</th>
<th>Joined On</th>
<th>Role</th>
<th>Last Active</th>
</tr>
</thead>
<tbody>
{
members.map((l, k) => {
return(
<tr key={k}>
<td>
<div className="flex items-center space-x-3">
<div className="avatar">
<div className="mask mask-circle w-12 h-12">
<img src={l.avatar} alt="Avatar" />
</div>
</div>
<div>
<div className="font-bold">{l.name}</div>
</div>
</div>
</td>
<td>{l.email}</td>
<td>{l.joinedOn}</td>
<td>{getRoleComponent(l.role)}</td>
<td>{l.lastActive}</td>
</tr>
)
})
}
</tbody>
</table>
</div>
</TitleCard>
</>
)
}
export default Team

View File

@ -0,0 +1,128 @@
import moment from "moment"
import { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux"
import { showNotification } from "../common/headerSlice"
import TitleCard from "../../components/Cards/TitleCard"
import { RECENT_TRANSACTIONS } from "../../utils/dummyData"
import FunnelIcon from '@heroicons/react/24/outline/FunnelIcon'
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'
import SearchBar from "../../components/Input/SearchBar"
const TopSideButtons = ({removeFilter, applyFilter, applySearch}) => {
const [filterParam, setFilterParam] = useState("")
const [searchText, setSearchText] = useState("")
const locationFilters = ["Paris", "London", "Canada", "Peru", "Tokyo"]
const showFiltersAndApply = (params) => {
applyFilter(params)
setFilterParam(params)
}
const removeAppliedFilter = () => {
removeFilter()
setFilterParam("")
setSearchText("")
}
useEffect(() => {
if(searchText == ""){
removeAppliedFilter()
}else{
applySearch(searchText)
}
}, [searchText])
return(
<div className="inline-block float-right">
<SearchBar searchText={searchText} styleClass="mr-4" setSearchText={setSearchText}/>
{filterParam != "" && <button onClick={() => removeAppliedFilter()} className="btn btn-xs mr-2 btn-active btn-ghost normal-case">{filterParam}<XMarkIcon className="w-4 ml-2"/></button>}
<div className="dropdown dropdown-bottom dropdown-end">
<label tabIndex={0} className="btn btn-sm btn-outline"><FunnelIcon className="w-5 mr-2"/>Filter</label>
<ul tabIndex={0} className="dropdown-content menu p-2 text-sm shadow bg-base-100 rounded-box w-52">
{
locationFilters.map((l, k) => {
return <li key={k}><a onClick={() => showFiltersAndApply(l)}>{l}</a></li>
})
}
<div className="divider mt-0 mb-0"></div>
<li><a onClick={() => removeAppliedFilter()}>Remove Filter</a></li>
</ul>
</div>
</div>
)
}
function Transactions(){
const [trans, setTrans] = useState(RECENT_TRANSACTIONS)
const removeFilter = () => {
setTrans(RECENT_TRANSACTIONS)
}
const applyFilter = (params) => {
let filteredTransactions = RECENT_TRANSACTIONS.filter((t) => {return t.location == params})
setTrans(filteredTransactions)
}
// Search according to name
const applySearch = (value) => {
let filteredTransactions = RECENT_TRANSACTIONS.filter((t) => {return t.email.toLowerCase().includes(value.toLowerCase()) || t.email.toLowerCase().includes(value.toLowerCase())})
setTrans(filteredTransactions)
}
return(
<>
<TitleCard title="Recent Transactions" topMargin="mt-2" TopSideButtons={<TopSideButtons applySearch={applySearch} applyFilter={applyFilter} removeFilter={removeFilter}/>}>
{/* Team Member list in table format loaded constant */}
<div className="overflow-x-auto w-full">
<table className="table w-full">
<thead>
<tr>
<th>Name</th>
<th>Email Id</th>
<th>Location</th>
<th>Amount</th>
<th>Transaction Date</th>
</tr>
</thead>
<tbody>
{
trans.map((l, k) => {
return(
<tr key={k}>
<td>
<div className="flex items-center space-x-3">
<div className="avatar">
<div className="mask mask-circle w-12 h-12">
<img src={l.avatar} alt="Avatar" />
</div>
</div>
<div>
<div className="font-bold">{l.name}</div>
</div>
</div>
</td>
<td>{l.email}</td>
<td>{l.location}</td>
<td>${l.amount}</td>
<td>{moment(l.date).format("D MMM")}</td>
</tr>
)
})
}
</tbody>
</table>
</div>
</TitleCard>
</>
)
}
export default Transactions

View File

@ -0,0 +1,86 @@
import {useState, useRef} from 'react'
import {Link} from 'react-router-dom'
import LandingIntro from './LandingIntro'
import ErrorText from '../../components/Typography/ErrorText'
import InputText from '../../components/Input/InputText'
import CheckCircleIcon from '@heroicons/react/24/solid/CheckCircleIcon'
function ForgotPassword(){
const INITIAL_USER_OBJ = {
emailId : ""
}
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState("")
const [linkSent, setLinkSent] = useState(false)
const [userObj, setUserObj] = useState(INITIAL_USER_OBJ)
const submitForm = (e) =>{
e.preventDefault()
setErrorMessage("")
if(userObj.emailId.trim() === "")return setErrorMessage("Email Id is required! (use any value)")
else{
setLoading(true)
// Call API to send password reset link
setLoading(false)
setLinkSent(true)
}
}
const updateFormValue = ({updateType, value}) => {
setErrorMessage("")
setUserObj({...userObj, [updateType] : value})
}
return(
<div className="min-h-screen bg-base-200 flex items-center">
<div className="card mx-auto w-full max-w-5xl shadow-xl">
<div className="grid md:grid-cols-2 grid-cols-1 bg-base-100 rounded-xl">
<div className=''>
<LandingIntro />
</div>
<div className='py-24 px-10'>
<h2 className='text-2xl font-semibold mb-2 text-center'>Forgot Password</h2>
{
linkSent &&
<>
<div className='text-center mt-8'><CheckCircleIcon className='inline-block w-32 text-success'/></div>
<p className='my-4 text-xl font-bold text-center'>Link Sent</p>
<p className='mt-4 mb-8 font-semibold text-center'>Check your email to reset password</p>
<div className='text-center mt-4'><Link to="/login"><button className="btn btn-block btn-primary ">Login</button></Link></div>
</>
}
{
!linkSent &&
<>
<p className='my-8 font-semibold text-center'>We will send password reset link on your email Id</p>
<form onSubmit={(e) => submitForm(e)}>
<div className="mb-4">
<InputText type="emailId" defaultValue={userObj.emailId} updateType="emailId" containerStyle="mt-4" labelTitle="Email Id" updateFormValue={updateFormValue}/>
</div>
<ErrorText styleClass="mt-12">{errorMessage}</ErrorText>
<button type="submit" className={"btn mt-2 w-full btn-primary" + (loading ? " loading" : "")}>Send Reset Link</button>
<div className='text-center mt-4'>Don't have an account yet? <Link to="/register"><button className=" inline-block hover:text-primary hover:underline hover:cursor-pointer transition duration-200">Register</button></Link></div>
</form>
</>
}
</div>
</div>
</div>
</div>
)
}
export default ForgotPassword

View File

@ -0,0 +1,27 @@
import TemplatePointers from "./components/TemplatePointers"
function LandingIntro(){
return(
<div className="hero min-h-full rounded-l-xl bg-base-200">
<div className="hero-content py-12">
<div className="max-w-md">
<h1 className='text-3xl text-center font-bold '><img src="/logo192.png" className="w-12 inline-block mr-2 mask mask-circle" alt="dashwind-logo" />DashWind</h1>
<div className="text-center mt-12"><img src="./intro.png" alt="Dashwind Admin Template" className="w-48 inline-block"></img></div>
{/* Importing pointers component */}
<TemplatePointers />
</div>
</div>
</div>
)
}
export default LandingIntro

View File

@ -0,0 +1,72 @@
import {useState, useRef} from 'react'
import {Link} from 'react-router-dom'
import LandingIntro from './LandingIntro'
import ErrorText from '../../components/Typography/ErrorText'
import InputText from '../../components/Input/InputText'
function Login(){
const INITIAL_LOGIN_OBJ = {
password : "",
emailId : ""
}
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState("")
const [loginObj, setLoginObj] = useState(INITIAL_LOGIN_OBJ)
const submitForm = (e) =>{
e.preventDefault()
setErrorMessage("")
if(loginObj.emailId.trim() === "")return setErrorMessage("Email Id is required! (use any value)")
if(loginObj.password.trim() === "")return setErrorMessage("Password is required! (use any value)")
else{
setLoading(true)
// Call API to check user credentials and save token in localstorage
localStorage.setItem("token", "DumyTokenHere")
setLoading(false)
window.location.href = '/app/welcome'
}
}
const updateFormValue = ({updateType, value}) => {
setErrorMessage("")
setLoginObj({...loginObj, [updateType] : value})
}
return(
<div className="min-h-screen bg-base-200 flex items-center">
<div className="card mx-auto w-full max-w-5xl shadow-xl">
<div className="grid md:grid-cols-2 grid-cols-1 bg-base-100 rounded-xl">
<div className=''>
<LandingIntro />
</div>
<div className='py-24 px-10'>
<h2 className='text-2xl font-semibold mb-2 text-center'>Login</h2>
<form onSubmit={(e) => submitForm(e)}>
<div className="mb-4">
<InputText type="emailId" defaultValue={loginObj.emailId} updateType="emailId" containerStyle="mt-4" labelTitle="Email Id" updateFormValue={updateFormValue}/>
<InputText defaultValue={loginObj.password} type="password" updateType="password" containerStyle="mt-4" labelTitle="Password" updateFormValue={updateFormValue}/>
</div>
<div className='text-right text-primary'><Link to="/forgot-password"><span className="text-sm inline-block hover:text-primary hover:underline hover:cursor-pointer transition duration-200">Forgot Password?</span></Link>
</div>
<ErrorText styleClass="mt-8">{errorMessage}</ErrorText>
<button type="submit" className={"btn mt-2 w-full btn-primary" + (loading ? " loading" : "")}>Login</button>
<div className='text-center mt-4'>Don't have an account yet? <Link to="/register"><span className=" inline-block hover:text-primary hover:underline hover:cursor-pointer transition duration-200">Register</span></Link></div>
</form>
</div>
</div>
</div>
</div>
)
}
export default Login

View File

@ -0,0 +1,73 @@
import {useState, useRef} from 'react'
import {Link} from 'react-router-dom'
import LandingIntro from './LandingIntro'
import ErrorText from '../../components/Typography/ErrorText'
import InputText from '../../components/Input/InputText'
function Register(){
const INITIAL_REGISTER_OBJ = {
name : "",
password : "",
emailId : ""
}
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState("")
const [registerObj, setRegisterObj] = useState(INITIAL_REGISTER_OBJ)
const submitForm = (e) =>{
e.preventDefault()
setErrorMessage("")
if(registerObj.name.trim() === "")return setErrorMessage("Name is required! (use any value)")
if(registerObj.emailId.trim() === "")return setErrorMessage("Email Id is required! (use any value)")
if(registerObj.password.trim() === "")return setErrorMessage("Password is required! (use any value)")
else{
setLoading(true)
// Call API to check user credentials and save token in localstorage
localStorage.setItem("token", "DumyTokenHere")
setLoading(false)
window.location.href = '/app/welcome'
}
}
const updateFormValue = ({updateType, value}) => {
setErrorMessage("")
setRegisterObj({...registerObj, [updateType] : value})
}
return(
<div className="min-h-screen bg-base-200 flex items-center">
<div className="card mx-auto w-full max-w-5xl shadow-xl">
<div className="grid md:grid-cols-2 grid-cols-1 bg-base-100 rounded-xl">
<div className=''>
<LandingIntro />
</div>
<div className='py-24 px-10'>
<h2 className='text-2xl font-semibold mb-2 text-center'>Register</h2>
<form onSubmit={(e) => submitForm(e)}>
<div className="mb-4">
<InputText defaultValue={registerObj.name} updateType="name" containerStyle="mt-4" labelTitle="Name" updateFormValue={updateFormValue}/>
<InputText defaultValue={registerObj.emailId} updateType="emailId" containerStyle="mt-4" labelTitle="Email Id" updateFormValue={updateFormValue}/>
<InputText defaultValue={registerObj.password} type="password" updateType="password" containerStyle="mt-4" labelTitle="Password" updateFormValue={updateFormValue}/>
</div>
<ErrorText styleClass="mt-8">{errorMessage}</ErrorText>
<button type="submit" className={"btn mt-2 w-full btn-primary" + (loading ? " loading" : "")}>Register</button>
<div className='text-center mt-4'>Already have an account? <Link to="/login"><span className=" inline-block hover:text-primary hover:underline hover:cursor-pointer transition duration-200">Login</span></Link></div>
</form>
</div>
</div>
</div>
</div>
)
}
export default Register

View File

@ -0,0 +1,14 @@
function TemplatePointers(){
return(
<>
<h1 className="text-2xl mt-8 font-bold">Admin Dashboard Starter Kit</h1>
<p className="py-2 mt-4"> <span className="font-semibold">Light/dark</span> mode toggle</p>
<p className="py-2 "> <span className="font-semibold">Redux toolkit</span> and other utility libraries configured</p>
<p className="py-2"> <span className="font-semibold">Calendar, Modal, Sidebar </span> components</p>
<p className="py-2 "> User-friendly <span className="font-semibold">documentation</span></p>
<p className="py-2 mb-4"> <span className="font-semibold">Daisy UI</span> components, <span className="font-semibold">Tailwind CSS</span> support</p>
</>
)
}
export default TemplatePointers

View File

@ -1,4 +1,4 @@
import ApplicationLogo from '@/Components/ApplicationLogo';
import ApplicationLogo from '../Components/ApplicationLogo';
import { Link } from '@inertiajs/react';
export default function Guest({ children }) {

View File

@ -1,10 +1,10 @@
import { useEffect } from 'react';
import Checkbox from '@/Components/Checkbox';
import GuestLayout from '@/Layouts/GuestLayout';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import Checkbox from '../../Components/Checkbox';
import GuestLayout from '../../Layouts/GuestLayout';
import InputError from '../../Components/InputError';
import InputLabel from '../../Components/InputLabel';
import PrimaryButton from '../../Components/PrimaryButton';
import TextInput from '../../Components/TextInput';
import { Head, Link, useForm } from '@inertiajs/react';
export default function Login({ status, canResetPassword }) {

View File

@ -1,13 +1,17 @@
import React, { useState } from 'react';
import { Head } from '@inertiajs/react';
import ModalInput from '@/Components/ModalInput';
import Layout from '@/Components/Layout'
export default function ManualPayment({ santri, penalty, bill, fields, options }) {
const [selectedSantri, setSelectedSantri] = useState(null);
// console.log(LeftSidebar)
return (
<div className='text-red-500'>
<Layout />
<Head title="Manual Payment" />
<ModalInput showPayments={true} fields={fields} options={options} tableName={['payments', 'detail_payments']} initialData={selectedSantri} onClose={() => setSelectedSantri(null)} />
<ModalInput showPayments={true} fields={fields} options={options} tableName='payments' initialData={selectedSantri} onClose={() => setSelectedSantri(null)} />
{santri && santri.length > 0 ? santri.map((item, i) => (
<div key={i} className="p-4 border-b">
<p>Nis: {item.user.nis}</p>
@ -18,8 +22,6 @@ export default function ManualPayment({ santri, penalty, bill, fields, options }
<p className="font-semibold">Payments:</p>
{item.payments.map((payment) => (
<div key={payment.id} className="ml-4">
{/* {payment.detail_payments && payment.detail_payments.length > 0 ? ( */}
{payment.detail_payments.map((detail) => (
<div key={detail.id}>
<p>{detail.penalty ? `Denda: Rp ${detail.penalty}` : 'Tidak ada denda'}</p>
@ -63,8 +65,6 @@ export default function ManualPayment({ santri, penalty, bill, fields, options }
</div>
</div>
))}
{/* ) : <p className="ml-4 text-gray-500">Tidak ada detail pembayaran.</p>} */}
</div>
))}
</div>
@ -73,6 +73,7 @@ export default function ManualPayment({ santri, penalty, bill, fields, options }
<div>
<button className='btn btn-accent text-white mt-2' onClick={() => {
setSelectedSantri({
id: item.id,
nama: item.nama,
alamat: item.alamat,
status_santri: item.status_santri,

View File

@ -0,0 +1,26 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '@/Components/features/common/headerSlice'
import FaceFrownIcon from '@heroicons/react/24/solid/FaceFrownIcon'
function InternalPage() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title: "" }))
}, [])
return (
<div className="hero h-4/5 bg-base-200">
<div className="hero-content text-accent text-center">
<div className="max-w-md">
<FaceFrownIcon className="h-48 w-48 inline-block" />
<h1 className="text-5xl font-bold">404 - Not Found</h1>
</div>
</div>
</div>
)
}
export default InternalPage

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '../../features/common/headerSlice'
import Billing from '../../features/settings/billing'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Bills"}))
}, [])
return(
<Billing />
)
}
export default InternalPage

View File

@ -0,0 +1,27 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '../../features/common/headerSlice'
import DocumentIcon from '@heroicons/react/24/solid/DocumentIcon'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Page Title"}))
}, [])
return(
<div className="hero h-4/5 bg-base-200">
<div className="hero-content text-accent text-center">
<div className="max-w-md">
<DocumentIcon className="h-48 w-48 inline-block"/>
<h1 className="text-5xl mt-2 font-bold">Blank Page</h1>
</div>
</div>
</div>
)
}
export default InternalPage

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '../../features/common/headerSlice'
import Calendar from '../../features/calendar'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Calendar"}))
}, [])
return(
<Calendar />
)
}
export default InternalPage

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Charts from '../../features/charts'
import { setPageTitle } from '../../features/common/headerSlice'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Analytics"}))
}, [])
return(
<Charts />
)
}
export default InternalPage

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '../../features/common/headerSlice'
import Dashboard from '../../features/dashboard/index'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Dashboard"}))
}, [])
return(
<Dashboard />
)
}
export default InternalPage

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '../../features/common/headerSlice'
import Integration from '../../features/integration'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Integrations"}))
}, [])
return(
<Integration />
)
}
export default InternalPage

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '../../features/common/headerSlice'
import Leads from '../../features/leads'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Leads"}))
}, [])
return(
<Leads />
)
}
export default InternalPage

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { setPageTitle } from '../../features/common/headerSlice'
import ProfileSettings from '../../features/settings/profilesettings'
function InternalPage(){
const dispatch = useDispatch()
useEffect(() => {
dispatch(setPageTitle({ title : "Settings"}))
}, [])
return(
<ProfileSettings />
)
}
export default InternalPage

Some files were not shown because too many files have changed in this diff Show More