Compare commits
17 Commits
get-starte
...
6b7e7fcd6d
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b7e7fcd6d | |||
|
|
96a027dafb | ||
|
|
d01b0aaa2a | ||
| 8f8c5f0d65 | |||
|
|
0438c30c97 | ||
| 8b75fa057d | |||
|
|
c927fd6087 | ||
| 1ed908b12e | |||
| ffae4acebd | |||
| daa4702904 | |||
|
|
c3faedf6f8 | ||
| a9eabcb683 | |||
| 66bd9ec890 | |||
| 9d3ef1a643 | |||
| 1661f7859d | |||
| 026c6c34b2 | |||
| 734fed06b8 |
187
package-lock.json
generated
187
package-lock.json
generated
@@ -12,15 +12,21 @@
|
||||
"@astrojs/tailwind": "^6.0.0",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@types/date-fns": "^2.5.3",
|
||||
"@types/react": "^19.0.12",
|
||||
"astro": "^5.5.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.484.0",
|
||||
"pocketbase": "^0.25.2",
|
||||
"postcss": "^8.5.3",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-to-print": "^3.0.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
@@ -1733,6 +1739,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
|
||||
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
|
||||
@@ -1794,6 +1831,70 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
|
||||
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-roving-focus": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||
"integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
@@ -2362,6 +2463,12 @@
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/date-fns": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz",
|
||||
"integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
@@ -3242,6 +3349,16 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@@ -6007,6 +6124,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.1.tgz",
|
||||
"integrity": "sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.1.tgz",
|
||||
"integrity": "sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -6029,6 +6195,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-to-print": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.5.tgz",
|
||||
"integrity": "sha512-Z15MwMOzYCHWi26CZeFNwflAg7Nr8uWD6FTj+EkfIOjYyjr0MXGbI0c7rF4Fgrbj3XG9hFndb1ourxpPz2RAiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -6463,6 +6638,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
@@ -7005,6 +7186,12 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz",
|
||||
|
||||
@@ -14,15 +14,21 @@
|
||||
"@astrojs/tailwind": "^6.0.0",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@types/date-fns": "^2.5.3",
|
||||
"@types/react": "^19.0.12",
|
||||
"astro": "^5.5.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.484.0",
|
||||
"pocketbase": "^0.25.2",
|
||||
"postcss": "^8.5.3",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-to-print": "^3.0.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
|
||||
8
public/.htaccess
Normal file
8
public/.htaccess
Normal file
@@ -0,0 +1,8 @@
|
||||
RewriteEngine On
|
||||
#RewriteCond %{HTTPS} !=on
|
||||
#RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,NE]
|
||||
#RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||
#RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*) data-not-updated-or-not-found
|
||||
39
public/.well-known/csaf/provider-metadata.json
Normal file
39
public/.well-known/csaf/provider-metadata.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"csaf_version": "2.0",
|
||||
"provider": {
|
||||
"name": "DWD Consultancy Services",
|
||||
"namespace": "com.siliconpin.dwd",
|
||||
"contact_details": [
|
||||
{
|
||||
"name": "Suvankar Sarkar",
|
||||
"email": "suvankar@siliconpin.com",
|
||||
"phone": "+91-700-160-1485",
|
||||
"website": "https://dwd.siliconpin.com/#about"
|
||||
}
|
||||
],
|
||||
"publisher": {
|
||||
"type": "vendor",
|
||||
"name": "SiliconPin",
|
||||
"namespace": "com.siliconpin"
|
||||
},
|
||||
"distribution": {
|
||||
"tlp_label": "WHITE",
|
||||
"url": "https://dwd.siliconpin.com/#about",
|
||||
"pgp_key": "https://siliconpin.com/pgp-key.asc"
|
||||
},
|
||||
"tracking": {
|
||||
"id": "EX-2025-0001",
|
||||
"status": "final",
|
||||
"initial_release_date": "2025-03-27T12:00:00Z",
|
||||
"current_release_date": "2025-03-27T12:00:00Z",
|
||||
"revision_history": [
|
||||
{
|
||||
"number": "1.0",
|
||||
"date": "2025-03-27T12:00:00Z",
|
||||
"summary": "Initial release"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
public/.well-known/security.txt
Normal file
7
public/.well-known/security.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Contact: mailto:suvankar@siliconpin.com
|
||||
Contact: https://siliconpin.com/contact/
|
||||
Expires: 2027-03-26T18:30:00.000Z
|
||||
Encryption: https://siliconpin.com/pgp-key.txt
|
||||
Acknowledgments: https://dwd.siliconpin.com
|
||||
Preferred-Languages: en,bn
|
||||
CSAF: https://siliconpin.com/.well-known/csaf/provider-metadata.json
|
||||
39
public/advisory.json
Normal file
39
public/advisory.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"csaf_version": "2.0",
|
||||
"provider": {
|
||||
"name": "DWD Consultancy Services",
|
||||
"namespace": "com.siliconpin.dwd",
|
||||
"contact_details": [
|
||||
{
|
||||
"name": "Suvankar Sarkar",
|
||||
"email": "suvankar@siliconpin.com",
|
||||
"phone": "+91-700-160-1485",
|
||||
"website": "https://dwd.siliconpin.com/#about"
|
||||
}
|
||||
],
|
||||
"publisher": {
|
||||
"type": "vendor",
|
||||
"name": "SiliconPin",
|
||||
"namespace": "com.siliconpin"
|
||||
},
|
||||
"distribution": {
|
||||
"tlp_label": "WHITE",
|
||||
"url": "https://dwd.siliconpin.com/#about",
|
||||
"pgp_key": "https://siliconpin.com/pgp-key.asc"
|
||||
},
|
||||
"tracking": {
|
||||
"id": "EX-2025-0001",
|
||||
"status": "final",
|
||||
"initial_release_date": "2025-03-27T12:00:00Z",
|
||||
"current_release_date": "2025-03-27T12:00:00Z",
|
||||
"revision_history": [
|
||||
{
|
||||
"number": "1.0",
|
||||
"date": "2025-03-27T12:00:00Z",
|
||||
"summary": "Initial release"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
public/advisory.json.asc
Normal file
14
public/advisory.json.asc
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCAAdFiEEL0GUnJpZXELqjphXMaCR/T68rscFAmflVEgACgkQMaCR/T68
|
||||
rse45Av6A0YL6qJSBvqN6tty7j0G68yfgsjeJxtuCxXiDwUjUIU4VwHORNeddZD/
|
||||
Lx5nmYf4BgszoFjgaix8dyu87bDn2QaA13+2/GMDkc3GT8yW8bzaR/saV1J1v070
|
||||
wVhButTIM48c4NpuqFX5Kytcvm632lZdeKBFF2rFfdZ3ajd7IJMr7CYVzziJdVa8
|
||||
pR00udgBYE05ewV9W8FLMmTwWqoOIQYtYR3+YP1rhuYInZ/7dobO5aI4/eliHIBo
|
||||
xe93UET9Zo19ywbnqhas+x4wJl8roI2qWDYsjp3+gRG4Ns5Myf2puhtVxexiY4xK
|
||||
6cvra2ujOFLksykPgy5lpw2WcpFFnm9K2s9NMvLAsv+3OXZHQANL7vS5ZFXxNxyU
|
||||
fm+F5uP8LiR1IXpFcJAP3MweaDdlWhL7TZp1tuA0J/i/oCFShZypzQLb9welYVbH
|
||||
a/CPm1pavGxEIlFkSOAlMmcp945s41qYia2BFOlW1ZjsDkrAR2561aLI/O7klpNw
|
||||
VLnngQsi
|
||||
=s4sX
|
||||
-----END PGP SIGNATURE-----
|
||||
1
public/assets/clock.svg
Normal file
1
public/assets/clock.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15px" height="15px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><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="M12 8V12L15 15" stroke="#6d9e37" stroke-width="2" stroke-linecap="round"></path> <circle cx="12" cy="12" r="9" stroke="#6d9e37" stroke-width="2"></circle> </g></svg>
|
||||
|
After Width: | Height: | Size: 430 B |
1
public/assets/send.svg
Normal file
1
public/assets/send.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="30px" height="30px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff"><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="M20 4L3 9.31372L10.5 13.5M20 4L14.5 21L10.5 13.5M20 4L10.5 13.5" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
||||
|
After Width: | Height: | Size: 446 B |
1
public/humans.txt
Normal file
1
public/humans.txt
Normal file
@@ -0,0 +1 @@
|
||||
SiliconPin is a Decentralized NonProfit Organization / Group, Creating some digital freedom, if you want to join -welcome.
|
||||
41
public/pgp-key.txt
Normal file
41
public/pgp-key.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=NDqk
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
41
public/public-key.asc
Normal file
41
public/public-key.asc
Normal file
@@ -0,0 +1,41 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=NDqk
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
7
public/robots.txt
Normal file
7
public/robots.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
User-agent: *
|
||||
Disallow: /secret-location/
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://siliconpin.com/sitemap.xml
|
||||
7
public/security.txt
Normal file
7
public/security.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Contact: mailto:suvankar@siliconpin.com
|
||||
Contact: https://siliconpin.com/contact/
|
||||
Expires: 2027-03-26T18:30:00.000Z
|
||||
Encryption: https://siliconpin.com/pgp-key.txt
|
||||
Acknowledgments: https://dwd.siliconpin.com
|
||||
Preferred-Languages: en,bn
|
||||
CSAF: https://siliconpin.com/.well-known/csaf/provider-metadata.json
|
||||
72
public/sitemap.xml
Normal file
72
public/sitemap.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
||||
|
||||
|
||||
<url>
|
||||
<loc>https://siliconpin.com/</loc>
|
||||
<lastmod>2025-03-27T14:20:53+00:00</lastmod>
|
||||
<priority>1.00</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/services/</loc>
|
||||
<lastmod>2025-03-27T14:20:54+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/contact/</loc>
|
||||
<lastmod>2025-03-27T14:20:55+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/get-started/</loc>
|
||||
<lastmod>2025-03-27T14:20:56+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/about-us/</loc>
|
||||
<lastmod>2025-03-27T14:20:57+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/suggestion-or-report/</loc>
|
||||
<lastmod>2025-03-27T14:20:58+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/hire-developer/</loc>
|
||||
<lastmod>2025-03-27T14:21:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/hire-ai-agent/</loc>
|
||||
<lastmod>2025-03-27T14:21:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/privacy-policy/</loc>
|
||||
<lastmod>2025-03-27T14:21:02+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/terms-and-conditions/</loc>
|
||||
<lastmod>2025-03-27T14:21:03+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/refund-policy/</loc>
|
||||
<lastmod>2025-03-27T14:21:04+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/legal-agreement/</loc>
|
||||
<lastmod>2025-03-27T14:21:05+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
|
||||
|
||||
</urlset>
|
||||
@@ -279,42 +279,42 @@ export default function HireAIAgent() {
|
||||
<div className="mt-16">
|
||||
<h2 className="text-2xl font-bold mb-8 text-center">AI Agents vs. Human Developers</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Feature</th>
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">AI Agents</th>
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Human Developers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Availability</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">24/7, instant access</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Limited by working hours and availability</td>
|
||||
</tr>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Cost</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Low monthly subscription</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Higher hourly or project-based rates</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Task Complexity</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Best for repetitive, well-defined tasks</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Better for complex, novel problems</td>
|
||||
</tr>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Creativity</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Limited to patterns in training data</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Higher level of creativity and innovation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Scalability</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Highly scalable at no extra cost</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Requires additional hiring and onboarding</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Feature</th>
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">AI Agents</th>
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Human Developers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Availability</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">24/7, instant access</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Limited by working hours and availability</td>
|
||||
</tr>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Cost</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Low monthly subscription</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Higher hourly or project-based rates</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Task Complexity</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Best for repetitive, well-defined tasks</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Better for complex, novel problems</td>
|
||||
</tr>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Creativity</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Limited to patterns in training data</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Higher level of creativity and innovation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Scalability</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Highly scalable at no extra cost</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Requires additional hiring and onboarding</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ interface AuthResponse {
|
||||
record: UserRecord;
|
||||
}
|
||||
|
||||
|
||||
const LoginPage = () => {
|
||||
const [email, setEmail] = useState('suvodip@siliconpin.com');
|
||||
const [password, setPassword] = useState('Simple2pass');
|
||||
@@ -35,18 +36,30 @@ const LoginPage = () => {
|
||||
|
||||
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
record: {
|
||||
query: string;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setStatus({ message: '', isError: false });
|
||||
|
||||
|
||||
try {
|
||||
const authData = await pb.collection("users").authWithPassword(email, password);
|
||||
|
||||
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
|
||||
|
||||
const authResponse: AuthResponse = {
|
||||
token: authData.token,
|
||||
record: {
|
||||
query: 'new',
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name || '',
|
||||
@@ -54,8 +67,8 @@ const LoginPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
await syncSessionWithBackend(authResponse);
|
||||
// window.location.href = '/profile';
|
||||
await syncSessionWithBackend(authResponse, avatarUrl);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
setStatus({
|
||||
@@ -77,20 +90,21 @@ const LoginPage = () => {
|
||||
if (!authData?.record) {
|
||||
throw new Error("No user record found");
|
||||
}
|
||||
|
||||
|
||||
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
|
||||
const authResponse: AuthResponse = {
|
||||
token: authData.token,
|
||||
record: {
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email || '',
|
||||
name: authData.record.name || '',
|
||||
avatar: authData.record.avatar || ''
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email || '',
|
||||
name: authData.record.name || '',
|
||||
avatar: authData.record.avatar || ''
|
||||
}
|
||||
};
|
||||
|
||||
await syncSessionWithBackend(authResponse);
|
||||
// window.location.href = '/profile';
|
||||
await syncSessionWithBackend(authResponse, avatarUrl);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error(`${provider} Login failed:`, error);
|
||||
setStatus({
|
||||
@@ -101,22 +115,21 @@ const LoginPage = () => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const syncSessionWithBackend = async (authData: AuthResponse) => {
|
||||
|
||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Important for cookies
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: 'new',
|
||||
accessToken: authData.token,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name,
|
||||
avatar: authData.record.avatar
|
||||
? pb.files.getUrl(authData.record, authData.record.avatar)
|
||||
: '',
|
||||
isAuthenticated: true,
|
||||
id: authData.record.id
|
||||
query: 'new',
|
||||
accessToken: authData.token,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name,
|
||||
avatar: avatarUrl,
|
||||
isAuthenticated: true,
|
||||
id: authData.record.id
|
||||
})
|
||||
});
|
||||
|
||||
@@ -128,6 +141,7 @@ const LoginPage = () => {
|
||||
console.log('Session synced with backend:', data);
|
||||
} catch (error) {
|
||||
console.error('Error syncing session:', error);
|
||||
throw error; // Re-throw the error if you want calling functions to handle it
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
273
src/components/PrintInvoice.tsx
Normal file
273
src/components/PrintInvoice.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Invoice {
|
||||
invoice_id: number;
|
||||
invoice_number: string;
|
||||
customer_id: string;
|
||||
invoice_date: string;
|
||||
due_date: string;
|
||||
subtotal: number;
|
||||
tax_amount: number;
|
||||
total_amount: number;
|
||||
status: string;
|
||||
payment_terms: string | null;
|
||||
notes: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export default function PrintInvoice() {
|
||||
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const getInvoiceListData = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data);
|
||||
} catch (error: any) {
|
||||
console.error('Fetch error:', error);
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
const viewInvoiceDetails = (invoice: Invoice) => {
|
||||
setSelectedInvoice(invoice);
|
||||
}
|
||||
|
||||
const closeInvoiceDetails = () => {
|
||||
setSelectedInvoice(null);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="p-4 text-red-600">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading invoice data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-6xl mx-auto">
|
||||
{/* Invoice List */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Invoices</h1>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-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">Invoice #</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Due Date</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{invoiceList.map((invoice) => (
|
||||
<tr key={invoice.invoice_id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">{invoice.invoice_number}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.invoice_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.due_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatCurrency(invoice.total_amount)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
${invoice.status === 'paid' ? 'bg-green-100 text-green-800' :
|
||||
invoice.status === 'draft' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'}`}>
|
||||
{invoice.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => viewInvoiceDetails(invoice)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-2"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Print
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Detail Modal */}
|
||||
{selectedInvoice && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Printable Invoice */}
|
||||
<div id="printable-invoice" className="p-8">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">INVOICE</h1>
|
||||
<p className="text-gray-600">{selectedInvoice.invoice_number}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-gray-600">Status: <span className={`font-semibold
|
||||
${selectedInvoice.status === 'paid' ? 'text-green-600' :
|
||||
selectedInvoice.status === 'draft' ? 'text-yellow-600' :
|
||||
'text-red-600'}`}>
|
||||
{selectedInvoice.status.toUpperCase()}
|
||||
</span></p>
|
||||
<p className="text-gray-600">Date: {formatDate(selectedInvoice.invoice_date)}</p>
|
||||
<p className="text-gray-600">Due: {formatDate(selectedInvoice.due_date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">From</h2>
|
||||
<p className="text-gray-800">Your Company Name</p>
|
||||
<p className="text-gray-600">123 Business Street</p>
|
||||
<p className="text-gray-600">City, State 12345</p>
|
||||
<p className="text-gray-600">contact@yourcompany.com</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Bill To</h2>
|
||||
<p className="text-gray-800">Customer ID: {selectedInvoice.customer_id}</p>
|
||||
<p className="text-gray-600">[Customer Name]</p>
|
||||
<p className="text-gray-600">[Customer Address]</p>
|
||||
<p className="text-gray-600">[Customer Email]</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-b border-gray-200 py-4 mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 font-semibold text-gray-700">
|
||||
<div className="col-span-6">Description</div>
|
||||
<div className="col-span-2 text-right">Subtotal</div>
|
||||
<div className="col-span-2 text-right">Tax</div>
|
||||
<div className="col-span-2 text-right">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 mb-2">
|
||||
<div className="col-span-6 text-gray-800">[Service/Product Description]</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.subtotal)}</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.tax_amount)}</div>
|
||||
<div className="col-span-2 text-right font-semibold">{formatCurrency(selectedInvoice.total_amount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-8">
|
||||
<div className="w-64">
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Subtotal:</span>
|
||||
<span>{formatCurrency(selectedInvoice.subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Tax:</span>
|
||||
<span>{formatCurrency(selectedInvoice.tax_amount)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 font-bold text-lg">
|
||||
<span>Total:</span>
|
||||
<span>{formatCurrency(selectedInvoice.total_amount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedInvoice.notes && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Notes</h2>
|
||||
<p className="text-gray-600">{selectedInvoice.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-8 border-t border-gray-200">
|
||||
<p className="text-gray-600 text-center">Thank you for your business!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-end">
|
||||
<button
|
||||
onClick={closeInvoiceDetails}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 mr-2"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Print Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Print Styles */}
|
||||
<style>
|
||||
{`
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
#printable-invoice, #printable-invoice * {
|
||||
visibility: visible;
|
||||
}
|
||||
#printable-invoice {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 20mm;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
365
src/components/Ticket.tsx
Normal file
365
src/components/Ticket.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
||||
import Table from "./ui/table";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||
import { Input } from "./ui/input";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import { Label } from "./ui/label";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
|
||||
import { CustomTabs } from "./ui/CustomTabs";
|
||||
import {localizeTime} from "../lib/localizeTime";
|
||||
interface Ticket {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
status: 'open' | 'in-progress' | 'resolved' | 'closed'; // Add 'closed' here
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: number;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
ticket_id: number;
|
||||
user_id: number;
|
||||
message: string;
|
||||
created_at: string;
|
||||
user_type: string;
|
||||
}
|
||||
|
||||
const API_URL = 'http://localhost:2058/host-api/v1/ticket/index.php';
|
||||
|
||||
function Ticketing() {
|
||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [selectedTicket, setSelectedTicket] = useState<(Ticket & { status: 'open' | 'in-progress' | 'resolved' | 'closed' }) | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'open' | 'in-progress' | 'resolved' | 'closed'>('open');
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isMessageDialogOpen, setIsMessageDialogOpen] = useState(false);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [formData, setFormData] = useState({title: '', description: '', status: 'open' as const, });
|
||||
|
||||
// Fetch tickets
|
||||
useEffect(() => {fetchTickets(); }, [activeTab]);
|
||||
|
||||
// Fetch messages when ticket is selected
|
||||
useEffect(() => {if (selectedTicket) {fetchMessages(selectedTicket.id);}}, [selectedTicket]);
|
||||
|
||||
const fetchTickets = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}?action=get_tickets&status=${activeTab}`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTickets(data.tickets);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tickets:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMessages = async (ticketId: number) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}?action=get_messages&ticket_id=${ticketId}`);
|
||||
const data = await response.json();
|
||||
console.log('Messages response:', data); // Debug log
|
||||
if (data.success) {
|
||||
setMessages(data.messages || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching messages:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'create_ticket',
|
||||
...formData,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
fetchTickets(); // Refresh the list
|
||||
setIsDialogOpen(false);
|
||||
setFormData({ title: '', description: '', status: 'open' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating ticket:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (ticketId: number, newStatus: 'open' | 'in-progress' | 'resolved' | 'closed') => {
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'update_ticket_status',
|
||||
ticket_id: ticketId,
|
||||
status: newStatus,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
fetchTickets(); // Refresh the list
|
||||
if (selectedTicket) {
|
||||
setSelectedTicket({ ...selectedTicket, status: newStatus });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating ticket status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!selectedTicket || !newMessage.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'add_message',
|
||||
ticket_id: selectedTicket.id,
|
||||
message: newMessage,
|
||||
user_type: 'user'
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
fetchMessages(selectedTicket.id); // Refresh messages
|
||||
setNewMessage('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTicket = async (ticketId: number) => {
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'delete_ticket',
|
||||
ticket_id: ticketId,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
fetchTickets(); // Refresh the list
|
||||
if (selectedTicket?.id === ticketId) {
|
||||
setIsMessageDialogOpen(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting ticket:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Table configuration
|
||||
const tableHeaders = ['ID', 'Title', 'Description', 'Status', 'Created At', 'Actions'];
|
||||
const tableData = tickets.map(ticket => ({
|
||||
id: ticket.id,
|
||||
title: ticket.title,
|
||||
description: ticket.description,
|
||||
status: ticket.status,
|
||||
created_at: localizeTime(ticket.created_at),
|
||||
actions: (
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" onClick={() => {setSelectedTicket(ticket); setIsMessageDialogOpen(true); }}>View/Reply</Button>
|
||||
<Button size="sm" onClick={() => handleDeleteTicket(ticket.id)} className="bg-red-500 hover:bg-red-600">Delete</Button>
|
||||
</div>
|
||||
)
|
||||
}));
|
||||
const statusColors = {open: 'bg-green-500', 'in-progress': 'bg-yellow-500', resolved: 'bg-blue-500', closed: 'bg-red-500', };
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Ticket / Report</h1>
|
||||
<Button size="sm" onClick={() => setIsDialogOpen(true)}>Create Ticket</Button>
|
||||
</div>
|
||||
|
||||
<CustomTabs
|
||||
tabs={[
|
||||
{ label: 'Open', value: 'open', content: null },
|
||||
{ label: 'In Progress', value: 'in-progress', content: null },
|
||||
{ label: 'Resolved', value: 'resolved', content: null },
|
||||
{ label: 'Closed', value: 'closed', content: null },
|
||||
]}
|
||||
defaultValue={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as any)}
|
||||
/>
|
||||
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
{
|
||||
tableHeaders.map((thead) => (
|
||||
<th className={`p-2 text-center border-[1px] border-[#6d9e37]`}>{thead}</th>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
{
|
||||
tableData.length > 0 ? (
|
||||
<tbody className="[&>tr:nth-child(even)]:bg-[#262626]">
|
||||
{
|
||||
tableData.map((data, index) => (
|
||||
<tr>
|
||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.id}</td>
|
||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.title}</td>
|
||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.description}</td>
|
||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium text-center">
|
||||
<span className={`text-white px-2 py-1 rounded ${statusColors[data.status] || ''}`}>{data.status}</span>
|
||||
</td>
|
||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium text-center">{data.created_at}</td>
|
||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium flex items-center justify-center p-1">{data.actions}</td>
|
||||
</tr>
|
||||
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody>
|
||||
<tr>
|
||||
<th colSpan={6} className="p-4 border-[1px] border-[#6d9e37] font-medium">No {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Ticket Found</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
)
|
||||
}
|
||||
</table>
|
||||
|
||||
{/* <Table headers={tableHeaders} data={tableData} striped hover /> */}
|
||||
|
||||
{/* Create Ticket Dialog */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Ticket</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="title">Title</Label>
|
||||
<Input id="title" name="title" value={formData.title} onChange={handleInputChange} required className="w-full bg-white rounded-md outline-none border border-[#6d9e37] p-2" placeholder="Write ticket title" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<textarea id="description" name="description" value={formData.description} onChange={handleInputChange} rows={4} className="w-full rounded-md outline-none border border-[#6d9e37] p-2" required placeholder="Write description here..."></textarea>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Ticket Messages Dialog */}
|
||||
<Dialog open={isMessageDialogOpen} onOpenChange={setIsMessageDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ticket #{selectedTicket?.id}: {selectedTicket?.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedTicket && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gray-50 rounded">
|
||||
<p className="font-semibold text-gray-500">Ticket Details</p>
|
||||
<p className="text-gray-500">{selectedTicket.description}</p>
|
||||
<p className="text-sm text-gray-500 mt-2 ">
|
||||
<strong>Status:</strong> <span className={`font-bold px-1 rounded-md text-white ${statusColors[selectedTicket.status] || ''}`}>{selectedTicket.status}</span> |
|
||||
<strong>Created:</strong> {new Date(selectedTicket.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-t pt-4">
|
||||
<p className="font-semibold mb-2 text-[#6d9e37]">Conversation</p>
|
||||
<div className="space-y-4 max-h-64 overflow-y-auto">
|
||||
{
|
||||
messages.length > 0 ? (
|
||||
messages.map(message => (
|
||||
<div key={message.id} className={`px-3 py-1 border-b rounded flex flex-col text-gray-500 ${message.user_type === 'user' ? 'justify-end items-end border-[#6d9e37]' : 'justify-start items-start border-gray-500'} `}>
|
||||
<p>{message.message}</p>
|
||||
<p className="text-xs text-gray-500 mt-1 inline-flex items-center gap-1">
|
||||
<img src="/assets/clock.svg" alt="" />
|
||||
{localizeTime(message.created_at)}</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500">No messages yet</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<Label htmlFor="newMessage" className="text-[#6d9e37]">Add Reply</Label>
|
||||
<div className="flex flex-row gap-x-2">
|
||||
<textarea placeholder="Write your message..." id="newMessage" rows={1} value={newMessage} onChange={(e) => setNewMessage(e.target.value)} className="w-full p-2 mb-2 border border-[#6d9e37] outline-none focus:outline-none rounded text-gray-500 resize-none"></textarea>
|
||||
<Button onClick={handleSendMessage} disabled={!newMessage.trim()} className="px-6">
|
||||
<img src="/assets/send.svg" alt="" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="space-x-2">
|
||||
{selectedTicket.status === 'closed' ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={() => handleStatusChange(selectedTicket.id, 'open')}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Reopen
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{selectedTicket.status !== 'in-progress' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleStatusChange(selectedTicket.id, 'in-progress')}
|
||||
>
|
||||
Mark as In Progress
|
||||
</Button>
|
||||
)}
|
||||
{selectedTicket.status !== 'resolved' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleStatusChange(selectedTicket.id, 'resolved')}
|
||||
>
|
||||
Mark as Resolved
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleStatusChange(selectedTicket.id, 'closed')}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Ticketing;
|
||||
590
src/components/Tickiting copy.tsx
Normal file
590
src/components/Tickiting copy.tsx
Normal file
@@ -0,0 +1,590 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||
import { CustomTabs } from "./ui/CustomTabs";
|
||||
import Table from "./ui/table";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||
import { Card, CardHeader, CardContent, CardFooter } from "./ui/card";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { formatDistanceToNow } from "./ui/date-format";
|
||||
|
||||
interface Message {
|
||||
id?: number;
|
||||
content: string;
|
||||
sender: 'user' | 'admin';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Ticket {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
messages: Message[];
|
||||
status: 'open' | 'in-progress' | 'resolved' | 'closed';
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
category?: string;
|
||||
assignedTo?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function Ticketing() {
|
||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||
const [currentTicket, setCurrentTicket] = useState<Partial<Ticket>>({
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'open',
|
||||
priority: 'medium',
|
||||
category: 'general'
|
||||
});
|
||||
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('open');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const API_URL = 'http://localhost:2058/host-api/v1/ticketing/index.php';
|
||||
|
||||
const fetchTickets = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch tickets');
|
||||
}
|
||||
const data = await response.json();
|
||||
// Convert admin_message to messages array for backward compatibility
|
||||
const processedData = data.map((ticket: any) => ({
|
||||
...ticket,
|
||||
messages: [
|
||||
{
|
||||
content: ticket.description,
|
||||
sender: 'user',
|
||||
createdAt: ticket.createdAt
|
||||
},
|
||||
...(ticket.admin_message ? [{
|
||||
content: ticket.admin_message,
|
||||
sender: 'admin',
|
||||
createdAt: ticket.updatedAt
|
||||
}] : [])
|
||||
]
|
||||
}));
|
||||
setTickets(processedData);
|
||||
filterTickets(processedData, activeTab, searchTerm);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filterTickets = (ticketsToFilter: Ticket[], status: string, search: string) => {
|
||||
let filtered = ticketsToFilter;
|
||||
|
||||
if (status !== 'all') {
|
||||
filtered = filtered.filter(ticket => ticket.status === status);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase();
|
||||
filtered = filtered.filter(ticket =>
|
||||
ticket.title.toLowerCase().includes(searchLower) ||
|
||||
ticket.description.toLowerCase().includes(searchLower) ||
|
||||
ticket.messages.some(msg => msg.content.toLowerCase().includes(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
setFilteredTickets(filtered);
|
||||
};
|
||||
|
||||
const saveTicket = async () => {
|
||||
try {
|
||||
const method = isEditing ? 'PUT' : 'POST';
|
||||
const url = isEditing ? `${API_URL}?id=${currentTicket.id}` : API_URL;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: currentTicket.title,
|
||||
description: currentTicket.description,
|
||||
status: currentTicket.status,
|
||||
priority: currentTicket.priority,
|
||||
category: currentTicket.category
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `Failed to ${isEditing ? 'update' : 'create'} ticket`);
|
||||
}
|
||||
|
||||
await fetchTickets();
|
||||
setIsDialogOpen(false);
|
||||
resetForm();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
}
|
||||
};
|
||||
|
||||
const addMessage = async (ticketId: number) => {
|
||||
try {
|
||||
if (!newMessage.trim()) return;
|
||||
|
||||
const response = await fetch(`${API_URL}?id=${ticketId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: newMessage
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to add message');
|
||||
}
|
||||
|
||||
setNewMessage('');
|
||||
await fetchTickets();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
}
|
||||
};
|
||||
|
||||
const reopenTicket = async (ticketId: number) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}?id=${ticketId}&action=reopen`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to reopen ticket');
|
||||
}
|
||||
|
||||
await fetchTickets();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTicket = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}?id=${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete ticket');
|
||||
}
|
||||
|
||||
await fetchTickets();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
}
|
||||
};
|
||||
|
||||
const viewTicketDetails = (ticket: Ticket) => {
|
||||
setSelectedTicket(ticket);
|
||||
setIsDetailOpen(true);
|
||||
};
|
||||
|
||||
const initNewTicket = () => {
|
||||
setCurrentTicket({
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'open',
|
||||
priority: 'medium',
|
||||
category: 'general'
|
||||
});
|
||||
setIsEditing(false);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const initEditTicket = (ticket: Ticket) => {
|
||||
setCurrentTicket({ ...ticket });
|
||||
setIsEditing(true);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setCurrentTicket({
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'open',
|
||||
priority: 'medium',
|
||||
category: 'general'
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setCurrentTicket(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSelectChange = (name: string, value: string) => {
|
||||
setCurrentTicket(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTickets();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterTickets(tickets, activeTab, searchTerm);
|
||||
}, [activeTab, searchTerm, tickets]);
|
||||
|
||||
const renderTicketTable = () => {
|
||||
if (loading) {
|
||||
return <div className="text-center py-8">Loading tickets...</div>;
|
||||
}
|
||||
|
||||
if (filteredTickets.length === 0) {
|
||||
return <div className="text-center py-8">No tickets found</div>;
|
||||
}
|
||||
|
||||
const tableHeaders = ['ID', 'Title', 'Category', 'Status', 'Priority', 'Created', 'Actions'];
|
||||
|
||||
const tableData = filteredTickets.map(ticket => ({
|
||||
'ID': ticket.id,
|
||||
'Title': (
|
||||
<button
|
||||
onClick={() => viewTicketDetails(ticket)}
|
||||
className="text-left hover:underline"
|
||||
>
|
||||
{ticket.title}
|
||||
</button>
|
||||
),
|
||||
'Category': ticket.category || 'General',
|
||||
'Status': (
|
||||
<Badge
|
||||
variant={
|
||||
ticket.status === 'open' ? 'default' :
|
||||
ticket.status === 'in-progress' ? 'secondary' :
|
||||
ticket.status === 'resolved' ? 'success' : 'outline'
|
||||
}
|
||||
>
|
||||
{ticket.status}
|
||||
</Badge>
|
||||
),
|
||||
'Priority': (
|
||||
<Badge
|
||||
variant={
|
||||
ticket.priority === 'low' ? 'success' :
|
||||
ticket.priority === 'medium' ? 'warning' : 'destructive'
|
||||
}
|
||||
>
|
||||
{ticket.priority}
|
||||
</Badge>
|
||||
),
|
||||
'Created': formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true }),
|
||||
'Actions': (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => viewTicketDetails(ticket)}
|
||||
className="h-8"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => initEditTicket(ticket)}
|
||||
className="h-8"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{ticket.status === 'closed' ? (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => reopenTicket(ticket.id)}
|
||||
className="h-8"
|
||||
>
|
||||
Reopen
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => deleteTicket(ticket.id)}
|
||||
className="h-8"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}));
|
||||
|
||||
return (
|
||||
<Table
|
||||
headers={tableHeaders}
|
||||
data={tableData}
|
||||
className="border rounded-lg shadow-sm"
|
||||
striped
|
||||
hover
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMessages = () => {
|
||||
if (!selectedTicket) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{selectedTicket.messages.map((message, index) => (
|
||||
<div key={index} className={`flex ${message.sender === 'admin' ? 'justify-start' : 'justify-end'}`}>
|
||||
<div className={`max-w-[80%] rounded-lg p-4 ${message.sender === 'admin' ? 'bg-gray-100' : 'bg-blue-100'}`}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarFallback>{message.sender === 'admin' ? 'A' : 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="font-medium">{message.sender === 'admin' ? 'Admin' : 'You'}</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatDistanceToNow(new Date(message.createdAt), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap">{message.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Open Tickets",
|
||||
value: "open",
|
||||
content: renderTicketTable()
|
||||
},
|
||||
{
|
||||
label: "In Progress",
|
||||
value: "in-progress",
|
||||
content: renderTicketTable()
|
||||
},
|
||||
{
|
||||
label: "Resolved",
|
||||
value: "resolved",
|
||||
content: renderTicketTable()
|
||||
},
|
||||
{
|
||||
label: "Closed Tickets",
|
||||
value: "closed",
|
||||
content: renderTicketTable()
|
||||
},
|
||||
{
|
||||
label: "All Tickets",
|
||||
value: "all",
|
||||
content: renderTicketTable()
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Ticketing System</h1>
|
||||
<Button onClick={initNewTicket}>
|
||||
Create New Ticket
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<div className="w-1/3">
|
||||
<Input
|
||||
placeholder="Search tickets..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomTabs
|
||||
tabs={tabs}
|
||||
defaultValue="open"
|
||||
onValueChange={(value) => setActiveTab(value)}
|
||||
/>
|
||||
|
||||
{/* Ticket Dialog */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditing ? 'Edit Ticket' : 'Create New Ticket'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEditing ? 'Update the ticket details' : 'Fill in the details for the new ticket'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="title" className="text-right">
|
||||
Title
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
value={currentTicket.title}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right">
|
||||
Description
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={currentTicket.description}
|
||||
onChange={handleInputChange}
|
||||
className="col-span-3"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="category" className="text-right">
|
||||
Category
|
||||
</Label>
|
||||
<Select
|
||||
value={currentTicket.category}
|
||||
onValueChange={(value) => handleSelectChange('category', value)}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="general">General</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
<SelectItem value="technical">Technical</SelectItem>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="status" className="text-right">
|
||||
Status
|
||||
</Label>
|
||||
<Select
|
||||
value={currentTicket.status}
|
||||
onValueChange={(value) => handleSelectChange('status', value)}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="open">Open</SelectItem>
|
||||
<SelectItem value="in-progress">In Progress</SelectItem>
|
||||
<SelectItem value="resolved">Resolved</SelectItem>
|
||||
<SelectItem value="closed">Closed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="priority" className="text-right">
|
||||
Priority
|
||||
</Label>
|
||||
<Select
|
||||
value={currentTicket.priority}
|
||||
onValueChange={(value) => handleSelectChange('priority', value)}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select priority" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={saveTicket}>
|
||||
{isEditing ? 'Update Ticket' : 'Create Ticket'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Ticket Detail Dialog */}
|
||||
<Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedTicket?.title}</DialogTitle>
|
||||
<div className="flex gap-4 items-center">
|
||||
<Badge variant={
|
||||
selectedTicket?.status === 'open' ? 'default' :
|
||||
selectedTicket?.status === 'in-progress' ? 'secondary' :
|
||||
selectedTicket?.status === 'resolved' ? 'success' : 'outline'
|
||||
}>
|
||||
{selectedTicket?.status}
|
||||
</Badge>
|
||||
<Badge variant={
|
||||
selectedTicket?.priority === 'low' ? 'success' :
|
||||
selectedTicket?.priority === 'medium' ? 'warning' : 'destructive'
|
||||
}>
|
||||
{selectedTicket?.priority}
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-500">
|
||||
Created {formatDistanceToNow(new Date(selectedTicket?.createdAt || ''), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4 max-h-[60vh] overflow-y-auto">
|
||||
{renderMessages()}
|
||||
</div>
|
||||
|
||||
{selectedTicket?.status !== 'closed' && (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={() => selectedTicket && addMessage(selectedTicket.id)}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
src/components/UpdateAvatar.tsx
Normal file
52
src/components/UpdateAvatar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
|
||||
interface SessionData {
|
||||
[key: string]: any;
|
||||
@@ -21,37 +22,61 @@ interface UserData {
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
const [invoiceList, setInvoiceList] = useState<any[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const fetchSessionData = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'http://localhost:2058/host-api/v1/users/get-profile-data/',
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
credentials: 'include', // Crucial for cookies
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || 'Session fetch failed');
|
||||
}
|
||||
const data: UserData = await response.json();
|
||||
// console.log('success message', data.success);
|
||||
if(data.success === true){
|
||||
setUserData(data);
|
||||
// console.log('User Data', data);
|
||||
}
|
||||
|
||||
setUserData(data);
|
||||
return data.session_data;
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setError(error instanceof Error ? error.message : 'An unknown error occurred');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const getInvoiceListData = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include', // Crucial for cookies
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data); // Fix: Use `data.data` instead of `data`
|
||||
return data.data; // Fix: `session_data` does not exist in response
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fetchData();
|
||||
|
||||
fetchSessionData();
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
@@ -63,12 +88,6 @@ export default function ProfilePage() {
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6 container mx-auto">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Profile</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Update your profile settings.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||
<div className="flex-1 lg:max-w-2xl">
|
||||
@@ -85,14 +104,8 @@ export default function ProfilePage() {
|
||||
<AvatarImage src={userData.session_data?.user_avatar} />
|
||||
<AvatarFallback>JP</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<Button size="sm">
|
||||
Change avatar
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
JPG, GIF or PNG. 1MB max.
|
||||
</p>
|
||||
</div>
|
||||
<UpdateAvatar />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
@@ -111,56 +124,39 @@ export default function ProfilePage() {
|
||||
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio">Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
defaultValue="I'm a software developer based in New York."
|
||||
className="resize-none"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your application preferences.
|
||||
</CardDescription>
|
||||
<CardTitle>Billing Information</CardTitle>
|
||||
<CardDescription>
|
||||
View your billing history.
|
||||
</CardDescription>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Invoice ID</th>
|
||||
<th className="text-left">Date</th>
|
||||
<th className="text-left">Description</th>
|
||||
<th className="text-right">Amount</th>
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
invoiceList.map((invoice) => (
|
||||
<tr key={invoice.id}>
|
||||
<td>{invoice.invoice_id}</td>
|
||||
<td>{invoice.date}</td>
|
||||
<td>{invoice.description}</td>
|
||||
<td className="text-right">{invoice.amount}</td>
|
||||
<td className="text-center"><a href="">Print</a></td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">Language</Label>
|
||||
<Select defaultValue="english">
|
||||
<SelectTrigger id="language">
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="english">English</SelectItem>
|
||||
<SelectItem value="french">French</SelectItem>
|
||||
<SelectItem value="german">German</SelectItem>
|
||||
<SelectItem value="spanish">Spanish</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<Select defaultValue="est">
|
||||
<SelectTrigger id="timezone">
|
||||
<SelectValue placeholder="Select timezone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="est">Eastern Standard Time (EST)</SelectItem>
|
||||
<SelectItem value="cst">Central Standard Time (CST)</SelectItem>
|
||||
<SelectItem value="mst">Mountain Standard Time (MST)</SelectItem>
|
||||
<SelectItem value="pst">Pacific Standard Time (PST)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -188,8 +184,7 @@ export default function ProfilePage() {
|
||||
<Button className="mt-4">Update password</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Danger Zone</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -198,7 +193,7 @@ export default function ProfilePage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Button >Delete account</Button>
|
||||
<Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
|
||||
<Button variant="outline">Export data</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
43
src/components/ui/CustomTabs.tsx
Normal file
43
src/components/ui/CustomTabs.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
|
||||
interface Tab {
|
||||
label: string;
|
||||
value: string;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
interface CustomTabsProps {
|
||||
tabs: Tab[];
|
||||
defaultValue?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function CustomTabs({ tabs, defaultValue, onValueChange }: CustomTabsProps) {
|
||||
const [activeTab, setActiveTab] = React.useState(defaultValue || tabs[0]?.value || '');
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
if (onValueChange) {
|
||||
onValueChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex border-b border-[#6d9e37]">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
className={`px-4 py-2 font-medium text-sm focus:outline-none ${activeTab === tab.value ? 'border-x border-t border-[#6d9e37] text-[#6d9e37] rounded-t-md' : 'text-[#c7cccb]'}`}
|
||||
onClick={() => handleTabChange(tab.value)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
{tabs.find((tab) => tab.value === activeTab)?.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/components/ui/badge.tsx
Normal file
40
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
success:
|
||||
"border-transparent bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
||||
warning:
|
||||
"border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
||||
info: "border-transparent bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
16
src/components/ui/container.tsx
Normal file
16
src/components/ui/container.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<div className={`max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Container;
|
||||
104
src/components/ui/date-format.tsx
Normal file
104
src/components/ui/date-format.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as React from "react";
|
||||
|
||||
// Utility function with proper error handling
|
||||
export function customFormatDistanceToNow(
|
||||
date: Date | string | number,
|
||||
options: { addSuffix?: boolean } = { addSuffix: true }
|
||||
): string {
|
||||
try {
|
||||
const dateObj = typeof date === "string" || typeof date === "number"
|
||||
? new Date(date)
|
||||
: date;
|
||||
|
||||
// Check for invalid date
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
return "Invalid date";
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const diffInSeconds = (now.getTime() - dateObj.getTime()) / 1000;
|
||||
|
||||
// Check for infinite or NaN values
|
||||
if (!isFinite(diffInSeconds)) {
|
||||
return "Invalid date range";
|
||||
}
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
||||
|
||||
const absoluteSeconds = Math.abs(diffInSeconds);
|
||||
let value: number;
|
||||
let unit: Intl.RelativeTimeFormatUnit;
|
||||
|
||||
if (absoluteSeconds < 60) {
|
||||
value = Math.round(diffInSeconds);
|
||||
unit = 'second';
|
||||
} else if (absoluteSeconds < 3600) {
|
||||
value = Math.round(diffInSeconds / 60);
|
||||
unit = 'minute';
|
||||
} else if (absoluteSeconds < 86400) {
|
||||
value = Math.round(diffInSeconds / 3600);
|
||||
unit = 'hour';
|
||||
} else if (absoluteSeconds < 2592000) {
|
||||
value = Math.round(diffInSeconds / 86400);
|
||||
unit = 'day';
|
||||
} else if (absoluteSeconds < 31536000) {
|
||||
value = Math.round(diffInSeconds / 2592000);
|
||||
unit = 'month';
|
||||
} else {
|
||||
value = Math.round(diffInSeconds / 31536000);
|
||||
unit = 'year';
|
||||
}
|
||||
|
||||
// Ensure value is finite before formatting
|
||||
if (!isFinite(value)) {
|
||||
return "Invalid date range";
|
||||
}
|
||||
|
||||
let result = rtf.format(value, unit);
|
||||
|
||||
if (!options.addSuffix) {
|
||||
result = result.replace(/^in /, '').replace(/ ago$/, '');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error);
|
||||
return "Invalid date";
|
||||
}
|
||||
}
|
||||
|
||||
// React component interface
|
||||
interface DateFormatProps {
|
||||
date: Date | string | number;
|
||||
className?: string;
|
||||
addSuffix?: boolean;
|
||||
fallback?: string;
|
||||
}
|
||||
|
||||
// React component implementation
|
||||
const DateFormat: React.FC<DateFormatProps> = ({
|
||||
date,
|
||||
className = "",
|
||||
addSuffix = true,
|
||||
fallback = "Invalid date",
|
||||
}) => {
|
||||
let formattedDate: string;
|
||||
try {
|
||||
formattedDate = customFormatDistanceToNow(date, { addSuffix });
|
||||
} catch (error) {
|
||||
formattedDate = fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<time
|
||||
dateTime={new Date(date).toISOString()}
|
||||
className={className}
|
||||
title={new Date(date).toLocaleString()}
|
||||
>
|
||||
{formattedDate}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
|
||||
// Export both with distinct names
|
||||
export { DateFormat, customFormatDistanceToNow as formatDistanceToNow };
|
||||
@@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -22,3 +22,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
|
||||
|
||||
62
src/components/ui/table.tsx
Normal file
62
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
interface TableProps {
|
||||
headers: string[];
|
||||
data: any[];
|
||||
className?: string;
|
||||
caption?: string;
|
||||
striped?: boolean;
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
const Table: React.FC<TableProps> = ({
|
||||
headers,
|
||||
data,
|
||||
className = '',
|
||||
caption,
|
||||
striped = false,
|
||||
hover = false
|
||||
}) => {
|
||||
return (
|
||||
<div className={`overflow-x-auto ${className}`}>
|
||||
<table className="min-w-full bg-white border border-gray-200 rounded-lg">
|
||||
{caption && (
|
||||
<caption className="text-sm text-gray-500 p-2 text-left">
|
||||
{caption}
|
||||
</caption>
|
||||
)}
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{headers.map((header, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={`${striped && index % 2 === 0 ? 'bg-gray-50' : ''} ${
|
||||
hover ? 'hover:bg-gray-100' : ''
|
||||
}`}
|
||||
>
|
||||
{Object.values(row).map((value, idx) => (
|
||||
<td
|
||||
key={idx}
|
||||
className="px-6 py-4 text-sm text-gray-800"
|
||||
>
|
||||
{value as React.ReactNode}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
33
src/components/ui/tabs.tsx
Normal file
33
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface CustomTabsProps {
|
||||
tabs: { label: string; value: string; content: React.ReactNode }[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CustomTabs: React.FC<CustomTabsProps> = ({ tabs, className }) => {
|
||||
return (
|
||||
<Tabs.Root defaultValue={tabs[0]?.value} className={cn("w-full", className)}>
|
||||
<Tabs.List className="flex border-b border-neutral-600 bg-neutral-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Trigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="px-4 py-2 text-sm text-neutral-400 hover:text-white focus-visible:ring-2 focus-visible:ring-[#6d9e37]"
|
||||
>
|
||||
{tab.label}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Content key={tab.value} value={tab.value} className="p-4 text-neutral-300">
|
||||
{tab.content}
|
||||
</Tabs.Content>
|
||||
))}
|
||||
</Tabs.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export { CustomTabs };
|
||||
42
src/components/ui/typography.tsx
Normal file
42
src/components/ui/typography.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { ElementType } from 'react';
|
||||
|
||||
interface TypographyProps {
|
||||
variant: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body1' | 'body2' | 'caption';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
color?: string; // Add color prop
|
||||
}
|
||||
|
||||
const Typography: React.FC<TypographyProps> = ({ variant, children, className = '', color }) => {
|
||||
const variantStyles: Record<string, React.CSSProperties> = {
|
||||
h1: { fontSize: '2.5rem', fontWeight: 'bold' },
|
||||
h2: { fontSize: '2rem', fontWeight: 'bold' },
|
||||
h3: { fontSize: '1.75rem', fontWeight: 'bold' },
|
||||
h4: { fontSize: '1.5rem', fontWeight: 'bold' },
|
||||
h5: { fontSize: '1.25rem', fontWeight: 'bold' },
|
||||
h6: { fontSize: '1rem', fontWeight: 'bold' },
|
||||
body1: { fontSize: '1rem', fontWeight: 'normal' },
|
||||
body2: { fontSize: '0.875rem', fontWeight: 'normal' },
|
||||
caption: { fontSize: '0.75rem', fontWeight: 'normal' },
|
||||
};
|
||||
|
||||
const combinedStyle = { ...variantStyles[variant], ...(color && { color }), ...(className ? { className } : {}) };
|
||||
|
||||
const variantToTag: Record<string, ElementType> = {
|
||||
h1: 'h1',
|
||||
h2: 'h2',
|
||||
h3: 'h3',
|
||||
h4: 'h4',
|
||||
h5: 'h5',
|
||||
h6: 'h6',
|
||||
body1: 'p',
|
||||
body2: 'p',
|
||||
caption: 'span',
|
||||
};
|
||||
|
||||
const Tag: ElementType = variantToTag[variant];
|
||||
|
||||
return <Tag style={combinedStyle}>{children}</Tag>;
|
||||
};
|
||||
|
||||
export default Typography;
|
||||
37
src/lib/localizeTime.tsx
Normal file
37
src/lib/localizeTime.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
export function localizeTime(timeValue: string, targetTimeZone?: string): string {
|
||||
// 1. Auto-detect user's timezone if none provided
|
||||
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 formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: targetTimeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
const [
|
||||
{ value: month },,
|
||||
{ value: day },,
|
||||
{ value: year },,
|
||||
{ value: hour },,
|
||||
{ value: minute },,
|
||||
{ value: second }
|
||||
] = formatter.formatToParts(date);
|
||||
|
||||
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)
|
||||
@@ -4,3 +4,9 @@ import { twMerge } from 'tailwind-merge';
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// Type helpers
|
||||
export type MergeElementProps<
|
||||
T extends React.ElementType,
|
||||
P extends object = {}
|
||||
> = Omit<React.ComponentPropsWithRef<T>, keyof P> & P;
|
||||
7
src/pages/print-invoice.astro
Normal file
7
src/pages/print-invoice.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Invoice from "../components/PrintInvoice";
|
||||
---
|
||||
<Layout title="">
|
||||
<Invoice client:load/>
|
||||
</Layout>
|
||||
7
src/pages/ticket.astro
Normal file
7
src/pages/ticket.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import TickitingSystem from "../components/Ticket";
|
||||
---
|
||||
<Layout title="">
|
||||
<TickitingSystem client:load />
|
||||
</Layout>
|
||||
Reference in New Issue
Block a user