s33
This commit is contained in:
106
package-lock.json
generated
106
package-lock.json
generated
@@ -39,7 +39,8 @@
|
||||
"simplemde": "^1.11.2",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"xlsx": "^0.18.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -3977,6 +3978,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-align": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
||||
@@ -4685,6 +4695,19 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
|
||||
@@ -4862,6 +4885,15 @@
|
||||
"typo-js": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
@@ -4950,6 +4982,18 @@
|
||||
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -5673,6 +5717,15 @@
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -14730,6 +14783,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
|
||||
@@ -16017,6 +16082,24 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
|
||||
@@ -16108,6 +16191,27 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"simplemde": "^1.11.2",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
1
public/assets/paypal-logo-white.svg
Normal file
1
public/assets/paypal-logo-white.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="64px" height="64px" viewBox="0 -140 780 780" enable-background="new 0 0 780 500" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="m168.38 169.35c-8.399-5.774-19.359-8.668-32.88-8.668h-52.346c-4.145 0-6.435 2.073-6.87 6.215l-21.264 133.48c-0.221 1.311 0.107 2.51 0.981 3.6 0.869 1.092 1.962 1.635 3.271 1.635h24.864c4.361 0 6.758-2.068 7.198-6.215l5.888-35.986c0.215-1.744 0.982-3.162 2.291-4.254 1.308-1.09 2.944-1.803 4.907-2.129 1.963-0.324 3.814-0.488 5.562-0.488 1.743 0 3.814 0.111 6.217 0.328 2.397 0.217 3.925 0.324 4.58 0.324 18.756 0 33.478-5.285 44.167-15.867 10.684-10.576 16.032-25.242 16.032-44.004 0-12.868-4.203-22.191-12.598-27.974zm-26.989 40.08c-1.094 7.635-3.926 12.649-8.506 15.049-4.581 2.403-11.124 3.599-19.629 3.599l-10.797 0.326 5.563-35.007c0.434-2.397 1.851-3.597 4.252-3.597h6.218c8.72 0 15.049 1.257 18.975 3.761 3.924 2.51 5.233 7.801 3.924 15.869z" fill="#003087"></path><path d="m720.79 160.68h-24.207c-2.406 0-3.822 1.2-4.254 3.601l-21.266 136.1-0.328 0.654c0 1.096 0.436 2.127 1.311 3.109 0.867 0.98 1.963 1.471 3.27 1.471h21.596c4.137 0 6.428-2.068 6.871-6.215l21.264-133.81v-0.325c-1e-3 -3.055-1.423-4.581-4.257-4.581z" fill="#009CDE"></path><path d="m428.31 213.36c0-1.088-0.438-2.126-1.305-3.105-0.875-0.981-1.857-1.475-2.945-1.475h-25.191c-2.404 0-4.367 1.096-5.891 3.271l-34.678 51.039-14.395-49.074c-1.096-3.487-3.492-5.236-7.197-5.236h-24.541c-1.093 0-2.074 0.492-2.941 1.475-0.875 0.979-1.309 2.019-1.309 3.105 0 0.439 2.127 6.871 6.379 19.303 4.252 12.436 8.832 25.85 13.74 40.246 4.908 14.393 7.469 22.031 7.688 22.896-17.886 24.432-26.825 37.518-26.825 39.26 0 2.838 1.415 4.254 4.253 4.254h25.191c2.398 0 4.36-1.088 5.89-3.27l83.427-120.4c0.433-0.432 0.65-1.192 0.65-2.29z" fill="#003087"></path><path d="m662.89 208.78h-24.865c-3.057 0-4.904 3.6-5.559 10.799-5.678-8.722-16.031-13.089-31.084-13.089-15.703 0-29.064 5.89-40.076 17.668-11.016 11.778-16.521 25.632-16.521 41.552 0 12.871 3.762 23.121 11.285 30.752 7.525 7.639 17.611 11.451 30.266 11.451 6.324 0 12.758-1.311 19.301-3.926 6.543-2.617 11.664-6.105 15.379-10.469 0 0.219-0.223 1.197-0.654 2.941-0.441 1.748-0.656 3.061-0.656 3.926 0 3.494 1.414 5.234 4.254 5.234h22.576c4.139 0 6.541-2.068 7.193-6.215l13.416-85.39c0.215-1.31-0.111-2.507-0.982-3.599-0.877-1.088-1.965-1.635-3.273-1.635zm-42.694 64.454c-5.562 5.453-12.27 8.178-20.121 8.178-6.328 0-11.449-1.742-15.377-5.234-3.928-3.482-5.891-8.281-5.891-14.395 0-8.064 2.727-14.886 8.182-20.447 5.445-5.562 12.213-8.342 20.283-8.342 6.102 0 11.174 1.799 15.213 5.396 4.031 3.6 6.055 8.562 6.055 14.889-2e-3 7.851-2.783 14.505-8.344 19.955z" fill="#009CDE"></path><path d="m291.23 208.78h-24.865c-3.058 0-4.908 3.6-5.563 10.799-5.889-8.722-16.25-13.089-31.081-13.089-15.704 0-29.065 5.89-40.078 17.668-11.016 11.778-16.521 25.632-16.521 41.552 0 12.871 3.763 23.121 11.288 30.752 7.525 7.639 17.61 11.451 30.262 11.451 6.104 0 12.433-1.311 18.975-3.926 6.543-2.617 11.778-6.105 15.704-10.469-0.875 2.615-1.309 4.906-1.309 6.867 0 3.494 1.417 5.234 4.253 5.234h22.574c4.141 0 6.543-2.068 7.198-6.215l13.413-85.39c0.215-1.31-0.111-2.507-0.981-3.599-0.873-1.088-1.962-1.635-3.269-1.635zm-42.695 64.616c-5.563 5.35-12.382 8.016-20.447 8.016-6.329 0-11.4-1.742-15.214-5.234-3.819-3.482-5.726-8.281-5.726-14.395 0-8.064 2.725-14.886 8.18-20.447 5.449-5.562 12.211-8.343 20.284-8.343 6.104 0 11.175 1.8 15.214 5.397 4.032 3.6 6.052 8.562 6.052 14.889-1e-3 8.07-2.781 14.779-8.343 20.117z" fill="#003087"></path><path d="m540.04 169.35c-8.398-5.774-19.355-8.668-32.879-8.668h-52.02c-4.363 0-6.764 2.073-7.197 6.215l-21.266 133.48c-0.221 1.311 0.107 2.51 0.982 3.6 0.865 1.092 1.961 1.635 3.27 1.635h26.826c2.617 0 4.361-1.416 5.236-4.252l5.889-37.949c0.217-1.744 0.98-3.162 2.291-4.254 1.309-1.09 2.943-1.803 4.908-2.129 1.961-0.324 3.812-0.488 5.561-0.488 1.744 0 3.814 0.111 6.215 0.328 2.398 0.217 3.93 0.324 4.58 0.324 18.76 0 33.479-5.285 44.168-15.867 10.688-10.576 16.031-25.242 16.031-44.004 1e-3 -12.868-4.2-22.192-12.595-27.974zm-33.533 53.819c-4.799 3.271-11.998 4.906-21.592 4.906l-10.471 0.328 5.562-35.008c0.432-2.396 1.85-3.598 4.252-3.598h5.887c4.799 0 8.615 0.219 11.455 0.654 2.83 0.438 5.561 1.799 8.178 4.088 2.619 2.291 3.926 5.619 3.926 9.979 0 9.164-2.402 15.377-7.197 18.651z" fill="#009CDE"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
22
public/assets/payu-logo.svg
Normal file
22
public/assets/payu-logo.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 514.56 280.44">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #414042;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #00ad7d;
|
||||
}
|
||||
.cls-2:hover {
|
||||
fill: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M0,97.77h25.72v12.86c2.93-4.46,7.37-8.1,13.3-10.92,5.93-2.82,12.3-4.23,19.11-4.23,8.1,0,15.56,2.06,22.37,6.17,6.81,4.11,12.27,9.98,16.38,17.62,4.11,7.64,6.16,16.56,6.16,26.78s-2.06,19.29-6.16,26.86c-4.11,7.58-9.6,13.36-16.47,17.35-6.87,3.99-14.3,5.99-22.28,5.99-6.93,0-13.36-1.38-19.29-4.14-5.93-2.76-10.31-6.37-13.12-10.83v99.18H0V97.77ZM69.67,165.94c4.76-5.17,7.13-11.8,7.13-19.9s-2.38-14.94-7.13-20.17c-4.76-5.22-10.89-7.84-18.41-7.84s-13.68,2.61-18.5,7.84c-4.82,5.23-7.22,11.95-7.22,20.17s2.41,14.74,7.22,19.9c4.81,5.17,10.98,7.75,18.5,7.75s13.65-2.58,18.41-7.75ZM134.67,190.25c-6.87-3.99-12.36-9.78-16.47-17.35-4.11-7.58-6.17-16.53-6.17-26.86s2.05-19.14,6.17-26.78c4.11-7.63,9.57-13.5,16.38-17.62,6.81-4.11,14.27-6.17,22.37-6.17,6.81,0,13.18,1.41,19.11,4.23,5.93,2.82,10.36,6.46,13.3,10.92v-12.33h25.72v95.13h-25.72v-12.15c-2.82,4.46-7.2,8.07-13.12,10.83-5.93,2.76-12.36,4.14-19.29,4.14-7.99,0-15.41-2-22.28-5.99ZM182.41,165.94c4.76-5.17,7.14-11.8,7.14-19.9s-2.38-14.94-7.14-20.17c-4.76-5.22-10.89-7.84-18.41-7.84s-13.68,2.61-18.5,7.84c-4.82,5.23-7.22,11.95-7.22,20.17s2.41,14.74,7.22,19.9c4.81,5.17,10.98,7.75,18.5,7.75s13.65-2.58,18.41-7.75ZM272.69,196.24c-14.56,0-25.31-3.7-32.24-11.1-6.93-7.4-10.39-17.73-10.39-31v-56.37h26.07v53.73c0,7.4,1.94,12.98,5.81,16.73,3.88,3.76,9.22,5.64,16.03,5.64,6.22,0,11.6-2.05,16.12-6.17,4.52-4.11,6.78-9.69,6.78-16.73v-53.2h25.72v182.68h-25.72v-96.71c-6.11,8.34-15.5,12.51-28.19,12.51ZM348.44,252.44l142.86-43.16V75.57l-164.71-41.93v40.87h-25.72V0l213.68,56.19v171.75l-166.12,51.62v-27.13Z"/>
|
||||
<path class="cls-2" d="M362.18,97.77h26.07v53.73c0,15.03,7.1,22.55,21.32,22.55s21.32-7.51,21.32-22.55v-53.73h26.07v56.37c0,13.15-4.05,23.46-12.15,30.91-8.1,7.46-19.85,11.19-35.23,11.19s-27.13-3.76-35.23-11.27c-8.1-7.52-12.16-17.79-12.16-30.83v-56.37Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
127
src/components/AvatarUpload.tsx
Normal file
127
src/components/AvatarUpload.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { X } from "lucide-react";
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('https://tst-pb.s38.siliconpin.com');
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
|
||||
export function AvatarUpload({ userId }: { userId: string }) {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setSelectedFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = () => {
|
||||
setSelectedFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile || !userId) return;
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
// 1. Upload to PocketBase
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', selectedFile);
|
||||
|
||||
// Update PocketBase user record
|
||||
const pbRecord = await pb.collection('users').update(userId, formData);
|
||||
|
||||
// Get the avatar URL from PocketBase
|
||||
const avatarUrl = pb.getFileUrl(pbRecord, pbRecord.avatar, {
|
||||
thumb: '100x100'
|
||||
});
|
||||
|
||||
// 2. Update PHP backend session
|
||||
const response = await fetch(`${INVOICE_API_URL}?query=login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Important for sessions to work
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
avatar: avatarUrl,
|
||||
query: 'avatar_update'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update session');
|
||||
}
|
||||
|
||||
// Success - you might want to refresh the user data or show a success message
|
||||
window.location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading avatar:', error);
|
||||
alert('Failed to update avatar');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!selectedFile ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label
|
||||
htmlFor="avatar"
|
||||
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
Change Avatar
|
||||
</Label>
|
||||
<Input
|
||||
type="file"
|
||||
id="avatar"
|
||||
ref={fileInputRef}
|
||||
accept="image/jpeg,image/png,image/gif"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
JPG, GIF or PNG. 1MB max.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-3 space-x-2">
|
||||
<div className="truncate max-w-[200px]">
|
||||
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-xs p-1 h-fit"
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{isUploading ? 'Uploading...' : 'Update'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleRemoveFile}
|
||||
className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit"
|
||||
disabled={isUploading}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
591
src/components/BillingInfo.jsx
Normal file
591
src/components/BillingInfo.jsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||
import Loader from "./ui/loader";
|
||||
import { PDFDownloadLink } from '@react-pdf/renderer';
|
||||
import InvoicePDF from "../lib/InvoicePDF";
|
||||
import { Eye, Download, ChevronUp, ChevronDown, Search } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||
|
||||
export default function UserBillingList() {
|
||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||
const [billingData, setBillingData] = useState([]);
|
||||
const [dataLoading, setDataLoading] = useState(true);
|
||||
const [apiError, setApiError] = useState(null);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
// Sorting state
|
||||
const [sortConfig, setSortConfig] = useState({ key: 'created_at', direction: 'desc' });
|
||||
|
||||
// Filtering state
|
||||
const [filters, setFilters] = useState({
|
||||
status: '',
|
||||
cycle: '',
|
||||
service: ''
|
||||
});
|
||||
|
||||
// Search state
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage] = useState(10);
|
||||
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
fetchBillingData();
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const fetchBillingData = async () => {
|
||||
try {
|
||||
const res = await fetch(`${INVOICE_API_URL}?query=invoice-info`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
// For regular users, filter to only show their own billing data
|
||||
const filteredData = sessionData?.user_type === 'admin'
|
||||
? data.data || []
|
||||
: (data.data || []).filter(item => item.user === sessionData?.email);
|
||||
|
||||
setBillingData(filteredData);
|
||||
} catch (err) {
|
||||
setApiError(err.message);
|
||||
} finally {
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewItem = (item) => {
|
||||
setSelectedItem(item);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setDialogOpen(false);
|
||||
setSelectedItem(null);
|
||||
};
|
||||
|
||||
// Sorting functionality
|
||||
const requestSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Filter and sort data
|
||||
const filteredAndSortedData = useMemo(() => {
|
||||
let filteredData = [...billingData];
|
||||
|
||||
// Apply search
|
||||
if (searchTerm) {
|
||||
filteredData = filteredData.filter(item =>
|
||||
Object.values(item).some(
|
||||
val => val && val.toString().toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if (filters.status) {
|
||||
filteredData = filteredData.filter(item => item.status === filters.status);
|
||||
}
|
||||
if (filters.cycle) {
|
||||
filteredData = filteredData.filter(item => item.cycle === filters.cycle);
|
||||
}
|
||||
if (filters.service) {
|
||||
filteredData = filteredData.filter(item => item.service === filters.service);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
if (sortConfig.key) {
|
||||
filteredData.sort((a, b) => {
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return filteredData;
|
||||
}, [billingData, searchTerm, filters, sortConfig]);
|
||||
|
||||
const currentItems = useMemo(() => {
|
||||
const indexOfLastItem = currentPage * itemsPerPage;
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
return filteredAndSortedData.slice(indexOfFirstItem, indexOfLastItem);
|
||||
}, [currentPage, itemsPerPage, filteredAndSortedData]);
|
||||
|
||||
const totalPages = Math.ceil(filteredAndSortedData.length / itemsPerPage);
|
||||
|
||||
const paginate = (pageNumber) => {
|
||||
if (pageNumber > 0 && pageNumber <= totalPages) {
|
||||
setCurrentPage(pageNumber);
|
||||
}
|
||||
};
|
||||
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const maxVisiblePages = 5;
|
||||
range.push(1);
|
||||
if (currentPage > 3) {
|
||||
range.push('...');
|
||||
}
|
||||
let start = Math.max(2, currentPage - 1);
|
||||
let end = Math.min(totalPages - 1, currentPage + 1);
|
||||
if (currentPage <= 3) {
|
||||
end = Math.min(4, totalPages - 1);
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
start = Math.max(totalPages - 3, 2);
|
||||
}
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i > 1 && i < totalPages) {
|
||||
range.push(i);
|
||||
}
|
||||
}
|
||||
if (currentPage < totalPages - 2) {
|
||||
range.push('...');
|
||||
}
|
||||
if (totalPages > 1) {
|
||||
range.push(totalPages);
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
// Get unique values for filter dropdowns
|
||||
const uniqueServices = [...new Set(billingData.map(item => item.service))];
|
||||
const uniqueStatuses = ['completed', 'pending', 'failed'];
|
||||
const uniquecycles = ['monthly', 'yearly', 'one-time'];
|
||||
|
||||
// Reset filters
|
||||
const resetFilters = () => {
|
||||
setFilters({
|
||||
status: '',
|
||||
cycle: '',
|
||||
service: ''
|
||||
});
|
||||
setSearchTerm('');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Handle filter change
|
||||
const handleFilterChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Handle search
|
||||
const handleSearch = (e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'bg-green-100 text-green-800';
|
||||
case 'pending': return 'bg-yellow-100 text-yellow-800';
|
||||
case 'failed': return 'bg-red-100 text-red-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return 'N/A';
|
||||
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
||||
return new Date(dateString).toLocaleDateString(undefined, options);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
if (!amount) return '$0.00';
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(parseFloat(amount));
|
||||
};
|
||||
|
||||
if (loading || dataLoading) return <Loader />;
|
||||
if (error || apiError) return <p>Error: {error?.message || apiError}</p>;
|
||||
if (!isLoggedIn) {
|
||||
return <p className="text-center mt-8">You need to be logged in to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">My Billing History</h1>
|
||||
</div>
|
||||
|
||||
{/* Results Count */}
|
||||
<div className="mb-2 text-sm text-gray-600">
|
||||
Showing {currentItems.length} of {filteredAndSortedData.length} results
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-t-lg shadow">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
className="bg-[#262626] pl-10 w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<select
|
||||
name="status"
|
||||
value={filters.status}
|
||||
onChange={handleFilterChange}
|
||||
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
{uniqueStatuses.map(status => (
|
||||
<option key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* cycle Filter */}
|
||||
<select
|
||||
name="cycle"
|
||||
value={filters.cycle}
|
||||
onChange={handleFilterChange}
|
||||
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="">All cycles</option>
|
||||
{uniquecycles.map(cycle => (
|
||||
<option key={cycle} value={cycle}>
|
||||
{cycle.charAt(0).toUpperCase() + cycle.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Service Filter */}
|
||||
<select
|
||||
name="service"
|
||||
value={filters.service}
|
||||
onChange={handleFilterChange}
|
||||
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="">All Services</option>
|
||||
{uniqueServices.map(service => (
|
||||
<option key={service} value={service}>
|
||||
{service}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Reset Filters Button */}
|
||||
{(filters.status || filters.cycle || filters.service || searchTerm) && (
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
onClick={resetFilters}
|
||||
size="sm"
|
||||
>
|
||||
Reset Filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-white rounded-b-lg shadow overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('billing_id')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Billing ID
|
||||
{sortConfig.key === 'billing_id' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('service')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Service
|
||||
{sortConfig.key === 'service' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{sortConfig.key === 'name' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('amount')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Amount
|
||||
{sortConfig.key === 'amount' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('status')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
{sortConfig.key === 'status' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created At
|
||||
{sortConfig.key === 'created_at' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{currentItems.length > 0 ? (
|
||||
currentItems.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{item.billing_id}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{item.service}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{item.name??item.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">
|
||||
{formatCurrency(item.amount)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(item.status)}`}>
|
||||
{item.status.charAt(0).toUpperCase() + item.status.slice(1)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(item.created_at)}
|
||||
</td>
|
||||
<td className="inline-flex px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
title="View Details"
|
||||
onClick={() => handleViewItem(item)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-2"
|
||||
>
|
||||
<Eye className="w-5 h-5" />
|
||||
</button>
|
||||
<PDFDownloadLink
|
||||
title="Download PDF"
|
||||
document={<InvoicePDF data={item} />}
|
||||
fileName={`invoice_${item.billing_id}.pdf`}
|
||||
className="text-[#6d9e37] hover:text-green-600"
|
||||
>
|
||||
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
|
||||
</PDFDownloadLink>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
|
||||
No records found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{filteredAndSortedData.length > itemsPerPage && (
|
||||
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() => paginate(1)}
|
||||
disabled={currentPage === 1}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
First
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
{getPaginationRange().map((item, index) => {
|
||||
if (item === '...') {
|
||||
return (
|
||||
<span key={`ellipsis-${index}`} className="px-2 py-1">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={`page-${item}`}
|
||||
onClick={() => paginate(item)}
|
||||
variant={currentPage === item ? "default" : "outline"}
|
||||
size="sm"
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => paginate(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Last
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dialog for View */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="">
|
||||
Billing Details
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Detailed information about your billing record
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">Billing ID</h3>
|
||||
<p className="text-neutral-400 font-medium">{selectedItem?.billing_id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">Service</h3>
|
||||
<p className="text-neutral-400 font-medium">{selectedItem?.service}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">User</h3>
|
||||
<p className="text-neutral-400 font-medium">{selectedItem?.user}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">cycle</h3>
|
||||
<p className="text-neutral-400 font-medium">{selectedItem?.cycle}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">Amount</h3>
|
||||
<p className="text-neutral-400 font-medium">{formatCurrency(selectedItem?.amount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">Status</h3>
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(selectedItem?.status)}`}>
|
||||
{selectedItem?.status?.charAt(0).toUpperCase() + selectedItem?.status?.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">Created At</h3>
|
||||
<p className="text-neutral-400 font-medium">{formatDate(selectedItem?.created_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-950">Updated At</h3>
|
||||
<p className="text-neutral-400 font-medium">{formatDate(selectedItem?.updated_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<PDFDownloadLink
|
||||
document={<InvoicePDF data={selectedItem} />}
|
||||
fileName={`invoice_${selectedItem?.billing_id}.pdf`}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||||
>
|
||||
{({ loading }) => (loading ? 'Preparing PDF...' : 'Download PDF')}
|
||||
</PDFDownloadLink>
|
||||
<Button
|
||||
onClick={closeModal}
|
||||
variant="outline"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -34,22 +34,23 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
const [userEmail, setUserEmail] = useState('');
|
||||
|
||||
const [panelType, setPanelType] = useState('');
|
||||
const [panelServicesId, setPanelServicesId] = useState('');
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
|
||||
// const BILLING_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
||||
|
||||
const [selectedTenure, setSelectedTenure] = useState('');
|
||||
const [selectedcycle, setSelectedcycle] = useState('');
|
||||
const [selectedPrice, setSelectedPrice] = useState(0);
|
||||
|
||||
const handleCheckboxChange = (tenure, price) => {
|
||||
if (selectedTenure === tenure) {
|
||||
setSelectedTenure('');
|
||||
const handleCheckboxChange = (cycle, price) => {
|
||||
if (selectedcycle === cycle) {
|
||||
setSelectedcycle('');
|
||||
setSelectedPrice(0);
|
||||
} else {
|
||||
setSelectedTenure(tenure);
|
||||
setSelectedcycle(cycle);
|
||||
setSelectedPrice(price);
|
||||
}
|
||||
// console.log(selectedTenure, ' ', selectedPrice);
|
||||
// console.log(selectedcycle, ' ', selectedPrice);
|
||||
};
|
||||
const handlePanelBuyNow = () => {
|
||||
// Disable button during processing
|
||||
@@ -59,9 +60,9 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
showToast('Loading...');
|
||||
const formData = new FormData();
|
||||
formData.append('service', panelType);
|
||||
formData.append('tenure', selectedTenure);
|
||||
formData.append('serviceId', panelServicesId);
|
||||
formData.append('cycle', selectedcycle);
|
||||
formData.append('amount', 1); //selectedPrice
|
||||
|
||||
fetch(`${API_URL}?query=initiate_payment`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
@@ -551,13 +552,16 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
<select
|
||||
id="app-type"
|
||||
value={panelType}
|
||||
onChange={(e) => setPanelType(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setPanelType(e.target.value);
|
||||
setPanelServicesId(e.target.options[e.target.selectedIndex].dataset.id);
|
||||
}}
|
||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||
>
|
||||
<option value="">-Select-</option>
|
||||
<option value="Hestia-Panel">🧰 Hestia Panel</option>
|
||||
<option value="Webmin">🖥️ Webmin</option>
|
||||
<option value="cPanel">📊 cPanel</option>
|
||||
<option value="Hestia-Panel" data-id="4ksYhjFy">🧰 Hestia Panel</option>
|
||||
<option value="Webmin" data-id="5ksYAhFz">🖥️ Webmin</option>
|
||||
<option value="cPanel" data-id="6jgThjZx">📊 cPanel</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
@@ -575,21 +579,21 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
))}
|
||||
</ul>
|
||||
<div className='flex flex-row justify-between items-center gap-x-6'>
|
||||
<label className={`border ${selectedTenure === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
||||
<input type="checkbox" checked={selectedTenure === 'monthly'} onChange={() => handleCheckboxChange('monthly', 200)} className="hidden" />
|
||||
<label className={`border ${selectedcycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
||||
<input type="checkbox" checked={selectedcycle === 'monthly'} onChange={() => handleCheckboxChange('monthly', 200)} className="hidden" />
|
||||
<p className='text-3xl font-bold text-center'>₹200</p>
|
||||
<span>Monthly</span>
|
||||
</label>
|
||||
|
||||
<label className={`border ${selectedTenure === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
||||
<input type="checkbox" checked={selectedTenure === 'yearly'} onChange={() => handleCheckboxChange('yearly', 2000)} className="hidden" />
|
||||
<label className={`border ${selectedcycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
||||
<input type="checkbox" checked={selectedcycle === 'yearly'} onChange={() => handleCheckboxChange('yearly', 2000)} className="hidden" />
|
||||
<p className='text-3xl font-bold text-center'>₹2000</p>
|
||||
<span>Yearly</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className='text-white'>
|
||||
{selectedTenure && (
|
||||
<p className={`${selectedTenure === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedTenure}</strong> plan at ₹{selectedPrice}</p>
|
||||
{selectedcycle && (
|
||||
<p className={`${selectedcycle === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedcycle}</strong> plan at ₹{selectedPrice}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -186,12 +186,7 @@ export default function HireHumanDeveloper() {
|
||||
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Specialized Skills:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedType.skills.map((skill, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
<span key={index} className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded">{skill}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import React, {useState, useEffect} from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import QRCode from "react-qr-code";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle} from "./ui/dialog";
|
||||
import { Toast } from './Toast';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
||||
import { FileX, Copy, CheckCircle2, AlertCircle, X } from "lucide-react";
|
||||
import Loader from "./ui/loader";
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
export default function MakePayment(){
|
||||
const [initialOrderData, setInitialOrderData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [showQRModal, setShowQRModal] = useState(false);
|
||||
const [showHelpMessage, setShowHelpMessage] = useState(false);
|
||||
const [transactionId, setTransactionId] = useState('');
|
||||
const [orderIdInput, setOrderIdInput] = useState('');
|
||||
const [upiPaymentLink, setUpiPaymentLink] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [getOrderId, setGetOrderId] = useState();
|
||||
const [saveUpiResponse, setSaveUpiResponse] = useState({ message: '', visible: false, isSuccess: false });
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -22,7 +30,7 @@ export default function MakePayment(){
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setGetOrderId(orderId);
|
||||
const getInitialOrderData = () => {
|
||||
setIsLoading(true);
|
||||
const formData = new FormData();
|
||||
@@ -67,7 +75,7 @@ export default function MakePayment(){
|
||||
|
||||
function generateUPILink(amount, transactionId) {
|
||||
// Replace these with your actual merchant details
|
||||
const merchantUPI = "siliconpin@ybl";
|
||||
const merchantUPI = "7001601485@okbizaxis";
|
||||
const merchantName = "SiliconPin";
|
||||
const currency = "INR";
|
||||
|
||||
@@ -106,6 +114,17 @@ export default function MakePayment(){
|
||||
form.submit();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (showQRModal) {
|
||||
setShowHelpMessage(false);
|
||||
timer = setTimeout(() => {
|
||||
setShowHelpMessage(true);
|
||||
}, 2000);
|
||||
}
|
||||
return () => clearTimeout(timer); // Cleanup on unmount or when modal closes
|
||||
}, [showQRModal]);
|
||||
|
||||
function handleQRPaymentClick() {
|
||||
if (!upiPaymentLink) {
|
||||
alert('Payment information is not ready. Please wait.');
|
||||
@@ -114,10 +133,71 @@ export default function MakePayment(){
|
||||
setShowQRModal(true);
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(getOrderId)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000); // Reset after 1 second
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy text: ', err);
|
||||
});
|
||||
};
|
||||
const handlePayPalPayment = () => {
|
||||
const rawInrAmount = initialOrderData.payment_data?.amount || 0;
|
||||
const exchangeRate = 80;
|
||||
const convertedUSD = rawInrAmount / exchangeRate;
|
||||
const usdAmount = Math.max(convertedUSD, 1).toFixed(2);
|
||||
const paypalUrl = `https://www.paypal.com/paypalme/dwdconsultancy/${usdAmount}`;
|
||||
window.open(paypalUrl, '_blank');
|
||||
};
|
||||
|
||||
const handleSaveUPIPayment = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}?query=save-upi-payment`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
orderId: orderIdInput,
|
||||
transactionId: transactionId
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success === true) {
|
||||
setSaveUpiResponse({
|
||||
message: `Transaction ID ${transactionId} saved. We'll contact you shortly.`,
|
||||
visible: true,
|
||||
isSuccess: true
|
||||
});
|
||||
setShowHelpMessage(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setSaveUpiResponse({
|
||||
message: 'Failed to save payment details. Please try again.',
|
||||
visible: true,
|
||||
isSuccess: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="flex items-center justify-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>;
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if(!initialOrderData){
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center flex-grow py-10 text-center px-4 text-gray-600 mt-20">
|
||||
<FileX className="mb-4 text-gray-500" size={100} />
|
||||
<p className="text-lg"> No bill was found. <a className="text-[#6d9e37] font-medium hover:underline" href="/profile"> Click here </a> to go to your profile.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -148,15 +228,45 @@ export default function MakePayment(){
|
||||
|
||||
<div className="flex flex-col">
|
||||
<span className=""><strong>Pay Using:</strong></span>
|
||||
<Button variant="outline" className="" onClick={handleQRPaymentClick} >UPI QR</Button>
|
||||
<span className="text-gray-500 text-center font-semibold my-2">OR</span>
|
||||
<Button className="" onClick={redirectToPayU} >Payment Gateway</Button>
|
||||
<span className="text-center mt-2 text-gray-400 text-xs">Applicable 2% Transaction Charge if using Payment Gateway</span>
|
||||
<hr className="border-b border-b-[#6d9e37] mb-4" />
|
||||
|
||||
<Button className="" onClick={handleQRPaymentClick}>UPI QR Code</Button>
|
||||
<span className="text-center mt-2 text-gray-400 text-xs">Banking Service by DWD Consultancy Services (0% Processing Fee)</span>
|
||||
|
||||
<div className="flex items-center my-2">
|
||||
<div className="flex-grow border-t border-gray-500"></div>
|
||||
<span className="mx-2 text-gray-500 font-semibold">OR</span>
|
||||
<div className="flex-grow border-t border-gray-500"></div>
|
||||
</div>
|
||||
|
||||
<Button className="text-[#414042] font-bold border-[2px] border-[#00ad7d] hover:bg-[#00ad7d]/80" onClick={redirectToPayU} variant="outline">
|
||||
Pay with
|
||||
<img className="w-[50px]" src="/assets/payu-logo.svg" alt="" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4 text-[#414042] font-bold border-[2px] border-b-[#003087] border-r-[#003087] border-t-[#009CDE] border-l-[#009CDE] hover:bg-gradient-to-r hover:from-[#009CDE] hover:to-[#003087]"
|
||||
onClick={handlePayPalPayment}
|
||||
>
|
||||
Pay with
|
||||
<img className="w-[50px]" src="/assets/paypal-logo-white.svg" alt="PayPal" />
|
||||
</Button>
|
||||
{/* <span className="text-center mt-2 text-gray-400 text-xs">2% Payment Gateway Charge</span> */}
|
||||
|
||||
{/* Add this divider and PayPal button */}
|
||||
{/* <div className="flex items-center my-2">
|
||||
<div className="flex-grow border-t border-gray-500"></div>
|
||||
<span className="mx-2 text-gray-500 font-semibold">OR</span>
|
||||
<div className="flex-grow border-t border-gray-500"></div>
|
||||
</div> */}
|
||||
|
||||
|
||||
{/* <span className="text-center mt-2 text-gray-400 text-xs">3.5% Payment Processing Fee</span> */}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* QR Code Modal */}
|
||||
<Dialog open={showQRModal} onOpenChange={setShowQRModal}>
|
||||
<Dialog open={showQRModal} onOpenChange={(open) => {setShowQRModal(open); if (!open) {setShowHelpMessage(false);}}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Scan QR Code to Pay</DialogTitle>
|
||||
@@ -168,13 +278,45 @@ export default function MakePayment(){
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
Scan this QR code with any UPI app to complete your payment
|
||||
</p>
|
||||
<div className="w-full p-3 bg-gray-100 text-gray-500 rounded-md break-all text-xs">
|
||||
{upiPaymentLink}
|
||||
<Button variant="outline" onClick={handleCopy} className="text-sm" >{copied ? 'Copied!' : getOrderId} {!copied && <Copy size={15} />}</Button>
|
||||
|
||||
{showHelpMessage && (
|
||||
<div className="w-full mt-4 p-4 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||
<p className="text-sm text-yellow-800 mb-3">
|
||||
If you haven't received payment confirmation, please share your Order Id & transaction ID with us.
|
||||
</p>
|
||||
<Input type="text" value={orderIdInput} onChange={(e) => setOrderIdInput(e.target.value)} placeholder="Enter your Order ID" className="w-full p-2 border border-gray-300 rounded-md text-sm mb-2" />
|
||||
<Input type="text" value={transactionId} onChange={(e) => setTransactionId(e.target.value)} placeholder="Enter your transaction ID" className="w-full p-2 border border-gray-300 rounded-md text-sm mb-2" />
|
||||
<Button onClick={handleSaveUPIPayment} className="w-full text-sm">Save Transaction ID</Button>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => navigator.clipboard.writeText(upiPaymentLink)} className="text-sm" >Copy UPI Link</Button>
|
||||
)}
|
||||
{saveUpiResponse.visible && (
|
||||
<div className={`fixed bottom-0 z-50 flex items-center justify-center`}>
|
||||
<div className={`relative p-6 rounded-lg shadow-lg max-w-xs md:max-w-md w-full ${saveUpiResponse.isSuccess ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800'} animate-fade-in-up`}>
|
||||
<button
|
||||
onClick={() => setSaveUpiResponse(prev => ({...prev, visible: false}))}
|
||||
className={`absolute top-3 right-3 p-1 rounded-full ${saveUpiResponse.isSuccess ? 'hover:bg-green-100 text-green-500' : 'hover:bg-red-100 text-red-500'} transition-colors`}>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col items-center text-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{saveUpiResponse.isSuccess ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-6 h-6 text-red-500" />
|
||||
)}
|
||||
<p className="text-base font-medium">{saveUpiResponse.message}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => window.location.href = '/'} className={saveUpiResponse.isSuccess ? 'border-green-300' : 'border-red-300'}>Back to Home</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Transaction ID ${transactionId} saved. We'll contact you shortly.
|
||||
@@ -1,34 +1,63 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||
import Loader from "../../components/ui/loader";
|
||||
import { PDFDownloadLink, PDFViewer } from '@react-pdf/renderer';
|
||||
import InvoicePDF from "../../lib/InvoicePDF"; // We'll create this component next
|
||||
import { PDFDownloadLink } from '@react-pdf/renderer';
|
||||
import InvoicePDF from "../../lib/InvoicePDF";
|
||||
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search, ArrowLeft, FileText } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
export default function AllSellingList() {
|
||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||
const [billingData, setBillingData] = useState([]);
|
||||
const [usersData, setUsersData] = useState([]);
|
||||
const [dataLoading, setDataLoading] = useState(true);
|
||||
const [apiError, setApiError] = useState(null);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
service: '',
|
||||
tenure: 'monthly',
|
||||
serviceId: 'service-1',
|
||||
cycle: 'monthly',
|
||||
amount: '',
|
||||
user: '',
|
||||
status: 'pending'
|
||||
siliconId: '',
|
||||
name: '',
|
||||
status: 'pending',
|
||||
remarks: ''
|
||||
});
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
||||
|
||||
// Sorting state
|
||||
const [sortConfig, setSortConfig] = useState({ key: 'created_at', direction: 'desc' });
|
||||
|
||||
// Filtering state
|
||||
const [filters, setFilters] = useState({ status: '', cycle: '', service: '' });
|
||||
|
||||
// Search state
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage] = useState(10);
|
||||
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && sessionData?.user_type === 'admin') {
|
||||
fetchBillingData();
|
||||
fetchUsersData();
|
||||
}
|
||||
}, [isLoggedIn, sessionData]);
|
||||
|
||||
const fetchBillingData = async () => {
|
||||
try {
|
||||
setDataLoading(true);
|
||||
const res = await fetch(`${INVOICE_API_URL}?query=all-selling-list`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
@@ -47,23 +76,55 @@ export default function AllSellingList() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUsersData = async () => {
|
||||
try {
|
||||
const res = await fetch(`${INVOICE_API_URL}?query=get-all-users`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
||||
const data = await res.json();
|
||||
setUsersData(data.data || []);
|
||||
} catch (err) {
|
||||
setApiError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter customers based on search term
|
||||
const filteredCustomers = useMemo(() => {
|
||||
return usersData.filter(user =>
|
||||
user.name.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
|
||||
user.siliconId.toLowerCase().includes(customerSearchTerm.toLowerCase())
|
||||
);
|
||||
}, [usersData, customerSearchTerm]);
|
||||
|
||||
const handleViewItem = (item) => {
|
||||
setSelectedItem(item);
|
||||
setShowModal(true);
|
||||
setEditMode(false);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditItem = (item) => {
|
||||
setSelectedItem(item);
|
||||
setFormData({
|
||||
service: item.service,
|
||||
tenure: item.tenure,
|
||||
serviceId: item.serviceId,
|
||||
cycle: item.cycle,
|
||||
amount: item.amount,
|
||||
user: item.user,
|
||||
status: item.status
|
||||
siliconId: item.siliconId,
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
remarks: item.remarks,
|
||||
billing_date: item.billing_date
|
||||
});
|
||||
setEditMode(true);
|
||||
setShowModal(true);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (id) => {
|
||||
@@ -87,15 +148,12 @@ export default function AllSellingList() {
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setSelectedItem(null);
|
||||
setFormData({
|
||||
service: '',
|
||||
tenure: 'monthly',
|
||||
amount: '',
|
||||
user: '',
|
||||
status: 'pending'
|
||||
});
|
||||
setFormData({ service: '', serviceId: 'service-2', cycle: 'monthly', amount: '', user: '', siliconId: '', name: '', status: 'pending', remarks: '', billing_date: new Date().toISOString().split('T')[0] });
|
||||
setEditMode(true);
|
||||
setShowModal(true);
|
||||
setCurrentStep(1);
|
||||
setSelectedCustomer(null);
|
||||
setCustomerSearchTerm('');
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
@@ -118,7 +176,8 @@ export default function AllSellingList() {
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
||||
fetchBillingData();
|
||||
setShowModal(false);
|
||||
setDialogOpen(false);
|
||||
setCurrentStep(1);
|
||||
} catch (err) {
|
||||
setApiError(err.message);
|
||||
}
|
||||
@@ -132,10 +191,157 @@ export default function AllSellingList() {
|
||||
}));
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
setSelectedItem(null);
|
||||
setEditMode(false);
|
||||
const handleCustomerSelect = (customer) => {
|
||||
setSelectedCustomer(customer);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
user: customer.email,
|
||||
siliconId: customer.siliconId,
|
||||
name: customer.name
|
||||
}));
|
||||
setCurrentStep(2);
|
||||
};
|
||||
|
||||
const handleBackToCustomerSelect = () => {
|
||||
setCurrentStep(1);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
// Export to Excel function
|
||||
const exportToExcel = () => {
|
||||
const worksheet = XLSX.utils.json_to_sheet(filteredAndSortedData.map(item => ({
|
||||
'Billing ID': item.billing_id,
|
||||
'Service': item.service,
|
||||
'Customer Name': item.name,
|
||||
'Customer Email': item.user,
|
||||
'Silicon ID': item.siliconId,
|
||||
'Amount': item.amount,
|
||||
'cycle': item.cycle,
|
||||
'Status': item.status,
|
||||
'Created At': formatDate(item.created_at),
|
||||
'Remarks': item.remarks
|
||||
})));
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "Billing Data");
|
||||
XLSX.writeFile(workbook, `billing_data_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
||||
};
|
||||
|
||||
// Sorting functionality
|
||||
const requestSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Filter and sort data
|
||||
const filteredAndSortedData = useMemo(() => {
|
||||
let filteredData = [...billingData];
|
||||
|
||||
if (searchTerm) {
|
||||
filteredData = filteredData.filter(item =>
|
||||
Object.values(item).some(
|
||||
val => val && val.toString().toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
filteredData = filteredData.filter(item => item.status === filters.status);
|
||||
}
|
||||
if (filters.cycle) {
|
||||
filteredData = filteredData.filter(item => item.cycle === filters.cycle);
|
||||
}
|
||||
if (filters.service) {
|
||||
filteredData = filteredData.filter(item => item.service === filters.service);
|
||||
}
|
||||
|
||||
if (sortConfig.key) {
|
||||
filteredData.sort((a, b) => {
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return filteredData;
|
||||
}, [billingData, searchTerm, filters, sortConfig]);
|
||||
|
||||
const currentItems = useMemo(() => {
|
||||
const indexOfLastItem = currentPage * itemsPerPage;
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
return filteredAndSortedData.slice(indexOfFirstItem, indexOfLastItem);
|
||||
}, [currentPage, itemsPerPage, filteredAndSortedData]);
|
||||
|
||||
const totalPages = Math.ceil(filteredAndSortedData.length / itemsPerPage);
|
||||
|
||||
const paginate = (pageNumber) => {
|
||||
if (pageNumber > 0 && pageNumber <= totalPages) {
|
||||
setCurrentPage(pageNumber);
|
||||
}
|
||||
};
|
||||
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const maxVisiblePages = 5;
|
||||
range.push(1);
|
||||
if (currentPage > 3) {
|
||||
range.push('...');
|
||||
}
|
||||
let start = Math.max(2, currentPage - 1);
|
||||
let end = Math.min(totalPages - 1, currentPage + 1);
|
||||
if (currentPage <= 3) {
|
||||
end = Math.min(4, totalPages - 1);
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
start = Math.max(totalPages - 3, 2);
|
||||
}
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i > 1 && i < totalPages) {
|
||||
range.push(i);
|
||||
}
|
||||
}
|
||||
if (currentPage < totalPages - 2) {
|
||||
range.push('...');
|
||||
}
|
||||
if (totalPages > 1) {
|
||||
range.push(totalPages);
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
const uniqueServices = [...new Set(billingData.map(item => item.service))];
|
||||
const uniqueStatuses = ['completed', 'pending', 'failed'];
|
||||
const uniquecycles = ['monthly', 'yearly', 'one-time'];
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters({
|
||||
status: '',
|
||||
cycle: '',
|
||||
service: ''
|
||||
});
|
||||
setSearchTerm('');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleFilterChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
@@ -171,30 +377,193 @@ export default function AllSellingList() {
|
||||
<section className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Billing Management</h1>
|
||||
<button
|
||||
onClick={handleCreateNew}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleCreateNew} variant="outline">
|
||||
Create New
|
||||
</button>
|
||||
</Button>
|
||||
<Button onClick={exportToExcel} variant="outline" className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" /> Export to Excel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
{/* Search and Filter Section */}
|
||||
<div className="mb-2 text-sm text-gray-600">
|
||||
Showing {currentItems.length} of {filteredAndSortedData.length} results
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-t-lg shadow">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
className="bg-[#262626] pl-10 w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
name="status"
|
||||
value={filters.status}
|
||||
onChange={handleFilterChange}
|
||||
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
{uniqueStatuses.map(status => (
|
||||
<option key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
name="cycle"
|
||||
value={filters.cycle}
|
||||
onChange={handleFilterChange}
|
||||
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="">All cycles</option>
|
||||
{uniquecycles.map(cycle => (
|
||||
<option key={cycle} value={cycle}>
|
||||
{cycle.charAt(0).toUpperCase() + cycle.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
name="service"
|
||||
value={filters.service}
|
||||
onChange={handleFilterChange}
|
||||
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="">All Services</option>
|
||||
{uniqueServices.map(service => (
|
||||
<option key={service} value={service}>
|
||||
{service}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{(filters.status || filters.cycle || filters.service || searchTerm) && (
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
onClick={resetFilters}
|
||||
size="sm"
|
||||
>
|
||||
Reset Filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-white rounded-b-lg shadow overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Billing ID</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('billing_id')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Billing ID
|
||||
{sortConfig.key === 'billing_id' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('service')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Service
|
||||
{sortConfig.key === 'service' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{sortConfig.key === 'name' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('user')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
User
|
||||
{sortConfig.key === 'user' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('amount')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Amount
|
||||
{sortConfig.key === 'amount' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('status')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
{sortConfig.key === 'status' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => requestSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created At
|
||||
{sortConfig.key === 'created_at' && (
|
||||
sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="ml-1 h-4 w-4" /> :
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{billingData.map((item) => (
|
||||
{currentItems.length > 0 ? (
|
||||
currentItems.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{item.billing_id}
|
||||
@@ -202,6 +571,9 @@ export default function AllSellingList() {
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{item.service}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{item.name ?? item.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{item.user}
|
||||
</td>
|
||||
@@ -216,61 +588,196 @@ export default function AllSellingList() {
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(item.created_at)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<td className="inline-flex px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
title="View Items"
|
||||
onClick={() => handleViewItem(item)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-2"
|
||||
>
|
||||
View
|
||||
<Eye className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
title="Edit Items"
|
||||
onClick={() => handleEditItem(item)}
|
||||
className="text-yellow-600 hover:text-yellow-900 mr-2"
|
||||
>
|
||||
Edit
|
||||
<Pencil className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
title="Delete Items"
|
||||
onClick={() => handleDeleteItem(item.id)}
|
||||
className="text-red-600 hover:text-red-900 mr-2"
|
||||
>
|
||||
Delete
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
<PDFDownloadLink
|
||||
title="Download PDF"
|
||||
document={<InvoicePDF data={item} />}
|
||||
fileName={`invoice_${item.billing_id}.pdf`}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
className="text-[#6d9e37] hover:text-green-600"
|
||||
>
|
||||
{({ loading }) => (loading ? 'Preparing...' : 'PDF')}
|
||||
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
|
||||
</PDFDownloadLink>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
|
||||
No records found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal for View/Edit */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<h2 className="text-xl font-bold mb-4">
|
||||
{editMode ? (selectedItem ? 'Edit Billing' : 'Create New Billing') : 'Billing Details'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
{/* Pagination */}
|
||||
{filteredAndSortedData.length > itemsPerPage && (
|
||||
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() => paginate(1)}
|
||||
disabled={currentPage === 1}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
First
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
{getPaginationRange().map((item, index) => {
|
||||
if (item === '...') {
|
||||
return (
|
||||
<span key={`ellipsis-${index}`} className="px-2 py-1">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={`page-${item}`}
|
||||
onClick={() => paginate(item)}
|
||||
variant={currentPage === item ? "default" : "outline"}
|
||||
size="sm"
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
onClick={() => paginate(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => paginate(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Last
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dialog for View/Edit/Create */}
|
||||
<Dialog open={dialogOpen} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCurrentStep(1);
|
||||
setSelectedCustomer(null);
|
||||
}
|
||||
setDialogOpen(open);
|
||||
}}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editMode ? (selectedItem ? 'Edit Billing' : 'Create New Billing') : 'Billing Details'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{editMode && !selectedItem && currentStep === 1 ? (
|
||||
<div>
|
||||
<div className="relative mb-4">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search customers..."
|
||||
value={customerSearchTerm}
|
||||
onChange={(e) => setCustomerSearchTerm(e.target.value)}
|
||||
className="pl-10 w-full px-3 py-2 border border-gray-300 rounded-md outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editMode ? (
|
||||
<h3 className="font-semibold mb-4">Select Customer</h3>
|
||||
{filteredCustomers.length > 0 ? (
|
||||
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
||||
{filteredCustomers.map(user => (
|
||||
<div
|
||||
key={user.id}
|
||||
onClick={() => handleCustomerSelect(user)}
|
||||
className="p-3 border rounded-md cursor-pointer hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="font-medium">{user.name}</p>
|
||||
<p className="text-sm text-gray-600">{user.email}</p>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="bg-gray-100 px-2 py-1 rounded">ID: {user.siliconId}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-gray-500 py-4">No customers found</p>
|
||||
)}
|
||||
</div>
|
||||
) : editMode ? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{!selectedItem && selectedCustomer && (
|
||||
<div className="mb-6 p-4 bg-gray-50 rounded-md">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-semibold">Customer Information</h3>
|
||||
<p className="text-sm"><span className="font-medium">Name:</span> {selectedCustomer.name}</p>
|
||||
<p className="text-sm"><span className="font-medium">Email:</span> {selectedCustomer.email}</p>
|
||||
<p className="text-sm"><span className="font-medium">Silicon ID:</span> {selectedCustomer.siliconId}</p>
|
||||
</div>
|
||||
{!selectedItem && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleBackToCustomerSelect}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-500"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-1" /> Change Customer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Service</label>
|
||||
@@ -279,17 +786,17 @@ export default function AllSellingList() {
|
||||
name="service"
|
||||
value={formData.service}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Tenure</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">cycle</label>
|
||||
<select
|
||||
name="tenure"
|
||||
value={formData.tenure}
|
||||
name="cycle"
|
||||
value={formData.cycle}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</option>
|
||||
@@ -303,120 +810,140 @@ export default function AllSellingList() {
|
||||
name="amount"
|
||||
value={formData.amount}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
step="0.01"
|
||||
min="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">User Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="user"
|
||||
value={formData.user}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<select
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="">
|
||||
<label htmlFor="billing_date" className="block text-sm font-medium text-gray-700 mb-1">Billing Date</label>
|
||||
<input
|
||||
type="date"
|
||||
name="billing_date"
|
||||
value={formData.billing_date}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">Remarks</label>
|
||||
<input
|
||||
type="text"
|
||||
name="remarks"
|
||||
value={formData.remarks}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-4">
|
||||
<button
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
{selectedItem ? 'Update' : 'Create'}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
|
||||
onClick={() => {
|
||||
setDialogOpen(false);
|
||||
setCurrentStep(1);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">Billing ID</h3>
|
||||
<p>{selectedItem.billing_id}</p>
|
||||
<p>{selectedItem?.billing_id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">Service</h3>
|
||||
<p>{selectedItem.service}</p>
|
||||
<p>{selectedItem?.service}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">User</h3>
|
||||
<p>{selectedItem.user}</p>
|
||||
<p>{selectedItem?.user}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">Tenure</h3>
|
||||
<p>{selectedItem.tenure}</p>
|
||||
<h3 className="font-semibold text-gray-700">Silicon ID</h3>
|
||||
<p>{selectedItem?.siliconId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">cycle</h3>
|
||||
<p>{selectedItem?.cycle}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">Amount</h3>
|
||||
<p>{formatCurrency(selectedItem.amount)}</p>
|
||||
<p>{formatCurrency(selectedItem?.amount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">Status</h3>
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(selectedItem.status)}`}>
|
||||
{selectedItem.status.charAt(0).toUpperCase() + selectedItem.status.slice(1)}
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(selectedItem?.status)}`}>
|
||||
{selectedItem?.status.charAt(0).toUpperCase() + selectedItem?.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">Created At</h3>
|
||||
<p>{formatDate(selectedItem.created_at)}</p>
|
||||
<p>{formatDate(selectedItem?.created_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-700">Updated At</h3>
|
||||
<p>{formatDate(selectedItem.updated_at)}</p>
|
||||
<p>{formatDate(selectedItem?.updated_at)}</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<h3 className="font-semibold text-gray-700">Remarks</h3>
|
||||
<p>{selectedItem?.remarks}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-4">
|
||||
<DialogFooter>
|
||||
<PDFDownloadLink
|
||||
document={<InvoicePDF data={selectedItem} />}
|
||||
fileName={`invoice_${selectedItem.billing_id}.pdf`}
|
||||
fileName={`invoice_${selectedItem?.billing_id}.pdf`}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||||
>
|
||||
{({ loading }) => (loading ? 'Preparing PDF...' : 'Download PDF')}
|
||||
</PDFDownloadLink>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleEditItem(selectedItem)}
|
||||
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setDialogOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { X } from "lucide-react"; // Import an icon for the remove button
|
||||
|
||||
export function AvatarUpload() {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setSelectedFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
const handleRemoveFile = () => {
|
||||
setSelectedFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''; // Reset file input
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!selectedFile ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label
|
||||
htmlFor="avatar"
|
||||
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colorsfocus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
Change Avatar
|
||||
</Label>
|
||||
<Input type="file" id="avatar" ref={fileInputRef} accept="image/jpeg,image/png,image/gif" className="hidden" onChange={handleFileChange}/>
|
||||
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>Browse</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
JPG, GIF or PNG. 1MB max.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-3 space-x-2">
|
||||
<div className="truncate max-w-[200px]">
|
||||
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
|
||||
</div>
|
||||
<Button size="sm" className="text-xs p-1 h-fit">Update</Button>
|
||||
<Button size="sm" onClick={handleRemoveFile} className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit">Remove</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default AvatarUpload;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||
import { Button } from "./ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
@@ -7,9 +8,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from ".
|
||||
import { Separator } from "./ui/separator";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import UpdateAvatar from './UpdateAvatar';
|
||||
import {AvatarUpload} from './AvatarUpload';
|
||||
import {localizeTime} from "../lib/localizeTime";
|
||||
import Loader from "./ui/loader";
|
||||
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search } from "lucide-react";
|
||||
import { PDFDownloadLink } from '@react-pdf/renderer';
|
||||
import InvoicePDF from "../lib/InvoicePDF";
|
||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||
|
||||
interface SessionData {
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -19,12 +25,35 @@ interface UserData {
|
||||
session_data: SessionData;
|
||||
user_avatar: string;
|
||||
}
|
||||
interface BillingItems {
|
||||
[key: string]: any;
|
||||
}
|
||||
type ToastState = {
|
||||
visible: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type Invoice = {
|
||||
billing_id: string;
|
||||
};
|
||||
|
||||
type PaymentData = {
|
||||
txn_id: string;
|
||||
user_email: string;
|
||||
};
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
|
||||
const typedSessionData = sessionData as SessionData | null;
|
||||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
const [invoiceList, setInvoiceList] = useState<any[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedData, setSelectedData] = useState<BillingItems | null>(null);
|
||||
const [toast, setToast] = useState<ToastState>({ visible: false, message: '' });
|
||||
const [txnId, setTxnId] = useState<string>('');
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
// console.log('isLoggedIn', sessionData.id)
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/invoice/';
|
||||
useEffect(() => {
|
||||
@@ -77,6 +106,27 @@ export default function ProfilePage() {
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const showToast = (message: string) => {
|
||||
setToast({ visible: true, message });
|
||||
setTimeout(() => setToast({ visible: false, message: '' }), 3000);
|
||||
};
|
||||
|
||||
const handlePanelBuyNow = (invoice: Invoice) => {
|
||||
// setTxnId(data.txn_id);
|
||||
// setUserEmail(data.user_email);
|
||||
window.location.href = `/make-payment?query=get-initiated_payment&orderId=${invoice.billing_id}`;
|
||||
showToast('Redirecting to payment page...');
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleViewItems = (items: BillingItems) => {
|
||||
setDialogOpen(true);
|
||||
setSelectedData(items);
|
||||
};
|
||||
|
||||
const sessionLogOut = () => {
|
||||
fetch(`${USER_API_URL}?query=logout`, {
|
||||
method: 'GET',
|
||||
@@ -97,6 +147,9 @@ export default function ProfilePage() {
|
||||
if (!userData) {
|
||||
return (<Loader />);
|
||||
}
|
||||
{
|
||||
// console.log('selectedData', selectedData)
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6 container mx-auto">
|
||||
<Separator />
|
||||
@@ -115,7 +168,7 @@ export default function ProfilePage() {
|
||||
<AvatarImage src={userData.session_data?.user_avatar} />
|
||||
<AvatarFallback>JP</AvatarFallback>
|
||||
</Avatar>
|
||||
<UpdateAvatar />
|
||||
{typedSessionData?.id && <AvatarUpload userId={typedSessionData.id} />}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
@@ -140,7 +193,7 @@ export default function ProfilePage() {
|
||||
<CardHeader>
|
||||
<div className="flex flex-row justify-between">
|
||||
<CardTitle>Billing Information</CardTitle>
|
||||
<a href="" className="hover:bg-[#6d9e37] hover:text-white transtion duration-500 py-1 px-2 rounded">View All</a>
|
||||
<a href="/profile/billing-info" className="hover:bg-[#6d9e37] hover:text-white transtion duration-500 py-1 px-2 rounded">View All</a>
|
||||
</div>
|
||||
|
||||
<CardDescription>
|
||||
@@ -149,25 +202,45 @@ export default function ProfilePage() {
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Invoice</th>
|
||||
<th className="text-left">Invoice Date</th>
|
||||
<th className="text-left">Order ID</th>
|
||||
<th className="text-center">Invoice Date</th>
|
||||
<th className="text-left">Description</th>
|
||||
<th className="text-right">Amount</th>
|
||||
<th className="text-right">Status</th>
|
||||
<th className="text-center">Amount</th>
|
||||
<th className="text-center">Status</th>
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
invoiceList.map((invoice, index) => (
|
||||
<tr key={index}>
|
||||
<td>{invoice.invoice_number}</td>
|
||||
<td>{invoice.invoice_date}</td>
|
||||
<td>{invoice.notes ? invoice.notes : ''}</td>
|
||||
<td className="text-right">{invoice.total_amount}</td>
|
||||
<td className="text-right">{invoice.status}</td>
|
||||
<td className="text-center">
|
||||
<a href="">View</a>
|
||||
invoiceList.slice(-5).map((invoice, index) => (
|
||||
<tr key={index} className="">
|
||||
<td>{invoice.billing_id}</td>
|
||||
<td className="text-center">{invoice?.created_at.split(' ')[0]}</td>
|
||||
<td className="line-clamp-1">{invoice.service}</td>
|
||||
<td className="text-center">{invoice.amount}</td>
|
||||
<td className={`text-center text-sm rounded-full h-fit ${invoice.status === 'pending' ? 'text-yellow-500' : invoice.status === 'completed' ? 'text-green-500' : 'text-red-500'}`}>{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}</td>
|
||||
<td className="text-center flex justify-center items-center gap-2 p-2">
|
||||
<button onClick={() => handleViewItems(invoice)}>
|
||||
<Eye />
|
||||
</button>
|
||||
<PDFDownloadLink
|
||||
title="Download PDF"
|
||||
document={<InvoicePDF data={invoice} />}
|
||||
fileName={`invoice_${invoice.billing_id}.pdf`}
|
||||
className="text-[#6d9e37] hover:text-green-600"
|
||||
>
|
||||
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
|
||||
</PDFDownloadLink>
|
||||
{
|
||||
invoice.status !== 'completed' ? (
|
||||
<Button onClick={() => {handlePanelBuyNow(invoice)}} variant="outline" size="sm">
|
||||
{
|
||||
invoice.status === 'pending' ? 'Pay' : invoice.status === 'failed' ? 'Retry' : ''
|
||||
}
|
||||
</Button>
|
||||
) : ''
|
||||
}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
@@ -205,9 +278,7 @@ export default function ProfilePage() {
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Danger Zone</CardTitle>
|
||||
<CardDescription>
|
||||
These actions are irreversible. Proceed with caution.
|
||||
</CardDescription>
|
||||
<CardDescription>These actions are irreversible. Proceed with caution.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-4">
|
||||
@@ -219,6 +290,56 @@ export default function ProfilePage() {
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="">Billing Details</DialogTitle>
|
||||
<DialogDescription className="">Review the details for Billing ID: <span className="font-bold">{selectedData?.billing_id}</span></DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 space-y-4 text-sm text-gray-700">
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">Service:</span>
|
||||
<span>{selectedData?.service}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">cycle:</span>
|
||||
<span className="capitalize">{selectedData?.cycle}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">Amount:</span>
|
||||
<span>${selectedData?.amount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">User:</span>
|
||||
<span>{selectedData?.user}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">Status:</span>
|
||||
<span className={`px-2 py-0.5 rounded text-white text-xs ${selectedData?.status === 'pending' ? 'bg-yellow-500' : selectedData?.status === 'completed' ? 'bg-green-500' : 'bg-red-500'}`}>
|
||||
{selectedData?.status}
|
||||
</span>
|
||||
</div>
|
||||
<hr className="my-2 border-gray-200" />
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">Created At:</span>
|
||||
<span>{localizeTime(selectedData?.created_at)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">Updated At:</span>
|
||||
<span>{selectedData?.updated_at ? localizeTime(selectedData.updated_at) : '—'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">Silicon ID:</span>
|
||||
<span>{selectedData?.siliconId}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-6">
|
||||
{/* Optional: Add action buttons here */}
|
||||
{/* <Button onClick={() => setDialogOpen(false)}>Close</Button> */}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -294,4 +294,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.3s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Page, Text, View, Document, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
import { Page, Text, View, Document, StyleSheet, Image, Font } from '@react-pdf/renderer';
|
||||
import {localizeTime} from "../lib/localizeTime";
|
||||
import { IndianRupee } from "lucide-react";
|
||||
import NotoSans from "../lib/fonts/static/NotoSans_Condensed-Bold.ttf";
|
||||
Font.register({
|
||||
family: "NotoSans",
|
||||
src: NotoSans,
|
||||
});
|
||||
// console.log('Fonts', Font)
|
||||
// Create styles
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 40
|
||||
padding: 40,
|
||||
},
|
||||
currencyValue: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
@@ -14,7 +26,18 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: '#6d9e37',
|
||||
paddingBottom: 10
|
||||
paddingBottom: 10,
|
||||
alignItems: 'center'
|
||||
},
|
||||
logoContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 5
|
||||
},
|
||||
logoImage: {
|
||||
width: 30,
|
||||
height: 30
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
@@ -54,15 +77,18 @@ const styles = StyleSheet.create({
|
||||
borderBottomColor: '#EEEEEE'
|
||||
},
|
||||
col1: {
|
||||
width: '40%'
|
||||
width: '40%',
|
||||
fontSize: '12px'
|
||||
},
|
||||
col2: {
|
||||
width: '30%',
|
||||
textAlign: 'right'
|
||||
textAlign: 'right',
|
||||
fontSize: '12px'
|
||||
},
|
||||
col3: {
|
||||
width: '30%',
|
||||
textAlign: 'right'
|
||||
textAlign: 'right',
|
||||
fontSize: '12px'
|
||||
},
|
||||
total: {
|
||||
flexDirection: 'row',
|
||||
@@ -90,6 +116,10 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Base64 encoded SVG logo (replace with your actual logo)
|
||||
const SILICONPIN_LOGO = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABgElEQVR4nO3bsW0UURRA0TfYLThEDreatUSGC7BdBi4D0QAiWiQ3g0OTQgvsONiICv4J7pF+PE//jmaiN/vMDpzHWWyfedhnzqvv4sPqi8j/CoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmE1Ylvn0eT7+PMzNyhmOv+bv6cf8vlo5xMxsi59/8WW+zjZPi6f4Ns/LZ+iTpSkIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYbWZ/WD3E3N/dzuFl6cLOvB7/zPfT28zqlZ3Zz8BZvsV1eTH3f6vv4nqULSrDNovvo38IpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmDeAd5vHuV+dwDGAAAAAElFTkSuQmCC';
|
||||
|
||||
const InvoicePDF = ({ data }) => {
|
||||
const statusColor = data.status === 'completed' ? '#10B981' : '#EF4444';
|
||||
|
||||
@@ -98,27 +128,29 @@ const InvoicePDF = ({ data }) => {
|
||||
<Page size="A4" style={styles.page}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.title}>INVOICE</Text>
|
||||
<Text style={styles.label}>Invoice #: {data.billing_id}</Text>
|
||||
<Text style={styles.label}>Date: {new Date(data.created_at).toLocaleDateString()}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.label}>Your Company</Text>
|
||||
<Text style={styles.value}>123 Business Street</Text>
|
||||
<Text style={styles.value}>City, Country</Text>
|
||||
<Text style={styles.value}>contact@yourcompany.com</Text>
|
||||
</View>
|
||||
<Text style={styles.label}>Date: {localizeTime(data.created_at)}</Text>
|
||||
</View>
|
||||
|
||||
{/* Bill To */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.row}>
|
||||
<View>
|
||||
<View style={{display: 'flex', flexDirection: 'row', gap: '10px'}}>
|
||||
<Text style={styles.label}>BILL TO:</Text>
|
||||
<Text style={styles.value}>{data.user}</Text>
|
||||
<Text style={styles.value}>{data.name}</Text>
|
||||
</View>
|
||||
<View>
|
||||
{
|
||||
data.status === 'completed' && (
|
||||
<View style={{display: 'flex', flexDirection: 'row', gap: '10px'}}>
|
||||
<Text style={styles.label}>PAID ON:</Text>
|
||||
<Text style={styles.value}>
|
||||
{data.payment_date}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<View style={{display: 'flex', flexDirection: 'row', gap: '10px'}}>
|
||||
<Text style={styles.label}>STATUS:</Text>
|
||||
<Text style={[styles.value, { color: statusColor }]}>
|
||||
{data.status.toUpperCase()}
|
||||
@@ -133,16 +165,16 @@ const InvoicePDF = ({ data }) => {
|
||||
<View style={styles.section}>
|
||||
<View style={styles.tableHeader}>
|
||||
<Text style={styles.col1}>DESCRIPTION</Text>
|
||||
<Text style={styles.col2}>TENURE</Text>
|
||||
<Text style={styles.col2}>CYCLE</Text>
|
||||
<Text style={styles.col3}>AMOUNT</Text>
|
||||
</View>
|
||||
<View style={styles.tableRow}>
|
||||
<Text style={styles.col1}>{data.service}</Text>
|
||||
<Text style={styles.col2}>{data.tenure}</Text>
|
||||
<Text style={styles.col3}>
|
||||
{new Intl.NumberFormat('en-US', {
|
||||
<Text style={styles.col2}>{data.cycle}</Text>
|
||||
<Text style={{fontFamily: 'NotoSans', ...styles.col3}}>
|
||||
{new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
currency: 'INR',
|
||||
}).format(parseFloat(data.amount))}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -155,30 +187,43 @@ const InvoicePDF = ({ data }) => {
|
||||
<View style={{width: '30%'}}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>SUBTOTAL:</Text>
|
||||
<Text style={styles.value}>
|
||||
{new Intl.NumberFormat('en-US', {
|
||||
<Text style={{fontFamily: 'NotoSans', ...styles.value}}>
|
||||
{new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
currency: 'INR',
|
||||
}).format(parseFloat(data.amount))}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>TOTAL:</Text>
|
||||
<Text style={styles.totalText}>
|
||||
{new Intl.NumberFormat('en-US', {
|
||||
<Text style={{fontFamily: 'NotoSans', ...styles.totalText}}>
|
||||
{new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
currency: 'INR',
|
||||
}).format(parseFloat(data.amount))}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.header} />
|
||||
<View style={{}}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image
|
||||
style={styles.logoImage}
|
||||
src={SILICONPIN_LOGO}
|
||||
/>
|
||||
<Text style={{fontSize: '12px', fontWeight: 'bold', color: '#6d9e37'}}>SiliconPin, Habra, W.B. 743271, India, contact@siliconpin.com</Text>
|
||||
</View>
|
||||
{/* <Text style={styles.value}></Text>
|
||||
<Text style={styles.value}></Text>
|
||||
<Text style={styles.value}></Text> */}
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
{/* <View style={styles.footer}>
|
||||
<Text>Thank you for your business!</Text>
|
||||
<Text>Please make payments payable to Your Company</Text>
|
||||
</View>
|
||||
<Text>Please make payments payable to SiliconPin</Text>
|
||||
</View> */}
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
||||
219
src/lib/InvoicePDF_V1.jsx
Normal file
219
src/lib/InvoicePDF_V1.jsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import React from 'react';
|
||||
import { Page, Text, View, Document, StyleSheet, Image, Font } from '@react-pdf/renderer';
|
||||
import {localizeTime} from "../lib/localizeTime";
|
||||
import { IndianRupee } from "lucide-react";
|
||||
import NotoSans from "../lib/fonts/static/NotoSans_Condensed-Bold.ttf";
|
||||
Font.register({
|
||||
family: "NotoSans",
|
||||
src: NotoSans,
|
||||
});
|
||||
// console.log('Fonts', Font)
|
||||
// Create styles
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 40,
|
||||
},
|
||||
currencyValue: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: '#6d9e37',
|
||||
paddingBottom: 10
|
||||
},
|
||||
logoContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 5
|
||||
},
|
||||
logoImage: {
|
||||
width: 30,
|
||||
height: 30
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#6d9e37'
|
||||
},
|
||||
section: {
|
||||
marginBottom: 20
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 5
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
value: {
|
||||
fontSize: 12
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#EEEEEE',
|
||||
marginVertical: 10
|
||||
},
|
||||
tableHeader: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#F5F5F5',
|
||||
padding: 5,
|
||||
marginBottom: 5
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
padding: 5,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#EEEEEE'
|
||||
},
|
||||
col1: {
|
||||
width: '40%'
|
||||
},
|
||||
col2: {
|
||||
width: '30%',
|
||||
textAlign: 'right'
|
||||
},
|
||||
col3: {
|
||||
width: '30%',
|
||||
textAlign: 'right'
|
||||
},
|
||||
total: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 10
|
||||
},
|
||||
totalText: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 30,
|
||||
left: 40,
|
||||
right: 40,
|
||||
textAlign: 'center',
|
||||
fontSize: 10,
|
||||
color: '#999999'
|
||||
},
|
||||
status: {
|
||||
padding: 3,
|
||||
borderRadius: 3,
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Base64 encoded SVG logo (replace with your actual logo)
|
||||
const SILICONPIN_LOGO = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABgElEQVR4nO3bsW0UURRA0TfYLThEDreatUSGC7BdBi4D0QAiWiQ3g0OTQgvsONiICv4J7pF+PE//jmaiN/vMDpzHWWyfedhnzqvv4sPqi8j/CoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmIJgCoIpCKYgmE1Ylvn0eT7+PMzNyhmOv+bv6cf8vlo5xMxsi59/8WW+zjZPi6f4Ns/LZ+iTpSkIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYbWZ/WD3E3N/dzuFl6cLOvB7/zPfT28zqlZ3Zz8BZvsV1eTH3f6vv4nqULSrDNovvo38IpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmAKgikIpiCYgmDeAd5vHuV+dwDGAAAAAElFTkSuQmCC';
|
||||
|
||||
const InvoicePDF = ({ data }) => {
|
||||
const statusColor = data.status === 'completed' ? '#10B981' : '#EF4444';
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.title}>INVOICE</Text>
|
||||
<Text style={styles.label}>Invoice #: {data.billing_id}</Text>
|
||||
<Text style={styles.label}>Date: {localizeTime(data.created_at)}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image
|
||||
style={styles.logoImage}
|
||||
src={SILICONPIN_LOGO}
|
||||
/>
|
||||
<Text style={styles.title}>SiliconPin</Text>
|
||||
</View>
|
||||
<Text style={styles.value}>121 Lalbari, GourBongo Road</Text>
|
||||
<Text style={styles.value}>Habra, W.B. 743271, India</Text>
|
||||
<Text style={styles.value}>contact@siliconpin.com</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Bill To */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.row}>
|
||||
<View>
|
||||
<Text style={styles.label}>BILL TO:</Text>
|
||||
<Text style={styles.value}>{data.name}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.label}>STATUS:</Text>
|
||||
<Text style={[styles.value, { color: statusColor }]}>
|
||||
{data.status.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* Items Table */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.tableHeader}>
|
||||
<Text style={styles.col1}>DESCRIPTION</Text>
|
||||
<Text style={styles.col2}>cycle</Text>
|
||||
<Text style={styles.col3}>AMOUNT</Text>
|
||||
</View>
|
||||
<View style={styles.tableRow}>
|
||||
<Text style={styles.col1}>{data.service}</Text>
|
||||
<Text style={styles.col2}>{data.cycle}</Text>
|
||||
<Text style={{fontFamily: 'NotoSans', ...styles.col3}}>
|
||||
{new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
}).format(parseFloat(data.amount))}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* Total */}
|
||||
<View style={styles.total}>
|
||||
<View style={{width: '30%'}}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>SUBTOTAL:</Text>
|
||||
<Text style={{fontFamily: 'NotoSans', ...styles.value}}>
|
||||
{new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
}).format(parseFloat(data.amount))}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>TOTAL:</Text>
|
||||
<Text style={{fontFamily: 'NotoSans', ...styles.totalText}}>
|
||||
{new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
}).format(parseFloat(data.amount))}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text>Thank you for your business!</Text>
|
||||
<Text>Please make payments payable to SiliconPin</Text>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvoicePDF;
|
||||
BIN
src/lib/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf
Normal file
BIN
src/lib/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/NotoSans-VariableFont_wdth,wght.ttf
Normal file
BIN
src/lib/fonts/NotoSans-VariableFont_wdth,wght.ttf
Normal file
Binary file not shown.
93
src/lib/fonts/OFL.txt
Normal file
93
src/lib/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
136
src/lib/fonts/README.txt
Normal file
136
src/lib/fonts/README.txt
Normal file
@@ -0,0 +1,136 @@
|
||||
Noto Sans Variable Font
|
||||
=======================
|
||||
|
||||
This download contains Noto Sans as both variable fonts and static fonts.
|
||||
|
||||
Noto Sans is a variable font with these axes:
|
||||
wdth
|
||||
wght
|
||||
|
||||
This means all the styles are contained in these files:
|
||||
NotoSans-VariableFont_wdth,wght.ttf
|
||||
NotoSans-Italic-VariableFont_wdth,wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for Noto Sans:
|
||||
static/NotoSans_ExtraCondensed-Thin.ttf
|
||||
static/NotoSans_ExtraCondensed-ExtraLight.ttf
|
||||
static/NotoSans_ExtraCondensed-Light.ttf
|
||||
static/NotoSans_ExtraCondensed-Regular.ttf
|
||||
static/NotoSans_ExtraCondensed-Medium.ttf
|
||||
static/NotoSans_ExtraCondensed-SemiBold.ttf
|
||||
static/NotoSans_ExtraCondensed-Bold.ttf
|
||||
static/NotoSans_ExtraCondensed-ExtraBold.ttf
|
||||
static/NotoSans_ExtraCondensed-Black.ttf
|
||||
static/NotoSans_Condensed-Thin.ttf
|
||||
static/NotoSans_Condensed-ExtraLight.ttf
|
||||
static/NotoSans_Condensed-Light.ttf
|
||||
static/NotoSans_Condensed-Regular.ttf
|
||||
static/NotoSans_Condensed-Medium.ttf
|
||||
static/NotoSans_Condensed-SemiBold.ttf
|
||||
static/NotoSans_Condensed-Bold.ttf
|
||||
static/NotoSans_Condensed-ExtraBold.ttf
|
||||
static/NotoSans_Condensed-Black.ttf
|
||||
static/NotoSans_SemiCondensed-Thin.ttf
|
||||
static/NotoSans_SemiCondensed-ExtraLight.ttf
|
||||
static/NotoSans_SemiCondensed-Light.ttf
|
||||
static/NotoSans_SemiCondensed-Regular.ttf
|
||||
static/NotoSans_SemiCondensed-Medium.ttf
|
||||
static/NotoSans_SemiCondensed-SemiBold.ttf
|
||||
static/NotoSans_SemiCondensed-Bold.ttf
|
||||
static/NotoSans_SemiCondensed-ExtraBold.ttf
|
||||
static/NotoSans_SemiCondensed-Black.ttf
|
||||
static/NotoSans-Thin.ttf
|
||||
static/NotoSans-ExtraLight.ttf
|
||||
static/NotoSans-Light.ttf
|
||||
static/NotoSans-Regular.ttf
|
||||
static/NotoSans-Medium.ttf
|
||||
static/NotoSans-SemiBold.ttf
|
||||
static/NotoSans-Bold.ttf
|
||||
static/NotoSans-ExtraBold.ttf
|
||||
static/NotoSans-Black.ttf
|
||||
static/NotoSans_ExtraCondensed-ThinItalic.ttf
|
||||
static/NotoSans_ExtraCondensed-ExtraLightItalic.ttf
|
||||
static/NotoSans_ExtraCondensed-LightItalic.ttf
|
||||
static/NotoSans_ExtraCondensed-Italic.ttf
|
||||
static/NotoSans_ExtraCondensed-MediumItalic.ttf
|
||||
static/NotoSans_ExtraCondensed-SemiBoldItalic.ttf
|
||||
static/NotoSans_ExtraCondensed-BoldItalic.ttf
|
||||
static/NotoSans_ExtraCondensed-ExtraBoldItalic.ttf
|
||||
static/NotoSans_ExtraCondensed-BlackItalic.ttf
|
||||
static/NotoSans_Condensed-ThinItalic.ttf
|
||||
static/NotoSans_Condensed-ExtraLightItalic.ttf
|
||||
static/NotoSans_Condensed-LightItalic.ttf
|
||||
static/NotoSans_Condensed-Italic.ttf
|
||||
static/NotoSans_Condensed-MediumItalic.ttf
|
||||
static/NotoSans_Condensed-SemiBoldItalic.ttf
|
||||
static/NotoSans_Condensed-BoldItalic.ttf
|
||||
static/NotoSans_Condensed-ExtraBoldItalic.ttf
|
||||
static/NotoSans_Condensed-BlackItalic.ttf
|
||||
static/NotoSans_SemiCondensed-ThinItalic.ttf
|
||||
static/NotoSans_SemiCondensed-ExtraLightItalic.ttf
|
||||
static/NotoSans_SemiCondensed-LightItalic.ttf
|
||||
static/NotoSans_SemiCondensed-Italic.ttf
|
||||
static/NotoSans_SemiCondensed-MediumItalic.ttf
|
||||
static/NotoSans_SemiCondensed-SemiBoldItalic.ttf
|
||||
static/NotoSans_SemiCondensed-BoldItalic.ttf
|
||||
static/NotoSans_SemiCondensed-ExtraBoldItalic.ttf
|
||||
static/NotoSans_SemiCondensed-BlackItalic.ttf
|
||||
static/NotoSans-ThinItalic.ttf
|
||||
static/NotoSans-ExtraLightItalic.ttf
|
||||
static/NotoSans-LightItalic.ttf
|
||||
static/NotoSans-Italic.ttf
|
||||
static/NotoSans-MediumItalic.ttf
|
||||
static/NotoSans-SemiBoldItalic.ttf
|
||||
static/NotoSans-BoldItalic.ttf
|
||||
static/NotoSans-ExtraBoldItalic.ttf
|
||||
static/NotoSans-BlackItalic.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (OFL.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them in your products & projects – print or digital,
|
||||
commercial or otherwise.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
BIN
src/lib/fonts/static/NotoSans-Black.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-Black.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-BlackItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-Bold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-Bold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-BoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-ExtraBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-ExtraBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-ExtraLight.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-ExtraLightItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-Italic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-Italic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-Light.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-Light.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-LightItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-Medium.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-Medium.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-MediumItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-Regular.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-Regular.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-SemiBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-SemiBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-Thin.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-Thin.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans-ThinItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-Black.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-Black.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-BlackItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-Bold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-Bold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-BoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraLight.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraLightItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-Italic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-Italic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-Light.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-Light.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-LightItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-Medium.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-Medium.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-MediumItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-Regular.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-Regular.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-SemiBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-SemiBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-Thin.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-Thin.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_Condensed-ThinItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_Condensed-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Black.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Black.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-BlackItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Bold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Bold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-BoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ExtraBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ExtraBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ExtraLight.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ExtraLight.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Italic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Italic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Light.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Light.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-LightItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Medium.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Medium.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-MediumItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Regular.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Regular.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-SemiBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-SemiBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Thin.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-Thin.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ThinItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_ExtraCondensed-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Black.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Black.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-BlackItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Bold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Bold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-BoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraLight.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraLightItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Italic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Italic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Light.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Light.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-LightItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Medium.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Medium.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-MediumItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Regular.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Regular.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-SemiBold.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-SemiBoldItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Thin.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-Thin.ttf
Normal file
Binary file not shown.
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ThinItalic.ttf
Normal file
BIN
src/lib/fonts/static/NotoSans_SemiCondensed-ThinItalic.ttf
Normal file
Binary file not shown.
@@ -1,11 +1,11 @@
|
||||
export function localizeTime(timeValue: string, targetTimeZone?: string): string {
|
||||
// 1. Auto-detect user's timezone if none provided
|
||||
export function localizeTime(timeValue?: string, targetTimeZone?: string): string {
|
||||
if (!timeValue) return 'Invalid date';
|
||||
|
||||
if (!targetTimeZone) {
|
||||
targetTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
// 2. Format the date in the target timezone
|
||||
const date = new Date(timeValue.replace(' ', 'T') + 'Z'); // Ensure UTC
|
||||
const date = new Date(timeValue.replace(' ', 'T') + 'Z');
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: targetTimeZone,
|
||||
year: 'numeric',
|
||||
@@ -28,10 +28,3 @@ export function localizeTime(timeValue: string, targetTimeZone?: string): string
|
||||
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
// Usage:
|
||||
// const localTime = localizeTime(ticket.created_at); // Auto-detects timezone
|
||||
// console.log(localTime); // "2025-04-03 20:14:50" (in user's local time)
|
||||
|
||||
// const timeInTokyo = localizeTime(ticket.created_at, "Asia/Tokyo");
|
||||
// console.log(timeInTokyo); // "2025-04-04 05:14:50" (Tokyo time)
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
// const phpHello = `$_SESSION['userName']`;
|
||||
import UserProfile from "../components/UserProfile";
|
||||
---
|
||||
|
||||
<Layout title="Profile Page">
|
||||
<UserProfile client:load />
|
||||
<!-- <div class="flex items-center justify-center min-h-screen bg-gray-700">
|
||||
<div class="w-96 p-6 shadow-lg rounded-lg bg-white text-center">
|
||||
<img class="w-24 h-24 rounded-full border-4 border-blue-500 mx-auto" src="/profile.jpg" alt="Profile Picture" />
|
||||
<h2 class="text-2xl font-semibold mt-4" set:html={phpHello ? phpHello : 'User Name'}></h2>
|
||||
<p class="text-gray-600">Frontend Developer</p>
|
||||
<p class="text-gray-500 text-sm mt-2">"Building amazing UI experiences one component at a time."</p>
|
||||
<div class="flex justify-center space-x-4 mt-4">
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg">Follow</button>
|
||||
<button class="bg-gray-200 text-black px-4 py-2 rounded-lg">Message</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</Layout>
|
||||
7
src/pages/profile/billing-info.astro
Normal file
7
src/pages/profile/billing-info.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import BillingInformation from "../../components/BillingInfo";
|
||||
---
|
||||
<Layout title="">
|
||||
<BillingInformation client:load />
|
||||
</Layout>
|
||||
9
src/pages/profile/index.astro
Normal file
9
src/pages/profile/index.astro
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
// const phpHello = `$_SESSION['userName']`;
|
||||
import UserProfile from "../../components/UserProfile";
|
||||
---
|
||||
|
||||
<Layout title="Profile Page">
|
||||
<UserProfile client:load />
|
||||
</Layout>
|
||||
58
yarn.lock
58
yarn.lock
@@ -1106,6 +1106,11 @@ acorn@^8.14.1:
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
|
||||
adler-32@~1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz"
|
||||
integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
|
||||
|
||||
ansi-align@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz"
|
||||
@@ -1449,6 +1454,14 @@ ccount@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"
|
||||
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
|
||||
|
||||
cfb@~1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz"
|
||||
integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
|
||||
dependencies:
|
||||
adler-32 "~1.3.0"
|
||||
crc-32 "~1.2.0"
|
||||
|
||||
chalk@^5.0.0, chalk@5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz"
|
||||
@@ -1557,6 +1570,11 @@ codemirror@*, codemirror@^5.65.15:
|
||||
resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.65.19.tgz"
|
||||
integrity sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==
|
||||
|
||||
codepage@~1.15.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz"
|
||||
integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
|
||||
@@ -1625,6 +1643,11 @@ cookie@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz"
|
||||
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
|
||||
|
||||
crc-32@~1.2.0, crc-32@~1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz"
|
||||
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
|
||||
|
||||
cross-spawn@^7.0.3, cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz"
|
||||
@@ -2047,6 +2070,11 @@ formdata-polyfill@^4.0.10:
|
||||
dependencies:
|
||||
fetch-blob "^3.1.2"
|
||||
|
||||
frac@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz"
|
||||
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
|
||||
|
||||
fraction.js@^4.3.7:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
|
||||
@@ -5028,6 +5056,13 @@ split-on-first@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz"
|
||||
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
|
||||
|
||||
ssf@~0.11.2:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz"
|
||||
integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
|
||||
dependencies:
|
||||
frac "~1.1.2"
|
||||
|
||||
stdin-discarder@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz"
|
||||
@@ -5707,6 +5742,16 @@ widest-line@^5.0.0:
|
||||
dependencies:
|
||||
string-width "^7.0.0"
|
||||
|
||||
wmf@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz"
|
||||
integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
|
||||
|
||||
word@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/word/-/word-0.3.0.tgz"
|
||||
integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
@@ -5734,6 +5779,19 @@ wrap-ansi@^9.0.0:
|
||||
string-width "^7.0.0"
|
||||
strip-ansi "^7.1.0"
|
||||
|
||||
xlsx@^0.18.5:
|
||||
version "0.18.5"
|
||||
resolved "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz"
|
||||
integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
|
||||
dependencies:
|
||||
adler-32 "~1.3.0"
|
||||
cfb "~1.2.1"
|
||||
codepage "~1.15.0"
|
||||
crc-32 "~1.2.1"
|
||||
ssf "~0.11.2"
|
||||
wmf "~1.0.1"
|
||||
word "~0.3.0"
|
||||
|
||||
"xml2js@^0.5.0 || ^0.6.2":
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user