diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index bcdef03..490b97b 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -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(); } } } diff --git a/package-lock.json b/package-lock.json index f65c348..5a272d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ab4f2c7..991955f 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/public/assets/android-chrome-192x192.png b/public/assets/android-chrome-192x192.png new file mode 100644 index 0000000..87472a7 Binary files /dev/null and b/public/assets/android-chrome-192x192.png differ diff --git a/public/assets/android-chrome-512x512.png b/public/assets/android-chrome-512x512.png new file mode 100644 index 0000000..73d4d45 Binary files /dev/null and b/public/assets/android-chrome-512x512.png differ diff --git a/public/assets/apple-touch-icon.png b/public/assets/apple-touch-icon.png new file mode 100644 index 0000000..22b4989 Binary files /dev/null and b/public/assets/apple-touch-icon.png differ diff --git a/public/assets/favicon-16x16.png b/public/assets/favicon-16x16.png new file mode 100644 index 0000000..c86c2e6 Binary files /dev/null and b/public/assets/favicon-16x16.png differ diff --git a/public/assets/favicon-32x32.png b/public/assets/favicon-32x32.png new file mode 100644 index 0000000..3aafbc0 Binary files /dev/null and b/public/assets/favicon-32x32.png differ diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico new file mode 100644 index 0000000..bbd266c Binary files /dev/null and b/public/assets/favicon.ico differ diff --git a/public/assets/intro.png b/public/assets/intro.png new file mode 100644 index 0000000..001ec91 Binary files /dev/null and b/public/assets/intro.png differ diff --git a/public/assets/logo.jpg b/public/assets/logo.jpg new file mode 100644 index 0000000..59a445e Binary files /dev/null and b/public/assets/logo.jpg differ diff --git a/public/assets/logo192.png b/public/assets/logo192.png new file mode 100644 index 0000000..87472a7 Binary files /dev/null and b/public/assets/logo192.png differ diff --git a/public/assets/logo512.png b/public/assets/logo512.png new file mode 100644 index 0000000..73d4d45 Binary files /dev/null and b/public/assets/logo512.png differ diff --git a/public/assets/manifest.json b/public/assets/manifest.json new file mode 100644 index 0000000..657587e --- /dev/null +++ b/public/assets/manifest.json @@ -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" +} diff --git a/public/assets/robots.txt b/public/assets/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/assets/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..5873f7f --- /dev/null +++ b/public/index.html @@ -0,0 +1,14 @@ + + + + + + + React Test + + + +
Loading...
+ + + diff --git a/public/utils/dummyData.jsx b/public/utils/dummyData.jsx new file mode 100644 index 0000000..084bdb2 --- /dev/null +++ b/public/utils/dummyData.jsx @@ -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')}, + + + ] +}); diff --git a/public/utils/globalConstantUtil.jsx b/public/utils/globalConstantUtil.jsx new file mode 100644 index 0000000..fdab882 --- /dev/null +++ b/public/utils/globalConstantUtil.jsx @@ -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", +}); diff --git a/resources/js/App.css b/resources/js/App.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/resources/js/App.css @@ -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); + } +} diff --git a/resources/js/Auth/auth.jsx b/resources/js/Auth/auth.jsx new file mode 100644 index 0000000..a85593b --- /dev/null +++ b/resources/js/Auth/auth.jsx @@ -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 \ No newline at end of file diff --git a/resources/js/Auth/init.jsx b/resources/js/Auth/init.jsx new file mode 100644 index 0000000..4ea9b76 --- /dev/null +++ b/resources/js/Auth/init.jsx @@ -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 \ No newline at end of file diff --git a/resources/js/Auth/store.jsx b/resources/js/Auth/store.jsx new file mode 100644 index 0000000..c4c8642 --- /dev/null +++ b/resources/js/Auth/store.jsx @@ -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 +}) \ No newline at end of file diff --git a/resources/js/Components/CalendarView/index.jsx b/resources/js/Components/CalendarView/index.jsx new file mode 100644 index 0000000..cd9b1cb --- /dev/null +++ b/resources/js/Components/CalendarView/index.jsx @@ -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( + <> +
+
+
+

+ {moment(firstDayOfMonth).format("MMMM yyyy").toString()}Beta +

+ + + + +
+
+ +
+ +
+
+
+ {weekdays.map((day, key) => { + return ( +
+ {day} +
+ ); + })} +
+ + +
+ {allDaysInMonth().map((day, idx) => { + return ( +
+

addNewEvent(day)}> { moment(day).format("D") }

+ { + getEventsForCurrentDate(day).map((e, k) => { + return

openAllEventsDetail(day, e.theme)} className={`text-xs px-2 mt-1 truncate ${THEME_BG[e.theme] || ""}`}>{e.title}

+ }) + } +
+ ); + })} +
+ + +
+ + ) +} + + +export default CalendarView \ No newline at end of file diff --git a/resources/js/Components/CalendarView/util.jsx b/resources/js/Components/CalendarView/util.jsx new file mode 100644 index 0000000..2421fa9 --- /dev/null +++ b/resources/js/Components/CalendarView/util.jsx @@ -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 }; diff --git a/resources/js/Components/Cards/TitleCard.jsx b/resources/js/Components/Cards/TitleCard.jsx new file mode 100644 index 0000000..a613c05 --- /dev/null +++ b/resources/js/Components/Cards/TitleCard.jsx @@ -0,0 +1,30 @@ +import Subtitle from "../Typography/Subtitle" + + + function TitleCard({title, children, topMargin, TopSideButtons}){ + return( +
+ + {/* Title for Card */} + + {title} + + {/* Top side button, show only if present */} + { + TopSideButtons &&
{TopSideButtons}
+ } +
+ +
+ + {/** Card Body */} +
+ {children} +
+
+ + ) + } + + + export default TitleCard \ No newline at end of file diff --git a/resources/js/Components/Header.jsx b/resources/js/Components/Header.jsx new file mode 100644 index 0000000..560201b --- /dev/null +++ b/resources/js/Components/Header.jsx @@ -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 ( +
+
+ +

{pageTitle}

+
+ +
+ {/* Light and dark theme selection toggle */} + + + {/* Notification icon */} + + + {/* Profile icon */} +
+ +
    +
  • Profile Settings
  • +
  • Bill History
  • +
    +
  • Logout
  • +
+
+
+
+ ) +} + +export default Header; diff --git a/resources/js/Components/Input/InputText.jsx b/resources/js/Components/Input/InputText.jsx new file mode 100644 index 0000000..05cb1d0 --- /dev/null +++ b/resources/js/Components/Input/InputText.jsx @@ -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( +
+ + updateInputValue(e.target.value)}className="input input-bordered w-full " /> +
+ ) +} + + +export default InputText \ No newline at end of file diff --git a/resources/js/Components/Input/SearchBar.jsx b/resources/js/Components/Input/SearchBar.jsx new file mode 100644 index 0000000..9ba2498 --- /dev/null +++ b/resources/js/Components/Input/SearchBar.jsx @@ -0,0 +1,22 @@ + + +import React, { useEffect } from 'react' + +function SearchBar({searchText, styleClass, placeholderText, setSearchText}) { + + + +const updateSearchInput = (value) => { + setSearchText(value) +} + + return ( +
+
+ updateSearchInput(e.target.value)} className="input input-sm input-bordered w-full max-w-xs" /> +
+
+ ) +} + +export default SearchBar diff --git a/resources/js/Components/Input/SelectBox.jsx b/resources/js/Components/Input/SelectBox.jsx new file mode 100644 index 0000000..fafc7f6 --- /dev/null +++ b/resources/js/Components/Input/SelectBox.jsx @@ -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 ( +
+ + + +
+ ) +} + +export default SelectBox diff --git a/resources/js/Components/Input/TextAreaInput.jsx b/resources/js/Components/Input/TextAreaInput.jsx new file mode 100644 index 0000000..6ad4874 --- /dev/null +++ b/resources/js/Components/Input/TextAreaInput.jsx @@ -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( +
+ + +
+ ) +} + + +export default TextAreaInput \ No newline at end of file diff --git a/resources/js/Components/Input/ToogleInput.jsx b/resources/js/Components/Input/ToogleInput.jsx new file mode 100644 index 0000000..8210845 --- /dev/null +++ b/resources/js/Components/Input/ToogleInput.jsx @@ -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( +
+ +
+ ) +} + + +export default ToogleInput diff --git a/resources/js/Components/Layout.jsx b/resources/js/Components/Layout.jsx new file mode 100644 index 0000000..5bc4192 --- /dev/null +++ b/resources/js/Components/Layout.jsx @@ -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) */} +
+ + + +
+ + { /* Right drawer - containing secondary content like notifications list etc.. */} + + + + {/** Notification layout container */} + {/* */} + + {/* Modal layout container */} + + + + ) +} + +export default Layout \ No newline at end of file diff --git a/resources/js/Components/LeftSidebar.jsx b/resources/js/Components/LeftSidebar.jsx new file mode 100644 index 0000000..f49ef41 --- /dev/null +++ b/resources/js/Components/LeftSidebar.jsx @@ -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 ( +
+ +
    + + +
  • + + DashWind LogoDashWind
  • + { + routes.map((route, k) => { + return ( +
  • + { + route.submenu ? + : + ( `${isActive ? 'font-semibold bg-base-200 ' : 'font-normal'}`} > + {route.icon} {route.name} + { + location.pathname === route.path ? () : null + } + ) + } + +
  • + ) + }) + } + +
+
+ ) +} + +export default LeftSidebar \ No newline at end of file diff --git a/resources/js/Components/ModalInput.jsx b/resources/js/Components/ModalInput.jsx index 8c7b678..08dfcb4 100644 --- a/resources/js/Components/ModalInput.jsx +++ b/resources/js/Components/ModalInput.jsx @@ -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" />
@@ -221,6 +229,6 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym ); }; -export default ModalInput; +export default ModalInput diff --git a/resources/js/Components/ModalLayout.jsx b/resources/js/Components/ModalLayout.jsx new file mode 100644 index 0000000..657e916 --- /dev/null +++ b/resources/js/Components/ModalLayout.jsx @@ -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 tag */} +
+
+ +

{title}

+ + + {/* Loading modal body according to different modal type */} + { + { + [MODAL_BODY_TYPES.LEAD_ADD_NEW]: , + [MODAL_BODY_TYPES.CONFIRMATION]: , + [MODAL_BODY_TYPES.DEFAULT]:
+ }[bodyType] + } +
+
+ + ) +} + +export default ModalLayout \ No newline at end of file diff --git a/resources/js/Components/PageContent.jsx b/resources/js/Components/PageContent.jsx new file mode 100644 index 0000000..89a2c17 --- /dev/null +++ b/resources/js/Components/PageContent.jsx @@ -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 ( +
+
+
+ }> + + { + routes.map((route, key) => { + return ( + } + /> + ) + }) + } + + {/* Redirecting unknown url to 404 page */} + {/* } /> */} + + +
+
+
+ ) +} + + +export default PageContent diff --git a/resources/js/Components/RightSidebar.jsx b/resources/js/Components/RightSidebar.jsx new file mode 100644 index 0000000..07763f9 --- /dev/null +++ b/resources/js/Components/RightSidebar.jsx @@ -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 ( +
+ +
+ +
+ + {/* Header */} +
+ + {header} +
+ + + {/* ------------------ Content Start ------------------ */} +
+
+ {/* Loading drawer body according to different drawer type */} + { + { + [RIGHT_DRAWER_TYPES.NOTIFICATION]: , + [RIGHT_DRAWER_TYPES.CALENDAR_EVENTS]: , + [RIGHT_DRAWER_TYPES.DEFAULT]:
+ }[bodyType] + } + +
+
+ {/* ------------------ Content End ------------------ */} +
+ +
+ +
close()} >
+
+ ) +} + +export default RightSidebar \ No newline at end of file diff --git a/resources/js/Components/SidebarSubmenu.jsx b/resources/js/Components/SidebarSubmenu.jsx new file mode 100644 index 0000000..f7642c5 --- /dev/null +++ b/resources/js/Components/SidebarSubmenu.jsx @@ -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 ( +
+ + {/** Route header */} +
setIsExpanded(!isExpanded)}> + {icon} {name} + +
+ + {/** Submenu list */} +
+ +
+
+ ) +} + +export default SidebarSubmenu \ No newline at end of file diff --git a/resources/js/Components/SuspenseContent.jsx b/resources/js/Components/SuspenseContent.jsx new file mode 100644 index 0000000..03f0add --- /dev/null +++ b/resources/js/Components/SuspenseContent.jsx @@ -0,0 +1,9 @@ +function SuspenseContent(){ + return( +
+ Loading... +
+ ) +} + +export default SuspenseContent \ No newline at end of file diff --git a/resources/js/Components/Typography/ErrorText.jsx b/resources/js/Components/Typography/ErrorText.jsx new file mode 100644 index 0000000..fc28674 --- /dev/null +++ b/resources/js/Components/Typography/ErrorText.jsx @@ -0,0 +1,7 @@ +function ErrorText({styleClass, children}){ + return( +

{children}

+ ) +} + +export default ErrorText \ No newline at end of file diff --git a/resources/js/Components/Typography/HelperText.jsx b/resources/js/Components/Typography/HelperText.jsx new file mode 100644 index 0000000..1519fb4 --- /dev/null +++ b/resources/js/Components/Typography/HelperText.jsx @@ -0,0 +1,7 @@ +function HelperText({className, children}){ + return( +
{children}
+ ) +} + +export default HelperText \ No newline at end of file diff --git a/resources/js/Components/Typography/Subtitle.jsx b/resources/js/Components/Typography/Subtitle.jsx new file mode 100644 index 0000000..40a6ebd --- /dev/null +++ b/resources/js/Components/Typography/Subtitle.jsx @@ -0,0 +1,7 @@ + function Subtitle({styleClass, children}){ + return( +
{children}
+ ) +} + +export default Subtitle \ No newline at end of file diff --git a/resources/js/Components/Typography/Title.jsx b/resources/js/Components/Typography/Title.jsx new file mode 100644 index 0000000..ff277df --- /dev/null +++ b/resources/js/Components/Typography/Title.jsx @@ -0,0 +1,7 @@ +function Title({className, children}){ + return( +

{children}

+ ) +} + +export default Title \ No newline at end of file diff --git a/resources/js/Components/features/calendar/CalendarEventsBodyRightDrawer.jsx b/resources/js/Components/features/calendar/CalendarEventsBodyRightDrawer.jsx new file mode 100644 index 0000000..6c8db92 --- /dev/null +++ b/resources/js/Components/features/calendar/CalendarEventsBodyRightDrawer.jsx @@ -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
+ {e.title} +
+ }) + } + + ) +} + +export default CalendarEventsBodyRightDrawer \ No newline at end of file diff --git a/resources/js/Components/features/calendar/index.jsx b/resources/js/Components/features/calendar/index.jsx new file mode 100644 index 0000000..4ef7a2e --- /dev/null +++ b/resources/js/Components/features/calendar/index.jsx @@ -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( + <> + + + ) +} + +export default Calendar \ No newline at end of file diff --git a/resources/js/Components/features/charts/components/BarChart.jsx b/resources/js/Components/features/charts/components/BarChart.jsx new file mode 100644 index 0000000..cfb605d --- /dev/null +++ b/resources/js/Components/features/charts/components/BarChart.jsx @@ -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( + + + + + ) +} + + +export default BarChart \ No newline at end of file diff --git a/resources/js/Components/features/charts/components/DoughnutChart.jsx b/resources/js/Components/features/charts/components/DoughnutChart.jsx new file mode 100644 index 0000000..f0050aa --- /dev/null +++ b/resources/js/Components/features/charts/components/DoughnutChart.jsx @@ -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( + + + + ) +} + + +export default DoughnutChart \ No newline at end of file diff --git a/resources/js/Components/features/charts/components/LineChart.jsx b/resources/js/Components/features/charts/components/LineChart.jsx new file mode 100644 index 0000000..e82ad6b --- /dev/null +++ b/resources/js/Components/features/charts/components/LineChart.jsx @@ -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( + + + + ) +} + + +export default LineChart \ No newline at end of file diff --git a/resources/js/Components/features/charts/components/PieChart.jsx b/resources/js/Components/features/charts/components/PieChart.jsx new file mode 100644 index 0000000..bbe5f96 --- /dev/null +++ b/resources/js/Components/features/charts/components/PieChart.jsx @@ -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( + + + + ) + } + + + export default PieChart \ No newline at end of file diff --git a/resources/js/Components/features/charts/components/ScatterChart.jsx b/resources/js/Components/features/charts/components/ScatterChart.jsx new file mode 100644 index 0000000..bb90074 --- /dev/null +++ b/resources/js/Components/features/charts/components/ScatterChart.jsx @@ -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( + + + + ) + } + + + export default ScatterChart \ No newline at end of file diff --git a/resources/js/Components/features/charts/components/StackBarChart.jsx b/resources/js/Components/features/charts/components/StackBarChart.jsx new file mode 100644 index 0000000..7fce2a7 --- /dev/null +++ b/resources/js/Components/features/charts/components/StackBarChart.jsx @@ -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( + + + + + ) + } + + + export default StackBarChart \ No newline at end of file diff --git a/resources/js/Components/features/charts/index.jsx b/resources/js/Components/features/charts/index.jsx new file mode 100644 index 0000000..338e956 --- /dev/null +++ b/resources/js/Components/features/charts/index.jsx @@ -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( + <> + + {/** ---------------------- Different charts ------------------------- */} +
+ + +
+ + +
+ + +
+ +
+ + +
+ + ) +} + +export default Charts \ No newline at end of file diff --git a/resources/js/Components/features/common/components/ConfirmationModalBody.jsx b/resources/js/Components/features/common/components/ConfirmationModalBody.jsx new file mode 100644 index 0000000..8ac935f --- /dev/null +++ b/resources/js/Components/features/common/components/ConfirmationModalBody.jsx @@ -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 ( + <> +

+ {message} +

+ +
+ + + + + +
+ + ) +} + +export default ConfirmationModalBody \ No newline at end of file diff --git a/resources/js/Components/features/common/components/NotificationBodyRightDrawer.jsx b/resources/js/Components/features/common/components/NotificationBodyRightDrawer.jsx new file mode 100644 index 0000000..792c859 --- /dev/null +++ b/resources/js/Components/features/common/components/NotificationBodyRightDrawer.jsx @@ -0,0 +1,15 @@ +function NotificationBodyRightDrawer(){ + return( + <> + { + [...Array(15)].map((_, i) => { + return
+ {i % 2 === 0 ? `Your sales has increased by 30% yesterday` : `Total likes for instagram post - New launch this week, has crossed 100k `} +
+ }) + } + + ) +} + +export default NotificationBodyRightDrawer \ No newline at end of file diff --git a/resources/js/Components/features/common/headerSlice.jsx b/resources/js/Components/features/common/headerSlice.jsx new file mode 100644 index 0000000..d91e198 --- /dev/null +++ b/resources/js/Components/features/common/headerSlice.jsx @@ -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 \ No newline at end of file diff --git a/resources/js/Components/features/common/modalSlice.jsx b/resources/js/Components/features/common/modalSlice.jsx new file mode 100644 index 0000000..874a5d0 --- /dev/null +++ b/resources/js/Components/features/common/modalSlice.jsx @@ -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 \ No newline at end of file diff --git a/resources/js/Components/features/common/rightDrawerSlice.jsx b/resources/js/Components/features/common/rightDrawerSlice.jsx new file mode 100644 index 0000000..e47acbe --- /dev/null +++ b/resources/js/Components/features/common/rightDrawerSlice.jsx @@ -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 \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/AmountStats.js b/resources/js/Components/features/dashboard/components/AmountStats.js new file mode 100644 index 0000000..d1e0414 --- /dev/null +++ b/resources/js/Components/features/dashboard/components/AmountStats.js @@ -0,0 +1,25 @@ + + +function AmountStats({}){ + return( +
+
+
Amount to be Collected
+
$25,600
+
+ +
+
+ +
+
Cash in hand
+
$5,600
+
+ +
+
+
+ ) +} + +export default AmountStats \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/BarChart.js b/resources/js/Components/features/dashboard/components/BarChart.js new file mode 100644 index 0000000..d243364 --- /dev/null +++ b/resources/js/Components/features/dashboard/components/BarChart.js @@ -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( + + + + + ) +} + + +export default BarChart \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/DashboardStats.js b/resources/js/Components/features/dashboard/components/DashboardStats.js new file mode 100644 index 0000000..ebac523 --- /dev/null +++ b/resources/js/Components/features/dashboard/components/DashboardStats.js @@ -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( +
+
+
{icon}
+
{title}
+
{value}
+
{description}
+
+
+ ) +} + +export default DashboardStats \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/DashboardTopBar.js b/resources/js/Components/features/dashboard/components/DashboardTopBar.js new file mode 100644 index 0000000..8f3183d --- /dev/null +++ b/resources/js/Components/features/dashboard/components/DashboardTopBar.js @@ -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( +
+
+ + {/* */} +
+
+ + + +
+ + +
+
+
+ ) +} + +export default DashboardTopBar \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/DoughnutChart.js b/resources/js/Components/features/dashboard/components/DoughnutChart.js new file mode 100644 index 0000000..f0050aa --- /dev/null +++ b/resources/js/Components/features/dashboard/components/DoughnutChart.js @@ -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( + + + + ) +} + + +export default DoughnutChart \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/LineChart.js b/resources/js/Components/features/dashboard/components/LineChart.js new file mode 100644 index 0000000..a845d63 --- /dev/null +++ b/resources/js/Components/features/dashboard/components/LineChart.js @@ -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( + + + + ) +} + + +export default LineChart \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/PageStats.js b/resources/js/Components/features/dashboard/components/PageStats.js new file mode 100644 index 0000000..31110fe --- /dev/null +++ b/resources/js/Components/features/dashboard/components/PageStats.js @@ -0,0 +1,30 @@ +import HeartIcon from '@heroicons/react/24/outline/HeartIcon' +import BoltIcon from '@heroicons/react/24/outline/BoltIcon' + + +function PageStats({}){ + return( +
+ +
+
+ +
+
Total Likes
+
25.6K
+
21% more than last month
+
+ +
+
+ +
+
Page Views
+
2.6M
+
14% more than last month
+
+
+ ) +} + +export default PageStats \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/components/UserChannels.js b/resources/js/Components/features/dashboard/components/UserChannels.js new file mode 100644 index 0000000..e02ab0c --- /dev/null +++ b/resources/js/Components/features/dashboard/components/UserChannels.js @@ -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( + + {/** Table Data */} +
+ + + + + + + + + + + { + userSourceData.map((u, k) => { + return( + + + + + + + ) + }) + } + +
SourceNo of UsersConversion
{k+1}{u.source}{u.count}{`${u.conversionPercent}%`}
+
+
+ ) +} + +export default UserChannels \ No newline at end of file diff --git a/resources/js/Components/features/dashboard/index.js b/resources/js/Components/features/dashboard/index.js new file mode 100644 index 0000000..e56733d --- /dev/null +++ b/resources/js/Components/features/dashboard/index.js @@ -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 : , description : "↗︎ 2300 (22%)"}, + {title : "Total Sales", value : "$34,545", icon : , description : "Current month"}, + {title : "Pending Leads", value : "450", icon : , description : "50 in hot leads"}, + {title : "Active Users", value : "5.6k", icon : , 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 ------------------------- */} + + + {/** ---------------------- Different stats content 1 ------------------------- */} +
+ { + statsData.map((d, k) => { + return ( + + ) + }) + } +
+ + + + {/** ---------------------- Different charts ------------------------- */} +
+ + +
+ + {/** ---------------------- Different stats content 2 ------------------------- */} + +
+ + +
+ + {/** ---------------------- User source channels table ------------------------- */} + +
+ + +
+ + ) +} + +export default Dashboard \ No newline at end of file diff --git a/resources/js/Components/features/documentation/DocComponents.js b/resources/js/Components/features/documentation/DocComponents.js new file mode 100644 index 0000000..23852ba --- /dev/null +++ b/resources/js/Components/features/documentation/DocComponents.js @@ -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( + <> +
+
+ +
+ +
+ +
+ +
+ + + ) +} + +export default DocComponents \ No newline at end of file diff --git a/resources/js/Components/features/documentation/DocFeatures.js b/resources/js/Components/features/documentation/DocFeatures.js new file mode 100644 index 0000000..54c6417 --- /dev/null +++ b/resources/js/Components/features/documentation/DocFeatures.js @@ -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( + <> +
+
+ +
+ +
+ +
+ +
+ + + ) +} + +export default Features \ No newline at end of file diff --git a/resources/js/Components/features/documentation/DocGettingStarted.js b/resources/js/Components/features/documentation/DocGettingStarted.js new file mode 100644 index 0000000..636785d --- /dev/null +++ b/resources/js/Components/features/documentation/DocGettingStarted.js @@ -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( + <> +
+
+ +
+ +
+ +
+ +
+ + + ) +} + +export default GettingStarted \ No newline at end of file diff --git a/resources/js/Components/features/documentation/components/DocComponentsContent.js b/resources/js/Components/features/documentation/components/DocComponentsContent.js new file mode 100644 index 0000000..f7c4e41 --- /dev/null +++ b/resources/js/Components/features/documentation/components/DocComponentsContent.js @@ -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( + <> +
+

Components

+ + We have added some global components that are used commonly inside the project. + + {/* Typography*/} +

Typography

+
+ These components are present under /components/Typography 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 - +
+
{'import  Title from "../components/Typography/Title"\n  Your Title here'}
+
+
    +
  • Title - Use this component to show title + Title Example +
  • +
  • Subtitle - Component that shows text smaller than title + Subtitle Example +
  • +
  • ErrorText - Used for showing error messages + Error Text Example +
  • +
  • HelperText - Used for showing secondary message + Helper Text Example
  • +
+
+ + + {/* Form Input*/} +

Form Input

+

+ 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 /components/Input folder. +

+ Ex- +
+
{'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'}
+
+ + + +

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 -

+
    +
  • type - Input type value like number, date, time etc..
  • +
  • updateType - This is used to update state variable in parent component
  • +
  • containerStyle - Style class for container of input, which include label as well
  • +
  • labelTitle - Title of the label
  • +
  • updateFormValue - Function of parent component to update state variable
  • +
+ + + + + {/* Cards */} +

Cards

+

+ Daisy UI 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). + +

+ Ex - +
+
{' 

Card Body

'}
+
+
+

Card Body

+
+ + + + +
+ + +
+ + ) +} + +export default DocComponentsContent \ No newline at end of file diff --git a/resources/js/Components/features/documentation/components/DocComponentsNav.js b/resources/js/Components/features/documentation/components/DocComponentsNav.js new file mode 100644 index 0000000..3e6d84c --- /dev/null +++ b/resources/js/Components/features/documentation/components/DocComponentsNav.js @@ -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( +
    +
  • Components
  • + + { + navs.map((n, k) => { + return( +
  • scrollToSection(k)} className={n.isActive ? "bordered" : ""}>{n.name}
  • + ) + }) + } +
+ ) +} + +export default DocComponentsNav \ No newline at end of file diff --git a/resources/js/Components/features/documentation/components/FeaturesContent.js b/resources/js/Components/features/documentation/components/FeaturesContent.js new file mode 100644 index 0000000..9401f4e --- /dev/null +++ b/resources/js/Components/features/documentation/components/FeaturesContent.js @@ -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( + <> +
+

Features

+ + + + {/* Authentication*/} +

Authentication

+

+ JWT based Authentication logic is present in /app/auth.js. In the file you can see we are adding bearer token in header for every request. Every routes under /routes/ folder will need authentication. For public routes like login, register you will have to add routes in App.js file and also include the path in PUBLIC_ROUTES variable under /app/auth.js file so that auto redirect to login page is not triggered. + +

+ + + + + {/* Left Sidebar*/} +

Left Sidebar

+

+ This is main internal navigation (for pages that will come after login only), all sidebar menu items with their icons are present in /routes/sidebar.js file, while path and page components mapping are respectively present in /routes/index.js file. +

+ + + + {/* Add New Page*/} +

Add New Page

+

All public routes are present in App.js file. Steps to add new public page - +

+ +
    +
  • Create Page inside /pages folder
  • +
  • Go to App.js and import the component and add its path
  • +
  • Add your new route path in /app/auth.js file under PUBLIC_ROUTES variable, this will allow the page to open without login.
  • +
+ +

All protected routes are present in /routes/sidebar.js file

+ +
    +
  • Create your page inside /pages/protected folder
  • +
  • Add your new routes in /routes/sidebar.js, this will show your new page in sidebar
  • +
  • Import your new routes component and map its path in /routes/index.js
  • +
+ + + + {/* Right Sidebar*/} +

Right Sidebar

+
+ 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: +
    +
  • Create new component file containing main body of your content
  • +
  • Create new variable in /utils/globalConstantUtils.js file under RIGHT_DRAWER_TYPES variable
  • +
  • Now include the file mapped with the new variable in /containers/RightSidebar.js file using switch.
    + For ex- If you new component name is TestRightSideBar.js and variable name is TEST_RIGHT_SIDEBAR, then add following code inside switch code block +
    +
    +
    {`[RIGHT_DRAWER_TYPES.TEST_RIGHT_SIDEBAR] : \n`}
    +
    + Here extraObject have variables that is passed from parent component while calling openRightDrawer method +
  • +
  • Now the last step, call dispatch method as follows +
    +
    {'import { useDispatch } from "react-redux"\n  const dispatch = useDispatch()\n  dispatch(openRightDrawer({header : "Test Right Drawer", \n  bodyType : RIGHT_DRAWER_TYPES.TEST_RIGHT_SIDEBAR}))'}
    +
    +
  • +
+
+ + + {/* Themes*/} +

Themes

+

+ 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 tailwind.config.js 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 Daisy UI documentation. +

+ + + + + {/* Modal*/} +

Modal

+
+ 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. +
+ To add new modal in any page follow following steps: +
    +
  • Create new component file containing main body of your modal content
  • +
  • Create new variable in /utils/globalConstantUtils.js file under MODAL_BODY_TYPES variable
  • +
  • Now include the file mapped with the new variable in /containers/ModalLayout.js file using switch.
    + For ex- If you new component name is TestModal.js and variable name is TEST_MODAL, then add following code inside switch code block +
    +
    +
    {`[RIGHT_DRAWER_TYPES.TEST_MODAL] : \n`}
    +
    + Here extraObject have variables that is passed from parent component while calling openModal method +
  • +
  • Now the last step, call dispatch method as follows +
    +
    {'import { useDispatch } from "react-redux"\n  const dispatch = useDispatch()\n   dispatch(openModal({title : "Test Modal Title", \n   bodyType : MODAL_BODY_TYPES.TEST_MODAL}))'}
    +
    +
  • +
+
+ + + + + + + + {/* Notification*/} +

Notification

+

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.

+ +

Code for showing notification is present in headerSlice and layout container component. To show notification just call showNotification() function of headerSlice using dispatch. To show success message notification pass status as 1 and for showing error message pass status as 0.

+ +
+
{'import { useDispatch } from "react-redux"\n  const dispatch = useDispatch()\n  dispatch(showNotification({message : "Message here", status : 1}))'}
+
+ +

Click on this button to check

+ + + + + + +
+ + +
+ + ) +} + +export default FeaturesContent \ No newline at end of file diff --git a/resources/js/Components/features/documentation/components/FeaturesNav.js b/resources/js/Components/features/documentation/components/FeaturesNav.js new file mode 100644 index 0000000..dcbb101 --- /dev/null +++ b/resources/js/Components/features/documentation/components/FeaturesNav.js @@ -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( +
    +
  • Features
  • + + { + navs.map((n, k) => { + return( +
  • scrollToSection(k)} className={n.isActive ? "bordered" : ""}>{n.name}
  • + ) + }) + } +
+ ) +} + +export default FeaturesNav \ No newline at end of file diff --git a/resources/js/Components/features/documentation/components/GettingStartedContent.js b/resources/js/Components/features/documentation/components/GettingStartedContent.js new file mode 100644 index 0000000..467632e --- /dev/null +++ b/resources/js/Components/features/documentation/components/GettingStartedContent.js @@ -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( + <> +
+

Getting Started

+ + + {/* Introduction */} +

Introduction

+

A free dashboard template using Daisy UI and react js. With the help of Dasisy UI, it comes with fully customizable and themable CSS and power of Tailwind CSS utility classes. We have also added redux toolkit and configured it for API calls and state management.

+

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.

+

Core libraries used -

+ +

Major features -

+

Almost all major UI components are available in Daisy UI library. Apart from this logic has been added for following -

+
    +
  • Light/dark mode toggle
  • +
  • Token based user authentication
  • +
  • Submenu support in sidebar
  • +
  • Store management using redux toolkit
  • +
  • Daisy UI components
  • +
  • Right and left sidebar, Universal loader, notifications and other components
  • +
  • React chart js 2 examples
  • +
+ + + + + + {/* How to Use */} +

How to use?

+

+ Just clone the repo from github and then run following command (Make sure you have node js installed )
+ Repo Link +
+ npm install
+ npm start +

+ + + {/* Tailwind CSS*/} +

Tailwind CSS

+

+ 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. +

+ + {/* Daisy UI */} +

Daisy UI

+ +

Daisy UI, 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.

+ +

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 documentation here. For Ex-

+ +
+

Creating a button

+
+
+ +
+

using only utility classes of tailwind

+
+
{'Button'}
+
+ +
+ +
+ +
+

using daisyUI component classes

+
+
{'\nButton'}
+
+ +
+
+ + + + {/* Chart JS */} +

Chart JS

+

+ Chart JS library has rich components of different charts available. It is based on Chart.js library, the most popular charting library. We have added this library and added couple of examples in seperate page. +

+ + + + {/* Redux Toolkit */} +

Redux Toolkit

+

+ The Redux Toolkit package helps in writing redux logic easily. It was originally created to help address three common concerns about Redux: +

  • Configuring a Redux store is too complicated
  • +
  • I have to add a lot of packages to get Redux to do anything useful
  • +
  • Redux requires too much boilerplate code"
  • + This library has been configured and used for showing notifications, modals and loading data from API in leads page. +

    + + + {/* Hero Icons */} +

    Hero Icons

    +

    HeroIcons 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.

    + +

    Each icon can be imported individually as a React component, check documentation

    + +
    {"import BeakerIcon from '@heroicons/react/24/solid/BeakerIcon'"}
    +

    Use as follows in your component

    +
    {""}
    + +
    + +
    +
    Note: Importing all icons in single line will increase your build time
    +
    + +

    Don't import like this (will load all icons and increase build time)

    +
    {"import {BeakerIcon, BellIcon } from '@heroicons/react/24/solid'"}
    + +

    Instead import as follows

    +
    {"import BeakerIcon from '@heroicons/react/24/solid/BeakerIcon'"}
    + {"import BellIcon from '@heroicons/react/24/solid/BellIcon'"}
    + +
    This is better way for importing icons
    + + + + {/* Project Structure */} +

    Project Structure

    +

    Folders -

    +
      +
    • app - store management, auth and libraries settings are present
    • +
    • components - this include all common components to be used in project
    • +
    • containers - components related to layout like sidebar, page layout, header etc..
    • +
    • 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.
    • +
    • 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
    • +
    • routes - all settings related to routes
    • +
    + +

    Files -

    +
      +
    • App.js - Main file containing different routes and components
    • +
    • index.css - Additional global css if required
    • +
    • index.js - Entry point of project
    • +
    • package.json - All dependencies and npm scripts
    • +
    • tailwind.config.js - Tailwind CSS configuration file, add theme customization and new themes in this file
    • +
    + + +
    + +
    + + ) +} + +export default GettingStartedContent \ No newline at end of file diff --git a/resources/js/Components/features/documentation/components/GettingStartedNav.js b/resources/js/Components/features/documentation/components/GettingStartedNav.js new file mode 100644 index 0000000..f1029bd --- /dev/null +++ b/resources/js/Components/features/documentation/components/GettingStartedNav.js @@ -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( +
      +
    • Getting Started
    • + + { + navs.map((n, k) => { + return( +
    • scrollToSection(k)} className={n.isActive ? "bordered" : ""}>{n.name}
    • + ) + }) + } +
    + ) +} + +export default GettingStartedNav \ No newline at end of file diff --git a/resources/js/Components/features/integration/index.js b/resources/js/Components/features/integration/index.js new file mode 100644 index 0000000..22c8e87 --- /dev/null +++ b/resources/js/Components/features/integration/index.js @@ -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( + <> +
    + { + integrationList.map((i, k) => { + return( + + +

    + icon + {i.description} +

    +
    + updateIntegrationStatus(k)}/> +
    + +
    + ) + + }) + } +
    + + ) +} + +export default Integration \ No newline at end of file diff --git a/resources/js/Components/features/leads/components/AddLeadModalBody.jsx b/resources/js/Components/features/leads/components/AddLeadModalBody.jsx new file mode 100644 index 0000000..b348678 --- /dev/null +++ b/resources/js/Components/features/leads/components/AddLeadModalBody.jsx @@ -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 ( + <> + + + + + + + + + {errorMessage} +
    + + +
    + + ) +} + +export default AddLeadModalBody \ No newline at end of file diff --git a/resources/js/Components/features/leads/index.jsx b/resources/js/Components/features/leads/index.jsx new file mode 100644 index 0000000..876b034 --- /dev/null +++ b/resources/js/Components/features/leads/index.jsx @@ -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( +
    + +
    + ) +} + +function Leads(){ + + const {leads } = useSelector(state => state.lead) + const dispatch = useDispatch() + + useEffect(() => { + dispatch(getLeadsContent()) + }, []) + + + + const getDummyStatus = (index) => { + if(index % 5 === 0)return
    Not Interested
    + else if(index % 5 === 1)return
    In Progress
    + else if(index % 5 === 2)return
    Sold
    + else if(index % 5 === 3)return
    Need Followup
    + else return
    Open
    + } + + 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( + <> + + }> + + {/* Leads List in table format loaded from slice after api call */} +
    + + + + + + + + + + + + + { + leads.map((l, k) => { + return( + + + + + + + + + ) + }) + } + +
    NameEmail IdCreated AtStatusAssigned To
    +
    +
    +
    + Avatar +
    +
    +
    +
    {l.first_name}
    +
    {l.last_name}
    +
    +
    +
    {l.email}{moment(new Date()).add(-5*(k+2), 'days').format("DD MMM YY")}{getDummyStatus(k)}{l.last_name}
    +
    +
    + + ) +} + + +export default Leads \ No newline at end of file diff --git a/resources/js/Components/features/leads/leadSlice.jsx b/resources/js/Components/features/leads/leadSlice.jsx new file mode 100644 index 0000000..487ae47 --- /dev/null +++ b/resources/js/Components/features/leads/leadSlice.jsx @@ -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 \ No newline at end of file diff --git a/resources/js/Components/features/settings/billing/index.js b/resources/js/Components/features/settings/billing/index.js new file mode 100644 index 0000000..92d450e --- /dev/null +++ b/resources/js/Components/features/settings/billing/index.js @@ -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
    {status}
    + if(status === "Pending")return
    {status}
    + else return
    {status}
    + } + + return( + <> + + + + {/* Invoice list in table format loaded constant */} +
    + + + + + + + + + + + + + { + bills.map((l, k) => { + return( + + + + + + + + + ) + }) + } + +
    Invoice NoInvoice Generated OnDescriptionAmountStatusInvoice Paid On
    {l.invoiceNo}{l.generatedOn}{l.description}${l.amount}{getPaymentStatus(l.status)}{l.paidOn}
    +
    +
    + + ) +} + + +export default Billing \ No newline at end of file diff --git a/resources/js/Components/features/settings/profilesettings/index.js b/resources/js/Components/features/settings/profilesettings/index.js new file mode 100644 index 0000000..2386337 --- /dev/null +++ b/resources/js/Components/features/settings/profilesettings/index.js @@ -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( + <> + + + +
    + + + + + +
    +
    + +
    + + + +
    + +
    +
    + + ) +} + + +export default ProfileSettings \ No newline at end of file diff --git a/resources/js/Components/features/settings/team/index.js b/resources/js/Components/features/settings/team/index.js new file mode 100644 index 0000000..07e4547 --- /dev/null +++ b/resources/js/Components/features/settings/team/index.js @@ -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( +
    + +
    + ) +} + + +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
    {role}
    + if(role === "Manager")return
    {role}
    + if(role === "Owner")return
    {role}
    + if(role === "Support")return
    {role}
    + else return
    {role}
    + } + + return( + <> + + }> + + {/* Team Member list in table format loaded constant */} +
    + + + + + + + + + + + + { + members.map((l, k) => { + return( + + + + + + + + ) + }) + } + +
    NameEmail IdJoined OnRoleLast Active
    +
    +
    +
    + Avatar +
    +
    +
    +
    {l.name}
    +
    +
    +
    {l.email}{l.joinedOn}{getRoleComponent(l.role)}{l.lastActive}
    +
    +
    + + ) +} + + +export default Team \ No newline at end of file diff --git a/resources/js/Components/features/transactions/index.js b/resources/js/Components/features/transactions/index.js new file mode 100644 index 0000000..071649b --- /dev/null +++ b/resources/js/Components/features/transactions/index.js @@ -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( +
    + + {filterParam != "" && } +
    + + +
    +
    + ) +} + + +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( + <> + + }> + + {/* Team Member list in table format loaded constant */} +
    + + + + + + + + + + + + { + trans.map((l, k) => { + return( + + + + + + + + ) + }) + } + +
    NameEmail IdLocationAmountTransaction Date
    +
    +
    +
    + Avatar +
    +
    +
    +
    {l.name}
    +
    +
    +
    {l.email}{l.location}${l.amount}{moment(l.date).format("D MMM")}
    +
    +
    + + ) +} + + +export default Transactions \ No newline at end of file diff --git a/resources/js/Components/features/user/ForgotPassword.js b/resources/js/Components/features/user/ForgotPassword.js new file mode 100644 index 0000000..ab61d85 --- /dev/null +++ b/resources/js/Components/features/user/ForgotPassword.js @@ -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( +
    +
    +
    +
    + +
    +
    +

    Forgot Password

    + + { + linkSent && + <> +
    +

    Link Sent

    +

    Check your email to reset password

    +
    + + + } + + { + !linkSent && + <> +

    We will send password reset link on your email Id

    +
    submitForm(e)}> + +
    + + + + +
    + + {errorMessage} + + +
    Don't have an account yet?
    +
    + + } + +
    +
    +
    +
    + ) +} + +export default ForgotPassword \ No newline at end of file diff --git a/resources/js/Components/features/user/LandingIntro.js b/resources/js/Components/features/user/LandingIntro.js new file mode 100644 index 0000000..7cd9c9c --- /dev/null +++ b/resources/js/Components/features/user/LandingIntro.js @@ -0,0 +1,27 @@ +import TemplatePointers from "./components/TemplatePointers" + + + +function LandingIntro(){ + + return( +
    +
    +
    + +

    dashwind-logoDashWind

    + +
    Dashwind Admin Template
    + + {/* Importing pointers component */} + + +
    + +
    +
    + ) + + } + + export default LandingIntro \ No newline at end of file diff --git a/resources/js/Components/features/user/Login.js b/resources/js/Components/features/user/Login.js new file mode 100644 index 0000000..cd36dda --- /dev/null +++ b/resources/js/Components/features/user/Login.js @@ -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( +
    +
    +
    +
    + +
    +
    +

    Login

    +
    submitForm(e)}> + +
    + + + + + +
    + +
    Forgot Password? +
    + + {errorMessage} + + +
    Don't have an account yet? Register
    +
    +
    +
    +
    +
    + ) +} + +export default Login \ No newline at end of file diff --git a/resources/js/Components/features/user/Register.js b/resources/js/Components/features/user/Register.js new file mode 100644 index 0000000..836f487 --- /dev/null +++ b/resources/js/Components/features/user/Register.js @@ -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( +
    +
    +
    +
    + +
    +
    +

    Register

    +
    submitForm(e)}> + +
    + + + + + + + +
    + + {errorMessage} + + +
    Already have an account? Login
    +
    +
    +
    +
    +
    + ) +} + +export default Register \ No newline at end of file diff --git a/resources/js/Components/features/user/components/TemplatePointers.js b/resources/js/Components/features/user/components/TemplatePointers.js new file mode 100644 index 0000000..208afd9 --- /dev/null +++ b/resources/js/Components/features/user/components/TemplatePointers.js @@ -0,0 +1,14 @@ +function TemplatePointers(){ + return( + <> +

    Admin Dashboard Starter Kit

    +

    Light/dark mode toggle

    +

    Redux toolkit and other utility libraries configured

    +

    Calendar, Modal, Sidebar components

    +

    ✓ User-friendly documentation

    +

    Daisy UI components, Tailwind CSS support

    + + ) +} + +export default TemplatePointers \ No newline at end of file diff --git a/resources/js/Layouts/GuestLayout.jsx b/resources/js/Layouts/GuestLayout.jsx index 090d571..ce4cdf9 100644 --- a/resources/js/Layouts/GuestLayout.jsx +++ b/resources/js/Layouts/GuestLayout.jsx @@ -1,4 +1,4 @@ -import ApplicationLogo from '@/Components/ApplicationLogo'; +import ApplicationLogo from '../Components/ApplicationLogo'; import { Link } from '@inertiajs/react'; export default function Guest({ children }) { diff --git a/resources/js/Pages/Auth/Login.jsx b/resources/js/Pages/Auth/Login.jsx index 11bee02..cf3915f 100644 --- a/resources/js/Pages/Auth/Login.jsx +++ b/resources/js/Pages/Auth/Login.jsx @@ -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 }) { diff --git a/resources/js/Pages/list-admin/payment/ManualPayment.jsx b/resources/js/Pages/list-admin/payment/ManualPayment.jsx index 2b4812a..34109e5 100644 --- a/resources/js/Pages/list-admin/payment/ManualPayment.jsx +++ b/resources/js/Pages/list-admin/payment/ManualPayment.jsx @@ -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 (
    + - setSelectedSantri(null)} /> + setSelectedSantri(null)} /> {santri && santri.length > 0 ? santri.map((item, i) => (

    Nis: {item.user.nis}

    @@ -18,8 +22,6 @@ export default function ManualPayment({ santri, penalty, bill, fields, options }

    Payments:

    {item.payments.map((payment) => (
    - {/* {payment.detail_payments && payment.detail_payments.length > 0 ? ( */} - {payment.detail_payments.map((detail) => (

    {detail.penalty ? `Denda: Rp ${detail.penalty}` : 'Tidak ada denda'}

    @@ -63,8 +65,6 @@ export default function ManualPayment({ santri, penalty, bill, fields, options }
    ))} - - {/* ) :

    Tidak ada detail pembayaran.

    } */}
    ))}
    @@ -73,6 +73,7 @@ export default function ManualPayment({ santri, penalty, bill, fields, options }
    +
    + + + ) +} + +export default InternalPage \ No newline at end of file diff --git a/resources/js/Routes/index.jsx b/resources/js/Routes/index.jsx new file mode 100644 index 0000000..0ea9812 --- /dev/null +++ b/resources/js/Routes/index.jsx @@ -0,0 +1,85 @@ +// All components mapping with path for internal routes + +import { lazy } from 'react' + +const Dashboard = lazy(() => import('../pages/protected/Dashboard')) +const Welcome = lazy(() => import('@/Pages/list-admin/santri/IndexSantri')) +// const Page404 = lazy(() => import('../pages/protected/404')) +// const Blank = lazy(() => import('../pages/protected/Blank')) +// const Charts = lazy(() => import('../pages/protected/Charts')) +const Leads = lazy(() => import('../pages/protected/Leads')) +// const Integration = lazy(() => import('../pages/protected/Integration')) +// const Calendar = lazy(() => import('../pages/protected/Calendar')) +// const Team = lazy(() => import('../pages/protected/Team')) +// const Transactions = lazy(() => import('../pages/protected/Transactions')) +// const Bills = lazy(() => import('../pages/protected/Bills')) +// const ProfileSettings = lazy(() => import('../pages/protected/ProfileSettings')) +// const GettingStarted = lazy(() => import('../pages/GettingStarted')) +// const DocFeatures = lazy(() => import('../pages/DocFeatures')) +// const DocComponents = lazy(() => import('../pages/DocComponents')) + + +const routes = [ + { + path: route('dashboard'), // the url + component: Dashboard, // view rendered + }, + { + path: route('indexSantri'), // the url + component: Welcome, // view rendered + }, + { + path: route('indexManualPayment'), + component: Leads, + }, + // { + // path: '/settings-team', + // component: Team, + // }, + // { + // path: '/calendar', + // component: Calendar, + // }, + // { + // path: '/transactions', + // component: Transactions, + // }, + // { + // path: '/settings-profile', + // component: ProfileSettings, + // }, + // { + // path: '/settings-billing', + // component: Bills, + // }, + // { + // path: '/getting-started', + // component: GettingStarted, + // }, + // { + // path: '/features', + // component: DocFeatures, + // }, + // { + // path: '/components', + // component: DocComponents, + // }, + // { + // path: '/integration', + // component: Integration, + // }, + // { + // path: '/charts', + // component: Charts, + // }, + // { + // path: '/404', + // component: Page404, + // }, + // { + // path: '/blank', + // component: Blank, + // }, +] + +export default routes diff --git a/resources/js/Routes/sidebar.jsx b/resources/js/Routes/sidebar.jsx new file mode 100644 index 0000000..9110582 --- /dev/null +++ b/resources/js/Routes/sidebar.jsx @@ -0,0 +1,46 @@ +/** Icons are imported separatly to reduce build time */ +import BellIcon from '@heroicons/react/24/outline/BellIcon' +import DocumentTextIcon from '@heroicons/react/24/outline/DocumentTextIcon' +import Squares2X2Icon from '@heroicons/react/24/outline/Squares2X2Icon' +import TableCellsIcon from '@heroicons/react/24/outline/TableCellsIcon' +import WalletIcon from '@heroicons/react/24/outline/WalletIcon' +import CodeBracketSquareIcon from '@heroicons/react/24/outline/CodeBracketSquareIcon' +import DocumentIcon from '@heroicons/react/24/outline/DocumentIcon' +import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon' +import CalendarDaysIcon from '@heroicons/react/24/outline/CalendarDaysIcon' +import ArrowRightOnRectangleIcon from '@heroicons/react/24/outline/ArrowRightOnRectangleIcon' +import UserIcon from '@heroicons/react/24/outline/UserIcon' +import Cog6ToothIcon from '@heroicons/react/24/outline/Cog6ToothIcon' +import BoltIcon from '@heroicons/react/24/outline/BoltIcon' +import ChartBarIcon from '@heroicons/react/24/outline/ChartBarIcon' +import CurrencyDollarIcon from '@heroicons/react/24/outline/CurrencyDollarIcon' +import InboxArrowDownIcon from '@heroicons/react/24/outline/InboxArrowDownIcon' +import UsersIcon from '@heroicons/react/24/outline/UsersIcon' +import KeyIcon from '@heroicons/react/24/outline/KeyIcon' +import DocumentDuplicateIcon from '@heroicons/react/24/outline/DocumentDuplicateIcon' + +const iconClasses = `h-6 w-6` +const submenuIconClasses = `h-5 w-5` + +const routes = [ + + { + path: route('dashboard'), + icon: , + name: 'Dashboard', + }, + { + path: route('indexSantri'), + icon: , // icon component + name: 'Data Santri', // name that appear in Sidebar + }, + { + path: route('indexManualPayment'), // url + icon: , // icon component + name: 'Data Pembayaran', // name that appear in Sidebar + }, +] + +export default routes + + diff --git a/resources/js/app.jsx b/resources/js/app.jsx index c150780..4310d44 100644 --- a/resources/js/app.jsx +++ b/resources/js/app.jsx @@ -1,11 +1,27 @@ import './bootstrap'; import '../css/app.css'; +import React, { lazy, useEffect } from 'react' import { createRoot } from 'react-dom/client'; import { createInertiaApp } from '@inertiajs/react'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; +import store from '../js/Auth/store'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import Header from '@/Components/Header'; +import LeftSidebar from '@/Components/LeftSidebar'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; +const Layout = lazy(() => import('./Components/Layout')) +// const Login = lazy(() => import('./pages/Login')) +// const ForgotPassword = lazy(() => import('./pages/ForgotPassword')) +// const Register = lazy(() => import('./pages/Register')) +// const Documentation = lazy(() => import('./pages/Documentation')) + +useEffect(() => { + // 👆 daisy UI themes initialization + themeChange(false) +}, []) createInertiaApp({ title: (title) => `${title} - ${appName}`, @@ -13,9 +29,19 @@ createInertiaApp({ setup({ el, App, props }) { const root = createRoot(el); - root.render(); + root.render( + + +
    + + + + + ); }, progress: { color: '#4B5563', }, }); + +// export default App \ No newline at end of file diff --git a/resources/js/index.css b/resources/js/index.css new file mode 100644 index 0000000..f438917 --- /dev/null +++ b/resources/js/index.css @@ -0,0 +1,32 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.loading-indicator:before { + content: ''; + background: #00000080; + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 1000; + } + + .loading-indicator:after { + content: ' '; + position: fixed; + top: 40%; + left: 45%; + z-index: 10010; + color:white; + text-align:center; + font-weight:bold; + font-size:1.2rem; + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #0474bf; /* Blue */ + border-radius: 50%; + width: 120px; + height: 120px; + animation: spin 2s linear infinite; +} \ No newline at end of file diff --git a/resources/js/reportWebVitals.jsx b/resources/js/reportWebVitals.jsx new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/resources/js/reportWebVitals.jsx @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/resources/js/store.js b/resources/js/store.js new file mode 100644 index 0000000..1decc15 --- /dev/null +++ b/resources/js/store.js @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit' +import headerSlice from '../features/common/headerSlice' +import modalSlice from '../features/common/modalSlice' +import rightDrawerSlice from '../features/common/rightDrawerSlice' +import leadsSlice from '../features/leads/leadSlice' + +const combinedReducer = { + header : headerSlice, + rightDrawer : rightDrawerSlice, + modal : modalSlice, + lead : leadsSlice +} + +export default configureStore({ + reducer: combinedReducer +}) \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 24c14bd..8fa62a2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -36,12 +36,13 @@ // payment type Route::get('/data-payment-type', [PaymentTypeController::class, 'index'])->name('indexPaymentType'); -Route::post('addpayment_types', [PaymentTypeController::class, 'store'])->name('storePaymentType'); +Route::post('/addpayment_types', [PaymentTypeController::class, 'store'])->name('storePaymentType'); Route::post('/updatepayment_types/{id}', [PaymentTypeController::class, 'update'])->name('updatePaymentType'); Route::post('/deletepayment_types/{id}', [PaymentTypeController::class, 'destroy'])->name('deletePaymentType'); // manual payment -Route::get('index-manual-payment', [PaymentController::class, 'indexManualPayment'])->name('indexManualPayment'); +Route::get('/index-manual-payment', [PaymentController::class, 'indexManualPayment'])->name('indexManualPayment'); +Route::post('/updatepayments/{paymentId}', [PaymentController::class, 'manualPayment'])->name('manualPayment'); Route::get('/dashboard', function () { return Inertia::render('Dashboard');