s33
parent
5a32c1a7d2
commit
9091219729
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
@ -78,8 +86,8 @@ export default function MakePayment(){
|
|||
// Construct UPI payment link
|
||||
return `upi://pay?pa=${merchantUPI}&pn=${encodedMerchantName}&am=${amount}&cu=${currency}&tn=${transactionNote}`;
|
||||
}
|
||||
// waiting payment update
|
||||
// Payment update not recieved if
|
||||
// waiting payment update
|
||||
// Payment update not recieved if
|
||||
function redirectToPayU() {
|
||||
if (!initialOrderData?.payment_data || !initialOrderData.payment_url) {
|
||||
console.error('Payment data not loaded yet');
|
||||
|
@ -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,33 +228,95 @@ 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>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="p-4 bg-white rounded-lg border border-gray-200">
|
||||
<QRCode value={upiPaymentLink} size={256} level="H" bgColor="#ffffff" fgColor="#000000" />
|
||||
<QRCode value={upiPaymentLink} size={256} level="H" bgColor="#ffffff" fgColor="#000000" />
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => navigator.clipboard.writeText(upiPaymentLink)} className="text-sm" >Copy UPI Link</Button>
|
||||
</p>
|
||||
<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>
|
||||
)}
|
||||
{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.
|
File diff suppressed because it is too large
Load Diff
|
@ -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,29 +202,49 @@ 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>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
{
|
||||
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>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardHeader>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -290,8 +290,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
font-size: 15px;
|
||||
body {
|
||||
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({
|
||||
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,35 +116,41 @@ 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';
|
||||
|
||||
|
||||
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: {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>
|
||||
);
|
||||
|
|
|
@ -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;
|
Binary file not shown.
Binary file not shown.
|
@ -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.
|
|
@ -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.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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>
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import BillingInformation from "../../components/BillingInfo";
|
||||
---
|
||||
<Layout title="">
|
||||
<BillingInformation client:load />
|
||||
</Layout>
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue