Compare commits

..

1 Commits

Author SHA1 Message Date
Suvodip dd8b979915 about page text justifyed 2025-03-21 19:20:52 +05:30
238 changed files with 1544 additions and 38153 deletions

3
.gitignore vendored
View File

@ -1,4 +1,3 @@
public/robots.txt
# build output
dist/
# generated types
@ -6,7 +5,7 @@ dist/
# dependencies
node_modules/
public/host-api/
# logs
npm-debug.log*
yarn-debug.log*

View File

@ -1,14 +0,0 @@
### git flow guide / protocol
# dev - any one is free to create / push to / any experiment on this branch
# staging - it is the stage for all developer. all branchout and push over here.
*start point / fork point is staging, i.e. any fix/feature/improvement - you must start branching out from staging.
*better to name your branch with 'z-' , i.e. z-layout-footer-improvement (z-: identifier for temporary and safe to remove from the central repo -> then the file identifier -> then short info regarding the intent)
*many developers prefer feat/feature_name, i faced issue managing those branch using cli (bsd even alpine) for the slash(/)
## staging must have a automated test and deploy mechanism, so that developers can can always be on the same page regarding build/merge conflict issue
# test - staging to test by tester
# master - test to master by tester after full test
# release - master to release with version tag
final packaging and release

View File

@ -3,20 +3,13 @@ import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
site: 'https://siliconpin.cs1.hz.siliconpin.com',
vite: {
server: {
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
},
preview: {
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
}
},
site: 'https://siliconpin.com',
integrations: [tailwind(), react()],
server: {
host: '0.0.0.0',
port: 4000,
port: 3000
},
output: 'static',
});

View File

@ -1,42 +0,0 @@
{
"meta": {
"expiry": "2025-06-29 11:36:07.506Z",
"rawUser": {
"email": "suvodipghosh35@gmail.com",
"id": "3913660032212288",
"name": "Suvodip Ghosh",
"picture": {
"data": {
"height": 201,
"is_silhouette": false,
"url": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
"width": 200
}
}
},
"id": "3913660032212288",
"name": "Suvodip Ghosh",
"username": "",
"email": "suvodipghosh35@gmail.com",
"avatarURL": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
"accessToken": "EAATKn4EGuGABO5BQDFZBCNX6QrVyfsUZBCfJKov5OzHHrQIAEu5wOePaLcvIZBklT9MPkoiZCrZCraJNIwtBzGrKxZCeUMwKJRAbicZAPuK1gesmDl27iL3J7vp0sAjMoue4yACIZAIRDZBzsDU0Yi91ztFJ6FOJ2qWVfLkTgjkKwEudTe95vUYcN1E9oS6bo6smA75KdVB6RgGiK217C4ez9aRDrQcvH3ZBSGZBY2QZBW1KynycojdqhKAQS68xT1Bpezv80kEP5DQfONMMNbaUOwBU9r0WxwS0viCbW0qZAk92mXYZAZBQhny5Ili4aP1jmAZD",
"refreshToken": "",
"avatarUrl": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3NzAsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.5Xz_0hoAod9YLgeOTHbWTEKdRxqusZPUDyb2D0SfVKQ"
}

View File

@ -1,77 +0,0 @@
{
"meta": {
"expiry": "",
"rawUser": {
"avatar_url": "https://avatars.githubusercontent.com/u/128118374?v=4",
"bio": null,
"blog": "",
"collaborators": 0,
"company": null,
"created_at": "2023-03-17T04:49:12Z",
"disk_usage": 55453,
"email": null,
"events_url": "https://api.github.com/users/suvodip35/events{/privacy}",
"followers": 0,
"followers_url": "https://api.github.com/users/suvodip35/followers",
"following": 1,
"following_url": "https://api.github.com/users/suvodip35/following{/other_user}",
"gists_url": "https://api.github.com/users/suvodip35/gists{/gist_id}",
"gravatar_id": "",
"hireable": null,
"html_url": "https://github.com/suvodip35",
"id": 128118374,
"location": null,
"login": "suvodip35",
"name": "Suvodip",
"node_id": "U_kgDOB6LuZg",
"notification_email": null,
"organizations_url": "https://api.github.com/users/suvodip35/orgs",
"owned_private_repos": 4,
"plan": {
"collaborators": 0,
"name": "free",
"private_repos": 10000,
"space": 976562499
},
"private_gists": 0,
"public_gists": 0,
"public_repos": 4,
"received_events_url": "https://api.github.com/users/suvodip35/received_events",
"repos_url": "https://api.github.com/users/suvodip35/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/suvodip35/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/suvodip35/subscriptions",
"total_private_repos": 4,
"twitter_username": null,
"two_factor_authentication": true,
"type": "User",
"updated_at": "2025-04-30T08:46:03Z",
"url": "https://api.github.com/users/suvodip35",
"user_view_type": "private"
},
"id": "128118374",
"name": "Suvodip",
"username": "suvodip35",
"email": "suvodipghosh35@gmail.com",
"avatarURL": "https://avatars.githubusercontent.com/u/128118374?v=4",
"accessToken": "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",
"refreshToken": "",
"avatarUrl": "https://avatars.githubusercontent.com/u/128118374?v=4"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc2NDQsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.XLlZwZ3TrHWlIoxOhkLoKW7AbTPr6VIB49fRpNCTVbY"
}

View File

@ -1,38 +0,0 @@
{
"meta": {
"expiry": "2025-04-30 12:35:11.096Z",
"rawUser": {
"email": "suvoairtel@gmail.com",
"family_name": "GHOSH",
"given_name": "SUBHODIP",
"id": "112026834578335832361",
"name": "SUBHODIP GHOSH",
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
"verified_email": true
},
"id": "112026834578335832361",
"name": "SUBHODIP GHOSH",
"username": "",
"email": "suvoairtel@gmail.com",
"avatarURL": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
"accessToken": "ya29.a0AZYkNZiV6bkpVlDOQy2ayy6WCULOE5uWYi_QvRSN9vUqJgUxuxcn3xlUzbe6rOHQV7UCjPb_aCmiOojHCMWsCvtky80pUBNCI2rGGhCZYjpSMTispb9qvPyO7CNgsqh5YGxTUUXAkuz37uXm68MFv5YOoofMZ1f39f2OU0sIhisaCgYKAbgSARMSFQHGX2MiEUQ1pMH6u7K92FgWTPp4Uw0178",
"refreshToken": "",
"avatarUrl": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3MTMsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.Wig_LpSxOd03z389n186t2AwUu1Ypxmj3Apf5LdciaQ"
}

12239
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,50 +3,22 @@
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev --port 4000 --host",
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview --port 4000",
"astro": "astro",
"push-prod": "rsync -rv --exclude .hta_config/conf.php dist/ u2@siliconpin.com:~/web/siliconpin.com/public_html/"
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^4.2.1",
"@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",
"@react-pdf/renderer": "^4.3.0",
"@shadcn/ui": "^0.0.4",
"@types/date-fns": "^2.5.3",
"@types/react": "^19.0.12",
"@uiw/react-md-editor": "^3.25.6",
"add": "^2.0.6",
"astro": "^5.5.2",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"image-resize-compress": "^2.1.1",
"lucide-react": "^0.484.0",
"marked": "^15.0.8",
"menubar": "^9.5.1",
"minio": "^8.0.5",
"pocketbase": "^0.25.2",
"postcss": "^8.5.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-qr-code": "^2.0.15",
"react-router-dom": "^7.5.1",
"react-simplemde-editor": "^5.2.0",
"react-to-print": "^3.0.5",
"rehype-rewrite": "^4.0.2",
"shadcn": "^2.5.0",
"simplemde": "^1.11.2",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"xlsx": "^0.18.5"
"tailwindcss": "^3.4.17"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -1,8 +0,0 @@
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

View File

@ -1,39 +0,0 @@
{
"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"
}
]
}
}
}

View File

@ -1,7 +0,0 @@
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

View File

@ -1,39 +0,0 @@
{
"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"
}
]
}
}
}

View File

@ -1,14 +0,0 @@
-----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-----

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 430 B

View File

@ -1 +0,0 @@
<svg width="30px" height="30px" 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="M4.4955 7.44088C3.54724 8.11787 2.77843 8.84176 2.1893 9.47978C0.857392 10.9222 0.857393 13.0778 2.1893 14.5202C3.9167 16.391 7.18879 19 12 19C13.2958 19 14.4799 18.8108 15.5523 18.4977L13.8895 16.8349C13.2936 16.9409 12.6638 17 12 17C7.9669 17 5.18832 14.82 3.65868 13.1634C3.03426 12.4872 3.03426 11.5128 3.65868 10.8366C4.23754 10.2097 4.99526 9.50784 5.93214 8.87753L4.4955 7.44088Z" fill="#6d9e37"></path> <path d="M8.53299 11.4784C8.50756 11.6486 8.49439 11.8227 8.49439 12C8.49439 13.933 10.0614 15.5 11.9944 15.5C12.1716 15.5 12.3458 15.4868 12.516 15.4614L8.53299 11.4784Z" fill="#6d9e37"></path> <path d="M15.4661 12.4471L11.5473 8.52829C11.6937 8.50962 11.8429 8.5 11.9944 8.5C13.9274 8.5 15.4944 10.067 15.4944 12C15.4944 12.1515 15.4848 12.3007 15.4661 12.4471Z" fill="#6d9e37"></path> <path d="M18.1118 15.0928C19.0284 14.4702 19.7715 13.7805 20.3413 13.1634C20.9657 12.4872 20.9657 11.5128 20.3413 10.8366C18.8117 9.18002 16.0331 7 12 7C11.3594 7 10.7505 7.05499 10.1732 7.15415L8.50483 5.48582C9.5621 5.1826 10.7272 5 12 5C16.8112 5 20.0833 7.60905 21.8107 9.47978C23.1426 10.9222 23.1426 13.0778 21.8107 14.5202C21.2305 15.1486 20.476 15.8603 19.5474 16.5284L18.1118 15.0928Z" fill="#6d9e37"></path> <path d="M2.00789 3.42207C1.61736 3.03155 1.61736 2.39838 2.00789 2.00786C2.39841 1.61733 3.03158 1.61733 3.4221 2.00786L22.0004 20.5862C22.391 20.9767 22.391 21.6099 22.0004 22.0004C21.6099 22.3909 20.9767 22.3909 20.5862 22.0004L2.00789 3.42207Z" fill="#6d9e37"></path> </g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1 +0,0 @@
<svg width="30px" height="30px" 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 fill-rule="evenodd" clip-rule="evenodd" d="M11.9944 15.5C13.9274 15.5 15.4944 13.933 15.4944 12C15.4944 10.067 13.9274 8.5 11.9944 8.5C10.0614 8.5 8.49439 10.067 8.49439 12C8.49439 13.933 10.0614 15.5 11.9944 15.5ZM11.9944 13.4944C11.1691 13.4944 10.5 12.8253 10.5 12C10.5 11.1747 11.1691 10.5056 11.9944 10.5056C12.8197 10.5056 13.4888 11.1747 13.4888 12C13.4888 12.8253 12.8197 13.4944 11.9944 13.4944Z" fill="#6d9e37"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 5C7.18879 5 3.9167 7.60905 2.1893 9.47978C0.857392 10.9222 0.857393 13.0778 2.1893 14.5202C3.9167 16.391 7.18879 19 12 19C16.8112 19 20.0833 16.391 21.8107 14.5202C23.1426 13.0778 23.1426 10.9222 21.8107 9.47978C20.0833 7.60905 16.8112 5 12 5ZM3.65868 10.8366C5.18832 9.18002 7.9669 7 12 7C16.0331 7 18.8117 9.18002 20.3413 10.8366C20.9657 11.5128 20.9657 12.4872 20.3413 13.1634C18.8117 14.82 16.0331 17 12 17C7.9669 17 5.18832 14.82 3.65868 13.1634C3.03426 12.4872 3.03426 11.5128 3.65868 10.8366Z" fill="#6d9e37"></path> </g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/>
</svg>

Before

Width:  |  Height:  |  Size: 373 B

View File

@ -1 +0,0 @@
<svg width="40px" height="40px" viewBox="0 0 32 32" 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"> <circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7208)"></circle> <path d="M21.2137 20.2816L21.8356 16.3301H17.9452V13.767C17.9452 12.6857 18.4877 11.6311 20.2302 11.6311H22V8.26699C22 8.26699 20.3945 8 18.8603 8C15.6548 8 13.5617 9.89294 13.5617 13.3184V16.3301H10V20.2816H13.5617V29.8345C14.2767 29.944 15.0082 30 15.7534 30C16.4986 30 17.2302 29.944 17.9452 29.8345V20.2816H21.2137Z" fill="white"></path> <defs> <linearGradient id="paint0_linear_87_7208" x1="16" y1="2" x2="16" y2="29.917" gradientUnits="userSpaceOnUse"> <stop stop-color="#18ACFE"></stop> <stop offset="1" stop-color="#0163E0"></stop> </linearGradient> </defs> </g></svg>

Before

Width:  |  Height:  |  Size: 909 B

View File

@ -1 +0,0 @@
<svg width="40px" height="40px" viewBox="-1.6 -1.6 19.20 19.20" xmlns="http://www.w3.org/2000/svg" fill="#000000" class="bi bi-github"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(1.3600000000000003,1.3600000000000003), scale(0.83)"><rect x="-1.6" y="-1.6" width="19.20" height="19.20" rx="9.6" fill="#ffffff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path> </g></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +0,0 @@
<svg width="40px" height="40px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill="#4285F4" d="M14.9 8.161c0-.476-.039-.954-.121-1.422h-6.64v2.695h3.802a3.24 3.24 0 01-1.407 2.127v1.75h2.269c1.332-1.22 2.097-3.02 2.097-5.15z"></path><path fill="#34A853" d="M8.14 15c1.898 0 3.499-.62 4.665-1.69l-2.268-1.749c-.631.427-1.446.669-2.395.669-1.836 0-3.393-1.232-3.952-2.888H1.85v1.803A7.044 7.044 0 008.14 15z"></path><path fill="#FBBC04" d="M4.187 9.342a4.17 4.17 0 010-2.68V4.859H1.849a6.97 6.97 0 000 6.286l2.338-1.803z"></path><path fill="#EA4335" d="M8.14 3.77a3.837 3.837 0 012.7 1.05l2.01-1.999a6.786 6.786 0 00-4.71-1.82 7.042 7.042 0 00-6.29 3.858L4.186 6.66c.556-1.658 2.116-2.89 3.952-2.89z"></path></g></svg>

Before

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,12 +0,0 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/image-resize-compress@2.1.1/dist/index.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
var h=e=>({png:"image/png",webp:"image/webp",bmp:"image/bmp",gif:"image/gif",jpeg:"image/jpeg"})[e]||"image/jpeg",w=(e,t="auto",r="auto")=>{let o=t==="auto"||t===0,m=r==="auto"||r===0;if(!o&&!m)return{width:t,height:r};if(!o){let n=e.naturalWidth/t;return{width:t,height:Math.round((e.naturalHeight/n+Number.EPSILON)*100)/100}}if(!m){let n=e.naturalHeight/r;return{width:Math.round((e.naturalWidth/n+Number.EPSILON)*100)/100,height:r}}return{width:e.naturalWidth,height:e.naturalHeight}},E=async(e,t=100,r="auto",o="auto",m=null,n=null)=>{if(!(e instanceof Blob))throw new TypeError(`Expected a Blob or File, but got ${typeof e}.`);if(e.size===0)throw new Error("Failed to load the image. The file might be corrupt or empty.");if(t<=0)throw new RangeError("Quality must be greater than 0.");if(typeof r=="number"&&r<0||typeof o=="number"&&o<0)throw new RangeError("Invalid width or height value!");let a=m?h(m):e.type,g=t<1?t:t/100;return new Promise((p,l)=>{let u=new FileReader;u.onload=()=>{let s=new Image;s.src=u.result,s.onload=()=>{let i=document.createElement("canvas"),d=w(s,r,o);i.width=d.width,i.height=d.height;let f=i.getContext("2d");if(!f){l(new Error("Failed to get canvas context."));return}n&&a==="image/png"&&(f.fillStyle=n,f.fillRect(0,0,i.width,i.height)),f.drawImage(s,0,0,i.width,i.height),i.toBlob(b=>{if(b===null)return l(new Error("Failed to generate image blob."));p(new Blob([b],{type:a}))},a,g)},s.onerror=()=>{l(new Error("Failed to load the image. The file might be corrupt or empty."))}},u.onerror=()=>l(new Error("Failed to read the blob as a Data URL.")),u.readAsDataURL(e)})},c=E;var F=async(e,t=100,r="auto",o="auto",m,n)=>{try{let a=await fetch(e,n);if(!a.ok)throw new Error(`Failed to fetch image: ${a.statusText}`);let g=await a.blob();return await c(g,t,r,o,m)}catch(a){throw new Error(`Failed to process the image from URL. Check CORS or network issues. Error: ${a}`)}},y=F;var I=e=>new Promise((t,r)=>{if(e.size===0)return r(new Error("Cannot convert empty Blob."));if(e.size>10485760)return r(new Error("File size exceeds the maximum allowed limit."));let o=new FileReader;o.onloadend=()=>{o.result?t(o.result):r(new Error("Failed to convert blob to DataURL."))},o.onerror=()=>r(new Error("Error reading blob.")),o.readAsDataURL(e)}),R=I;var L=async(e,t)=>{try{let r=await fetch(e,t);if(!r.ok)throw new Error(`Failed to fetch image: ${r.statusText}`);return await r.blob()}catch(r){throw new Error(`Failed to fetch image from URL. Error: ${r}`)}},T=L;export{R as blobToURL,c as fromBlob,y as fromURL,T as urlToBlob};
/*!
* image-resize-compress
* Copyright(c) 2024 Álef Duarte
* MIT Licensed
*/

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
</svg>

Before

Width:  |  Height:  |  Size: 437 B

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
<svg width="64px" height="64px" viewBox="0 -140 780 780" enable-background="new 0 0 780 500" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="m168.38 169.35c-8.399-5.774-19.359-8.668-32.88-8.668h-52.346c-4.145 0-6.435 2.073-6.87 6.215l-21.264 133.48c-0.221 1.311 0.107 2.51 0.981 3.6 0.869 1.092 1.962 1.635 3.271 1.635h24.864c4.361 0 6.758-2.068 7.198-6.215l5.888-35.986c0.215-1.744 0.982-3.162 2.291-4.254 1.308-1.09 2.944-1.803 4.907-2.129 1.963-0.324 3.814-0.488 5.562-0.488 1.743 0 3.814 0.111 6.217 0.328 2.397 0.217 3.925 0.324 4.58 0.324 18.756 0 33.478-5.285 44.167-15.867 10.684-10.576 16.032-25.242 16.032-44.004 0-12.868-4.203-22.191-12.598-27.974zm-26.989 40.08c-1.094 7.635-3.926 12.649-8.506 15.049-4.581 2.403-11.124 3.599-19.629 3.599l-10.797 0.326 5.563-35.007c0.434-2.397 1.851-3.597 4.252-3.597h6.218c8.72 0 15.049 1.257 18.975 3.761 3.924 2.51 5.233 7.801 3.924 15.869z" fill="#003087"></path><path d="m720.79 160.68h-24.207c-2.406 0-3.822 1.2-4.254 3.601l-21.266 136.1-0.328 0.654c0 1.096 0.436 2.127 1.311 3.109 0.867 0.98 1.963 1.471 3.27 1.471h21.596c4.137 0 6.428-2.068 6.871-6.215l21.264-133.81v-0.325c-1e-3 -3.055-1.423-4.581-4.257-4.581z" fill="#009CDE"></path><path d="m428.31 213.36c0-1.088-0.438-2.126-1.305-3.105-0.875-0.981-1.857-1.475-2.945-1.475h-25.191c-2.404 0-4.367 1.096-5.891 3.271l-34.678 51.039-14.395-49.074c-1.096-3.487-3.492-5.236-7.197-5.236h-24.541c-1.093 0-2.074 0.492-2.941 1.475-0.875 0.979-1.309 2.019-1.309 3.105 0 0.439 2.127 6.871 6.379 19.303 4.252 12.436 8.832 25.85 13.74 40.246 4.908 14.393 7.469 22.031 7.688 22.896-17.886 24.432-26.825 37.518-26.825 39.26 0 2.838 1.415 4.254 4.253 4.254h25.191c2.398 0 4.36-1.088 5.89-3.27l83.427-120.4c0.433-0.432 0.65-1.192 0.65-2.29z" fill="#003087"></path><path d="m662.89 208.78h-24.865c-3.057 0-4.904 3.6-5.559 10.799-5.678-8.722-16.031-13.089-31.084-13.089-15.703 0-29.064 5.89-40.076 17.668-11.016 11.778-16.521 25.632-16.521 41.552 0 12.871 3.762 23.121 11.285 30.752 7.525 7.639 17.611 11.451 30.266 11.451 6.324 0 12.758-1.311 19.301-3.926 6.543-2.617 11.664-6.105 15.379-10.469 0 0.219-0.223 1.197-0.654 2.941-0.441 1.748-0.656 3.061-0.656 3.926 0 3.494 1.414 5.234 4.254 5.234h22.576c4.139 0 6.541-2.068 7.193-6.215l13.416-85.39c0.215-1.31-0.111-2.507-0.982-3.599-0.877-1.088-1.965-1.635-3.273-1.635zm-42.694 64.454c-5.562 5.453-12.27 8.178-20.121 8.178-6.328 0-11.449-1.742-15.377-5.234-3.928-3.482-5.891-8.281-5.891-14.395 0-8.064 2.727-14.886 8.182-20.447 5.445-5.562 12.213-8.342 20.283-8.342 6.102 0 11.174 1.799 15.213 5.396 4.031 3.6 6.055 8.562 6.055 14.889-2e-3 7.851-2.783 14.505-8.344 19.955z" fill="#009CDE"></path><path d="m291.23 208.78h-24.865c-3.058 0-4.908 3.6-5.563 10.799-5.889-8.722-16.25-13.089-31.081-13.089-15.704 0-29.065 5.89-40.078 17.668-11.016 11.778-16.521 25.632-16.521 41.552 0 12.871 3.763 23.121 11.288 30.752 7.525 7.639 17.61 11.451 30.262 11.451 6.104 0 12.433-1.311 18.975-3.926 6.543-2.617 11.778-6.105 15.704-10.469-0.875 2.615-1.309 4.906-1.309 6.867 0 3.494 1.417 5.234 4.253 5.234h22.574c4.141 0 6.543-2.068 7.198-6.215l13.413-85.39c0.215-1.31-0.111-2.507-0.981-3.599-0.873-1.088-1.962-1.635-3.269-1.635zm-42.695 64.616c-5.563 5.35-12.382 8.016-20.447 8.016-6.329 0-11.4-1.742-15.214-5.234-3.819-3.482-5.726-8.281-5.726-14.395 0-8.064 2.725-14.886 8.18-20.447 5.449-5.562 12.211-8.343 20.284-8.343 6.104 0 11.175 1.8 15.214 5.397 4.032 3.6 6.052 8.562 6.052 14.889-1e-3 8.07-2.781 14.779-8.343 20.117z" fill="#003087"></path><path d="m540.04 169.35c-8.398-5.774-19.355-8.668-32.879-8.668h-52.02c-4.363 0-6.764 2.073-7.197 6.215l-21.266 133.48c-0.221 1.311 0.107 2.51 0.982 3.6 0.865 1.092 1.961 1.635 3.27 1.635h26.826c2.617 0 4.361-1.416 5.236-4.252l5.889-37.949c0.217-1.744 0.98-3.162 2.291-4.254 1.309-1.09 2.943-1.803 4.908-2.129 1.961-0.324 3.812-0.488 5.561-0.488 1.744 0 3.814 0.111 6.215 0.328 2.398 0.217 3.93 0.324 4.58 0.324 18.76 0 33.479-5.285 44.168-15.867 10.688-10.576 16.031-25.242 16.031-44.004 1e-3 -12.868-4.2-22.192-12.595-27.974zm-33.533 53.819c-4.799 3.271-11.998 4.906-21.592 4.906l-10.471 0.328 5.562-35.008c0.432-2.396 1.85-3.598 4.252-3.598h5.887c4.799 0 8.615 0.219 11.455 0.654 2.83 0.438 5.561 1.799 8.178 4.088 2.619 2.291 3.926 5.619 3.926 9.979 0 9.164-2.402 15.377-7.197 18.651z" fill="#009CDE"></path></g></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,22 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 514.56 280.44">
<defs>
<style>
.cls-1 {
fill: #414042;
}
.cls-1, .cls-2 {
stroke-width: 0px;
}
.cls-2 {
fill: #00ad7d;
}
.cls-2:hover {
fill: #FFFFFF;
}
</style>
</defs>
<path class="cls-1" d="M0,97.77h25.72v12.86c2.93-4.46,7.37-8.1,13.3-10.92,5.93-2.82,12.3-4.23,19.11-4.23,8.1,0,15.56,2.06,22.37,6.17,6.81,4.11,12.27,9.98,16.38,17.62,4.11,7.64,6.16,16.56,6.16,26.78s-2.06,19.29-6.16,26.86c-4.11,7.58-9.6,13.36-16.47,17.35-6.87,3.99-14.3,5.99-22.28,5.99-6.93,0-13.36-1.38-19.29-4.14-5.93-2.76-10.31-6.37-13.12-10.83v99.18H0V97.77ZM69.67,165.94c4.76-5.17,7.13-11.8,7.13-19.9s-2.38-14.94-7.13-20.17c-4.76-5.22-10.89-7.84-18.41-7.84s-13.68,2.61-18.5,7.84c-4.82,5.23-7.22,11.95-7.22,20.17s2.41,14.74,7.22,19.9c4.81,5.17,10.98,7.75,18.5,7.75s13.65-2.58,18.41-7.75ZM134.67,190.25c-6.87-3.99-12.36-9.78-16.47-17.35-4.11-7.58-6.17-16.53-6.17-26.86s2.05-19.14,6.17-26.78c4.11-7.63,9.57-13.5,16.38-17.62,6.81-4.11,14.27-6.17,22.37-6.17,6.81,0,13.18,1.41,19.11,4.23,5.93,2.82,10.36,6.46,13.3,10.92v-12.33h25.72v95.13h-25.72v-12.15c-2.82,4.46-7.2,8.07-13.12,10.83-5.93,2.76-12.36,4.14-19.29,4.14-7.99,0-15.41-2-22.28-5.99ZM182.41,165.94c4.76-5.17,7.14-11.8,7.14-19.9s-2.38-14.94-7.14-20.17c-4.76-5.22-10.89-7.84-18.41-7.84s-13.68,2.61-18.5,7.84c-4.82,5.23-7.22,11.95-7.22,20.17s2.41,14.74,7.22,19.9c4.81,5.17,10.98,7.75,18.5,7.75s13.65-2.58,18.41-7.75ZM272.69,196.24c-14.56,0-25.31-3.7-32.24-11.1-6.93-7.4-10.39-17.73-10.39-31v-56.37h26.07v53.73c0,7.4,1.94,12.98,5.81,16.73,3.88,3.76,9.22,5.64,16.03,5.64,6.22,0,11.6-2.05,16.12-6.17,4.52-4.11,6.78-9.69,6.78-16.73v-53.2h25.72v182.68h-25.72v-96.71c-6.11,8.34-15.5,12.51-28.19,12.51ZM348.44,252.44l142.86-43.16V75.57l-164.71-41.93v40.87h-25.72V0l213.68,56.19v171.75l-166.12,51.62v-27.13Z"/>
<path class="cls-2" d="M362.18,97.77h26.07v53.73c0,15.03,7.1,22.55,21.32,22.55s21.32-7.51,21.32-22.55v-53.73h26.07v56.37c0,13.15-4.05,23.46-12.15,30.91-8.1,7.46-19.85,11.19-35.23,11.19s-27.13-3.76-35.23-11.27c-8.1-7.52-12.16-17.79-12.16-30.83v-56.37Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.5.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.12-.07 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.963-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 446 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/>
</svg>

Before

Width:  |  Height:  |  Size: 533 B

View File

@ -1 +0,0 @@
SiliconPin is a Decentralized NonProfit Organization / Group, Creating some digital freedom, if you want to join -welcome.

View File

@ -1,48 +0,0 @@
TLDR;
=====================================================
Nirvana License, i.e. Beings needs no license
=====================================================
Preamble
------------
GNU, MIT, APACHE, Opensource, Source available -
So many options, why a new one?
To add / remove confusion may be, or
Just to remind Humans, you have evolved to as
being, you don't need one. Having license is like
having limitation, boundation not freedom, You
can have a Driving license and drive irresponsibly,
license provider / authority can impose some
nonsense rules. No matter how hard we try, Beings
don't like to be bound, limited...
They want freedom... little more - Nirvana.
-----------------------------------------------------
Experience
------------
Best of the best GPL(copyleft) and it's derivatives
are confusing which one to choose & its protected
by copyright laws, but i get the idea of 4 freedoms.
Developers don't want to deal with lawyers (seems
wasting years), we want to build something.
I have seen license violation a lot, as they have
a fleet of lawyers.
I have seen Open Source is being used a suppressor
to GNU,GPL -Freedom centric ones. Going against the
ideology of creator of the open source definition
Bruce Perens, Eric S. Raymond,
-----------------------------------------------------
Quote
--------
Every rule / law has an exception except this.
Beings do not need a license, enough to have sense.
- Suvankar Sarkar (AKA Kar)
-----------------------------------------------------
=====================================================
Mentioned some licenses above good to bad, there may be
some reason to use any existing license.
you are free to choose this one, defined below.
.....................................................

View File

@ -1,41 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGflUDcBDADbKdXMyHvxChQ2TVdNx0vemulONqiPeun2kL2PKCQ/U+us/S2i
P6JMlGIdQVzhp90R8JnLV/knydT/lPKmyKqwl1TUc+Z2oE7QtafF+E1OiF264709
9gY0d9LH9PTzsO+3456QfeT/N1HrLJcwZ79EDBa8uIwx3YFCmhzosEoNpeFFSKbr
Q74dG2WK6wXwr9xTyWjSfxSdpPTXz1DaxdzGzndmHuS87mlZM39uCdv5+x5lT66D
KmESaf1jXXTJ1SVcXO0rkwUzPeRjrFN2P0XoIhrW1zhEUIW6aV5NLBdnr0nS+WRM
LZRcfB+GLFo41eP4PNfcHAXpj7Tbn3ewqdiqS4zvpZ8EQH4pQqMU/IflEcF0zKqT
KiBqtOQL2WoI82S+pkhchMH/r+6oBzvbQK5tluQwb4+1n/tHbI3Sm+f5Rnd012vO
Iz87Hh1dj1Iaq642xaP85H4nLQseMKy8aTKyLtRXCm71jj5IffUYp+XUokF9nkwY
lGwssQ5dBq47M5EAEQEAAbQvc3V2YW5rYXIgc2Fya2FyIChLYXIpIDxzdXZhbmth
ckBzaWxpY29ucGluLmNvbT6JAdcEEwEIAEEWIQQvQZScmllcQuqOmFcxoJH9Pryu
xwUCZ+VQNwIbAwUJA8JnAAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRAx
oJH9Pryux0XlC/4xs1nfC5momwL24UhoTHPhvkNUN0vufJLPAgum71Pbe7gNYwMp
pRGzfVAlYkLB7O+JfPDcWbOSm8/fi1274eeAJOu8BhbLH4zPKZlXSXM1C2R5dKat
0ZuSO4q1a7vsrhlsDVzbgLq75la5+LjJRicRTIPEqhoX56tcwHUc0YlDLW7wViB3
xWXGemr27anqFG5sb41rqKoMgBOIcK3M9t4qgqut3WFpiIfwF9mRaI7j2yq/IlGQ
NwR+af/AttETyIT3QWxQP+zT3JMfV8++WbniI6f/ywbHlvcx5JIvoHQSKAofYJdY
O0uECg2E158Q+zvRsmFaAjudyCcKPc/9YOCQBj/N4u++INuqHVNAY5KO2IyPhB09
Q+J+N1UVbsZRB5JC5oMuCntDeATcjSlyVMgj5gaBjA1loUOjj0lc+gs4zY6cMwPe
747WsS6DE2L4C8mz1PlDacK/BhR7dle4aA8CCr4MJfsdz3zzSHs0afaVRQHNf9ac
jIiF5wvmCpSztAS5AY0EZ+VQNwEMANQ/lyRGDmC6vWWPckh04N+BnTWLff+iuMxp
LdU4h8843BEB7RdNcynwPEGV3CP9H/NjKp6kzybKzTOaGqONtuwYTTMfHQEHzdrL
E2afN3fAxPQ6e1/Bqdsz4F9z3UIKVTIJjc5CGoFeHINgCGtGbWvD9HDeHQuH/rO0
4lQjUpJtl8sbaLsBIiMabwPHnabjYvZ3E8knUuqbifaIsdPuFoqQvC0Uy3z7CaU3
qAER7sVdY9NjjiD20lG8lLivKGYTq9QXZFHJz2DLvW8aP+HY+G/kIjuw8MwGPg38
55FkR0pT8L5lJvSUDEDzMTd9i1TUImeRqw/F9AYK9dsXLPxNZ7EcMwRs/6nUjAkh
04tfHJCPwn9HeMtLFrQrg3NfhBIV0/wp8lp8lxCsgd8Cwc/lH3bU/KHdzZQwHPNi
i5F378AlrTlWoSBq5/3rkJRWPU7DavwGJwRnmFCcx1iqyfZB80/G+N0WMbL8gZjY
JoLKoytdLNWS/wlGTx2DIb4E3sStlwARAQABiQG8BBgBCAAmFiEEL0GUnJpZXELq
jphXMaCR/T68rscFAmflUDcCGwwFCQPCZwAACgkQMaCR/T68rse+7wv8CJFMnAGH
hjedAXCODUztEtak8fZkHTiJBow3tx9XxYjnQe99zhZOswBJ8CF7cakpTXMYtWRf
yA30Qf1YNwYeb5WwIG1nsep2bQzeC/LP1pu7XDmRHyI+kakhnEul/27kPHNubSIr
9h0MXtTxRFkde27DD+QzJoqvUKCNtpyhKJY7jv6+9UfpSNIZ6FmpN9hxQRv6oFgn
uoFCKjfUxhGsN2j0DxHF+WJjQ+jBcRzpORD4TfAjqej++OdUv/3NR88UMxima26H
PMZP7chiojFzMAPLniYEFXMgh+l7Z+ssdWbCxfPQq90DoZ/Vylnfmq7E9pjE54Dr
PhIcMfEYlH+erdKrHgCaCzew6Y9kNHPVTuKqWA0/105J7+umk/5Z84eVfmWzJysQ
30KHyfM5lMe1Eb++QECfzknD9xQvAb/YoVT0yHPUhCExzVZUc6InmhwvkX+nilk4
zCPxkkNCwSX1LKNkCt9dW09MLJ2Gyf3r+goOqpx7mcs5g7HQGwc9u2Vp
=NDqk
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,41 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGflUDcBDADbKdXMyHvxChQ2TVdNx0vemulONqiPeun2kL2PKCQ/U+us/S2i
P6JMlGIdQVzhp90R8JnLV/knydT/lPKmyKqwl1TUc+Z2oE7QtafF+E1OiF264709
9gY0d9LH9PTzsO+3456QfeT/N1HrLJcwZ79EDBa8uIwx3YFCmhzosEoNpeFFSKbr
Q74dG2WK6wXwr9xTyWjSfxSdpPTXz1DaxdzGzndmHuS87mlZM39uCdv5+x5lT66D
KmESaf1jXXTJ1SVcXO0rkwUzPeRjrFN2P0XoIhrW1zhEUIW6aV5NLBdnr0nS+WRM
LZRcfB+GLFo41eP4PNfcHAXpj7Tbn3ewqdiqS4zvpZ8EQH4pQqMU/IflEcF0zKqT
KiBqtOQL2WoI82S+pkhchMH/r+6oBzvbQK5tluQwb4+1n/tHbI3Sm+f5Rnd012vO
Iz87Hh1dj1Iaq642xaP85H4nLQseMKy8aTKyLtRXCm71jj5IffUYp+XUokF9nkwY
lGwssQ5dBq47M5EAEQEAAbQvc3V2YW5rYXIgc2Fya2FyIChLYXIpIDxzdXZhbmth
ckBzaWxpY29ucGluLmNvbT6JAdcEEwEIAEEWIQQvQZScmllcQuqOmFcxoJH9Pryu
xwUCZ+VQNwIbAwUJA8JnAAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRAx
oJH9Pryux0XlC/4xs1nfC5momwL24UhoTHPhvkNUN0vufJLPAgum71Pbe7gNYwMp
pRGzfVAlYkLB7O+JfPDcWbOSm8/fi1274eeAJOu8BhbLH4zPKZlXSXM1C2R5dKat
0ZuSO4q1a7vsrhlsDVzbgLq75la5+LjJRicRTIPEqhoX56tcwHUc0YlDLW7wViB3
xWXGemr27anqFG5sb41rqKoMgBOIcK3M9t4qgqut3WFpiIfwF9mRaI7j2yq/IlGQ
NwR+af/AttETyIT3QWxQP+zT3JMfV8++WbniI6f/ywbHlvcx5JIvoHQSKAofYJdY
O0uECg2E158Q+zvRsmFaAjudyCcKPc/9YOCQBj/N4u++INuqHVNAY5KO2IyPhB09
Q+J+N1UVbsZRB5JC5oMuCntDeATcjSlyVMgj5gaBjA1loUOjj0lc+gs4zY6cMwPe
747WsS6DE2L4C8mz1PlDacK/BhR7dle4aA8CCr4MJfsdz3zzSHs0afaVRQHNf9ac
jIiF5wvmCpSztAS5AY0EZ+VQNwEMANQ/lyRGDmC6vWWPckh04N+BnTWLff+iuMxp
LdU4h8843BEB7RdNcynwPEGV3CP9H/NjKp6kzybKzTOaGqONtuwYTTMfHQEHzdrL
E2afN3fAxPQ6e1/Bqdsz4F9z3UIKVTIJjc5CGoFeHINgCGtGbWvD9HDeHQuH/rO0
4lQjUpJtl8sbaLsBIiMabwPHnabjYvZ3E8knUuqbifaIsdPuFoqQvC0Uy3z7CaU3
qAER7sVdY9NjjiD20lG8lLivKGYTq9QXZFHJz2DLvW8aP+HY+G/kIjuw8MwGPg38
55FkR0pT8L5lJvSUDEDzMTd9i1TUImeRqw/F9AYK9dsXLPxNZ7EcMwRs/6nUjAkh
04tfHJCPwn9HeMtLFrQrg3NfhBIV0/wp8lp8lxCsgd8Cwc/lH3bU/KHdzZQwHPNi
i5F378AlrTlWoSBq5/3rkJRWPU7DavwGJwRnmFCcx1iqyfZB80/G+N0WMbL8gZjY
JoLKoytdLNWS/wlGTx2DIb4E3sStlwARAQABiQG8BBgBCAAmFiEEL0GUnJpZXELq
jphXMaCR/T68rscFAmflUDcCGwwFCQPCZwAACgkQMaCR/T68rse+7wv8CJFMnAGH
hjedAXCODUztEtak8fZkHTiJBow3tx9XxYjnQe99zhZOswBJ8CF7cakpTXMYtWRf
yA30Qf1YNwYeb5WwIG1nsep2bQzeC/LP1pu7XDmRHyI+kakhnEul/27kPHNubSIr
9h0MXtTxRFkde27DD+QzJoqvUKCNtpyhKJY7jv6+9UfpSNIZ6FmpN9hxQRv6oFgn
uoFCKjfUxhGsN2j0DxHF+WJjQ+jBcRzpORD4TfAjqej++OdUv/3NR88UMxima26H
PMZP7chiojFzMAPLniYEFXMgh+l7Z+ssdWbCxfPQq90DoZ/Vylnfmq7E9pjE54Dr
PhIcMfEYlH+erdKrHgCaCzew6Y9kNHPVTuKqWA0/105J7+umk/5Z84eVfmWzJysQ
30KHyfM5lMe1Eb++QECfzknD9xQvAb/YoVT0yHPUhCExzVZUc6InmhwvkX+nilk4
zCPxkkNCwSX1LKNkCt9dW09MLJ2Gyf3r+goOqpx7mcs5g7HQGwc9u2Vp
=NDqk
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,3 +0,0 @@
User-agent: *
Disallow: /*

View File

@ -1,7 +0,0 @@
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

View File

@ -1,72 +0,0 @@
<?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>

View File

@ -1,127 +0,0 @@
import { useState, useRef } from 'react';
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { X } from "lucide-react";
import PocketBase from 'pocketbase';
const pb = new PocketBase('https://tst-pb.s38.siliconpin.com');
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
export function AvatarUpload({ userId }: { userId: string }) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setSelectedFile(e.target.files[0]);
}
};
const handleRemoveFile = () => {
setSelectedFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleUpload = async () => {
if (!selectedFile || !userId) return;
setIsUploading(true);
try {
// 1. Upload to PocketBase
const formData = new FormData();
formData.append('avatar', selectedFile);
// Update PocketBase user record
const pbRecord = await pb.collection('users').update(userId, formData);
// Get the avatar URL from PocketBase
const avatarUrl = pb.getFileUrl(pbRecord, pbRecord.avatar, {
thumb: '100x100'
});
// 2. Update PHP backend session
const response = await fetch(`${INVOICE_API_URL}?query=login`, {
method: 'POST',
credentials: 'include', // Important for sessions to work
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
avatar: avatarUrl,
query: 'avatar_update'
})
});
if (!response.ok) {
throw new Error('Failed to update session');
}
// Success - you might want to refresh the user data or show a success message
window.location.reload();
} catch (error) {
console.error('Error uploading avatar:', error);
alert('Failed to update avatar');
} finally {
setIsUploading(false);
}
};
return (
<div className="space-y-4">
{!selectedFile ? (
<div className="space-y-2">
<div className="flex items-center gap-4">
<Label
htmlFor="avatar"
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
Change Avatar
</Label>
<Input
type="file"
id="avatar"
ref={fileInputRef}
accept="image/jpeg,image/png,image/gif"
className="hidden"
onChange={handleFileChange}
/>
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>
Browse
</Button>
</div>
<p className="text-xs text-muted-foreground">
JPG, GIF or PNG. 1MB max.
</p>
</div>
) : (
<div className="flex items-center justify-between p-3 space-x-2">
<div className="truncate max-w-[200px]">
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
</div>
<Button
size="sm"
className="text-xs p-1 h-fit"
onClick={handleUpload}
disabled={isUploading}
>
{isUploading ? 'Uploading...' : 'Update'}
</Button>
<Button
size="sm"
onClick={handleRemoveFile}
className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit"
disabled={isUploading}
>
Remove
</Button>
</div>
)}
</div>
);
}

View File

@ -1,607 +0,0 @@
import React, { useEffect, useState, useMemo } from "react";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from "./ui/loader";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../lib/InvoicePDF";
import { Eye, Download, ChevronUp, ChevronDown, Search, FileText } from "lucide-react";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
import * as XLSX from 'xlsx';
export default function UserBillingList() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [billingData, setBillingData] = useState([]);
const [dataLoading, setDataLoading] = useState(true);
const [apiError, setApiError] = useState(null);
const [selectedItem, setSelectedItem] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
// Sorting state
const [sortConfig, setSortConfig] = useState({ key: 'created_at', direction: 'desc' });
// Filtering state
const [filters, setFilters] = useState({ status: '', cycle: '', service: '' });
// Search state
const [searchTerm, setSearchTerm] = useState('');
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(20);
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
useEffect(() => {
if (isLoggedIn) {
fetchBillingData();
}
}, [isLoggedIn]);
const fetchBillingData = async () => {
try {
const res = await fetch(`${INVOICE_API_URL}?query=invoice-info`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
// console.log('Resoponse Data', data)
// For regular users, filter to only show their own billing data
const filteredData = sessionData?.user_type === 'admin' || 'user' ? data.data || [] : (data.data || []).filter(item => item.user === sessionData?.email);
// console.log('Session Data', sessionData);
setBillingData(filteredData);
} catch (err) {
setApiError(err.message);
} finally {
setDataLoading(false);
}
};
// console.log('billing data', billingData)
const handleViewItem = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeModal = () => {
setDialogOpen(false);
// setSelectedItem(null);
};
// Sorting functionality
const requestSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
setCurrentPage(1);
};
// Filter and sort data
const filteredAndSortedData = useMemo(() => {
let filteredData = [...billingData];
// Apply search
if (searchTerm) {
filteredData = filteredData.filter(item =>
Object.values(item).some(
val => val && val.toString().toLowerCase().includes(searchTerm.toLowerCase())
)
);
}
// Apply filters
if (filters.status) {
filteredData = filteredData.filter(item => item.status === filters.status);
}
if (filters.cycle) {
filteredData = filteredData.filter(item => item.cycle === filters.cycle);
}
if (filters.service) {
filteredData = filteredData.filter(item => item.service === filters.service);
}
// Apply sorting
if (sortConfig.key) {
filteredData.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}
return filteredData;
}, [billingData, searchTerm, filters, sortConfig]);
const currentItems = useMemo(() => {
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
return filteredAndSortedData.slice(indexOfFirstItem, indexOfLastItem);
}, [currentPage, itemsPerPage, filteredAndSortedData]);
const totalPages = Math.ceil(filteredAndSortedData.length / itemsPerPage);
const paginate = (pageNumber) => {
if (pageNumber > 0 && pageNumber <= totalPages) {
setCurrentPage(pageNumber);
}
};
const getPaginationRange = () => {
const range = [];
const maxVisiblePages = 5;
range.push(1);
if (currentPage > 3) {
range.push('...');
}
let start = Math.max(2, currentPage - 1);
let end = Math.min(totalPages - 1, currentPage + 1);
if (currentPage <= 3) {
end = Math.min(4, totalPages - 1);
} else if (currentPage >= totalPages - 2) {
start = Math.max(totalPages - 3, 2);
}
for (let i = start; i <= end; i++) {
if (i > 1 && i < totalPages) {
range.push(i);
}
}
if (currentPage < totalPages - 2) {
range.push('...');
}
if (totalPages > 1) {
range.push(totalPages);
}
return range;
};
// Get unique values for filter dropdowns
const uniqueServices = [...new Set(billingData.map(item => item.service))];
const uniqueStatuses = ['completed', 'pending', 'failed'];
const uniquecycles = ['monthly', 'yearly', 'one-time'];
// Reset filters
const resetFilters = () => {
setFilters({
status: '',
cycle: '',
service: ''
});
setSearchTerm('');
setCurrentPage(1);
};
// Handle filter change
const handleFilterChange = (e) => {
const { name, value } = e.target;
setFilters(prev => ({
...prev,
[name]: value
}));
setCurrentPage(1);
};
// Handle search
const handleSearch = (e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
};
const exportToExcel = () => {
const worksheet = XLSX.utils.json_to_sheet(filteredAndSortedData.map(item => ({
'Billing ID': item.billing_id,
'Service': item.service,
'Customer Name': item.name,
'Customer Email': item.user,
'Silicon ID': item.siliconId,
'Amount': item.amount,
'cycle': item.cycle,
'Status': item.status,
'Created At': formatDate(item.created_at),
'Remarks': item.remarks
})));
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Billing Data");
XLSX.writeFile(workbook, `billing_data_${new Date().toISOString().slice(0, 10)}.xlsx`);
};
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'bg-green-100 text-green-800';
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'failed': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
return new Date(dateString).toLocaleDateString(undefined, options);
};
const formatCurrency = (amount) => {
if (!amount) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(parseFloat(amount));
};
if (loading || dataLoading) return <Loader />;
if (error || apiError) return <p>Error: {error?.message || apiError}</p>;
if (!isLoggedIn) {
return <p className="text-center mt-8">You need to be logged in to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
// console.log('currentItems', currentItems)
return (
<section className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">My Billing History</h1>
<Button onClick={exportToExcel} variant="outline" className="flex items-center gap-2">
<FileText className="w-4 h-4" /> Export to Excel
</Button>
</div>
{/* Results Count */}
<div className="mb-2 text-sm text-gray-600">
Showing {currentItems.length} of {filteredAndSortedData.length} results
</div>
<div className="bg-white p-4 rounded-t-lg shadow">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Search */}
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearch}
className="bg-[#262626] pl-10 w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
/>
</div>
{/* Status Filter */}
<select
name="status"
value={filters.status}
onChange={handleFilterChange}
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<option value="">All Statuses</option>
{uniqueStatuses.map(status => (
<option key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</option>
))}
</select>
{/* cycle Filter */}
<select
name="cycle"
value={filters.cycle}
onChange={handleFilterChange}
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<option value="">All cycles</option>
{uniquecycles.map(cycle => (
<option key={cycle} value={cycle}>
{cycle.charAt(0).toUpperCase() + cycle.slice(1)}
</option>
))}
</select>
{/* Service Filter */}
<select
name="service"
value={filters.service}
onChange={handleFilterChange}
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<option value="">All Services</option>
{uniqueServices.map(service => (
<option key={service} value={service}>
{service}
</option>
))}
</select>
</div>
{/* Reset Filters Button */}
{(filters.status || filters.cycle || filters.service || searchTerm) && (
<div className="mt-3">
<Button
onClick={resetFilters}
size="sm"
>
Reset Filters
</Button>
</div>
)}
</div>
<div className="bg-white rounded-b-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('billing_id')}
>
<div className="flex items-center">
Billing ID
{sortConfig.key === 'billing_id' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('service')}
>
<div className="flex items-center">
Service
{sortConfig.key === 'service' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('name')}
>
<div className="flex items-center">
Name
{sortConfig.key === 'name' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('amount')}
>
<div className="flex items-center">
Amount
{sortConfig.key === 'amount' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('status')}
>
<div className="flex items-center">
Status
{sortConfig.key === 'status' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('created_at')}
>
<div className="flex items-center">
Created At
{sortConfig.key === 'created_at' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentItems.length > 0 ? (
currentItems.map((item) => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{item.billing_id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.service}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.name??item.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">
{formatCurrency(item.amount)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(item.status)}`}>
{item.status.charAt(0).toUpperCase() + item.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(item.created_at)}
</td>
<td className="inline-flex px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
title="View Details"
onClick={() => handleViewItem(item)}
className="text-indigo-600 hover:text-indigo-900 mr-2"
>
<Eye className="w-5 h-5" />
</button>
<PDFDownloadLink
title="Download PDF"
document={<InvoicePDF data={item} />}
fileName={`invoice_${item.billing_id}.pdf`}
className="text-[#6d9e37] hover:text-green-600"
>
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
</PDFDownloadLink>
</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
No records found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Pagination */}
{filteredAndSortedData.length > itemsPerPage && (
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600">
Page {currentPage} of {totalPages}
</div>
<div className="flex flex-wrap gap-2">
<Button
onClick={() => paginate(1)}
disabled={currentPage === 1}
variant="outline"
size="sm"
>
First
</Button>
<Button
onClick={() => paginate(currentPage - 1)}
disabled={currentPage === 1}
variant="outline"
size="sm"
>
Previous
</Button>
{getPaginationRange().map((item, index) => {
if (item === '...') {
return (
<span key={`ellipsis-${index}`} className="px-2 py-1">
...
</span>
);
}
return (
<Button
key={`page-${item}`}
onClick={() => paginate(item)}
variant={currentPage === item ? "default" : "outline"}
size="sm"
>
{item}
</Button>
);
})}
<Button
onClick={() => paginate(currentPage + 1)}
disabled={currentPage === totalPages}
variant="outline"
size="sm"
>
Next
</Button>
<Button
onClick={() => paginate(totalPages)}
disabled={currentPage === totalPages}
variant="outline"
size="sm"
>
Last
</Button>
</div>
</div>
)}
{/* Dialog for View */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="">
Billing Details
</DialogTitle>
<DialogDescription>
Detailed information about your billing record
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<h3 className="font-semibold text-neutral-950">Billing ID</h3>
<p className="text-neutral-400 font-medium">{selectedItem?.billing_id}</p>
</div>
<div>
<h3 className="font-semibold text-neutral-950">Service</h3>
<p className="text-neutral-400 font-medium">{selectedItem?.service}</p>
</div>
<div>
<h3 className="font-semibold text-neutral-950">User</h3>
<p className="text-neutral-400 font-medium">{selectedItem?.user}</p>
</div>
<div>
<h3 className="font-semibold text-neutral-950">cycle</h3>
<p className="text-neutral-400 font-medium">{selectedItem?.cycle}</p>
</div>
<div>
<h3 className="font-semibold text-neutral-950">Amount</h3>
<p className="text-neutral-400 font-medium">{formatCurrency(selectedItem?.amount)}</p>
</div>
<div>
<h3 className="font-semibold text-neutral-950">Status</h3>
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(selectedItem?.status)}`}>
{selectedItem?.status?.charAt(0).toUpperCase() + selectedItem?.status?.slice(1)}
</span>
</div>
<div>
<h3 className="font-semibold text-neutral-950">Created At</h3>
<p className="text-neutral-400 font-medium">{formatDate(selectedItem?.created_at)}</p>
</div>
<div>
<h3 className="font-semibold text-neutral-950">Updated At</h3>
<p className="text-neutral-400 font-medium">{formatDate(selectedItem?.updated_at)}</p>
</div>
</div>
<DialogFooter>
<PDFDownloadLink
document={<InvoicePDF data={selectedItem} />}
fileName={`invoice_${selectedItem?.billing_id}.pdf`}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
{({ loading }) => (loading ? 'Preparing PDF...' : 'Download PDF')}
</PDFDownloadLink>
<Button
onClick={closeModal}
variant="outline"
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
);
}

View File

@ -1,185 +0,0 @@
import React, { useState } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Loader2 } from "lucide-react";
export default function BuyVPN() {
// API URL - make sure to set this correctly
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
// State for VPN configuration
const [vpnContinent, setVpnContinent] = useState("");
const [selectedCycle, setSelectedCycle] = useState("");
const [selectedPrice, setSelectedPrice] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState(null);
// Price configuration
const PRICE_CONFIG = {
monthly: 100,
yearly: 1000
};
// Handle billing cycle selection
const handleBillingCycleChange = (cycle) => {
setSelectedCycle(cycle);
setSelectedPrice(cycle === 'monthly' ? PRICE_CONFIG.monthly : PRICE_CONFIG.yearly);
setError(null); // Clear any previous errors when user makes a selection
};
// Handle VPN purchase
const handlePurchase = async () => {
// Validate inputs
if (!vpnContinent) {
setError('Please select a continent');
return;
}
if (!selectedCycle) {
setError('Please select a billing cycle');
return;
}
setIsProcessing(true);
setError(null);
try {
const formData = new FormData();
formData.append('service', 'VPN WireGuard');
formData.append('serviceId', 'vpnservices');
formData.append('cycle', selectedCycle);
formData.append('amount', selectedPrice.toString());
formData.append('vpn_continent', vpnContinent);
formData.append('service_type', 'vpn');
const response = await fetch(`${USER_API_URL}?query=initiate_payment`, {
method: 'POST',
body: formData, // Using FormData instead of JSON
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
// Redirect to payment success page with order ID
window.location.href = `/success?service=vpn&continent=${vpnContinent}&orderId=${data.order_id}`;
} else {
throw new Error(data.message || 'Payment initialization failed');
}
} catch (error) {
console.error('Purchase error:', error);
setError(error.message || 'An error occurred during purchase. Please try again.');
} finally {
setIsProcessing(false);
}
};
return (
<Card className="max-w-2xl mx-auto mt-8">
<CardHeader>
<CardTitle>VPN WireGuard</CardTitle>
<CardDescription>
Purchase your secure VPN subscription
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Continent Selection */}
<div>
<label htmlFor="vpn-continent" className="block text-sm font-medium mb-1">
Select Continent
</label>
<Select name="vpn-continent" onValueChange={setVpnContinent} value={vpnContinent} disabled={isProcessing}>
<SelectTrigger className="w-full">
<SelectValue placeholder="-Select-" />
</SelectTrigger>
<SelectContent>
<SelectItem value="india">India</SelectItem>
<SelectItem value="america">America</SelectItem>
<SelectItem value="europe">Europe</SelectItem>
</SelectContent>
</Select>
</div>
{/* VPN Features */}
<ul className="flex flex-wrap justify-between gap-2 text-xs">
{["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"].map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className='text-zinc-400'>{feature}</span>
</li>
))}
</ul>
{/* Billing Cycle Selection */}
{
vpnContinent && (
<div className='flex flex-row justify-between items-center gap-x-6'>
<label className={`border ${selectedCycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
<input
type="radio"
name="vpn-cycle"
checked={selectedCycle === 'monthly'}
onChange={() => handleBillingCycleChange('monthly')}
className="hidden"
disabled={isProcessing}
/>
<p className='text-3xl font-bold text-center'>{PRICE_CONFIG.monthly}</p>
<span>Monthly</span>
</label>
<label className={`border ${selectedCycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
<input
type="radio"
name="vpn-cycle"
checked={selectedCycle === 'yearly'}
onChange={() => handleBillingCycleChange('yearly')}
className="hidden"
disabled={isProcessing}
/>
<p className='text-3xl font-bold text-center'>{PRICE_CONFIG.yearly}</p>
<span>Yearly</span>
</label>
</div>
)
}
{/* Selection Summary */}
{selectedCycle && (
<p className={`text-white ${selectedCycle === 'monthly' ? 'text-left' : 'text-end'}`}>
You selected <strong>{selectedCycle}</strong> plan at {selectedPrice}
</p>
)}
{/* Error Message */}
{error && (
<div className="p-3 bg-red-900/20 border border-red-700/50 rounded-md">
<p className="text-red-400">{error}</p>
</div>
)}
{/* Purchase Button */}
<Button
onClick={handlePurchase}
className={`w-full`}
disabled={!vpnContinent || !selectedCycle || isProcessing}
>
{isProcessing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
'Proceed'
)}
</Button>
</CardContent>
</Card>
);
}
// Cash Expense:- 950
// Card Expense:- 580

View File

@ -1,214 +0,0 @@
import React, { useState } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Loader2 } from "lucide-react";
import { useToast } from "../ui/toast";
export default function NewDroplet() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
name: "",
region: "",
size: "",
image: "",
backups: false,
ipv6: true,
monitoring: false
});
const regions = [
{ value: "nyc1", label: "New York 1" },
{ value: "nyc3", label: "New York 3" },
{ value: "sfo3", label: "San Francisco 3" },
{ value: "ams3", label: "Amsterdam 3" },
];
const sizes = [
{ value: "s-1vcpu-1gb", label: "1 vCPU, 1GB RAM" },
{ value: "s-1vcpu-2gb", label: "1 vCPU, 2GB RAM" },
{ value: "s-2vcpu-2gb", label: "2 vCPU, 2GB RAM" },
];
const images = [
{ value: "ubuntu-22-04-x64", label: "Ubuntu 22.04" },
{ value: "ubuntu-20-04-x64", label: "Ubuntu 20.04" },
{ value: "debian-11-x64", label: "Debian 11" },
];
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSelectChange = (name, value) => {
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const response = await fetch("https://api.digitalocean.com/v2/droplets", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer dop_v1_b6a075ece5786faf7c58d21761dbf95d47af372da062d68a870ce2a0bae51adf"
},
body: JSON.stringify({
...formData,
ssh_keys: [47441478]
})
});
const data = await response.json();
if (response.ok) {
showToast(`Droplet ${formData.name} created successfully!`, { type: 'success' });
// Reset form
setFormData({
name: "",
region: "nyc3",
size: "s-1vcpu-1gb",
image: "ubuntu-22-04-x64",
backups: false,
ipv6: true,
monitoring: false
});
} else {
throw new Error(data.message || "Failed to create droplet");
}
} catch (error) {
showToast(error.message, { type: 'error' });
} finally {
setIsLoading(false);
}
};
return (
<Card className="max-w-xl mx-auto px-4 my-4">
<CardHeader>
<CardTitle>Create New Droplet</CardTitle>
<CardDescription>Configure your new cloud server</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Droplet Name</Label>
<Input
id="name"
name="name"
type="text"
placeholder="my-droplet"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="space-y-2">
<Label>Region</Label>
<Select
value={formData.region}
onValueChange={(value) => handleSelectChange("region", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select region" />
</SelectTrigger>
<SelectContent>
{regions.map(region => (
<SelectItem key={region.value} value={region.value}>
{region.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Size</Label>
<Select
value={formData.size}
onValueChange={(value) => handleSelectChange("size", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select size" />
</SelectTrigger>
<SelectContent>
{sizes.map(size => (
<SelectItem key={size.value} value={size.value}>
{size.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Image</Label>
<Select
value={formData.image}
onValueChange={(value) => handleSelectChange("image", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select OS image" />
</SelectTrigger>
<SelectContent>
{images.map(image => (
<SelectItem key={image.value} value={image.value}>
{image.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="backups"
name="backups"
checked={formData.backups}
onChange={handleChange}
className="h-4 w-4"
/>
<Label htmlFor="backups">Enable Backups</Label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="ipv6"
name="ipv6"
checked={formData.ipv6}
onChange={handleChange}
className="h-4 w-4"
/>
<Label htmlFor="ipv6">Enable IPv6</Label>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Droplet"
)}
</Button>
</form>
</CardContent>
</Card>
);
}

View File

@ -1,374 +0,0 @@
import React, { useState, useEffect } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import { Textarea } from "../ui/textarea";
import { Switch } from "../ui/switch";
export default function NewHetznerInstance() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [serverTypes, setServerTypes] = useState([]);
const [locations, setLocations] = useState([]);
const [images, setImages] = useState([]);
const [sshKeys, setSshKeys] = useState([]);
const [showAddSshKey, setShowAddSshKey] = useState(false);
const HETZNER_API_KEY = "uMSBR9nxdtbuvazsVM8YMMDd0PvuynpgJbmzFIO47HblMlh7tlHT8wV05sQ28Squ"; // Replace with your actual API key
const [formData, setFormData] = useState({
name: "",
server_type: "",
image: "",
location: "nbg1", // Default to Nuremberg
ssh_keys: [],
user_data: "",
backups: false,
start_after_create: true
});
useEffect(() => {
const fetchData = async () => {
try {
// Fetch all required data in parallel
const [serverTypesResponse, locationsResponse, imagesResponse, sshKeysResponse] = await Promise.all([
fetch("https://api.hetzner.cloud/v1/server_types", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/locations", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/images", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/ssh_keys", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
})
]);
const serverTypesData = await serverTypesResponse.json();
const locationsData = await locationsResponse.json();
const imagesData = await imagesResponse.json();
const sshKeysData = await sshKeysResponse.json();
setServerTypes(serverTypesData.server_types || []);
setLocations(locationsData.locations || []);
setImages(imagesData.images.filter(img => img.type === "system") || []);
setSshKeys(sshKeysData.ssh_keys || []);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const payload = {
name: formData.name,
server_type: formData.server_type,
image: formData.image,
location: formData.location,
backups: formData.backups,
ssh_keys: formData.ssh_keys,
start_after_create: formData.start_after_create
};
// Add user_data if provided
if (formData.user_data) {
payload.user_data = formData.user_data;
}
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
method: "POST",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.server) {
showToast({
title: "Deployment Started",
description: `Server ${data.server.name} is being deployed`,
variant: "success"
});
// Reset form after successful deployment
setFormData({
name: "",
server_type: "",
image: "",
location: "nbg1",
ssh_keys: [],
user_data: "",
backups: false,
start_after_create: true
});
} else {
throw new Error(data.message || "Failed to deploy instance");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleAddSshKey = async () => {
try {
const response = await fetch('https://api.hetzner.cloud/v1/ssh_keys', {
method: "POST",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: formData.newSshKeyName,
public_key: formData.newSshKeyValue
})
});
const data = await response.json();
if (data.ssh_key) {
// Refresh SSH keys list
const sshResponse = await fetch("https://api.hetzner.cloud/v1/ssh_keys", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
});
const sshData = await sshResponse.json();
setSshKeys(sshData.ssh_keys || []);
showToast({
title: "SSH Key Added",
description: "Your SSH key has been successfully added",
variant: "success"
});
setShowAddSshKey(false);
}
} catch (error) {
showToast({
title: "Error",
description: "Failed to add SSH key",
variant: "destructive"
});
}
};
if (isFetchingData) {
return <Loader />;
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Hetzner Cloud Server</CardTitle>
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Server Name *</Label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="my-hetzner-server"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="location">Location *</Label>
<Select
name="location"
value={formData.location}
onValueChange={(value) => setFormData({...formData, location: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
{locations.map(location => (
<SelectItem key={location.name} value={location.name}>
{`${location.city} (${location.name.toUpperCase()})`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="server_type">Server Type *</Label>
<Select
name="server_type"
value={formData.server_type}
onValueChange={(value) => setFormData({...formData, server_type: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select a server type" />
</SelectTrigger>
<SelectContent>
{serverTypes.map(type => (
<SelectItem key={type.name} value={type.name}>
{`${type.name} - ${type.cores} vCPU, ${type.memory}GB RAM`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{images.map(image => (
<SelectItem key={image.id} value={image.id.toString()}>
{image.description}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">SSH Keys</h3>
<div className="space-y-2">
<Label>Select SSH Keys</Label>
<div className="flex gap-2">
<Select
value={formData.ssh_keys[0] || ""}
onValueChange={(value) => setFormData({...formData, ssh_keys: [value]})}
>
<SelectTrigger>
<SelectValue placeholder="Select SSH key" />
</SelectTrigger>
<SelectContent>
{sshKeys.map(key => (
<SelectItem key={key.id} value={key.id.toString()}>{key.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
className="whitespace-nowrap"
onClick={() => setShowAddSshKey(!showAddSshKey)}
>
{showAddSshKey ? "Cancel" : "Add New"}
</Button>
</div>
</div>
{showAddSshKey && (
<div className="space-y-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label htmlFor="newSshKeyName">Key Name</Label>
<Input
id="newSshKeyName"
name="newSshKeyName"
value={formData.newSshKeyName}
onChange={handleChange}
placeholder="My Laptop Key"
/>
</div>
<div className="space-y-2">
<Label htmlFor="newSshKeyValue">Public Key</Label>
<Textarea
id="newSshKeyValue"
name="newSshKeyValue"
value={formData.newSshKeyValue}
onChange={handleChange}
placeholder="ssh-rsa AAAAB3NzaC1yc2E..."
rows={4}
/>
</div>
<Button
type="button"
onClick={handleAddSshKey}
disabled={!formData.newSshKeyName || !formData.newSshKeyValue}
>
Add SSH Key
</Button>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="user_data">Cloud-Init User Data (Optional)</Label>
<Textarea
id="user_data"
name="user_data"
value={formData.user_data}
onChange={handleChange}
placeholder="#cloud-config\nwrite_files:\n - content: |\n Hello World\n path: /tmp/hello.txt"
rows={6}
/>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Options</h3>
<div className="flex flex-col gap-4">
<div className="flex items-center space-x-2">
<Switch
id="backups"
checked={formData.backups}
onCheckedChange={(checked) => setFormData({...formData, backups: checked})}
/>
<Label htmlFor="backups">Enable Backups (+20% cost)</Label>
</div>
</div>
</div>
<div className="flex justify-end pt-4">
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
{isLoading ? "Deploying..." : "Deploy Server"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -1,486 +0,0 @@
import React, { useState, useEffect } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { Switch } from "../ui/switch";
export default function NewKubernetesService() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [vpcs, setVpcs] = useState([]);
const [subnets, setSubnets] = useState([]);
const UTHO_API_KEY = "Bearer IoNXhkRJsQPyOEqFMceSfzuKaDLrpxUCATgZjiVdvYlBHbwWmGtn";
const [formData, setFormData] = useState({
dcslug: "innoida",
cluster_version: "1.24",
cluster_label: "",
nodepools: [{
label: "default-pool",
size: "",
count: 1,
maxCount: 1
}],
firewall: "",
vpc: "",
subnet: "",
network_type: "public",
cpumodel: "intel"
});
useEffect(() => {
const fetchInitialData = async () => {
setIsFetchingData(true);
try {
const [plansResponse, vpcsResponse] = await Promise.all([
fetch("https://api.utho.com/v2/plans", {
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
}
}),
fetch("https://api.utho.com/v2/vpc", {
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
}
})
]);
const plansData = await plansResponse.json();
const vpcsData = await vpcsResponse.json();
setPlans(plansData.plans || []);
setVpcs(vpcsData.vpc || []);
// Set default VPC if available
if (vpcsData.vpc?.length > 0) {
const firstVpc = vpcsData.vpc[0];
setFormData(prev => ({
...prev,
vpc: firstVpc.id
}));
// Fetch subnets for the default VPC
await fetchSubnets(firstVpc.id);
}
} catch (error) {
console.error("Initial data fetch error:", error);
showToast({
title: "Error",
description: "Failed to load initial configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchInitialData();
}, []);
const fetchSubnets = async (vpcId) => {
try {
// First try to get subnets from the VPC data
const selectedVpc = vpcs.find(vpc => vpc.id === vpcId);
if (selectedVpc?.subnet?.length > 0) {
setSubnets(selectedVpc.subnet);
setFormData(prev => ({
...prev,
subnet: selectedVpc.subnet[0].id
}));
return;
}
// If no subnets in VPC data, try the subnets endpoint
const response = await fetch(`https://api.utho.com/v2/vpc/${vpcId}/subnets`, {
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
}
});
if (response.ok) {
const data = await response.json();
setSubnets(data.subnets || []);
if (data.subnets?.length > 0) {
setFormData(prev => ({
...prev,
subnet: data.subnets[0].id
}));
}
}
} catch (error) {
console.error("Error fetching subnets:", error);
}
};
const handleVpcChange = async (vpcId) => {
setFormData(prev => ({
...prev,
vpc: vpcId,
subnet: ""
}));
await fetchSubnets(vpcId);
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
// Validate required fields including subnet
if (!formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet) {
throw new Error("Please fill all required fields including VPC and Subnet");
}
const payload = {
dcslug: formData.dcslug,
cluster_version: formData.cluster_version,
cluster_label: formData.cluster_label,
nodepools: formData.nodepools.map(pool => ({
label: pool.label,
size: pool.size,
count: pool.count.toString(),
maxCount: pool.maxCount.toString()
})),
firewall: formData.firewall || undefined,
vpc: formData.vpc,
subnet: formData.subnet, // Now required based on API behavior
network_type: formData.network_type,
cpumodel: formData.cpumodel
};
console.log("Deployment payload:", payload);
const response = await fetch("https://api.utho.com/v2/kubernetes/deploy", {
method: "POST",
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
}
if (data.status === "success") {
showToast({
title: "Success",
description: `Cluster ${data.cluster_id || data.id} is being deployed`,
variant: "success"
});
// Reset form
setFormData(prev => ({
...prev,
cluster_label: "",
nodepools: [{
label: "default-pool",
size: "",
count: 1,
maxCount: 1
}],
vpc: vpcs[0]?.id || "",
subnet: subnets[0]?.id || ""
}));
} else {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
console.error("Deployment error:", error);
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleNodePoolChange = (index, field, value) => {
const updatedNodePools = [...formData.nodepools];
updatedNodePools[index][field] = value;
setFormData(prev => ({
...prev,
nodepools: updatedNodePools
}));
};
if (isFetchingData) {
return (
<div className="flex items-center justify-center h-64">
<Loader />
<span className="ml-2">Loading configuration...</span>
</div>
);
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Kubernetes Cluster</CardTitle>
<CardDescription>
Configure your Kubernetes cluster with the required parameters
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Cluster Label */}
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Label *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
/>
</div>
{/* Data Center and Version */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="innoida">Noida</SelectItem>
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
<SelectItem value="indelhi">Delhi</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
<Select
name="cluster_version"
value={formData.cluster_version}
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1.24">1.24</SelectItem>
<SelectItem value="1.25">1.25</SelectItem>
<SelectItem value="1.26">1.26</SelectItem>
<SelectItem value="1.27">1.27</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Network Type and CPU Model */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="network_type">Network Type *</Label>
<Select
name="network_type"
value={formData.network_type}
onValueChange={(value) => setFormData({...formData, network_type: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select network type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="public">Public</SelectItem>
<SelectItem value="private">Private</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cpumodel">CPU Model *</Label>
<Select
name="cpumodel"
value={formData.cpumodel}
onValueChange={(value) => setFormData({...formData, cpumodel: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select CPU model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="intel">Intel</SelectItem>
<SelectItem value="amd">AMD</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Node Pool Configuration */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Node Pool Configuration</h3>
{formData.nodepools.map((pool, index) => (
<div key={index} className="p-4 border rounded-lg space-y-4">
<div className="space-y-2">
<Label htmlFor={`pool-label-${index}`}>Pool Label *</Label>
<Input
id={`pool-label-${index}`}
value={pool.label}
onChange={(e) => handleNodePoolChange(index, 'label', e.target.value)}
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor={`pool-size-${index}`}>Node Size *</Label>
<Select
value={pool.size}
onValueChange={(value) => handleNodePoolChange(index, 'size', value)}
required
>
<SelectTrigger>
<SelectValue placeholder="Select node size" />
</SelectTrigger>
<SelectContent>
{plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${Math.floor(parseInt(plan.ram)/1024)}GB RAM`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor={`pool-count-${index}`}>Node Count *</Label>
<Input
id={`pool-count-${index}`}
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(index, 'count', parseInt(e.target.value))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor={`pool-maxCount-${index}`}>Max Nodes</Label>
<Input
id={`pool-maxCount-${index}`}
type="number"
min={pool.count}
value={pool.maxCount}
onChange={(e) => handleNodePoolChange(index, 'maxCount', parseInt(e.target.value))}
/>
</div>
</div>
</div>
))}
</div>
{/* VPC and Subnet */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="vpc">VPC *</Label>
<Select
name="vpc"
value={formData.vpc}
onValueChange={handleVpcChange}
required
>
<SelectTrigger>
<SelectValue placeholder="Select VPC" />
</SelectTrigger>
<SelectContent>
{vpcs.map(vpc => (
<SelectItem key={vpc.id} value={vpc.id}>
{vpc.name || vpc.id}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="subnet">Subnet *</Label>
<Select
name="subnet"
value={formData.subnet}
onValueChange={(value) => setFormData({...formData, subnet: value})}
required
disabled={!formData.vpc || subnets.length === 0}
>
<SelectTrigger>
<SelectValue placeholder={subnets.length === 0 ? "No subnets available" : "Select subnet"}>
{formData.subnet || (subnets.length === 0 ? "No subnets available" : "Select subnet")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{subnets.map(subnet => (
<SelectItem key={subnet.id} value={subnet.id}>
{subnet.name || subnet.id} ({subnet.network}/{subnet.size})
</SelectItem>
))}
</SelectContent>
</Select>
{subnets.length === 0 && formData.vpc && (
<p className="text-sm text-muted-foreground mt-1">
No subnets found in this VPC. Please create a subnet first.
</p>
)}
</div>
</div>
{/* Firewall (Optional) */}
<div className="space-y-2">
<Label htmlFor="firewall">Firewall ID (Optional)</Label>
<Input
id="firewall"
name="firewall"
value={formData.firewall}
onChange={handleChange}
placeholder="Firewall ID"
/>
</div>
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={isLoading || !formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet}
className="w-full md:w-auto"
>
{isLoading ? (
<>
<Loader className="mr-2 h-4 w-4" />
Deploying...
</>
) : "Deploy Kubernetes Cluster"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -1,386 +0,0 @@
import React, { useState, useEffect } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import { Textarea } from "../ui/textarea";
import { Switch } from "../ui/switch";
export default function NewCloudInstance() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [sshKeys, setSshKeys] = useState([]);
const [showAddSshKey, setShowAddSshKey] = useState(false);
const UTHO_API_KEY = "Bearer IoNXhkRJsQPyOEqFMceSfzuKaDLrpxUCATgZjiVdvYlBHbwWmGtn";
const [formData, setFormData] = useState({
dcslug: "inmumbaizone2",
planid: "",
hostname: "",
image: "debian-12-x86_64",
auth: "ssh_key",
sshkeys: "",
password: "",
enable_publicip: true,
enablebackup: false,
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
billingcycle: "hourly"
});
useEffect(() => {
const fetchData = async () => {
try {
// Fetch plans and SSH keys in parallel
const [plansResponse, sshResponse] = await Promise.all([
fetch("https://api.utho.com/v2/plans", {
headers: { "Authorization": UTHO_API_KEY }
}),
fetch("https://api.utho.com/v2/key", {
headers: { "Authorization": UTHO_API_KEY }
})
]);
const plansData = await plansResponse.json();
const sshData = await sshResponse.json();
setPlans(plansData.plans || []);
setSshKeys(sshData.key || []);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const payload = {
dcslug: formData.dcslug,
planid: formData.planid,
hostname: formData.hostname,
image: formData.image,
enable_publicip: formData.enable_publicip ? "true" : "false",
enablebackup: formData.enablebackup ? "true" : "false",
cloud: [{ hostname: formData.hostname }],
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
billingcycle: "hourly"
};
// Add authentication based on selected method
if (formData.auth === "ssh_key") {
payload.sshkeys = formData.sshkeys;
} else {
payload.password = formData.password;
}
const response = await fetch("https://api.utho.com/v2/cloud/deploy", {
method: "POST",
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.status === "success") {
showToast({
title: "Deployment Started",
description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
variant: "success"
});
// Reset form after successful deployment
setFormData(prev => ({
...prev,
hostname: "",
planid: ""
}));
} else {
throw new Error(data.message || "Failed to deploy instance");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleAddSshKey = async () => {
try {
const response = await fetch('https://api.utho.com/v2/key/import', {
method: "POST",
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: formData.newSshKeyName,
sshkey: formData.newSshKeyValue
})
});
const data = await response.json();
if (data.status === 'success') {
// Refresh SSH keys list
const sshResponse = await fetch("https://api.utho.com/v2/key", {
headers: { "Authorization": UTHO_API_KEY }
});
const sshData = await sshResponse.json();
setSshKeys(sshData.key || []);
showToast({
title: "SSH Key Added",
description: "Your SSH key has been successfully added",
variant: "success"
});
setShowAddSshKey(false);
}
} catch (error) {
showToast({
title: "Error",
description: "Failed to add SSH key",
variant: "destructive"
});
}
};
if (isFetchingData) {
return <Loader />;
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Cloud Instance</CardTitle>
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="hostname">Hostname *</Label>
<Input
id="hostname"
name="hostname"
value={formData.hostname}
onChange={handleChange}
placeholder="server1.example.com"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
<SelectItem value="innoida">Noida</SelectItem>
<SelectItem value="indelhi">Delhi</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="planid">Plan *</Label>
<Select
name="planid"
value={formData.planid}
onValueChange={(value) => setFormData({...formData, planid: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{/* {plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
</SelectItem>
))} */}
<SelectItem key={plans[0].id} value={plans[0].id}>
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
<SelectItem value="debian-12-x86_64">Debian 12</SelectItem>
<SelectItem value="ubuntu-22.04">Ubuntu 22.04</SelectItem>
<SelectItem value="centos-7.4-x86_64">CentOS 7.4</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Authentication</h3>
<Tabs
value={formData.auth}
onValueChange={(value) => setFormData({...formData, auth: value})}
className="w-full">
<TabsList className="grid w-full grid-cols-2 gap-4">
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'ssh_key' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="ssh_key">SSH Key</TabsTrigger>
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'password' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="ssh_key" className="space-y-4">
<div className="space-y-2">
<Label>Select SSH Key</Label>
<div className="flex gap-2">
<Select
value={formData.sshkeys}
onValueChange={(value) => setFormData({...formData, sshkeys: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select SSH key" />
</SelectTrigger>
<SelectContent>
{sshKeys.map(key => (
<SelectItem key={key.id} value={key.id}>{key.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
className="whitespace-nowrap"
onClick={() => setShowAddSshKey(!showAddSshKey)}
>
{showAddSshKey ? "Cancel" : "Add New"}
</Button>
</div>
</div>
{showAddSshKey && (
<div className="space-y-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label htmlFor="newSshKeyName">Key Name</Label>
<Input
id="newSshKeyName"
name="newSshKeyName"
value={formData.newSshKeyName}
onChange={handleChange}
placeholder="My Laptop Key"
/>
</div>
<div className="space-y-2">
<Label htmlFor="newSshKeyValue">Public Key</Label>
<Textarea
id="newSshKeyValue"
name="newSshKeyValue"
value={formData.newSshKeyValue}
onChange={handleChange}
placeholder="ssh-rsa AAAAB3NzaC1yc2E..."
rows={4}
/>
</div>
<Button
type="button"
onClick={handleAddSshKey}
disabled={!formData.newSshKeyName || !formData.newSshKeyValue}
>
Add SSH Key
</Button>
</div>
)}
</TabsContent>
<TabsContent value="password" className="space-y-2">
<div className="space-y-2">
<Label htmlFor="password">Root Password *</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter root password"
required={formData.auth === "password"}
/>
</div>
</TabsContent>
</Tabs>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Options</h3>
<div className="flex flex-col gap-4">
<div className="flex items-center space-x-2">
<Switch
id="enable_publicip"
checked={formData.enable_publicip}
onCheckedChange={(checked) => setFormData({...formData, enable_publicip: checked})}
/>
<Label htmlFor="enable_publicip">Enable Public IP</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="enablebackup"
checked={formData.enablebackup}
onCheckedChange={(checked) => setFormData({...formData, enablebackup: checked})}
/>
<Label htmlFor="enablebackup">Enable Weekly Backups (+20%)</Label>
</div>
</div>
</div>
<div className="flex justify-end pt-4">
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
{isLoading ? "Deploying..." : "Deploy Instance"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -1,114 +0,0 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import Loader from "../ui/loader";
import { Loader2 } from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
const PRICE_CONFIG = [
{purchaseType: 'loose', price: 2000, minute: 10000},
{purchaseType: 'bulk', price: 1000, minute: 10000}
];
export default function STTStreaming(){
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const [purchaseType, setPurchaseType] = useState();
const [amount, setAmount] = useState();
const [dataError, setDataError] = useState();
const [isProcessing, setIsProcessing] = useState(false);
const handlePurchaseType = (type) => {
const selected = PRICE_CONFIG.find(item => item.purchaseType === type);
setAmount(type === 'bulk' ? selected.price : type === 'loose' ? selected.price : '')
setPurchaseType(type);
}
const handlePurchase = async () => {
// Validate inputs
if (!purchaseType) {
setDataError('Please select a Plan');
return;
}
setIsProcessing(true);
setDataError(null);
try {
const formData = new FormData();
formData.append('service', 'STT Streaming API');
formData.append('serviceId', 'sttstreamingapi');
formData.append('cycle', 'monthly');
formData.append('amount', amount.toString());
formData.append('service_type', 'streaming_api');
const response = await fetch(`${USER_API_URL}?query=initiate_payment`, {
method: 'POST',
body: formData, // Using FormData instead of JSON
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
// Redirect to payment success page with order ID
window.location.href = `/success?service=streaming_api&orderId=${data.order_id}`;
} else {
throw new Error(data.message || 'Payment initialization failed');
}
} catch (error) {
console.error('Purchase error:', error);
setDataError(error.message || 'An error occurred during purchase. Please try again.');
} finally {
setIsProcessing(false);
}
};
if(!purchaseType){
<div>
<p>{dataError}</p>
</div>
}
return(
<>
<div className='container mx-auto'>
<Card className='max-w-2xl mx-auto px-4 mt-8'>
<CardContent>
<CardHeader>
<CardTitle>STT Streaming API</CardTitle>
<CardDescription>Real-time Speech-to-Text (STT) streaming that converts spoken words into accurate, readable text</CardDescription>
</CardHeader>
<div className='flex flex-row justify-between items-center gap-x-6'>
<label htmlFor="purchase-type-loose" className={`border ${purchaseType === 'loose' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
<input onChange={() => handlePurchaseType('loose')} type="radio" name="purchase-type" id="purchase-type-loose" className='hidden' />
<div className='flex flex-col'>
<span className='text-2xl font-bold'>Loose Plan</span>
<span className=''>&#x1F550;10,000 Min &#8377;2000</span>
</div>
</label>
<label htmlFor="purchase-type-bulk" className={`border ${purchaseType === 'bulk' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
<input onChange={() => handlePurchaseType('bulk')} type="radio" name="purchase-type" id="purchase-type-bulk" className='hidden' />
<div className='flex flex-col'>
<span className='text-2xl font-bold'>Bulk Plan</span>
<span className=''>&#x1F550;10,000 Min &#8377;1000</span>
</div>
</label>
</div>
<ul className="grid grid-cols-2 justify-between mt-2 gap-x-2 text-xs">
{['Setup Charge 5000', '10000 Min INR 2000 in Loose Plan', '10000 Min INR 1000 in Bulk Plan', 'Real-time transcription with high accuracy', 'Supports multiple languages', 'Custom vocabulary for domain-specific terms', 'Integrates easily with video/audio streams', 'Secure and scalable API access', 'Live subtitle overlay options'].map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className='text-zinc-400'>{feature}</span>
</li>
))}
</ul>
<div className='flex my-8 w-full'>
<Button onClick={handlePurchase} disabled={!purchaseType} className={`w-full ${!purchaseType ? 'cursor-not-allowed' : ''}`}>{isProcessing ? (<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Processing...</>) : 'Proceed'}</Button>
</div>
</CardContent>
</Card>
</div>
</>
)
}

View File

@ -1,371 +0,0 @@
import React, { useEffect, useState } from 'react';
import MDEditor, { commands } from '@uiw/react-md-editor';
import { Card } from "./ui/card";
import { Label } from "./ui/label";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
import { Input } from "./ui/input";
import { useIsLoggedIn } from '../lib/isLoggedIn';
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
export default function Comment(props) {
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState({ comment: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [submitError, setSubmitError] = useState('');
const [isLoadingComments, setIsLoadingComments] = useState(true);
const [editorMode, setEditorMode] = useState('edit');
const [imageDialogOpen, setImageDialogOpen] = useState(false);
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [uploadProgress, setUploadProgress] = useState(0);
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
// Custom image command for MDEditor
const customImageCommand = {
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
</svg>
),
execute: () => {
setImageDialogOpen(true);
},
};
// Get all default commands and replace the image command
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
// Load comments when component mounts
useEffect(() => {
if (props.topicId) {
fetchComments(props.topicId);
}
}, [props.topicId]);
const fetchComments = async (topicId) => {
setIsLoadingComments(true);
try {
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to fetch comments');
}
if (data.success && data.comments) {
setComments(data.comments);
} else {
throw new Error('Invalid response format');
}
} catch (error) {
console.error('Error fetching comments:', error);
setSubmitError('Failed to load comments');
} finally {
setIsLoadingComments(false);
}
};
// Upload file to MinIO
const uploadToMinIO = async (file) => {
const formData = new FormData();
formData.append('file', file);
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
try {
const response = await fetch(MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include'
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
return data.publicUrl;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// Handle image URL insertion
const handleInsertImageUrl = () => {
if (imageUrlInput) {
const imgMarkdown = `![Image](${imageUrlInput})`;
setNewComment(prev => ({
...prev,
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
}));
setImageDialogOpen(false);
setImageUrlInput('');
}
};
// Handle image file selection
const handleImageFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setImageUploadFile(file);
// Create preview
const reader = new FileReader();
reader.onload = () => {
setImageUploadPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
// Upload image file to MinIO and insert into editor
const handleImageUpload = async () => {
if (!imageUploadFile) return;
try {
setIsSubmitting(true);
setUploadProgress(0);
const uploadedUrl = await uploadToMinIO(imageUploadFile);
// Insert markdown for the uploaded image
const imgMarkdown = `![Image](${uploadedUrl})`;
setNewComment(prev => ({
...prev,
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
}));
setImageDialogOpen(false);
setImageUploadFile(null);
setImageUploadPreview('');
} catch (error) {
setSubmitError('Failed to upload image: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
const handleSubmitComment = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitError('');
try {
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...newComment,
topicId: props.topicId
}),
});
if (response.ok) {
setNewComment({ comment: '' });
setSubmitSuccess(true);
setTimeout(() => setSubmitSuccess(false), 3000);
// Refresh comments after successful submission
await fetchComments(props.topicId);
} else {
const errorData = await response.json();
setSubmitError(errorData.message || 'Failed to submit comment');
}
} catch (error) {
setSubmitError('Network error. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const getUserInitials = (name) => {
if (!name) return 'U';
const words = name.trim().split(' ');
return words
.slice(0, 2)
.map(word => word[0].toUpperCase())
.join('');
};
return (
<>
{/* Comments List */}
<div className="space-y-6 border-t">
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
{isLoadingComments ? (
<div className="flex justify-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div>
</div>
) : comments.length === 0 ? (
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
) : (
comments.map(comment => (
<div key={comment.id} className="border-b pb-6 last:border-b-0">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
{getUserInitials(comment.userName)}
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-[#6d9e37]">
{comment.userName || 'User'}
</h4>
<span className="text-xs text-gray-500">
{new Date(comment.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</span>
</div>
<div data-color-mode="light" className="markdown-body">
<MDEditor.Markdown source={comment.comment} />
</div>
</div>
</div>
</div>
))
)}
</div>
{/* Comments Section with MDEditor */}
<Card className="mt-16 border-t">
<div className="relative">
<div className="px-6 pb-6 rounded-lg">
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
{submitSuccess && (
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
Thank you for your comment!
</div>
)}
{submitError && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
{submitError}
</div>
)}
<form onSubmit={handleSubmitComment}>
<div className="mb-4">
<Label htmlFor="comment">Comment *</Label>
<div data-color-mode="light">
<MDEditor
placeholder="Write your comment (markdown supported)"
value={newComment.comment}
onChange={(value) => setNewComment(prev => ({ ...prev, comment: value || '' }))}
height={300}
preview={editorMode}
commands={allCommands}
/>
</div>
<div className="flex justify-end mt-2">
<button
type="button"
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
className={`text-sm ${editorMode !== 'edit' ? 'bg-[#6d9e37] text-white' : 'text-[#6d9e37]'} px-2 py-1 rounded-md border border-[#6d9e37]`}
>
{editorMode === 'edit' ? 'Preview' : 'Edit'}
</button>
</div>
</div>
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
{isSubmitting ? 'Submitting...' : 'Post Comment'}
</Button>
</form>
</div>
{/* Image Upload Dialog */}
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Insert Image</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="image-url">From URL</Label>
<Input
id="image-url"
type="text"
placeholder="Enter image URL"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
/>
<Button
type="button"
onClick={handleInsertImageUrl}
disabled={!imageUrlInput}
className="mt-2"
>
Insert Image
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="image-upload">Upload Image</Label>
<Input
id="image-upload"
type="file"
accept="image/*"
onChange={handleImageFileSelect}
/>
{imageUploadPreview && (
<div className="mt-2">
<img
src={imageUploadPreview}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
<Button
type="button"
onClick={handleImageUpload}
disabled={!imageUploadFile || isSubmitting}
className="mt-2"
>
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{loading ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
<span className="sr-only">Loading...</span>
</div>
</>
) : !isLoggedIn ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
Join the conversation! Log in or sign up to post a comment
</p>
</>
) : null}
</div>
</Card>
</>
);
}

View File

@ -1,200 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Card } from "./ui/card";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { Button } from "./ui/button";
import { useIsLoggedIn } from '../lib/isLoggedIn';
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
export default function Comment(props) {
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState({ comment: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [submitError, setSubmitError] = useState('');
const [isLoadingComments, setIsLoadingComments] = useState(true);
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
// Load comments when component mounts or when topicId changes
useEffect(() => {
if (props.topicId) {
fetchComments(props.topicId);
}
}, [props.topicId]);
const fetchComments = async (topicId) => {
setIsLoadingComments(true);
try {
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to fetch comments');
}
if (data.success && data.comments) {
setComments(data.comments);
} else {
throw new Error('Invalid response format');
}
} catch (error) {
console.error('Error fetching comments:', error);
setSubmitError('Failed to load comments');
} finally {
setIsLoadingComments(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setNewComment(prev => ({
...prev,
[name]: value
}));
};
const handleSubmitComment = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitError('');
try {
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...newComment,
topicId: props.topicId
}),
});
if (response.ok) {
setNewComment({ comment: '' });
setSubmitSuccess(true);
setTimeout(() => setSubmitSuccess(false), 3000);
// Refresh comments after successful submission
await fetchComments(props.topicId);
} else {
const errorData = await response.json();
setSubmitError(errorData.message || 'Failed to submit comment');
}
} catch (error) {
setSubmitError('Network error. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const getUserInitials = (name) => {
if (!name) return 'U';
const words = name.trim().split(' ');
return words
.slice(0, 2)
.map(word => word[0].toUpperCase())
.join('');
};
return (
<>
{/* Comments List */}
<div className="space-y-6 border-t">
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
{isLoadingComments ? (
<div className="flex justify-center py-4">
{/* <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div> */}
</div>
) : comments.length === 0 ? (
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
) : (
comments.map(comment => (
<div key={comment.id} className="border-b pb-6 last:border-b-0">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
{getUserInitials(comment.userName)}
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-[#6d9e37]">
{comment.userName || 'User'}
</h4>
<span className="text-xs text-gray-500">
{new Date(comment.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</span>
</div>
<p className="">{comment.comment}</p>
</div>
</div>
</div>
))
)}
</div>
{/* Comments Section */}
<Card className="mt-16 border-t">
<div className="relative">
<div className="px-6 pb-6 rounded-lg">
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
{submitSuccess && (
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
Thank you for your comment!
</div>
)}
{submitError && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
{submitError}
</div>
)}
<form onSubmit={handleSubmitComment}>
<div className="mb-4">
<Label htmlFor="comment">Comment *</Label>
<Textarea
className="mt-2"
id="comment"
name="comment"
rows="4"
value={newComment.comment}
onChange={handleInputChange}
required
disabled={!isLoggedIn}
/>
</div>
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
{isSubmitting ? 'Submitting...' : 'Post Comment'}
</Button>
</form>
</div>
{loading ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
<span className="sr-only">Loading...</span>
</div>
</>
) : !isLoggedIn ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
Join the conversation! Log in or sign up to post a comment
</p>
</>
) : null}
</div>
</Card>
</>
);
}

View File

@ -1,187 +0,0 @@
import React, { useState } from 'react';
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 { Button } from './ui/button';
export function ContactForm() {
const [formState, setFormState] = useState({ name: '', email: '', company: '', service: '', message: '' });
const [formStatus, setFormStatus] = useState('idle');
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormState(prev => ({ ...prev, [name]: value }));
};
const handleSelectChange = (value) => {
setFormState(prev => ({ ...prev, service: value }));
// console.log(formState)
};
const handleSubmit = async (e) => {
e.preventDefault();
setFormStatus('submitting');
try {
const payload = {
name: formState.name,
email: formState.email,
company: formState.company,
service_intrest: formState.service,
message: formState.message
};
const response = await fetch(`${USER_API_URL}?query=contact-form`, {
method: "POST",
credentials: "include",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
const data = await response.json();
// console.log('Form Response', data);
if(data.success) {
setFormStatus('success');
// Reset form only after successful submission
setFormState({ name: '', email: '', company: '', service: '', message: '' });
} else {
setFormStatus('error');
console.error('Backend error:', data.message);
}
} catch (error) {
console.error('Submission error:', error);
setFormStatus('error');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6" id='contact-form'>
<div className="space-y-3 sm:space-y-4">
{/* Name and Email */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="name" className="text-sm sm:text-base">Name*</Label>
<Input
id="name"
name="name"
placeholder="Your name"
value={formState.name}
onChange={handleInputChange}
required
disabled={formStatus === 'submitting'}
className="text-sm sm:text-base"
/>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="email" className="text-sm sm:text-base">Email*</Label>
<Input
id="email"
name="email"
type="email"
placeholder="your.email@example.com"
value={formState.email}
onChange={handleInputChange}
required
disabled={formStatus === 'submitting'}
className="text-sm sm:text-base"
/>
</div>
</div>
{/* Company and Service Interest */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="company" className="text-sm sm:text-base">Company</Label>
<Input
id="company"
name="company"
placeholder="Your company name"
value={formState.company}
onChange={handleInputChange}
disabled={formStatus === 'submitting'}
className="text-sm sm:text-base"
/>
</div>
{/* dop_v1_b6a075ece5786faf7c58d21761dbf95d47af372da062d68a870ce2a0bae51adf */}
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="service" className="text-sm sm:text-base">Service Interest*</Label>
<Select
value={formState.service}
onValueChange={handleSelectChange}
disabled={formStatus === 'submitting'}
>
<SelectTrigger className="text-sm sm:text-base">
<SelectValue placeholder="Select a service" /></SelectTrigger>
<SelectContent>
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
<SelectItem value="python">Python Hosting</SelectItem>
<SelectItem value="kubernetes">Kubernetes (K8s)</SelectItem>
<SelectItem value="k3s">K3s Lightweight Kubernetes</SelectItem>
<SelectItem value="custom">Custom Solution</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Message */}
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="message" className="text-sm sm:text-base">Message*</Label>
<Textarea
id="message"
name="message"
placeholder="Tell us about your project requirements..."
value={formState.message}
onChange={handleInputChange}
required
disabled={formStatus === 'submitting'}
className="min-h-[100px] sm:min-h-[150px] text-sm sm:text-base"
/>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
size="lg"
className="w-full mt-2 text-sm sm:text-base py-2 sm:py-3"
disabled={formStatus === 'submitting'}
>
{formStatus === 'submitting' ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</>
) : 'Send Message'}
</Button>
{/* Status Messages */}
{formStatus === 'success' && (
<div
className="p-3 sm:p-4 bg-green-900/30 border border-green-800 rounded-md text-green-400 text-center text-sm sm:text-base"
aria-live="polite"
>
Thank you for your message! We'll get back to you soon.
</div>
)}
{formStatus === 'error' && (
<div
className="p-3 sm:p-4 bg-red-900/30 border border-red-800 rounded-md text-red-400 text-center text-sm sm:text-base"
aria-live="assertive"
>
There was an error sending your message. Please try again.
</div>
)}
</form>
);
}

View File

@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { Input } from './ui/input';
import { Textarea } from './ui/textarea';
import { Label } from './ui/label';
import { Select } from './ui/select';
import { Button } from './ui/button';
export function ContactForm() {
const [formState, setFormState] = useState({
name: '',
email: '',
company: '',
service: '',
message: '',
});
const [formStatus, setFormStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormState(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setFormStatus('submitting');
// Simulate form submission with a timeout
setTimeout(() => {
// In a real app, you would send the data to a server here
console.log('Form submitted:', formState);
setFormStatus('success');
// Reset form after successful submission
setFormState({
name: '',
email: '',
company: '',
service: '',
message: '',
});
// Reset status after showing success message for a while
setTimeout(() => {
setFormStatus('idle');
}, 3000);
}, 1500);
};
return (
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
<div className="space-y-3 sm:space-y-4">
{/* Name and Email - Stack on mobile, side-by-side on tablet+ */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="name" className="text-sm sm:text-base">Name</Label>
<Input
id="name"
name="name"
placeholder="Your name"
value={formState.name}
onChange={handleChange}
required
className="text-sm sm:text-base"
/>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="email" className="text-sm sm:text-base">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="your.email@example.com"
value={formState.email}
onChange={handleChange}
required
className="text-sm sm:text-base"
/>
</div>
</div>
{/* Company and Service Interest - Stack on mobile, side-by-side on tablet+ */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="company" className="text-sm sm:text-base">Company</Label>
<Input
id="company"
name="company"
placeholder="Your company name"
value={formState.company}
onChange={handleChange}
className="text-sm sm:text-base"
/>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="service" className="text-sm sm:text-base">Service Interest</Label>
<Select
id="service"
name="service"
value={formState.service}
onChange={handleChange}
required
className="text-sm sm:text-base"
>
<option value="" disabled>Select a service</option>
<option value="php">PHP Hosting</option>
<option value="nodejs">Node.js Hosting</option>
<option value="python">Python Hosting</option>
<option value="kubernetes">Kubernetes (K8s)</option>
<option value="k3s">K3s Lightweight Kubernetes</option>
<option value="custom">Custom Solution</option>
</Select>
</div>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="message" className="text-sm sm:text-base">Message</Label>
<Textarea
id="message"
name="message"
placeholder="Tell us about your project requirements..."
value={formState.message}
onChange={handleChange}
required
className="min-h-[100px] sm:min-h-[150px] text-sm sm:text-base"
/>
</div>
</div>
<Button
type="submit"
size="lg"
className="w-full mt-2 text-sm sm:text-base py-2 sm:py-3"
disabled={formStatus === 'submitting'}
>
{formStatus === 'submitting' ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</>
) : 'Send Message'}
</Button>
{formStatus === 'success' && (
<div className="p-3 sm:p-4 bg-green-900/30 border border-green-800 rounded-md text-green-400 text-center text-sm sm:text-base">
Thank you for your message! We'll get back to you soon.
</div>
)}
{formStatus === 'error' && (
<div className="p-3 sm:p-4 bg-red-900/30 border border-red-800 rounded-md text-red-400 text-center text-sm sm:text-base">
There was an error sending your message. Please try again.
</div>
)}
</form>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,963 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Toast } from './Toast';
import { TemplatePreview } from './TemplatePreview';
import { Button } from './ui/button';
import { Input } from "./ui/input";
import { Label, Select } from '@radix-ui/react-select';
export const DomainSetupForm = ({ defaultSubdomain }) => {
// Deployment type and app selections
const [deploymentType, setDeploymentType] = useState('app');
const [appType, setAppType] = useState('wordpress');
const [sampleWebAppType, setSampleWebAppType] = useState('developer'); // New state for sample web app type
const [sourceType, setSourceType] = useState('public');
const [repoUrl, setRepoUrl] = useState('');
const [deploymentKey, setDeploymentKey] = useState('');
const [fileName, setFileName] = useState('');
// Domain configuration
const [useSubdomain, setUseSubdomain] = useState(true);
const [useCustomDomain, setUseCustomDomain] = useState(false);
const [customDomain, setCustomDomain] = useState('');
const [customSubdomain, setCustomSubdomain] = useState('');
const [domainType, setDomainType] = useState('domain'); // 'domain' or 'subdomain'
// Domain validation states
const [isValidating, setIsValidating] = useState(false);
const [isValidDomain, setIsValidDomain] = useState(false);
const [validationMessage, setValidationMessage] = useState('');
// DNS configuration
const [dnsMethod, setDnsMethod] = useState('cname');
const [showDnsConfig, setShowDnsConfig] = useState(false);
const [dnsVerified, setDnsVerified] = useState({ cname: false, ns: false, a: false, ip: false });
const [txnId, setTxnId] = useState('');
const [userEmail, setUserEmail] = useState('');
const [panelType, setPanelType] = useState('');
const [panelServicesId, setPanelServicesId] = useState('');
const [vpnContinent, setVpnContinent] = useState('');
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
// const BILLING_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
const [selectedcycle, setSelectedcycle] = useState('');
const [selectedPrice, setSelectedPrice] = useState(0);
const handleCheckboxChange = (cycle, price) => {
if (selectedcycle === cycle) {
setSelectedcycle('');
setSelectedPrice(0);
} else {
setSelectedcycle(cycle);
setSelectedPrice(price);
}
// console.log(selectedcycle, ' ', selectedPrice);
};
const handlePanelBuyNow = (whichService) => {
// console.log(whichService);
// Disable button during processing
const buyButton = document.getElementById('buy-button'); // Add ID to your button
if (buyButton) buyButton.disabled = true;
showToast('Loading...');
if(whichService === 'php-mysql-with-admin-panel'){
// console.log('php-mysql-with-admin-panel: ', whichService);
const formData = new FormData();
formData.append('service', panelType);
formData.append('serviceId', panelServicesId);
formData.append('cycle', selectedcycle);
formData.append('amount', selectedPrice); //selectedPrice
fetch(`${API_URL}?query=initiate_payment`, {
method: 'POST',
body: formData,
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if(data.success === true){
setTxnId(data.txn_id);
setUserEmail(data.user_email);
window.location.href = `/make-payment?query=get-initiated_payment&orderId=${data.order_id}`
// redirectToPayU(data);
showToast('Redirecting to payment page...');
} else {
throw new Error(data.message || 'Payment initialization failed');
}
})
.catch(error => {
showToast(error.message || 'Payment failed. Please try again.');
console.error('An error occurred:', error);
})
.finally(() => {
if (buyButton) buyButton.disabled = false;
});
} else if(whichService === 'vpn'){
const formData = new FormData();
formData.append('service', deploymentType);
formData.append('serviceId', 'vpnservices');
formData.append('cycle', selectedcycle);
formData.append('amount', selectedPrice);
fetch(`${API_URL}?query=initiate_payment`, {
method: 'POST',
body: formData,
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if(data.success === true){
setTxnId(data.txn_id);
setUserEmail(data.user_email);
// window.location.href = `/make-payment?query=get-initiated_payment&orderId=${data.order_id}`
window.location.href = `/success?service=vpn&orderId=${data.order_id}`
// redirectToPayU(data);
showToast('Redirecting to payment page...');
} else {
throw new Error(data.message || 'Payment initialization failed');
}
})
.catch(error => {
showToast(error.message || 'Payment failed. Please try again.');
console.error('An error occurred:', error);
})
.finally(() => {
if (buyButton) buyButton.disabled = false;
});
}
};
// Form validation
const [formValid, setFormValid] = useState(true);
// Toast notification
const [toast, setToast] = useState({ visible: false, message: '' });
// File upload reference
const fileInputRef = React.useRef(null);
// Effect for handling domain type changes
useEffect(() => {
if (!useCustomDomain) {
setShowDnsConfig(false);
setIsValidDomain(false);
setValidationMessage('');
setIsValidating(false);
}
validateForm();
}, [useCustomDomain, dnsVerified.cname, dnsVerified.ns, dnsVerified.ns, domainType, dnsMethod]);
// Show toast notification
const showToast = (message) => {
setToast({ visible: true, message });
setTimeout(() => setToast({ visible: false, message: '' }), 3000);
};
// Handle deployment type change
const handleDeploymentTypeChange = (e) => {
setDeploymentType(e.target.value);
};
// Handle source type change
const handleSourceTypeChange = (e) => {
setSourceType(e.target.value);
};
// Handle file upload
const handleFileChange = (e) => {
if (e.target.files.length > 0) {
setFileName(e.target.files[0].name);
} else {
setFileName('');
}
};
// Handle domain checkbox changes
const handleUseSubdomainChange = (e) => {
setUseSubdomain(e.target.checked);
// If CNAME record is selected, SiliconPin subdomain must be enabled
if (useCustomDomain && dnsMethod === 'cname' && !e.target.checked) {
setUseCustomDomain(false);
}
validateForm();
};
const handleUseCustomDomainChange = (e) => {
setUseCustomDomain(e.target.checked);
if (!e.target.checked) {
setShowDnsConfig(false);
setIsValidDomain(false);
setValidationMessage('');
setIsValidating(false);
setDnsVerified({
cname: false,
ns: false,
a: false,
ip: false
});
} else {
// Force SiliconPin subdomain to be checked if custom domain is checked
setUseSubdomain(true);
}
validateForm();
};
// Handle domain type change
const handleDomainTypeChange = (e) => {
setDomainType(e.target.value);
// Reset validation when changing domain type
setIsValidDomain(false);
setValidationMessage('');
setDnsVerified({
cname: false,
ns: false,
a: false
});
validateForm();
};
// Handle domain and subdomain input changes
const handleDomainChange = (e) => {
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?(www\.)?/i, '').replace(/\/+$/, '').trim();
setCustomDomain(cleanedValue);
};
const handleSubdomainChange = (e) => {
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?/i, '').replace(/\/+$/, '').trim();
setCustomSubdomain(cleanedValue);
};
// Handle DNS method change
const handleDnsMethodChange = (e) => {
setDnsMethod(e.target.value);
// If changing to CNAME, ensure SiliconPin subdomain is enabled
if (e.target.value === 'cname') {
setUseSubdomain(true);
}
// Reset DNS verification
setDnsVerified(prev => ({
...prev,
[e.target.value]: false
}));
validateForm();
};
// Validate domain
const validateDomain = async () => {
const domain = domainType === 'domain' ? customDomain : customSubdomain;
// Reset validation state
setIsValidating(true);
setIsValidDomain(false);
setValidationMessage('');
setShowDnsConfig(false);
// Check if domain is empty
if (!domain) {
setValidationMessage('Please enter a domain name.');
setIsValidating(false);
setIsValidDomain(false);
return;
}
// Validate domain format
const domainFormatRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
if (!domainFormatRegex.test(domain)) {
setValidationMessage('Domain format is invalid. Please check your entry.');
setIsValidating(false);
setIsValidDomain(false);
return;
}
try {
// Make API call to validate domain
const response = await fetch(`${SERVICES_API_URL}?query=validate-domain`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domain })
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
if (data.valid) {
setValidationMessage('Domain is valid and registered.');
setIsValidDomain(true);
setShowDnsConfig(true);
} else {
setValidationMessage(data.message || 'Domain appears to be unregistered or unavailable.');
setIsValidDomain(false);
}
} catch (error) {
console.error('Domain validation error:', error);
setValidationMessage('Error validating domain. Please try again.');
setIsValidDomain(false);
} finally {
setIsValidating(false);
validateForm();
}
};
// Check DNS configuration
const checkSubDomainCname = () => {
const domainToCheck = customDomain || customSubdomain;
fetch(`${SERVICES_API_URL}?query=check-c-name`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `domain=${encodeURIComponent(domainToCheck)}`
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
checkDnsConfig('cname');
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
// console.log('CNAME record:', data.cname);
// Handle success - update UI
} else {
console.error('Error:', data.message);
// Handle error
}
})
.catch(error => {
console.error('Fetch error:', error);
// Show error to user
});
};
const checkDnsConfig = (type) => {
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
// Simulate DNS check
setTimeout(() => {
setDnsVerified(prev => ({
...prev,
[type]: true
}));
showToast(`${type} verified successfully!`);
validateForm();
}, 1500);
};
// Copy to clipboard
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => {
showToast('Copied to clipboard!');
})
.catch(err => {
showToast('Failed to copy: ' + err);
});
};
// Validate form
const validateForm = () => {
// For custom domain, require DNS verification
if (useCustomDomain) {
if (dnsMethod === 'cname' && !dnsVerified.cname) {
setFormValid(false);
return;
}
if (dnsMethod === 'ns' && !dnsVerified.ns) {
setFormValid(false);
return;
}
if (dnsMethod === 'ip' && !dnsVerified.ip) {
setFormValid(false);
return;
}
}
setFormValid(true);
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
if (!formValid) {
showToast('Please complete DNS verification before deploying.');
return;
}
// In a real app, this would submit the form data to the server
// console.log([{ deploymentType, appType, sampleWebAppType, sourceType, repoUrl, deploymentKey, useSubdomain, useCustomDomain, customDomain, customSubdomain, domainType, dnsMethod}]);
showToast('Form submitted successfully!');
};
return (
<div className="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Deployment Type Selection */}
<div className="space-y-2">
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
<select
id="deployment-type"
value={deploymentType}
onChange={handleDeploymentTypeChange}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="app">💻 Deploy an App</option>
<option value="source"> From Source</option>
<option value="static">📄 Static Site Upload</option>
<option value="sample-web-app">🌐 Sample Web App</option>
<option value="php-mysql-with-admin-panel">🗄 PHP MYSQL with a admin panel</option>
<option value="vpn">🔒 VPN</option>
</select>
</div>
{/* App Options */}
{deploymentType === 'app' && (
<div className="space-y-2">
<label htmlFor="app-type" className="block text-white font-medium">Select Application</label>
<select
id="app-type"
value={appType}
onChange={(e) => setAppType(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="wordpress">🔌 WordPress</option>
<option value="prestashop">🛒 PrestaShop</option>
<option value="laravel">🚀 Laravel</option>
<option value="cakephp">🍰 CakePHP</option>
<option value="symfony">🎯 Symfony</option>
</select>
</div>
)}
{/* Sample Web App Options */}
{deploymentType === 'sample-web-app' && (
<div className="space-y-2">
<label htmlFor="sample-web-app-type" className="block text-white font-medium">Select Template Type</label>
<select
id="sample-web-app-type"
value={sampleWebAppType}
onChange={(e) => setSampleWebAppType(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="developer">👨💻 Developer Portfolio</option>
<option value="designer">🎨 Designer Portfolio</option>
<option value="photographer">📸 Photographer Portfolio</option>
<option value="documentation">📚 Documentation Site</option>
<option value="business">🏢 Single Page Business Site</option>
</select>
<div className="mt-4 p-4 bg-neutral-700/30 rounded-md border border-neutral-600">
<div className="flex items-start">
<div className="mr-3 text-2xl"></div>
<div className="flex-1">
<p className="text-sm text-neutral-300">
Deploy a ready-to-use template that you can customize. We'll set up the basic structure and you can modify it to fit your needs.
</p>
</div>
</div>
</div>
{/* Template Preview */}
<TemplatePreview templateType={sampleWebAppType} />
</div>
)}
{/* Source Options */}
{deploymentType === 'source' && (
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="source-type" className="block text-white font-medium">Source Type</label>
<select
id="source-type"
value={sourceType}
onChange={handleSourceTypeChange}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="public">🌐 Public Repository</option>
<option value="private">🔒 Private Repository</option>
</select>
</div>
<div className="pt-2">
<label htmlFor="repo-url" className="block text-white font-medium mb-2">Repository URL</label>
<input
type="text"
id="repo-url"
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/username/repository"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
</div>
{sourceType === 'private' && (
<div className="pt-2">
<div className="flex items-center gap-2 mb-2">
<label htmlFor="deployment-key" className="block text-white font-medium">Deployment Key</label>
<button type="button" onClick={() => showToast('Deployment keys are used for secure access to private repositories')} className="text-neutral-400 hover:text-white focus:outline-none" aria-label="Deployment Key Information">
<span className="text-lg"></span>
</button>
</div>
<textarea id="deployment-key" value={deploymentKey} onChange={(e) => setDeploymentKey(e.target.value)} placeholder="Paste your SSH private key here" rows="6" className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37] font-mono text-sm" />
<p className="mt-1 text-sm text-neutral-400">Your private key is used only for deploying and is never stored on our servers.</p>
</div>
)}
</div>
)}
{/* Static Site Options */}
{deploymentType === 'static' && (
<div className="space-y-4">
<div className="p-4 bg-neutral-700/50 rounded-md text-neutral-300 text-center">
Upload the zip file containing your static website
</div>
<div className="pt-2">
<label htmlFor="file-upload" className="block text-white font-medium mb-2">Upload File (ZIP/TAR)</label>
<div className="flex items-center justify-center w-full">
<label
htmlFor="file-upload"
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer border-neutral-600 hover:border-[#6d9e37]"
>
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<span className="text-3xl mb-3 text-neutral-400">📁</span>
<p className="mb-2 text-sm text-neutral-400">
<span className="font-semibold">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-neutral-500">ZIP or TAR files only (max. 100MB)</p>
</div>
<input
id="file-upload"
ref={fileInputRef}
type="file"
onChange={handleFileChange}
className="hidden"
accept=".zip,.tar"
/>
</label>
</div>
{fileName && (
<div className="mt-2 text-sm text-neutral-400">
Selected file: {fileName}
</div>
)}
</div>
</div>
)}
{
deploymentType === 'php-mysql-with-admin-panel' && (
<div className="space-y-2">
<label htmlFor="app-type" className="block text-white font-medium">Select Panel</label>
<select
id="app-type"
value={panelType}
onChange={(e) => {
setPanelType(e.target.value);
setPanelServicesId(e.target.options[e.target.selectedIndex].dataset.id);
}}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="">-Select-</option>
<option value="Hestia-Panel" data-id="4ksYhjFy">🧰 Hestia Panel</option>
<option value="Webmin" data-id="5ksYAhFz">🖥 Webmin</option>
<option value="cPanel" data-id="6jgThjZx">📊 cPanel</option>
</select>
</div>
)
}
{
deploymentType === 'vpn' && (
<div className="space-y-2">
<label htmlFor="app-type" className="block text-white font-medium">Select Contient</label>
<select
id="app-type"
value={vpnContinent}
onChange={(e) => {
setVpnContinent(e.target.value);
// setVpnContinent(e.target.options[e.target.selectedIndex].dataset.id);
}}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="">-Select-</option>
<option value="India" data-id="7ksXhkFy">India</option>
<option value="America" data-id="8ksAshGz">America</option>
<option value="Europe" data-id="9LgFhjVx">Europe</option>
</select>
</div>
)
}
{
deploymentType === 'vpn' && (
<>
<ul className="flex justify-between text-sm">
{["Unlimited bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"].map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" ><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
<span className='text-zinc-400'>{feature}</span>
</li>
))}
</ul>
<div className='flex flex-row justify-between items-center gap-x-6'>
<label className={`border ${selectedcycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'monthly'} onChange={() => handleCheckboxChange('monthly', 100)} className="hidden" />
<p className='text-3xl font-bold text-center'>100</p>
<span>Monthly</span>
</label>
<label className={`border ${selectedcycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'yearly'} onChange={() => handleCheckboxChange('yearly', 1000)} className="hidden" />
<p className='text-3xl font-bold text-center'>1000</p>
<span>Yearly</span>
</label>
</div>
<div className='text-white'>
{selectedcycle && (
<p className={`${selectedcycle === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedcycle}</strong> plan at {selectedPrice}</p>
)}
</div>
</>
)
}
{
deploymentType === 'php-mysql-with-admin-panel' && panelType === 'Hestia-Panel' && (
<>
<ul className="flex justify-between text-sm">
{["5 Domains", "free Let's Encrypt SSL", "1 MariaDB database", "phpMyAdmin"].map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" ><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
<span className='text-zinc-400'>{feature}</span>
</li>
))}
</ul>
<div className='flex flex-row justify-between items-center gap-x-6'>
<label className={`border ${selectedcycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'monthly'} onChange={() => handleCheckboxChange('monthly', 200)} className="hidden" />
<p className='text-3xl font-bold text-center'>200</p>
<span>Monthly</span>
</label>
<label className={`border ${selectedcycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'yearly'} onChange={() => handleCheckboxChange('yearly', 2000)} className="hidden" />
<p className='text-3xl font-bold text-center'>2000</p>
<span>Yearly</span>
</label>
</div>
<div className='text-white'>
{selectedcycle && (
<p className={`${selectedcycle === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedcycle}</strong> plan at {selectedPrice}</p>
)}
</div>
</>
)
}
{
deploymentType === 'php-mysql-with-admin-panel' && panelType === 'cPanel' ? (
<div>
<p>
cPanel is a proprietary software. Here at Siliconpin, we encourage using freedom-oriented software.
If you need a cPanel, you can visit &nbsp;
<a className='text-[#6d9e37]' href="https://cicdhosting.com" target='_blank'>https://cicdhosting.com</a>
</p>
</div>
) : deploymentType === 'php-mysql-with-admin-panel' && (
<Button onClick={() => {handlePanelBuyNow('php-mysql-with-admin-panel')}} className='w-full'>Proceed to Pay</Button>
)
}
{
deploymentType === 'vpn' && (
<Button onClick={() => {handlePanelBuyNow('vpn')}} className='w-full'>Proceed to Pay</Button>
)
}
{
deploymentType !== 'php-mysql-with-admin-panel' && deploymentType !== 'vpn' &&(
<>
{/* Domain Configuration */}
<div className="pt-4 border-t border-neutral-700">
<h3 className="text-lg font-medium text-white mb-4">Destination</h3>
<div className="space-y-4">
{/* SiliconPin Subdomain */}
<div className="flex items-start gap-2">
<input type="checkbox" id="use-subdomain" checked={useSubdomain} onChange={handleUseSubdomainChange} className="mt-1 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="use-subdomain" className="block text-white font-medium">Use SiliconPin Subdomain</label>
<div className="mt-2 flex">
<input
type="text"
id="subdomain"
name="destination-domain"
value={defaultSubdomain}
className="rounded-l-md py-2 px-3 bg-neutral-600 border-y border-l border-neutral-600 text-neutral-300 focus:outline-none w-1/3 font-mono"
readOnly
/>
<span className="rounded-r-md py-2 px-3 bg-neutral-800 border border-neutral-700 text-neutral-400 w-2/3">.subdomain.siliconpin.com</span>
</div>
</div>
</div>
{/* Custom Domain */}
<div className="flex items-start gap-2">
<input type="checkbox" id="use-custom-domain" name="destination-domain" checked={useCustomDomain} onChange={handleUseCustomDomainChange} className="mt-1 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="use-custom-domain" className="block text-white font-medium">Use Custom Domain</label>
{useCustomDomain && (
<div className="mt-3 space-y-4">
{/* Domain Type Selection */}
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
<div className="flex items-center">
<input type="radio" id="domain-type-domain" name="domain-type" value="domain" checked={domainType === 'domain'} onChange={handleDomainTypeChange} className="mr-2 accent-[#6d9e37]" />
<label htmlFor="domain-type-domain" className="text-white">Root Domain</label>
</div>
<div className="flex items-center">
<input type="radio" id="domain-type-subdomain" name="domain-type" value="subdomain" checked={domainType === 'subdomain'} onChange={handleDomainTypeChange} className="mr-2 accent-[#6d9e37]"/>
<label htmlFor="domain-type-subdomain" className="text-white">Subdomain</label>
</div>
</div>
{/* Domain Input */}
{domainType === 'domain' ? (
<div>
<input
type="text"
id="custom-domain"
value={customDomain}
onChange={handleDomainChange}
placeholder="yourdomain.com"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
<p className="mt-1 text-xs text-neutral-400">
Enter domain without http://, www, or trailing slashes (example.com). You can configure www or other subdomains later.
</p>
</div>
) : (
<div>
<input
type="text"
id="custom-subdomain"
value={customSubdomain}
onChange={handleSubdomainChange}
placeholder="blog.yourdomain.com"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
<p className="mt-1 text-xs text-neutral-400">
Enter the full subdomain without http:// or trailing slashes. www and protocol prefixes will be automatically removed.
</p>
</div>
)}
{/* Domain Validation */}
<button
disabled={!customDomain && !customSubdomain}
type="button"
onClick={validateDomain}
className={`px-4 py-2 ${ !customDomain && !customSubdomain ? 'bg-neutral-600 cursor-not-allowed' : 'bg-[#6d9e37] focus:ring-[#6d9e37] transition-colors'} text-white font-medium rounded-md transition-colors focus:outline-none`}
>
Validate Domain
</button>
{/* Validation Status */}
{useCustomDomain && isValidating && (
<div className="p-3 bg-neutral-700/50 rounded-md">
<div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
<p className="text-white">Verifying domain registration and availability...</p>
</div>
</div>
)}
{useCustomDomain && !isValidating && validationMessage && (
<div className={`p-3 rounded-md ${isValidDomain ? 'bg-green-900/20 border border-green-700/50' : 'bg-red-900/20 border border-red-700/50'}`}>
<p className={isValidDomain ? 'text-green-400' : 'text-red-400'}>
{validationMessage}
</p>
</div>
)}
{/* DNS Configuration Options */}
{showDnsConfig && (
<div className="mt-4 space-y-4 p-4 rounded-md bg-neutral-800/50 border border-neutral-700">
<h4 className="text-white font-medium">Connect Your Domain</h4>
{/* CNAME Record Option */}
<div className={`p-4 bg-neutral-700/30 rounded-md space-y-3 ${dnsMethod === 'cname' ? 'border-2 border-[#6d9e37]' : 'border border-neutral-600'}`}>
<label for="dns-cname" className={`flex items-start cursor-pointer`}>
<input type="radio" id="dns-cname" name="dns-method" value="cname" checked={dnsMethod === 'cname'} onChange={handleDnsMethodChange} className="mt-1 mr-2 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="dns-cname" className="block text-white font-medium">Use CNAME Record</label>
<p className="text-sm text-neutral-300">Point your domain to our SiliconPin subdomain</p>
<div className="mt-3 flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">
{defaultSubdomain}.subdomain.siliconpin.com
</div>
<button
type="button"
onClick={() => copyToClipboard(`${defaultSubdomain}.subdomain.siliconpin.com`)}
className="ml-2 text-[#6d9e37] hover:text-white"
aria-label="Copy CNAME value"
>
<span className="text-lg">📋</span>
</button>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => (checkSubDomainCname())}
className={`px-3 py-1 text-white text-sm rounded
${dnsVerified.cname
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{dnsVerified.cname ? '✓ CNAME Verified' : 'Check CNAME'}
</button>
</div>
</div>
</label>
</div>
{/* Nameserver Option (only for full domains, not subdomains) */}
{domainType === 'domain' && (
<>
<div className={`p-4 bg-neutral-700/30 rounded-md space-y-3 ${dnsMethod === 'ns' ? 'border-2 border-[#6d9e37]' : 'border border-neutral-600'}`}>
<label for="dns-ns" className="flex items-start cursor-pointer">
<input type="radio" id="dns-ns" name="dns-method" value="ns" checked={dnsMethod === 'ns'} onChange={handleDnsMethodChange} className="mt-1 mr-2 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
<div className="mt-3 space-y-2">
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
<button type="button" onClick={() => copyToClipboard('ns1.siliconpin.com')} className="ml-2 text-[#6d9e37] hover:text-white" aria-label="Copy nameserver value">
<span className="text-lg">📋</span>
</button>
</div>
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
<button type="button" onClick={() => copyToClipboard('ns2.siliconpin.com')} className="ml-2 text-[#6d9e37] hover:text-white" aria-label="Copy nameserver value">
<span className="text-lg">📋</span>
</button>
</div>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => checkDnsConfig('ns')}
className={`px-3 py-1 text-white text-sm rounded
${dnsVerified.ns
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{dnsVerified.ns ? '✓ Nameservers Verified' : 'Check Nameservers'}
</button>
</div>
</div>
</label>
</div>
<div className={`p-4 bg-neutral-700/30 rounded-md space-y-3 ${dnsMethod === 'ip' ? 'border-2 border-[#6d9e37]' : 'border border-neutral-600'}`}>
<label for="dns-ip" className="flex items-start cursor-pointer">
<input type="radio" id="dns-ip" name="dns-method" value="ip" checked={dnsMethod === 'ip'} onChange={handleDnsMethodChange} className="mt-1 mr-2 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="dns-ip" className="block text-white font-medium">Use Our IP Address</label>
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
<div className="mt-3 space-y-2">
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">xxx.xxx.x.xx</div>
<button type="button" onClick={() => copyToClipboard('xxx.xxx.x.xx')} className="ml-2 text-[#6d9e37] hover:text-white" aria-label="Copy nameserver value">
<span className="text-lg">📋</span>
</button>
</div>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => checkDnsConfig('ip')}
className={`px-3 py-1 text-white text-sm rounded
${dnsMethod === 'ip'
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{/* {dnsVerified.ip ? '✓ IP Address Verified' : 'Check IP Address'} */}
Proceed to Pay
</button>
</div>
</div>
</label>
</div>
</>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
{/* Form Submit Button */}
<Button
type="submit"
disabled={useCustomDomain && !formValid}
className={`w-full mt-6 px-6 py-3 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-800
${useCustomDomain && !formValid
? 'bg-neutral-600 cursor-not-allowed'
: 'bg-[#6d9e37] hover:bg-[#598035] focus:ring-[#6d9e37] transition-colors'
}`}
>
Start the Deployment
</Button>
</>
)
}
</form>
<Toast visible={toast.visible} message={toast.message} />
</div>
);
};
// upi://pay?pa=merchant@bank&pn=Merchant%20Inc&am=100.00&cu=INR&tn=Payment%20for%20goods

View File

@ -1,333 +0,0 @@
"use client";
import { useState } from "react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
import { Input } from "./ui/input";
// Define the AI Agent type
interface AIAgent {
id: number;
title: string;
description: string;
capabilities: string[];
responseTime: string;
pricing: string;
useCases: string[];
}
// AI Agent types data
const aiAgentTypes: AIAgent[] = [
{
id: 1,
title: "Code Assistant",
description: "AI-powered coding assistant that helps write, debug, and optimize code across multiple languages",
capabilities: ["Code Generation", "Code Review", "Bug Detection", "Code Explanation", "Documentation"],
responseTime: "Instant",
pricing: "$29/month",
useCases: ["Software Development", "Web Development", "Mobile Apps", "Enterprise Applications"]
},
{
id: 2,
title: "Data Analyst",
description: "Specialized AI for data analytics, visualization, and insights extraction",
capabilities: ["Data Cleaning", "Statistical Analysis", "Data Visualization", "Predictive Modeling"],
responseTime: "Within minutes",
pricing: "$49/month",
useCases: ["Business Intelligence", "Market Research", "Financial Analysis", "Customer Insights"]
},
{
id: 3,
title: "Content Creator",
description: "AI agent for generating and optimizing various types of content",
capabilities: ["Blog Writing", "Social Media Content", "Email Marketing", "SEO Optimization"],
responseTime: "Instant to minutes",
pricing: "$39/month",
useCases: ["Marketing Teams", "Content Publishers", "Social Media Managers", "Small Businesses"]
},
{
id: 4,
title: "UI/UX Design Assistant",
description: "AI that helps create design mockups, wireframes, and UI elements",
capabilities: ["Wireframing", "UI Design", "Visual Elements", "Design Feedback"],
responseTime: "Within minutes",
pricing: "$59/month",
useCases: ["Product Teams", "Web Designers", "App Developers", "Creative Agencies"]
},
{
id: 5,
title: "Research Assistant",
description: "AI agent that helps with research, information gathering, and summarization",
capabilities: ["Literature Review", "Data Collection", "Summarization", "Citation Management"],
responseTime: "Within minutes",
pricing: "$45/month",
useCases: ["Academic Research", "Market Research", "Competitive Analysis", "Legal Research"]
}
];
export default function HireAIAgent() {
const [selectedAgent, setSelectedAgent] = useState<AIAgent | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
// Filter AI agents based on search term
const filteredAgents = aiAgentTypes.filter(agent =>
agent.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
agent.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
agent.capabilities.some(cap => cap.toLowerCase().includes(searchTerm.toLowerCase())) ||
agent.useCases.some(useCase => useCase.toLowerCase().includes(searchTerm.toLowerCase()))
);
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-center text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">Hire an AI Agent</h1>
<p className="text-center text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300 mb-10">
Leverage our AI agents to automate tasks, generate content, analyze data, and more
</p>
{/* Hero Banner */}
<div className="bg-neutral-800 text-white rounded-lg p-8 mb-12">
<div className="flex flex-col md:flex-row md:items-center">
<div className="md:w-2/3 mb-6 md:mb-0 md:pr-6">
<h2 className="text-2xl md:text-3xl font-bold mb-4 text-[#6d9e37]">Why Choose AI Agents?</h2>
<p className="mb-4">
Our AI agents work 24/7, provide instant results, and cost a fraction of human labor. They excel at repetitive tasks, data processing, and creative content generation.
</p>
<div className="grid grid-cols-2 gap-4 mt-6">
<div className="flex items-start">
<div className="bg-white bg-opacity-20 p-2 rounded mr-3">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#6d9e37">
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
</svg>
</div>
<div>
<h3 className="font-semibold text-sm">Instant Results</h3>
<p className="text-xs text-zinc-300">No waiting for human availability</p>
</div>
</div>
<div className="flex items-start">
<div className="bg-white bg-opacity-20 p-2 rounded mr-3">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#6d9e37">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
</div>
<div>
<h3 className="font-semibold text-sm">Cost Effective</h3>
<p className="text-xs text-zinc-300">Fraction of human labor costs</p>
</div>
</div>
</div>
</div>
<div className="md:w-1/3">
<div className="bg-white bg-opacity-10 p-6 rounded-lg text-center">
<h3 className="font-bold mb-2">Start with an AI Agent Today</h3>
<p className="text-sm mb-4 text-zinc-300">
All plans come with a 7-day free trial
</p>
<Button className="w-full">View All Agents</Button>
</div>
</div>
</div>
</div>
{/* Search */}
<div className="mb-8">
<div className="max-w-md mx-auto">
<Input
type="text"
placeholder="Search AI agents by capability or use case..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="mb-4"
/>
</div>
</div>
{/* AI Agent Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredAgents.map((agent) => (
<Card key={agent.id} className="hover:shadow-md transition">
<CardHeader>
<CardTitle>{agent.title}</CardTitle>
<CardDescription>{agent.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="mb-4">
<h4 className="text-sm font-semibold mb-2">Key Capabilities:</h4>
<div className="flex flex-wrap gap-2">
{agent.capabilities.map((capability, index) => (
<span
key={index}
className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded"
>
{capability}
</span>
))}
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-zinc-500">Response Time:</span>
<span className="font-medium text-green-600">{agent.responseTime}</span>
</div>
<div className="flex justify-between">
<span className="text-zinc-500">Pricing:</span>
<span className="font-medium">{agent.pricing}</span>
</div>
</div>
</CardContent>
<CardFooter>
<Button className="w-full" onClick={() => {setSelectedAgent(agent); setDialogOpen(true);}}>View Details</Button>
</CardFooter>
</Card>
))}
</div>
{/* AI Agent Details Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-md">
{selectedAgent && (
<>
<DialogHeader>
<DialogTitle>{selectedAgent.title}</DialogTitle>
<DialogDescription>
{selectedAgent.description}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<h4 className="text-sm font-semibold mb-1">Capabilities:</h4>
<div className="flex flex-wrap gap-2">
{selectedAgent.capabilities.map((capability, index) => (
<span
key={index}
className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded"
>
{capability}
</span>
))}
</div>
</div>
<div>
<h4 className="text-sm font-semibold mb-1">Common Use Cases:</h4>
<ul className="list-disc pl-5 text-zinc-600 text-sm">
{selectedAgent.useCases.map((useCase, index) => (
<li key={index}>{useCase}</li>
))}
</ul>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-sm font-semibold mb-1">Response Time:</h4>
<p className="font-medium text-green-600">{selectedAgent.responseTime}</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-1">Pricing:</h4>
<p className="font-medium">{selectedAgent.pricing}</p>
</div>
</div>
<div className="border-t border-gray-200 pt-4">
<h4 className="text-sm font-semibold mb-1">Subscription Includes:</h4>
<ul className="text-sm text-zinc-600 space-y-1">
<li className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
Unlimited requests
</li>
<li className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
24/7 availability
</li>
<li className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
API access
</li>
<li className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
Export capabilities
</li>
</ul>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
Cancel
</Button>
<Button type="button">
Subscribe Now
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
{/* Comparison Section */}
<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>
</div>
</div>
{/* CTA Section */}
<div className="mt-16 bg-neutral-800 p-8 rounded-lg text-center">
<h2 className="text-2xl font-bold mb-4 text-[#6d9e37]">Not sure which AI agent is right for you?</h2>
<p className="text-neutral-300 max-w-2xl mx-auto mb-8">
Our AI solution experts can help you determine which AI agent best fits your specific needs and use cases.
</p>
<Button size="lg">
Schedule a Consultation
</Button>
</div>
</div>
);
}

View File

@ -1,258 +0,0 @@
"use client";
import { useState } from "react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
// Define the Developer type
interface Developer {
id: number;
title: string;
description: string;
skills: string[];
availability: "High" | "Medium" | "Low";
minBudget: string;
contractFee: string;
}
// Developer types data
const developerTypes: Developer[] = [
{
id: 1,
title: "Web Developer",
description: "Frontend, backend or full-stack developers specialized in web technologies",
skills: ["React", "Angular", "Vue", "Node.js", "Django", "Ruby on Rails", "Laravel", "Express", "Flask", "PHP", "JavaScript", "TypeScript", "HTML", "CSS", "SASS", "LESS", "Bootstrap", "Tailwind CSS", "Material-UI", "Ant Design", "REST APIs", "GraphQL", "WebSockets", "OAuth", "JWT", "WebRTC", "PWA", "SEO", "Accessibility", "Performance", "Security"],
availability: "High",
minBudget: "Budget-friendly",
contractFee: "Nominal"
},
{
id: 2,
title: "App Developer",
description: "iOS and Android native or cross-platform mobile app developers",
skills: ["Swift", "Kotlin", "React Native", "Flutter", "Xamarin", "Ionic", "PhoneGap", "Cordova", "NativeScript", "Appcelerator", "Java", "Objective-C", "Firebase", "Realm", "Core Data", "SQLite", "Push Notifications", "In-App Purchases", "ARKit", "Core ML", "Android Jetpack", "Material Design", "Google Play Services", "App Store Connect", "Google Play Console"],
availability: "Medium",
minBudget: "Limited",
contractFee: "Modest"
},
{
id: 3,
title: "C/C++ Developer",
description: "Systems programmers and embedded systems specialists",
skills: ["C", "C++", "Embedded Systems", "Linux Kernel", "Real-time Systems", "RTOS", "Device Drivers", "Firmware", "Microcontrollers", "ARM", "AVR", "PIC", "Raspberry Pi", "Arduino", "BeagleBone", "Zephyr", "FreeRTOS", "VxWorks", "QNX", "Bare Metal"],
availability: "Low",
minBudget: "Increased",
contractFee: "Considerable"
},
{
id: 4,
title: "Go Developer",
description: "Backend and systems engineers specializing in Go language",
skills: ["Go", "Microservices", "Docker", "Kubernetes", "API Development", "gRPC", "Protobuf", "WebSockets", "WebAssembly", "Concurrency", "Performance Tuning", "Module Building", "Error Handling", "Testing", "Security"],
availability: "Medium",
minBudget: "Boosted",
contractFee: "Elevated"
},
{
id: 5,
title: "Machine Learning Engineer",
description: "AI and ML specialists for implementing intelligent solutions",
skills: ["Python", "TensorFlow", "PyTorch", "Data Science", "MLOps", "NLP", "Computer Vision", "Reinforcement Learning", "Time Series Analysis", "Anomaly Detection", "Recommendation Systems", "Predictive Modeling", "Deep Learning", "Fraud Detection", "Sentiment Analysis", "Speech Recognition", "Image Processing", "Object Detection", "Generative Models", "AutoML", "Image Recognition", "Text Classification", "Chatbots", "AI Ethics"],
availability: "Low",
minBudget: "Premium",
contractFee: "Lavish"
},
{
id: 6,
title: "DevOps Engineer",
description: "Specialists in CI/CD, cloud infrastructure, and deployment automation",
skills: ["AWS", "Azure", "GCP", "Terraform", "Ansible", "Jenkins", "Docker", "Kubernetes", "GitOps", "Monitoring", "Logging", "Security", "Compliance", "Scalability", "Reliability", "Resilience", "Performance", "Cost Optimization", "Disaster Recovery", "Incident Response", "SRE"],
availability: "Medium",
minBudget: "Pricier",
contractFee: "Substantial"
}
];
export default function HireHumanDeveloper() {
const [selectedType, setSelectedType] = useState<Developer | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
// Filter developers based on search term
const filteredDevelopers = developerTypes.filter(dev =>
dev.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
dev.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
dev.skills.some(skill => skill.toLowerCase().includes(searchTerm.toLowerCase()))
);
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-center text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">Hire a Human Developer</h1>
<p className="text-center text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300 mb-10">
Connect with skilled developers across various technologies and specialties
</p>
{/* Search and Filter */}
<div className="mb-8">
<div className="max-w-md mx-auto">
<Input
type="text"
placeholder="Search by technology, skill, or type..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="mb-4"
/>
</div>
</div>
{/* Developer Types Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredDevelopers.map((developer) => (
<Card key={developer.id} className="hover:shadow-md transition">
<CardHeader>
<CardTitle>{developer.title}</CardTitle>
<CardDescription>{developer.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="mb-4">
<h4 className="text-sm font-semibold mb-2 text-neutral-300">Key Skills:</h4>
<div className="flex flex-wrap gap-2">
{developer.skills.map((skill, index) => (
<span
key={index}
className="inline-block bg-neutral-300 text-zinc-800 text-xs px-2 py-1 rounded"
>
{skill}
</span>
))}
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-zinc-500">Availability:</span>
<span className={`font-medium ${
developer.availability === "High" ? "text-green-600" :
developer.availability === "Medium" ? "text-amber-600" : "text-red-600"
}`}>
{developer.availability}
</span>
</div>
<div className="flex justify-between">
<span className="text-zinc-500">Starting at:</span>
<span className="font-medium">{developer.minBudget}</span>
</div>
<div className="flex justify-between">
<span className="text-zinc-500">Contract Fee:</span>
<span className="font-medium">{developer.contractFee}</span>
</div>
</div>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={() => {
setSelectedType(developer);
setDialogOpen(true);
}}
>
Hire Developer
</Button>
</CardFooter>
</Card>
))}
</div>
{/* Developer Details Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-md">
{selectedType && (
<>
<DialogHeader>
<DialogTitle>{selectedType.title} Details</DialogTitle>
<DialogDescription>
Review the details and proceed to hire
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Overview:</h4>
<p className="text-neutral-400">{selectedType.description}</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Specialized Skills:</h4>
<div className="flex flex-wrap gap-2">
{selectedType.skills.map((skill, index) => (
<span key={index} className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded">{skill}</span>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Availability:</h4>
<p className={`font-medium ${
selectedType.availability === "High" ? "text-green-600" :
selectedType.availability === "Medium" ? "text-amber-600" : "text-red-600"
}`}>
{selectedType.availability}
</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Minimum Budget:</h4>
<p className="font-medium text-neutral-950">{selectedType.minBudget}</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Contract Fee:</h4>
<p className="font-medium text-neutral-950">{selectedType.contractFee}</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Usual Turnaround:</h4>
<p className="font-medium text-neutral-950">1-3 weeks</p>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
Cancel
</Button>
<Button type="button">
Proceed to Hire
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
{/* Additional Information */}
<div className="mt-16 bg-neutral-800 p-8 rounded-lg">
<h2 className="text-2xl font-bold mb-4 text-[#6d9e37]">Why Hire Our Human Developers?</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h3 className="text-lg font-semibold mb-2 text-[#6d9e37]">Vetted Experts</h3>
<p className="text-[#fff]">
All our developers go through a rigorous vetting process to ensure top-quality talent.
</p>
</div>
<div>
<h3 className="text-lg font-semibold mb-2 text-[#6d9e37]">Flexible Engagement</h3>
<p className="text-[#fff]">
Hire on hourly, project-based, or long-term contracts based on your needs.
</p>
</div>
<div>
<h3 className="text-lg font-semibold mb-2 text-[#6d9e37]">Satisfaction Guarantee</h3>
<p className="text-[#fff]">
Not satisfied with the talent? We'll match you with another developer at no extra cost.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,344 +0,0 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import PocketBase from 'pocketbase';
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Separator } from "./ui/separator";
import { Label } from "./ui/label";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from "./ui/loader";
interface AuthStatus {
message: string;
isError: boolean;
}
interface UserRecord {
id: string;
email: string;
name?: string;
type?: string;
avatar?: string;
[key: string]: any;
}
interface AuthResponse {
token: string;
record: UserRecord;
}
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordVisible, setPasswordVisible] = useState(false);
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
const [isLoading, setIsLoading] = useState(false);
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
interface AuthResponse {
token: string;
record: {
query: string;
id: string;
email: string;
name: string;
type: 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);
if (!authData?.token || !authData?.record) {
throw new Error("Authentication failed: No token or user record received");
}
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 || authData.record.email.split('@')[0], // Fallback name
type: authData.record.type || 'user',
avatar: authData.record.avatar || ''
}
};
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error: any) {
console.error("Login failed:", error);
setStatus({
message: error.message || "Login failed. Please check your credentials.",
isError: true
});
} finally {
setIsLoading(false);
}
};
const loginWithOAuth2 = async (provider: 'google' | 'facebook' | 'github') => {
try {
setIsLoading(true);
setStatus({ message: '', isError: false });
const authData = await pb.collection('users').authWithOAuth2({ provider });
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 || '',
type: authData.record.type || '',
avatar: authData.record.avatar || ''
}
};
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error) {
console.error(`${provider} Login failed:`, error);
setStatus({
message: `${provider} login failed. Please try again.`,
isError: true
});
} finally {
setIsLoading(false);
}
};
// Set Coockie Function
function setCookie(name : string, value : string, daysToExpire : number) {
const date = new Date();
date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = `${name}=${value}; ${expires}; path=/`;
}
const handleCreateUserInMongo = async (siliconId: string) => {
try {
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
siliconId: siliconId
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('MongoDB Response:', data);
return data;
} catch (error) {
console.error('MongoDB Creation Error:', error);
throw error; // Return Error
}
};
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
try {
// Step 1: Sync with SiliconPin backend
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
query: 'new',
accessToken: authData.token,
email: authData.record.email,
name: authData.record.name,
type: authData.record.type,
avatar: avatarUrl,
isAuthenticated: true,
id: authData.record.id
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Backend Sync Data:', data);
// Step 2: creating user in MongoDB
await handleCreateUserInMongo(data.userData.userId);
// Step 3: Redirect to profile if all are ok
window.location.href = '/profile';
setCookie('token', data.userData.accessToken, 7);
return data;
} catch (error: any) {
console.error('Sync Error:', error);
throw new Error(`Session sync failed: ${error.message}`);
}
};
const { isLoggedIn, loading, error } = useIsLoggedIn();
// console.log(isLoggedIn)
if(loading){
return <Loader />
}
if(isLoggedIn){
return (
// <></>
window.location.href = '/'
)
}
return (
<div className="min-h-screen flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg rounded-xl overflow-hidden">
<CardHeader className="text-center space-y-1">
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
<CardDescription className="">
Sign in to access your account
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{status.message && (
<div className={`p-3 rounded-md text-sm ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{status.message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" className="">
Email Address
</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="password" className="">
Password
</Label>
<button
type="button"
onClick={() => setPasswordVisible(!passwordVisible)}
className="text-sm text-[#6d9e37]"
>
</button>
</div>
<div className="relative">
<Input
id="password"
type={passwordVisible ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setPasswordVisible(!passwordVisible)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{passwordVisible ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<Button
variant="outline"
onClick={() => loginWithOAuth2('google')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/google.svg" alt="Google" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => loginWithOAuth2('facebook')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/facebook.svg" alt="Facebook" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => loginWithOAuth2('github')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
</Button>
</div>
</CardContent>
<div className="px-6 pb-6 text-center text-sm text-gray-500">
Don't have an account?{' '}
<a href="/sign-up" className="font-medium text-[#6d9e37] hover:underline">
Sign up
</a>
</div>
</Card>
</div>
);
};
export default LoginPage;

View File

@ -1,63 +0,0 @@
import React from "react";
import { useIsLoggedIn } from '../lib/isLoggedIn';
export default function LoginOrProfile({ deviceType }) {
const { isLoggedIn, loading } = useIsLoggedIn();
if (loading) {
return (
<>
{
deviceType === 'desktop' && (
<p className="w-6 animate-pulse hover:text-[#6d9e37] transition-colors"></p>
)
}
{
deviceType === 'mobile' && (
<svg className="animate-spin flex flex-col items-center justify-center w-[45px] h-[45px] text-[#6d9e37] hover:text-white transition-colors" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Download_x5F_25_x25_"> </g> <g id="Download_x5F_50_x25_"> </g> <g id="Download_x5F_75_x25_"> </g> <g id="Download_x5F_100_x25_"> </g> <g id="Upload"> </g> <g id="Next"> </g> <g id="Last"> </g> <g id="OK"> </g> <g id="Fail"> </g> <g id="Add"> </g> <g id="Spinner_x5F_0_x25_"> </g> <g id="Spinner_x5F_25_x25_"> <g> <path fill="none" stroke="#6d9e37" stroke-width="4" stroke-linejoin="round" stroke-miterlimit="10" d="M50.188,26.812 c12.806,0,23.188,10.381,23.188,23.188"></path> <g> <circle cx="73.375" cy="50" r="1.959"></circle> <circle cx="72.029" cy="57.704" r="1.959"></circle> <circle cx="68.237" cy="64.579" r="1.959"></circle> <circle cx="62.324" cy="69.759" r="1.959"></circle> <circle cx="55.013" cy="72.699" r="1.959"></circle> </g> <g> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.0326 2.3121)" cx="27.045" cy="51.843" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.628 2.4912)" cx="28.998" cy="59.416" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.1348 2.8556)" cx="33.325" cy="65.967" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.4877 3.3713)" cx="39.63" cy="70.661" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.6506 3.9762)" cx="47.152" cy="73.012" rx="1.959" ry="1.959"></ellipse> </g> <g> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.713 2.567)" cx="27.71" cy="44.049" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.079 2.8158)" cx="30.892" cy="36.872" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.567 3.266)" cx="36.334" cy="31.199" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.2318 3.8617)" cx="43.363" cy="27.636" rx="1.959" ry="1.959"></ellipse> </g> </g> </g> <g id="Spinner_x5F_50_x25_"> </g> <g id="Spinner_x5F_75_x25_"> </g> <g id="Brightest_x5F_25_x25_"> </g> <g id="Brightest_x5F_50_x25_"> </g> <g id="Brightest_x5F_75_x25_"> </g> <g id="Brightest_x5F_100_x25_"> </g> <g id="Reload"> </g> <g id="Forbidden"> </g> <g id="Clock"> </g> <g id="Compass"> </g> <g id="World"> </g> <g id="Speed"> </g> <g id="Microphone"> </g> <g id="Options"> </g> <g id="Chronometer"> </g> <g id="Lock"> </g> <g id="User"> </g> <g id="Position"> </g> <g id="No_x5F_Signal"> </g> <g id="Low_x5F_Signal"> </g> <g id="Mid_x5F_Signal"> </g> <g id="High_x5F_Signal"> </g> <g id="Options_1_"> </g> <g id="Flash"> </g> <g id="No_x5F_Signal_x5F_02"> </g> <g id="Low_x5F_Signal_x5F_02"> </g> <g id="Mid_x5F_Signal_x5F_02"> </g> <g id="High_x5F_Signal_x5F_02"> </g> <g id="Favorite"> </g> <g id="Search"> </g> <g id="Stats_x5F_01"> </g> <g id="Stats_x5F_02"> </g> <g id="Turn_x5F_On_x5F_Off"> </g> <g id="Full_x5F_Height"> </g> <g id="Full_x5F_Width"> </g> <g id="Full_x5F_Screen"> </g> <g id="Compress_x5F_Screen"> </g> <g id="Chat"> </g> <g id="Bluetooth"> </g> <g id="Share_x5F_iOS"> </g> <g id="Share_x5F_Android"> </g> <g id="Love__x2F__Favorite"> </g> <g id="Hamburguer"> </g> <g id="Flying"> </g> <g id="Take_x5F_Off"> </g> <g id="Land"> </g> <g id="City"> </g> <g id="Nature"> </g> <g id="Pointer"> </g> <g id="Prize"> </g> <g id="Extract"> </g> <g id="Play"> </g> <g id="Pause"> </g> <g id="Stop"> </g> <g id="Forward"> </g> <g id="Reverse"> </g> <g id="Next_1_"> </g> <g id="Last_1_"> </g> <g id="Empty_x5F_Basket"> </g> <g id="Add_x5F_Basket"> </g> <g id="Delete_x5F_Basket"> </g> <g id="Error_x5F_Basket"> </g> <g id="OK_x5F_Basket"> </g> </g></svg>
)
}
</>
);
}
if (!isLoggedIn) {
return (
<>
{deviceType === 'desktop' && (
<a href="/login" className="hover:text-[#6d9e37] transition-colors">Login</a>
)}
{deviceType === 'mobile' && (
<a href="/login" className="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="10" r="3"></circle>
<path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path>
</svg>
<span className="text-xs mt-1">Login</span>
</a>
)}
</>
);
}
return (
<>
{deviceType === 'desktop' && (
<a href="/profile" className="hover:text-[#6d9e37] transition-colors">Profile</a>
)}
{deviceType === 'mobile' && (
<a href="/profile" className="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="10" r="3"></circle>
<path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path>
</svg>
<span className="text-xs mt-1">Profile</span>
</a>
)}
</>
);
}

View File

@ -1,322 +0,0 @@
import React, {useState, useEffect} from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import QRCode from "react-qr-code";
import { Dialog, DialogContent, DialogHeader, DialogTitle} from "./ui/dialog";
import { Toast } from './Toast';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
import { FileX, Copy, CheckCircle2, AlertCircle, X } from "lucide-react";
import Loader from "./ui/loader";
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
export default function MakePayment(){
const [initialOrderData, setInitialOrderData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [showQRModal, setShowQRModal] = useState(false);
const [showHelpMessage, setShowHelpMessage] = useState(false);
const [transactionId, setTransactionId] = useState('');
const [orderIdInput, setOrderIdInput] = useState('');
const [upiPaymentLink, setUpiPaymentLink] = useState("");
const [copied, setCopied] = useState(false);
const [getOrderId, setGetOrderId] = useState();
const [saveUpiResponse, setSaveUpiResponse] = useState({ message: '', visible: false, isSuccess: false });
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const orderId = urlParams.get('orderId');
if (!orderId) {
setError('Order ID is missing from URL');
setIsLoading(false);
return;
}
setGetOrderId(orderId);
const getInitialOrderData = () => {
setIsLoading(true);
const formData = new FormData();
formData.append('order_id', orderId);
fetch(`${API_URL}?query=get-initiated_payment`, {
method: 'POST',
body: formData,
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!data.success) {
throw new Error(data.message || 'Failed to initialize payment');
}
setInitialOrderData(data);
setError(null);
// Generate UPI payment link when data is loaded
if (data.payment_data?.amount) {
const amount = data.payment_data.amount;
const upiLink = generateUPILink(amount, data.txn_id);
setUpiPaymentLink(upiLink);
}
})
.catch(error => {
setError(error.message || 'Payment failed. Please try again.');
console.error('An error occurred:', error);
})
.finally(() => {
setIsLoading(false);
});
};
getInitialOrderData();
}, []);
function generateUPILink(amount, transactionId) {
// Replace these with your actual merchant details
const merchantUPI = "7001601485@okbizaxis";
const merchantName = "SiliconPin";
const currency = "INR";
// Encode parameters for URL
const encodedMerchantName = encodeURIComponent(merchantName);
const transactionNote = encodeURIComponent(`Payment for order #${transactionId}`);
// Construct UPI payment link
return `upi://pay?pa=${merchantUPI}&pn=${encodedMerchantName}&am=${amount}&cu=${currency}&tn=${transactionNote}`;
}
// waiting payment update
// Payment update not recieved if
function redirectToPayU() {
if (!initialOrderData?.payment_data || !initialOrderData.payment_url) {
console.error('Payment data not loaded yet');
alert('Payment information is not ready. Please wait.');
return;
}
// Create a form dynamically
const form = document.createElement('form');
form.method = 'POST';
form.action = initialOrderData.payment_url;
form.style.display = 'none';
Object.entries(initialOrderData.payment_data).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'text';
input.name = key;
input.value = value;
form.appendChild(input);
console.log(`Adding field: ${key}=${value}`);
});
document.body.appendChild(form);
form.submit();
}
useEffect(() => {
let timer;
if (showQRModal) {
setShowHelpMessage(false);
timer = setTimeout(() => {
setShowHelpMessage(true);
}, 2000);
}
return () => clearTimeout(timer); // Cleanup on unmount or when modal closes
}, [showQRModal]);
function handleQRPaymentClick() {
if (!upiPaymentLink) {
alert('Payment information is not ready. Please wait.');
return;
}
setShowQRModal(true);
}
const handleCopy = () => {
navigator.clipboard.writeText(getOrderId)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1000); // Reset after 1 second
})
.catch(err => {
console.error('Failed to copy text: ', err);
});
};
const handlePayPalPayment = () => {
const rawInrAmount = initialOrderData.payment_data?.amount || 0;
const exchangeRate = 80;
const convertedUSD = rawInrAmount / exchangeRate;
const usdAmount = Math.max(convertedUSD, 1).toFixed(2);
const paypalUrl = `https://www.paypal.com/paypalme/dwdconsultancy/${usdAmount}`;
window.open(paypalUrl, '_blank');
};
const handleSaveUPIPayment = async () => {
try {
const response = await fetch(`${API_URL}?query=save-upi-payment`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderId: orderIdInput,
transactionId: transactionId
})
});
const data = await response.json();
if (data.success === true) {
setSaveUpiResponse({
message: `Transaction ID ${transactionId} saved. We'll contact you shortly.`,
visible: true,
isSuccess: true
});
setShowHelpMessage(false);
}
} catch (error) {
console.error('Error:', error);
setSaveUpiResponse({
message: 'Failed to save payment details. Please try again.',
visible: true,
isSuccess: false
});
}
};
if (isLoading) {
return <Loader />;
}
if(!initialOrderData){
return (
<div className="flex flex-col items-center justify-center flex-grow py-10 text-center px-4 text-gray-600 mt-20">
<FileX className="mb-4 text-gray-500" size={100} />
<p className="text-lg"> No bill was found. <a className="text-[#6d9e37] font-medium hover:underline" href="/profile"> Click here </a> to go to your profile.</p>
</div>
);
}
if (error) {
return <div className="flex items-center justify-center min-h-screen">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<strong>Error:</strong> {error}
</div>
</div>;
}
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4">
<Card className="max-w-md w-full bg-white p-6 rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4">Complete Your Payment</h1>
{initialOrderData && (
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<div className="flex justify-between mb-2">
<span className="font-bold">Order ID:</span>
<span>{initialOrderData.txn_id}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Amount:</span>
<span>{initialOrderData.payment_data?.amount}</span>
</div>
</div>
)}
<div className="flex flex-col">
<span className=""><strong>Pay Using:</strong></span>
<hr className="border-b border-b-[#6d9e37] mb-4" />
<Button className="" onClick={handleQRPaymentClick}>UPI QR Code</Button>
<span className="text-center mt-2 text-gray-400 text-xs">Banking Service by DWD Consultancy Services (0% Processing Fee)</span>
<div className="flex items-center my-2">
<div className="flex-grow border-t border-gray-500"></div>
<span className="mx-2 text-gray-500 font-semibold">OR</span>
<div className="flex-grow border-t border-gray-500"></div>
</div>
<Button className="text-[#414042] font-bold border-[2px] border-[#00ad7d] hover:bg-[#00ad7d]/80" onClick={redirectToPayU} variant="outline">
Pay with&nbsp;
<img className="w-[50px]" src="/assets/payu-logo.svg" alt="" />
</Button>
<Button
variant="outline"
className="mt-4 text-[#414042] font-bold border-[2px] border-b-[#003087] border-r-[#003087] border-t-[#009CDE] border-l-[#009CDE] hover:bg-gradient-to-r hover:from-[#009CDE] hover:to-[#003087]"
onClick={handlePayPalPayment}
>
Pay with&nbsp;
<img className="w-[50px]" src="/assets/paypal-logo-white.svg" alt="PayPal" />
</Button>
{/* <span className="text-center mt-2 text-gray-400 text-xs">2% Payment Gateway Charge</span> */}
{/* Add this divider and PayPal button */}
{/* <div className="flex items-center my-2">
<div className="flex-grow border-t border-gray-500"></div>
<span className="mx-2 text-gray-500 font-semibold">OR</span>
<div className="flex-grow border-t border-gray-500"></div>
</div> */}
{/* <span className="text-center mt-2 text-gray-400 text-xs">3.5% Payment Processing Fee</span> */}
</div>
</Card>
{/* QR Code Modal */}
<Dialog open={showQRModal} onOpenChange={(open) => {setShowQRModal(open); if (!open) {setShowHelpMessage(false);}}}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Scan QR Code to Pay</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center space-y-4">
<div className="p-4 bg-white rounded-lg border border-gray-200">
<QRCode value={upiPaymentLink} size={256} level="H" bgColor="#ffffff" fgColor="#000000" />
</div>
<p className="text-sm text-gray-500 text-center">
Scan this QR code with any UPI app to complete your payment
</p>
<Button variant="outline" onClick={handleCopy} className="text-sm" >{copied ? 'Copied!' : getOrderId} &nbsp; {!copied && <Copy size={15} />}</Button>
{showHelpMessage && (
<div className="w-full mt-4 p-4 bg-yellow-50 rounded-lg border border-yellow-200">
<p className="text-sm text-yellow-800 mb-3">
If you haven't received payment confirmation, please share your Order Id & transaction ID with us.
</p>
<Input type="text" value={orderIdInput} onChange={(e) => setOrderIdInput(e.target.value)} placeholder="Enter your Order ID" className="w-full p-2 border border-gray-300 rounded-md text-sm mb-2" />
<Input type="text" value={transactionId} onChange={(e) => setTransactionId(e.target.value)} placeholder="Enter your transaction ID" className="w-full p-2 border border-gray-300 rounded-md text-sm mb-2" />
<Button onClick={handleSaveUPIPayment} className="w-full text-sm">Save Transaction ID</Button>
</div>
)}
{saveUpiResponse.visible && (
<div className={`fixed bottom-0 z-50 flex items-center justify-center`}>
<div className={`relative p-6 rounded-lg shadow-lg max-w-xs md:max-w-md w-full ${saveUpiResponse.isSuccess ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800'} animate-fade-in-up`}>
<button
onClick={() => setSaveUpiResponse(prev => ({...prev, visible: false}))}
className={`absolute top-3 right-3 p-1 rounded-full ${saveUpiResponse.isSuccess ? 'hover:bg-green-100 text-green-500' : 'hover:bg-red-100 text-red-500'} transition-colors`}>
<X className="w-4 h-4" />
</button>
<div className="flex flex-col items-center text-center gap-4">
<div className="flex items-center gap-3">
{saveUpiResponse.isSuccess ? (
<CheckCircle2 className="w-6 h-6 text-green-500" />
) : (
<AlertCircle className="w-6 h-6 text-red-500" />
)}
<p className="text-base font-medium">{saveUpiResponse.message}</p>
</div>
<Button variant="outline" size="sm" onClick={() => window.location.href = '/'} className={saveUpiResponse.isSuccess ? 'border-green-300' : 'border-red-300'}>Back to Home</Button>
</div>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
);
}
// Transaction ID ${transactionId} saved. We'll contact you shortly.

View File

@ -1,591 +0,0 @@
import React, { useState, useEffect } from "react";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import Loader from "../../components/ui/loader";
import { ChevronUp, ChevronDown, Eye, Pencil, Trash2, Download, CircleArrowRight } from "lucide-react";
import { Button } from "../../components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
export default function AllCustomerList() {
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [usersData, setUsersData] = useState([]);
const [dataLoading, setDataLoading] = useState(true);
const [apiError, setApiError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
if (isLoggedIn && sessionData?.user_type === 'admin') {
fetchUsersData();
}
}, [isLoggedIn, sessionData]);
const fetchUsersData = async () => {
setDataLoading(true);
try {
const res = await fetch(`${API_URL}?query=get-all-users`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
setUsersData(data.data || []);
} catch (err) {
setApiError(err.message);
} finally {
setDataLoading(false);
}
};
const handleSearch = (e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
};
const requestSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
};
const handleEdit = (user) => {
setCurrentUser(user);
setIsEditModalOpen(true);
};
const handleViewUser = (user) => {
setCurrentUser(user);
setIsViewModalOpen(true)
}
const handleDelete = (user) => {
setCurrentUser(user);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
try {
const res = await fetch(`${API_URL}${currentUser.id}`, {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
// Refresh the user list
await fetchUsersData();
setIsDeleteModalOpen(false);
} catch (err) {
setApiError(err.message);
}
};
const saveUserChanges = async (updatedUser) => {
try {
const res = await fetch(`${API_URL}${updatedUser.id}`, {
method: 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedUser)
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
// Refresh the user list
await fetchUsersData();
setIsEditModalOpen(false);
} catch (err) {
setApiError(err.message);
}
};
const filteredData = usersData.filter(user => {
const searchLower = searchTerm.toLowerCase();
return (
user.name.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.siliconId.toLowerCase().includes(searchLower) ||
user.pbId.toLowerCase().includes(searchLower)
);
});
const sortedData = [...filteredData].sort((a, b) => {
if (sortConfig.key) {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
}
return 0;
});
// Pagination logic
const totalPages = Math.ceil(sortedData.length / itemsPerPage);
const currentItems = sortedData.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const paginate = (pageNumber) => setCurrentPage(pageNumber);
const getPaginationRange = () => {
const totalPageCount = totalPages;
const currentPageNum = currentPage;
const siblingCount = 1;
const DOTS = '...';
// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS
const totalPageNumbers = siblingCount + 5;
if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount);
}
const leftSiblingIndex = Math.max(currentPageNum - siblingCount, 1);
const rightSiblingIndex = Math.min(
currentPageNum + siblingCount,
totalPageCount
);
const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const firstPageIndex = 1;
const lastPageIndex = totalPageCount;
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount);
return [...leftRange, DOTS, totalPageCount];
}
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount;
let rightRange = range(
totalPageCount - rightItemCount + 1,
totalPageCount
);
return [firstPageIndex, DOTS, ...rightRange];
}
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex);
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
}
};
const range = (start, end) => {
let length = end - start + 1;
return Array.from({ length }, (_, idx) => idx + start);
};
const formatDate = (dateString) => {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
};
if (loading || dataLoading) return <Loader />;
if (error || apiError) return <p>Error: {error?.message || apiError}</p>;
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<h1 className="text-2xl font-bold">Customer Management</h1>
<div className="relative w-full md:w-64">
<input
type="text"
placeholder="Search customers..."
className="w-full pl-4 pr-10 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
value={searchTerm}
onChange={handleSearch}
/>
<svg
className="absolute right-3 top-2.5 h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
></path>
</svg>
</div>
</div>
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('siliconId')}
>
<div className="flex items-center">
Silicon ID
{sortConfig.key === 'siliconId' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('name')}
>
<div className="flex items-center">
Name
{sortConfig.key === 'name' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('email')}
>
<div className="flex items-center">
Email
{sortConfig.key === 'email' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('pbId')}
>
<div className="flex items-center">
PB ID
{sortConfig.key === 'pbId' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('type')}
>
<div className="flex items-center">
Type
{sortConfig.key === 'type' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('created_at')}
>
<div className="flex items-center">
Created At
{sortConfig.key === 'created_at' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentItems.length > 0 ? (
currentItems.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{user.siliconId}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.email}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.pbId}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user.type === 'admin' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800' }`}>
{user.type.charAt(0).toUpperCase() + user.type.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(user.created_at)}
</td>
<td className="inline-flex px-6 py-4 space-x-4 whitespace-nowrap text-sm font-medium">
<button onClick={() => window.open(`/manager/view-as?siliconId=${user.siliconId}`)} title={`View as ${user.name}`}>
<CircleArrowRight className="text-[#6d9e37] w-5 h-5 hover:bg-gray-700 rounded-full transition-all transform hover:scale-105 duration-500" />
</button>
<button title="View User" onClick={() => {handleViewUser(user)}} className="text-indigo-600 hover:text-indigo-900">
<Eye className="w-5 h-5" />
</button>
<button title="Edit User" onClick={() => handleEdit(user)} className="text-yellow-600 hover:text-yellow-900">
<Pencil className="w-5 h-5" />
</button>
<button title="Delete User" onClick={() => handleDelete(user)} className="text-red-600 hover:text-red-900">
<Trash2 className="w-5 h-5" />
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
No customers found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{sortedData.length > itemsPerPage && (
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600">
Page {currentPage} of {totalPages}
</div>
<div className="flex flex-wrap gap-2">
{currentPage > 1 && (
<>
<Button onClick={() => paginate(1)} disabled={currentPage === 1} variant="outline" size="sm">First</Button>
<Button onClick={() => paginate(currentPage - 1)} disabled={currentPage === 1} variant="outline" size="sm">Previous</Button>
</>
)}
{getPaginationRange().map((item, index) => {
if (item === '...') {
return (
<span key={`ellipsis-${index}`} className="px-2 py-1">...</span>
);
}
return (
<Button key={`page-${item}`} onClick={() => paginate(item)} variant={currentPage === item ? "default" : "outline"} size="sm">{item}</Button>
);
})}
{currentPage < totalPages && (
<>
<Button onClick={() => paginate(currentPage + 1)} disabled={currentPage === totalPages} variant="outline" size="sm">Next</Button>
<Button onClick={() => paginate(totalPages)} disabled={currentPage === totalPages} variant="outline" size="sm">Last</Button>
</>
)}
</div>
</div>
)}
{/* Edit Modal */}
{isEditModalOpen && currentUser && (
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit User</DialogTitle>
<DialogDescription>
Make changes to the user profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="name" className="text-right">
Name
</label>
<input
id="name"
type="text"
className="col-span-3 border rounded-md px-3 py-2"
value={currentUser?.name || ''}
onChange={(e) => setCurrentUser({...currentUser, name: e.target.value})}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="email" className="text-right">
Email
</label>
<input
id="email"
type="email"
className="col-span-3 border rounded-md px-3 py-2"
value={currentUser?.email || ''}
onChange={(e) => setCurrentUser({...currentUser, email: e.target.value})}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="type" className="text-right">
Type
</label>
<select
id="type"
className="col-span-3 border rounded-md px-3 py-2"
value={currentUser?.type || 'user'}
onChange={(e) => setCurrentUser({...currentUser, type: e.target.value})}
>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setIsEditModalOpen(false)}>Cancel</Button>
<Button type="button" onClick={() => {saveUserChanges(currentUser); setIsEditModalOpen(false);}}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* View User Modal */}
{isViewModalOpen && currentUser && (
<Dialog open={isViewModalOpen} onOpenChange={setIsViewModalOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-2xl">User Details</DialogTitle>
<DialogDescription>
Detailed information about {currentUser.name}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Avatar
</div>
<div className="col-span-3">
{currentUser.avatar ? (
<img src={currentUser.avatar} alt={`${currentUser.name}'s avatar`} className="h-16 w-16 rounded-full object-cover" />
) : (
<div className="h-16 w-16 rounded-full bg-gray-200 flex items-center text-center text-sm justify-center text-gray-500">
No Image
</div>
)}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Full Name
</div>
<div className="col-span-3 font-medium">
{currentUser.name}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Email
</div>
<div className="col-span-3">
<a
href={`mailto:${currentUser.email}`}
className="text-blue-600 hover:underline"
>
{currentUser.email}
</a>
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
User Type
</div>
<div className="col-span-3">
{currentUser.type && (
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${
currentUser.type === 'admin'
? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800'
}`}>
{currentUser.type.charAt(0).toUpperCase() + currentUser.type.slice(1)}
</span>
)}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Silicon ID
</div>
<div className="col-span-3 font-mono">
{currentUser.siliconId}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
PB ID
</div>
<div className="col-span-3 font-mono">
{currentUser.pbId}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Account Created
</div>
<div className="col-span-3">
{formatDate(currentUser.created_at)}
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsViewModalOpen(false)}
className="mt-4"
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && currentUser && (
<Dialog open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Deletion</DialogTitle>
<DialogDescription>
Are you sure you want to delete user {currentUser?.name} ({currentUser?.email})?
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
<Button onClick={() => {confirmDelete();setIsDeleteModalOpen(false);}}>Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
);
}

View File

@ -1,860 +0,0 @@
import React, { useEffect, useState, useMemo } from "react";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import Loader from "../../components/ui/loader";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../../lib/InvoicePDF";
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search, ArrowLeft, FileText, ArrowRight } from "lucide-react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
import * as XLSX from 'xlsx';
export default function AllSellingList() {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [billingData, setBillingData] = useState([]);
const [usersData, setUsersData] = useState([]);
const [dataLoading, setDataLoading] = useState(true);
const [apiError, setApiError] = useState(null);
const [selectedItem, setSelectedItem] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState({ service: '', serviceId: 'service-1', cycle: 'monthly', amount: '', user: '', siliconId: '', name: '', status: 'pending', remarks: '' });
const [currentStep, setCurrentStep] = useState(1);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
// Sorting state
const [sortConfig, setSortConfig] = useState({ key: 'created_at', direction: 'desc' });
// Filtering state
const [filters, setFilters] = useState({ status: '', cycle: '', service: '' });
// Search state
const [searchTerm, setSearchTerm] = useState('');
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
useEffect(() => {
if (isLoggedIn && sessionData?.user_type === 'admin') {
fetchBillingData();
fetchUsersData();
}
}, [isLoggedIn, sessionData]);
const fetchBillingData = async () => {
try {
setDataLoading(true);
const res = await fetch(`${INVOICE_API_URL}?query=all-selling-list`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
setBillingData(data.data || []);
} catch (err) {
setApiError(err.message);
} finally {
setDataLoading(false);
}
};
const fetchUsersData = async () => {
try {
const res = await fetch(`${INVOICE_API_URL}?query=get-all-users`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
setUsersData(data.data || []);
} catch (err) {
setApiError(err.message);
}
};
// Filter customers based on search term
const filteredCustomers = useMemo(() => {
return usersData.filter(user =>
user.name.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
user.siliconId.toLowerCase().includes(customerSearchTerm.toLowerCase())
);
}, [usersData, customerSearchTerm]);
const handleViewItem = (item) => {
setSelectedItem(item);
setEditMode(false);
setDialogOpen(true);
};
const handleEditItem = (item) => {
setSelectedItem(item);
setFormData({
service: item.service,
serviceId: item.serviceId,
cycle: item.cycle,
amount: item.amount,
user: item.user,
siliconId: item.siliconId,
name: item.name,
status: item.status,
remarks: item.remarks,
billing_date: item.billing_date
});
setEditMode(true);
setDialogOpen(true);
};
const handleDeleteItem = async (billingId, serviceType) => {
if (!window.confirm('Are you sure you want to delete this billing record?')) return;
try {
const res = await fetch(`${INVOICE_API_URL}?query=delete-billing&billingId=${billingId}&serviceType=${serviceType}`, {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
fetchBillingData();
} catch (err) {
setApiError(err.message);
}
};
const handleCreateNew = () => {
setSelectedItem(null);
setFormData({ service: '', serviceId: 'service-2', cycle: 'monthly', amount: '', user: '', siliconId: '', name: '', status: 'pending', remarks: '', billing_date: new Date().toISOString().split('T')[0] });
setEditMode(true);
setCurrentStep(1);
setSelectedCustomer(null);
setCustomerSearchTerm('');
setDialogOpen(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const url = selectedItem ? `${INVOICE_API_URL}?query=update-billing&id=${selectedItem.id}` : `${INVOICE_API_URL}?query=create-billing`;
const method = selectedItem ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
fetchBillingData();
setDialogOpen(false);
setCurrentStep(1);
} catch (err) {
setApiError(err.message);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleCustomerSelect = (customer) => {
setSelectedCustomer(customer);
setFormData(prev => ({
...prev,
user: customer.email,
siliconId: customer.siliconId,
name: customer.name
}));
setCurrentStep(2);
};
const handleBackToCustomerSelect = () => {
setCurrentStep(1);
setSelectedCustomer(null);
};
// Export to Excel function
const exportToExcel = () => {
const worksheet = XLSX.utils.json_to_sheet(filteredAndSortedData.map(item => ({
'Billing ID': item.billing_id,
'Service': item.service,
'Customer Name': item.name,
'Customer Email': item.user,
'Silicon ID': item.siliconId,
'Amount': item.amount,
'cycle': item.cycle,
'Status': item.status,
'Service Status': item.service_status,
'Created At': formatDate(item.created_at),
'Remarks': item.remarks
})));
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Billing Data");
XLSX.writeFile(workbook, `billing_data_${new Date().toISOString().slice(0, 10)}.xlsx`);
};
// Sorting functionality
const requestSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
setCurrentPage(1);
};
// Filter and sort data
const filteredAndSortedData = useMemo(() => {
let filteredData = [...billingData];
if (searchTerm) {
filteredData = filteredData.filter(item =>
Object.values(item).some(
val => val && val.toString().toLowerCase().includes(searchTerm.toLowerCase())
)
);
}
if (filters.status) {
filteredData = filteredData.filter(item => item.status === filters.status);
}
if (filters.cycle) {
filteredData = filteredData.filter(item => item.cycle === filters.cycle);
}
if (filters.service) {
filteredData = filteredData.filter(item => item.service === filters.service);
}
if (sortConfig.key) {
filteredData.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}
return filteredData;
}, [billingData, searchTerm, filters, sortConfig]);
const currentItems = useMemo(() => {
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
return filteredAndSortedData.slice(indexOfFirstItem, indexOfLastItem);
}, [currentPage, itemsPerPage, filteredAndSortedData]);
const totalPages = Math.ceil(filteredAndSortedData.length / itemsPerPage);
const paginate = (pageNumber) => {
if (pageNumber > 0 && pageNumber <= totalPages) {
setCurrentPage(pageNumber);
}
};
const getPaginationRange = () => {
const range = [];
const maxVisiblePages = 5;
range.push(1);
if (currentPage > 3) {
range.push('...');
}
let start = Math.max(2, currentPage - 1);
let end = Math.min(totalPages - 1, currentPage + 1);
if (currentPage <= 3) {
end = Math.min(4, totalPages - 1);
} else if (currentPage >= totalPages - 2) {
start = Math.max(totalPages - 3, 2);
}
for (let i = start; i <= end; i++) {
if (i > 1 && i < totalPages) {
range.push(i);
}
}
if (currentPage < totalPages - 2) {
range.push('...');
}
if (totalPages > 1) {
range.push(totalPages);
}
return range;
};
const uniqueServices = [...new Set(billingData.map(item => item.service))];
const uniqueStatuses = ['completed', 'pending', 'failed'];
const uniquecycles = ['monthly', 'yearly', 'one-time'];
const resetFilters = () => {
setFilters({
status: '',
cycle: '',
service: ''
});
setSearchTerm('');
setCurrentPage(1);
};
const handleFilterChange = (e) => {
const { name, value } = e.target;
setFilters(prev => ({
...prev,
[name]: value
}));
setCurrentPage(1);
};
const handleSearch = (e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
};
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'bg-green-100 text-green-800';
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'failed': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
return new Date(dateString).toLocaleDateString(undefined, options);
};
const formatCurrency = (amount) => {
if (!amount) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(parseFloat(amount));
};
if (loading || (isLoggedIn && sessionData?.user_type === 'admin' && dataLoading)) {
return <Loader />;
}
if (error || apiError) return <p>Error: {error?.message || apiError.message}</p>;
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<section className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Billing Management</h1>
<div className="flex gap-2">
<div className="relative inline-block">
<Button onClick={() => setIsDropdownOpen(!isDropdownOpen)} variant={isDropdownOpen ? 'default' : 'outline'}>Manage Users</Button>
{isDropdownOpen && (
<div className="absolute mt-1 w-44 bg-white border rounded-md shadow-lg z-10">
<a href="/manager/customer-lists" className="block px-4 py-2 hover:bg-gray-100 rounded-md text-sm text-[#6d9e37] font-medium">Manage Customer</a>
<a href="/manager/selling-list" className="block px-4 py-2 hover:bg-gray-100 rounded-md text-sm text-[#6d9e37] font-medium">Manage Billing</a>
</div>
)}
</div>
<Button onClick={handleCreateNew} variant="outline">Create New</Button>
<Button onClick={exportToExcel} variant="outline" className="flex items-center gap-2">
<FileText className="w-4 h-4" /> Export to Excel
</Button>
</div>
</div>
{/* Search and Filter Section */}
<div className="mb-2 text-sm text-gray-600">
Showing {currentItems.length} of {filteredAndSortedData.length} results
</div>
<div className="bg-white p-4 rounded-t-lg shadow">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={handleSearch}
className="bg-[#262626] pl-10 w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
/>
</div>
<select
name="status"
value={filters.status}
onChange={handleFilterChange}
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<option value="">All Statuses</option>
{uniqueStatuses.map(status => (
<option key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</option>
))}
</select>
<select
name="cycle"
value={filters.cycle}
onChange={handleFilterChange}
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<option value="">All cycles</option>
{uniquecycles.map(cycle => (
<option key={cycle} value={cycle}>
{cycle.charAt(0).toUpperCase() + cycle.slice(1)}
</option>
))}
</select>
<select
name="service"
value={filters.service}
onChange={handleFilterChange}
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<option value="">All Services</option>
{uniqueServices.map(service => (
<option key={service} value={service}>
{service}
</option>
))}
</select>
</div>
{(filters.status || filters.cycle || filters.service || searchTerm) && (
<div className="mt-3">
<Button
onClick={resetFilters}
size="sm"
>
Reset Filters
</Button>
</div>
)}
</div>
<div className="bg-white rounded-b-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('billing_id')}
>
<div className="flex items-center">
Billing ID
{sortConfig.key === 'billing_id' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('service')}
>
<div className="flex items-center">
Service
{sortConfig.key === 'service' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('name')}
>
<div className="flex items-center">
Name
{sortConfig.key === 'name' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('user')}
>
<div className="flex items-center">
User
{sortConfig.key === 'user' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('amount')}
>
<div className="flex items-center">
Amount
{sortConfig.key === 'amount' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('status')}>
<div className="flex items-center">
Status
{sortConfig.key === 'status' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('service_status')}>
<div className="flex items-center">
Ser.. Status
{sortConfig.key === 'service_status' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" onClick={() => requestSort('created_at')}>
<div className="flex items-center">
Created At
{sortConfig.key === 'created_at' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentItems.length > 0 ? (
currentItems.map((item) => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{item.billing_id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{item.service}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{item.name ?? item.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{item.user}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">{formatCurrency(item.amount)}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(item.status)}`}>
{item.status.charAt(0).toUpperCase() + item.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${item.service_status == 1 ? 'bg-green-100 text-green-800' : item.service_status == 0 ? 'bg-red-100 text-red-800' : ''}`}>
{item.service_status == 1 ? 'Active' : item.service_status == 0 ? 'Deactivate' : ''}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(item.created_at)}
</td>
<td className="inline-flex px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
title="View Items"
onClick={() => handleViewItem(item)}
className="text-indigo-600 hover:text-indigo-900 mr-2"
>
<Eye className="w-5 h-5" />
</button>
<button
title="Edit Items"
onClick={() => handleEditItem(item)}
className="text-yellow-600 hover:text-yellow-900 mr-2"
>
<Pencil className="w-5 h-5" />
</button>
<button
title="Delete Items"
onClick={() => handleDeleteItem(item.billing_id, item.service_type)}
className="text-red-600 hover:text-red-900 mr-2"
>
<Trash2 className="w-5 h-5" />
</button>
<PDFDownloadLink
title="Download PDF"
document={<InvoicePDF data={item} />}
fileName={`invoice_${item.billing_id}.pdf`}
className="text-[#6d9e37] hover:text-green-600"
>
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
</PDFDownloadLink>
</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
No records found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Pagination */}
{filteredAndSortedData.length > itemsPerPage && (
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600">
Page {currentPage} of {totalPages}
</div>
<div className="flex flex-wrap gap-2">
{
currentPage > 1 ? (
<>
<Button onClick={() => paginate(1)} disabled={currentPage === 1} variant="outline" size="sm">First</Button>
<Button onClick={() => paginate(currentPage - 1)} disabled={currentPage === 1} variant="outline" size="sm">Previous</Button >
</>
) : ''
}
{
getPaginationRange().map((item, index) => {
if (item === '...') {
return (
<span key={`ellipsis-${index}`} className="px-2 py-1">...</span>
);
}
return (
<Button key={`page-${item}`} onClick={() => paginate(item)} variant={currentPage === item ? "default" : "outline"} size="sm">{item}</Button>
);
})
}
<Button onClick={() => paginate(currentPage + 1)} disabled={currentPage === totalPages} variant="outline" size="sm">Next</Button>
<Button onClick={() => paginate(totalPages)} disabled={currentPage === totalPages} variant="outline" size="sm">Last</Button>
</div>
</div>
)}
{/* Dialog for View/Edit/Create */}
<Dialog open={dialogOpen} onOpenChange={(open) => {
if (!open) {
setCurrentStep(1);
setSelectedCustomer(null);
}
setDialogOpen(open);
}}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editMode ? (selectedItem ? 'Edit Billing' : 'Create New Billing') : 'Billing Details'}
</DialogTitle>
</DialogHeader>
{editMode && !selectedItem && currentStep === 1 ? (
<div>
<div className="relative mb-4">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
placeholder="Search customers..."
value={customerSearchTerm}
onChange={(e) => setCustomerSearchTerm(e.target.value)}
className="pl-10 w-full px-3 py-2 border border-gray-300 rounded-md outline-none"
/>
</div>
<h3 className="font-semibold mb-4 text-neutral-400">Select Customer</h3>
{filteredCustomers.length > 0 ? (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{filteredCustomers.map(user => (
<div
key={user.id}
onClick={() => handleCustomerSelect(user)}
className="group p-3 mx-2 border rounded-md cursor-pointer hover:bg-gray-100 hover:border-[#6d9e37] transition-border duration-700"
>
<div className="flex justify-between items-center">
<div>
<p className="font-medium text-neutral-950">{user.name}</p>
<p className="text-sm text-neutral-400">{user.email}</p>
</div>
<div className="text-sm inline-flex gap-2">
<span className="bg-gray-100 px-2 py-1 rounded text-neutral-950">SiliconId: {user.siliconId}</span>
<ArrowRight className="text-[#6d9e37] opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
</div>
</div>
</div>
))}
</div>
) : (
<p className="text-center text-gray-500 py-4">No customers found</p>
)}
</div>
) : editMode ? (
<form onSubmit={handleSubmit}>
{!selectedItem && selectedCustomer && (
<div className="mb-6 p-4 bg-gray-50 rounded-md">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-neutral-950">Customer Information</h3>
<p className="text-sm text-neutral-400"><span className="font-medium">Name:</span> {selectedCustomer.name}</p>
<p className="text-sm text-neutral-400"><span className="font-medium">Email:</span> {selectedCustomer.email}</p>
<p className="text-sm text-neutral-400"><span className="font-medium">Silicon ID:</span> {selectedCustomer.siliconId}</p>
</div>
{!selectedItem && (
<Button type="button" onClick={handleBackToCustomerSelect} variant="ghost" size="sm" className="text-gray-500">
<ArrowLeft className="w-4 h-4 mr-1" /> Change Customer
</Button>
)}
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Service</label>
<input type="text" name="service" value={formData.service} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" required />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">cycle</label>
<select name="cycle" value={formData.cycle} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" >
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
<option value="one-time">One-time</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Amount</label>
<input type="number" name="amount" value={formData.amount} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" step="0.01" min="0" required />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select name="status" value={formData.status} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" >
<option value="pending">Pending</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="">
<label htmlFor="billing_date" className="block text-sm font-medium text-gray-700 mb-1">Billing Date</label>
<input type="date" name="billing_date" value={formData.billing_date} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" />
</div>
<div className="">
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">Remarks</label>
<input type="text" name="remarks" value={formData.remarks} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" />
</div>
</div>
<DialogFooter>
<Button type="submit" className="" >{selectedItem ? 'Update' : 'Create'}</Button>
<Button type="button" onClick={() => { setDialogOpen(false); setCurrentStep(1);}} variant="outline">Cancel</Button>
</DialogFooter>
</form>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<h3 className="font-semibold text-gray-700">Billing ID</h3>
<p>{selectedItem?.billing_id}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Service</h3>
<p>{selectedItem?.service}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">User</h3>
<p>{selectedItem?.user}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Silicon ID</h3>
<p>{selectedItem?.siliconId}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">cycle</h3>
<p>{selectedItem?.cycle}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Amount</h3>
<p>{formatCurrency(selectedItem?.amount)}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Status</h3>
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(selectedItem?.status)}`}>
{selectedItem?.status.charAt(0).toUpperCase() + selectedItem?.status.slice(1)}
</span>
</div>
<div>
<h3 className="font-semibold text-gray-700">Created At</h3>
<p>{formatDate(selectedItem?.created_at)}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Updated At</h3>
<p>{formatDate(selectedItem?.updated_at)}</p>
</div>
<div className="md:col-span-2">
<h3 className="font-semibold text-gray-700">Remarks</h3>
<p>{selectedItem?.remarks}</p>
</div>
</div>
<DialogFooter>
<PDFDownloadLink
document={<InvoicePDF data={selectedItem} />}
fileName={`invoice_${selectedItem?.billing_id}.pdf`}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
{({ loading }) => (loading ? 'Preparing PDF...' : 'Download PDF')}
</PDFDownloadLink>
<Button
onClick={() => handleEditItem(selectedItem)}
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
>
Edit
</Button>
<Button
onClick={() => setDialogOpen(false)}
variant="outline"
>
Close
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
</section>
);
}

View File

@ -1,214 +0,0 @@
import React, { useState, useEffect } from "react";
import { useToast } from "../ui/toast";
import Loader from "../ui/loader";
import { Button } from "../ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import Table from "../ui/table"; // Import your custom Table component
import { useIsLoggedIn } from '../../lib/isLoggedIn';
export default function HetznerServicesList() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(true);
const [servers, setServers] = useState([]);
const HETZNER_API_KEY = "uMSBR9nxdtbuvazsVM8YMMDd0PvuynpgJbmzFIO47HblMlh7tlHT8wV05sQ28Squ"; // Replace with your actual API key
// Define fetchServers outside useEffect so it can be reused
const fetchServers = async () => {
try {
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
});
const data = await response.json();
setServers(data.servers || []);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load servers",
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchServers();
}, []);
const handlePowerAction = async (serverId, action) => {
try {
setIsLoading(true);
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}/actions/${action}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
}
});
const data = await response.json();
if (data.action) {
showToast({
title: "Success",
description: `Server ${action} action initiated`,
variant: "success"
});
// Refresh server list after a short delay
setTimeout(() => {
fetchServers();
}, 3000);
}
} catch (error) {
showToast({
title: "Error",
description: `Failed to ${action} server`,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleDeleteServer = async (serverId) => {
try {
setIsLoading(true);
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
}
});
if (response.ok) {
showToast({
title: "Success",
description: "Server deleted successfully",
variant: "success"
});
// Remove the server from local state
setServers(prev => prev.filter(server => server.id !== serverId));
}
} catch (error) {
showToast({
title: "Error",
description: "Failed to delete server",
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const getStatusBadge = (status) => {
let colorClass = "";
switch (status) {
case "running":
colorClass = "bg-green-100 text-green-800";
break;
case "off":
colorClass = "bg-gray-100 text-gray-800";
break;
case "starting":
case "stopping":
colorClass = "bg-yellow-100 text-yellow-800";
break;
default:
colorClass = "bg-blue-100 text-blue-800";
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
{status}
</span>
);
};
// Prepare data for your Table component
const tableData = servers.map(server => ({
name: server.name,
status: getStatusBadge(server.status),
type: server.server_type.name,
location: server.datacenter.location.city,
ip: server.public_net.ipv4 ? server.public_net.ipv4.ip : "None",
backups: server.backup_window ? "Enabled" : "Disabled",
actions: (
<div className="flex gap-2">
{server.status === "running" ? (
<>
<Button
variant="outline"
size="sm"
onClick={() => handlePowerAction(server.id, "shutdown")}
disabled={isLoading}
>
Stop
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePowerAction(server.id, "reboot")}
disabled={isLoading}
>
Reboot
</Button>
</>
) : (
<Button
className=""
variant="outline"
size="sm"
onClick={() => handlePowerAction(server.id, "poweron")}
disabled={isLoading}
>
Start
</Button>
)}
<Button
className="bg-red-500 hover:bg-red-600 transition duration-500"
size="sm"
onClick={() => handleDeleteServer(server.id)}
disabled={isLoading}
>
Delete
</Button>
</div>
)
}));
const tableHeaders = ["Name", "Status", "Type", "Location", "IPv4", "Backups", "Actions"];
if (loading || (isLoggedIn && sessionData?.user_type === 'admin' && isLoading && servers.length === 0)) {
return <Loader />;
}
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<Card className="container mx-auto my-4">
<CardHeader>
<CardTitle>Your Hetzner Cloud Servers</CardTitle>
</CardHeader>
<CardContent>
{servers.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">No servers found. Create your first server to get started.</p>
</div>
) : (
<Table
headers={tableHeaders}
data={tableData}
striped
hover
caption="A list of your Hetzner Cloud servers"
className="mt-4"
/>
)}
</CardContent>
</Card>
);
}

View File

@ -1,246 +0,0 @@
import React, {useState, useEffect} from "react";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Button } from "../ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Separator } from "../ui/separator";
import { Eye, Download } from "lucide-react";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../../lib/InvoicePDF";
import Loader from "../ui/loader";
import { localizeTime } from "../../lib/localizeTime";
export default function ViewAsUser() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [userData, setUserData] = useState(null);
const [billingData, setBillingData] = useState([]);
const [dataLoading, setDataLoading] = useState(false);
const [dataError, setDataError] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedInvoice, setSelectedInvoice] = useState(null);
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('siliconId');
if (id) {
fetchUserData(id);
}
}, []);
const fetchUserData = async (id) => {
setDataLoading(true);
setDataError(null);
try {
const response = await fetch(
`${USER_API_URL}?query=login-from-admin&siliconId=${id}`,
{
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) {
setUserData(data.user || null);
setBillingData(data.billing || []);
} else {
throw new Error(data.message || 'Failed to fetch data');
}
} catch (err) {
console.error('Error fetching data:', err);
setDataError(err.message);
} finally {
setDataLoading(false);
}
};
const handleViewInvoice = (invoice) => {
setSelectedInvoice(invoice);
setDialogOpen(true);
};
if (loading || (isLoggedIn && sessionData?.user_type === 'admin' && dataLoading)) {
return <Loader />;
}
if (error || dataError) return <p>Error: {error?.message || dataError.message}</p>;
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<div className="space-y-6 container mx-auto p-4">
<h1 className="text-2xl font-bold">View as {userData.name || 'User'}</h1>
<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-3xl">
<Card>
<CardHeader>
<CardTitle>User Information</CardTitle>
<CardDescription>
Details for user: {userData.siliconid}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-16 w-16">
<AvatarImage src={userData.avatar || ''} />
<AvatarFallback>
{userData.name ? userData.name.charAt(0).toUpperCase() : 'U'}
</AvatarFallback>
</Avatar>
<div>
<h3 className="text-lg font-medium">{userData.name}</h3>
<p className="text-sm text-gray-500">{userData.email}</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<p className="text-sm font-medium">Silicon ID</p>
<p className="text-sm">{userData.siliconId}</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">PB ID</p>
<p className="text-sm">{userData.pbId}</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Account Created</p>
<p className="text-sm">
{userData.created_at ? localizeTime(userData.created_at) : 'N/A'}
</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Last Updated</p>
<p className="text-sm">
{userData.updated_at ? localizeTime(userData.updated_at) : 'N/A'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="mt-6">
<CardHeader>
<CardTitle>Billing Information</CardTitle>
<CardDescription>
Billing history for this user
</CardDescription>
</CardHeader>
<CardContent>
{billingData.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr>
<th className="text-left">Invoice ID</th>
<th className="text-center">Date</th>
<th className="text-left">Description</th>
<th className="text-center">Amount</th>
<th className="text-center">Status</th>
<th className="text-center">Actions</th>
</tr>
</thead>
<tbody>
{billingData.map((invoice) => (
<tr key={invoice.id} className="">
<td>{invoice.billing_id}</td>
<td className="text-center">
{invoice?.created_at?.split(' ')[0] || 'N/A'}
</td>
<td className="line-clamp-1">{invoice.service}</td>
<td className="text-center">${invoice.amount}</td>
<td className={`text-center text-sm rounded-full h-fit ${
invoice.status === 'pending' ? 'text-yellow-500' :
invoice.status === 'completed' ? 'text-green-500' :
'text-red-500'
}`}>
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</td>
<td className="text-center flex justify-center items-center gap-2 p-2">
<button
onClick={() => handleViewInvoice(invoice)}
className="text-gray-600 hover:text-gray-900"
>
<Eye className="w-5 h-5" />
</button>
<PDFDownloadLink
document={<InvoicePDF data={invoice} />}
fileName={`invoice_${invoice.billing_id}.pdf`}
className="text-gray-600 hover:text-gray-900"
>
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
</PDFDownloadLink>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-center text-gray-500 py-4">No billing records found</p>
)}
</CardContent>
</Card>
</div>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Invoice Details</DialogTitle>
<DialogDescription>
Details for Invoice ID: <span className="font-bold">{selectedInvoice?.billing_id}</span>
</DialogDescription>
</DialogHeader>
{selectedInvoice && (
<div className="mt-4 space-y-4 text-sm text-gray-700">
<div className="flex justify-between">
<span className="font-bold">Service:</span>
<span>{selectedInvoice.service}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Amount:</span>
<span>${selectedInvoice.amount}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Status:</span>
<span className={`px-2 py-0.5 rounded text-white text-xs ${
selectedInvoice.status === 'pending' ? 'bg-yellow-500' :
selectedInvoice.status === 'completed' ? 'bg-green-500' :
'bg-red-500'
}`}>
{selectedInvoice.status}
</span>
</div>
<Separator className="my-2" />
<div className="flex justify-between">
<span className="font-bold">Created At:</span>
<span>{localizeTime(selectedInvoice.created_at)}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Updated At:</span>
<span>{selectedInvoice.updated_at ? localizeTime(selectedInvoice.updated_at) : '—'}</span>
</div>
</div>
)}
<DialogFooter className="mt-6">
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,59 +0,0 @@
import React, { useEffect, useState } from "react";
import TopicItems from "./TopicItem";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from "./ui/loader";
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
export default function TopicCreation(props) {
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
const [topics, setTopics] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchTopics = async () => {
try {
const res = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=my-topics', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const data = await res.json();
setTopics(data.data || []);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchTopics();
}, []);
if (authLoading || loading) {
return <Loader />;
}
if (authError || error) {
return <div className="error-message">Error loading data: {authError || error}</div>;
}
return (
<>
{isLoggedIn && (
<div className="container mx-auto flex justify-end gap-x-4 mb-4">
<a href="/topic/new" className="create-new-link">Create New</a>
<a href="/topic/my-topic">My Topics</a>
</div>
)}
<TopicItems topics={topics} mytopic={props.mytopic || false} title="SoliconPin Topics" description={topicPageDesc} />
</>
);
}

View File

@ -1,485 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import MDEditor, { commands } from '@uiw/react-md-editor';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
const NewTopic = () => {
// Form state
const [formData, setFormData] = useState({ status: 'draft', category: '', title: '', content: '', imageUrl: '' });
// UI state
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const [imageFile, setImageFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [imageDialogOpen, setImageDialogOpen] = useState(false);
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [editorMode, setEditorMode] = useState('edit');
// Upload file to MinIO
const uploadToMinIO = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
try {
const response = await fetch(MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include',
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
console.log(data)
return data.url;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// // Generate slug from title
// useEffect(() => {
// if (formData.title) {
// const slug = formData.title
// .toLowerCase()
// .replace(/[^\w\s]/g, '')
// .replace(/\s+/g, '-');
// setFormData(prev => ({ ...prev, slug }));
// }
// }, [formData.title]);
const customImageCommand = {
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
</svg>
),
execute: () => {
setImageDialogOpen(true);
},
};
// Get all default commands and replace the image command
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
// Handle image URL insertion
const handleInsertImageUrl = () => {
if (imageUrlInput) {
const imgMarkdown = `![Image](${imageUrlInput})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUrlInput('');
}
};
// Handle image file selection
const handleImageFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setImageUploadFile(file);
// Create preview
const reader = new FileReader();
reader.onload = () => {
setImageUploadPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
// Upload image file to MinIO and insert into editor
const handleImageUpload = async () => {
if (!imageUploadFile) return;
try {
setIsSubmitting(true);
setUploadProgress(0);
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
setUploadProgress(progress);
});
// Insert markdown for the uploaded image
const imgMarkdown = `![Image](${uploadedUrl})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUploadFile(null);
setImageUploadPreview('');
} catch (error) {
setError('Failed to upload image: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
// Form submission
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
setUploadProgress(0);
try {
// Upload featured image if selected
let imageUrl = formData.imageUrl;
if (imageFile) {
imageUrl = await uploadToMinIO(imageFile, (progress) => {
setUploadProgress(progress);
});
}
// Prepare payload
const payload = {
...formData,
imageUrl
};
// Submit to API
const response = await fetch(`${TOPIC_API_URL}?query=create-new-topic`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create topic');
}
setSuccess(true);
} catch (err) {
setError(err.message);
} finally {
setIsSubmitting(false);
setUploadProgress(0);
}
};
// Handle form field changes
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Handle featured image file selection
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setImageFile(file);
// Preview image
const reader = new FileReader();
reader.onload = () => {
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
};
reader.readAsDataURL(file);
}
};
// Reset form
const resetForm = () => {
setFormData({
status: 'draft',
category: '',
title: '',
slug: '',
content: '',
imageUrl: ''
});
setImageFile(null);
setSuccess(false);
setError(null);
};
// Success state
if (success) {
return (
<div className="container mx-auto px-4 text-center py-8">
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic created successfully!</h3>
<div className="flex justify-center gap-4">
<Button onClick={resetForm}>Create Another</Button>
<Button variant="outline" onClick={() => window.location.href = '/'}>Return Home</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Create New Topic</h2>
<p className="text-gray-600 mb-6">Start a new discussion in the SiliconPin community</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
name="status"
required
value={formData.status}
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
>
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="published">Published</SelectItem>
<SelectItem value="archived">Archived</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category *</Label>
<Select
name="category"
required
value={formData.category}
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
>
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent position="popper" className="z-50">
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Title and Slug */}
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
</div>
{/* <div className="space-y-2">
<Label htmlFor="slug">URL Slug</Label>
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
</div> */}
{/* Content Editor with Preview Toggle */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="content">Content *</Label>
<button
type="button"
size="sm"
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
>
{editorMode === 'edit' ? 'Preview' : 'Edit'}
</button>
</div>
<div data-color-mode="light">
<MDEditor
placeholder="Write your content"
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}
preview={editorMode}
commands={allCommands}
/>
</div>
</div>
{/* Image Upload Dialog */}
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Insert Image</DialogTitle>
</DialogHeader>
<CustomTabs
tabs={[
{
label: "From URL",
value: "url",
content: (
<div className="space-y-4">
<Input
type="text"
placeholder="Enter image URL"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
/>
<div className="flex justify-end">
<Button
type="button"
onClick={handleInsertImageUrl}
disabled={!imageUrlInput}
>
Insert Image
</Button>
</div>
</div>
)
},
{
label: "Upload",
value: "upload",
content: (
<div className="space-y-4">
<Input
type="file"
accept="image/*"
onChange={handleImageFileSelect}
/>
{imageUploadPreview && (
<div className="mt-2">
<img
src={imageUploadPreview}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
<div className="flex justify-end">
<Button
type="button"
onClick={handleImageUpload}
disabled={!imageUploadFile || isSubmitting}
>
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
</Button>
</div>
</div>
)
}
]}
/>
</DialogContent>
</Dialog>
{/* Featured Image Upload */}
<div className="space-y-2">
<Label htmlFor="image">Featured Image</Label>
<Input
type="file"
id="image"
onChange={handleFileChange}
accept="image/*"
className="cursor-pointer"
/>
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
{formData.imageUrl && (
<div className="mt-2">
<img
src={formData.imageUrl}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
</div>
<Separator />
{/* Form Actions */}
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={resetForm}
disabled={isSubmitting}
>
Reset
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="min-w-32"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<span className="animate-spin"></span>
Creating...
</span>
) : 'Create Topic'}
</Button>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 rounded-md">
Error: {error}
</div>
)}
</form>
</div>
</div>
);
};
export default NewTopic;

View File

@ -1,485 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import MDEditor, { commands } from '@uiw/react-md-editor';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
const NewTopic = () => {
// Form state
const [formData, setFormData] = useState({ status: 'draft', category: '', title: '', content: '', imageUrl: '' });
// UI state
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const [imageFile, setImageFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [imageDialogOpen, setImageDialogOpen] = useState(false);
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [editorMode, setEditorMode] = useState('edit');
// Upload file to MinIO
const uploadToMinIO = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
try {
const response = await fetch(MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include'
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
console.log(data.publicUrl)
return data.publicUrl;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// // Generate slug from title
// useEffect(() => {
// if (formData.title) {
// const slug = formData.title
// .toLowerCase()
// .replace(/[^\w\s]/g, '')
// .replace(/\s+/g, '-');
// setFormData(prev => ({ ...prev, slug }));
// }
// }, [formData.title]);
const customImageCommand = {
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
</svg>
),
execute: () => {
setImageDialogOpen(true);
},
};
// Get all default commands and replace the image command
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
// Handle image URL insertion
const handleInsertImageUrl = () => {
if (imageUrlInput) {
const imgMarkdown = `![Image](${imageUrlInput})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUrlInput('');
}
};
// Handle image file selection
const handleImageFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setImageUploadFile(file);
// Create preview
const reader = new FileReader();
reader.onload = () => {
setImageUploadPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
// Upload image file to MinIO and insert into editor
const handleImageUpload = async () => {
if (!imageUploadFile) return;
console.log(imageUploadFile)
try {
setIsSubmitting(true);
setUploadProgress(0);
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
setUploadProgress(progress);
});
// Insert markdown for the uploaded image
const imgMarkdown = `![Image](${uploadedUrl})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUploadFile(null);
setImageUploadPreview('');
} catch (error) {
setError('Failed to upload image: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
// Form submission
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
setUploadProgress(0);
try {
// Upload featured image if selected
let imageUrl = formData.imageUrl;
if (imageFile) {
imageUrl = await uploadToMinIO(imageFile, (progress) => {
console.log('imageUrl', imageUrl)
setUploadProgress(progress);
});
}
// let finalImageUrl = imageUrl.publicUrl;
// Prepare payload
const payload = {
...formData,
imageUrl
};
// Submit to API
const response = await fetch(`${TOPIC_API_URL}?query=create-new-topic`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create topic');
}
setSuccess(true);
} catch (err) {
setError(err.message);
} finally {
setIsSubmitting(false);
setUploadProgress(0);
}
};
// Handle form field changes
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Handle featured image file selection
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setImageFile(file);
// Preview image
const reader = new FileReader();
reader.onload = () => {
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
};
reader.readAsDataURL(file);
}
};
// Reset form
const resetForm = () => {
setFormData({
status: 'draft',
category: '',
title: '',
slug: '',
content: '',
imageUrl: ''
});
setImageFile(null);
setSuccess(false);
setError(null);
};
// Success state
if (success) {
return (
<div className="container mx-auto px-4 text-center py-8">
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic created successfully!</h3>
<div className="flex justify-center gap-4">
<Button onClick={resetForm}>Create Another</Button>
<Button variant="outline" onClick={() => window.location.href = '/'}>Return Home</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Create New Topic</h2>
<p className="text-gray-600 mb-6">Start a new discussion in the SiliconPin community</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
name="status"
required
value={formData.status}
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
>
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="published">Published</SelectItem>
<SelectItem value="archived">Archived</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category *</Label>
<Select
name="category"
required
value={formData.category}
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
>
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent position="popper" className="z-50">
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Title and Slug */}
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
</div>
{/* <div className="space-y-2">
<Label htmlFor="slug">URL Slug</Label>
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
</div> */}
{/* Content Editor with Preview Toggle */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="content">Content *</Label>
<button
type="button"
size="sm"
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
>
{editorMode === 'edit' ? 'Preview' : 'Edit'}
</button>
</div>
<div data-color-mode="light">
<MDEditor
placeholder="Write your content"
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}
preview={editorMode}
commands={allCommands}
/>
</div>
</div>
{/* Image Upload Dialog */}
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Insert Image</DialogTitle>
</DialogHeader>
<CustomTabs
tabs={[
{
label: "From URL",
value: "url",
content: (
<div className="space-y-4">
<Input
type="text"
placeholder="Enter image URL"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
/>
<div className="flex justify-end">
<Button
type="button"
onClick={handleInsertImageUrl}
disabled={!imageUrlInput}
>
Insert Image
</Button>
</div>
</div>
)
},
{
label: "Upload",
value: "upload",
content: (
<div className="space-y-4">
<Input
type="file"
accept="image/*"
onChange={handleImageFileSelect}
/>
{imageUploadPreview && (
<div className="mt-2">
<img
src={imageUploadPreview}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
<div className="flex justify-end">
<Button
type="button"
onClick={handleImageUpload}
disabled={!imageUploadFile || isSubmitting}
>
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
</Button>
</div>
</div>
)
}
]}
/>
</DialogContent>
</Dialog>
{/* Featured Image Upload */}
<div className="space-y-2">
<Label htmlFor="image">Featured Image</Label>
<Input
type="file"
id="image"
onChange={handleFileChange}
accept="image/*"
className="cursor-pointer"
/>
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
{formData.imageUrl && (
<div className="mt-2">
<img
src={formData.imageUrl}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
</div>
<Separator />
{/* Form Actions */}
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={resetForm}
disabled={isSubmitting}
>
Reset
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="min-w-32"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<span className="animate-spin"></span>
Creating...
</span>
) : 'Create Topic'}
</Button>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 rounded-md">
Error: {error}
</div>
)}
</form>
</div>
</div>
);
};
export default NewTopic;

View File

@ -1,286 +0,0 @@
import { useState, useEffect } from 'react';
import PocketBase from 'pocketbase';
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Label } from "./ui/label";
// import type { RecordModel } from 'pocketbase';
const pb = new PocketBase('https://tst-pb.s38.siliconpin.com');
interface RecordModel {
id: string;
collectionId: string;
collectionName: string;
created: string;
updated: string;
[key: string]: any;
}
interface UserRecord extends RecordModel {
email: string;
verified?: boolean;
emailVisibility?: boolean;
username?: string;
name?: string;
avatar?: string;
provider?: string;
providerData?: {
[key: string]: {
id: string;
name?: string;
email?: string;
avatarUrl?: string;
}
};
lastPasswordReset?: string;
passwordHash?: string;
}
const PasswordUpdateCard = ({ userId, onLogout }: { userId: string, onLogout: () => void }) => {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [status, setStatus] = useState({ message: '', isError: false });
const [isLoading, setIsLoading] = useState(false);
const [isOAuthUser, setIsOAuthUser] = useState(false);
useEffect(() => {
const checkOAuthStatus = async () => {
try {
// Get user with all fields (including internal ones)
const user = await pb.collection('users').getOne(userId, {
fields: '*,provider,verified,passwordHash'
});
// New reliable OAuth detection logic:
const isOAuthUser = (
// Case 1: No password but verified (typical OAuth pattern)
(!user.passwordHash && user.verified) ||
// Case 2: Has provider specified (older OAuth users)
(user.provider && user.provider !== 'email') ||
// Case 3: Check externalId field if exists (some OAuth implementations)
(user.externalId && typeof user.externalId === 'string')
);
console.log('OAuth determination:', {
email: user.email,
hasPassword: !!user.passwordHash,
verified: user.verified,
provider: user.provider,
isOAuthUser
});
setIsOAuthUser(isOAuthUser);
} catch (error) {
console.error("Failed to fetch user data:", error);
// Fallback to checking verification status
setIsOAuthUser(pb.authStore.model?.verified === true);
}
};
checkOAuthStatus();
}, [userId]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setStatus({ message: '', isError: false });
try {
// Validate inputs
if (newPassword !== confirmPassword) {
throw new Error("New passwords don't match");
}
if (newPassword.length < 8) {
throw new Error("Password must be at least 8 characters");
}
// SPECIAL HANDLING FOR OAUTH USERS
if (isOAuthUser) {
// For OAuth users, we need to use a different endpoint or method
// since the regular update requires oldPassword
const authData = await pb.collection('users').update(userId, {
password: newPassword,
passwordConfirm: confirmPassword
}, {
// This header might be needed depending on your PocketBase version
headers: { 'X-Require-Password-Confirm': 'false' }
});
} else {
// Regular password update flow
await pb.collection('users').update(userId, {
password: newPassword,
passwordConfirm: confirmPassword,
oldPassword: currentPassword
});
}
// Clear form
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
setStatus({
message: isOAuthUser
? 'Password successfully set! You can now login with email'
: 'Password updated successfully!',
isError: false
});
// Logout user after password change
onLogout();
} catch (error: any) {
console.error("Password update failed:", error);
// Special handling for OAuth users if the first method fails
if (isOAuthUser && error.data?.oldPassword?.message) {
try {
// Alternative method for OAuth users - create a new auth record
await pb.collection('users').authWithPassword(
pb.authStore.model?.email,
newPassword
);
setStatus({
message: 'Password successfully set!',
isError: false
});
onLogout();
return;
} catch (secondError) {
error = secondError;
}
}
// Handle specific PocketBase validation errors
let errorMessage = error.message;
if (error.data?.oldPassword?.message) {
errorMessage = "Please provide your current password";
} else if (error.data?.password?.message) {
errorMessage = error.data.password.message;
}
setStatus({
message: errorMessage || "Failed to update password. Please try again.",
isError: true
});
} finally {
setIsLoading(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Security</CardTitle>
<CardDescription>
{isOAuthUser
? "Set a password to enable email login"
: "Update your password"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<form onSubmit={handleSubmit}>
{status.message && (
<div className={`p-3 rounded-md text-sm mb-4 ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{status.message}
</div>
)}
{!isOAuthUser && (
<div className="space-y-2">
<Label htmlFor="currentPassword">Current password</Label>
<div className="relative">
<Input
id="currentPassword"
type={showCurrentPassword ? "text" : "password"}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required={!isOAuthUser}
/>
<button
type="button"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showCurrentPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="newPassword">
{isOAuthUser ? "Set new password" : "New password"}
</Label>
<div className="relative">
<Input
id="newPassword"
type={showNewPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showNewPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
Password must be at least 8 characters
</p>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">
{isOAuthUser ? "Confirm new password" : "Confirm password"}
</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
className="mt-4 w-full"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{isOAuthUser ? "Setting password..." : "Updating..."}
</>
) : (
isOAuthUser ? "Set Password" : "Update Password"
)}
</Button>
</form>
</CardContent>
</Card>
);
};
export default PasswordUpdateCard;

View File

@ -1,202 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { FileDown } from "lucide-react";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from './ui/loader';
export default function HestiaCredentialsFetcher() {
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
const [creds, setCreds] = useState(null);
const [dataLoading, setDataLoading] = useState(false);
const [error, setError] = useState('');
const [serviceName, setServiceName] = useState(null);
const [serviceOrderId, setServiceOrderId] = useState(null);
const [userSiliconId, setUserSiliconId] = useState();
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
useEffect(() => {
const fetchData = async () => {
const urlParams = new URLSearchParams(window.location.search);
const service = urlParams.get('service');
const orderId = urlParams.get('orderId');
setUserSiliconId(sessionData.siliconId);
// console.log(sessionData.siliconId)
setServiceName(service);
setServiceOrderId(orderId);
if (service && orderId ) {
await fetchCredentials(service, orderId); // Pass orderId directly
}
};
fetchData();
}, [sessionData]);
const handleSaveDataInMongoDB = async (servicesData, currentBillingId, serviceToEndpoint, siliconId) => {
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? 'hostings' : '';
try {
const serviceDataPayload = {
success: servicesData.success,
service: servicesData.service,
password: servicesData.password,
output: servicesData.output,
publicKey: servicesData.publicKey,
endpoint: servicesData.endpoint,
qr_code: servicesData.qr_code,
continent: servicesData.continent,
billingId: currentBillingId,
status: "active"
}
// console.log('serviceDataPayload', serviceDataPayload)
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users/${siliconId}/${query}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(serviceDataPayload)
});
const data = await response.json();
console.log('Node Backend API', data);
return data; // Return the data for further processing if needed
} catch (error) {
console.error('An error occurred while saving to MongoDB', error);
throw error; // Re-throw to handle in the calling function
}
};
// Update fetchCredentials to accept orderId as parameter
const fetchCredentials = async (serviceToCall, orderIdParam = null) => {
setDataLoading(true);
setError('');
setCreds(null);
try {
// Use the passed orderIdParam or fall back to state
const effectiveOrderId = orderIdParam || serviceOrderId;
const query = serviceToCall === 'vpn' ? 'get-vpn-cred' : 'get-hestia-cred';
const response = await fetch(`${SERVICES_API_URL}?query=${query}&orderId=${effectiveOrderId}`, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.error || `HTTP error: ${response.status}`);
}
const data = await response.json();
if (sessionData?.siliconId) {
const mongoResponse = await handleSaveDataInMongoDB(data, effectiveOrderId, serviceToCall, sessionData.siliconId);
console.log('mongoResponse', mongoResponse)
}
setCreds(data);
} catch (err) {
console.error("FETCH ERROR:", err);
setError(err.message);
} finally {
setDataLoading(false);
}
};
const downloadConfig = () => {
if (!creds?.output) return;
const element = document.createElement('a');
const file = new Blob([creds.output], { type: 'application/octet-stream' });
element.href = URL.createObjectURL(file);
element.download = `${creds.username || 'vpn-config'}.conf`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
console.log('serviceName', serviceName)
if (loading) {
return <Loader />;
}
// Then handle not logged in state
if (!isLoggedIn) {
window.location.href = '/';
return;
}
return (
<div className="space-y-4">
{serviceName === 'vpn' ? (
<div className="p-4 bg-gray-100 rounded shadow max-w-lg mx-auto mt-10">
{
!creds ? (
<>
<h2 className="text-xl font-bold mb-4">Get VPN Credentials</h2>
{!dataLoading && (
<Button onClick={() => fetchCredentials('vpn')} disabled={dataLoading} className="w-full">
{dataLoading ? 'Fetching...' : 'Get Credentials'}
</Button>
)}
</>
) : (
<>
<h2 className="text-xl font-bold mb-4 text-neutral-950">VPN Credentials</h2>
<p className="text-yellow-600 bg-yellow-50 p-3 rounded-md mb-4 text-sm"> Make sure to save these credentials in a safe place. We do not store them.</p>
</>
)
}
{dataLoading && <p>Loading credentials...</p>}
{creds && creds.service === 'vpn' && (
<div className="mt-4 bg-white p-4 rounded border space-y-4">
{creds.qr_code && (
<div className="flex flex-col items-center">
<img src={`data:image/png;base64,${creds.qr_code}`} alt="VPN Configuration QR Code" className="w-48 h-48 border border-gray-300 rounded" />
<p className="text-sm text-neutral-400 mt-2">Scan this QR code with your WireGuard app</p>
</div>
)}
{creds.output && (
<div>
<div className="flex justify-between items-center mb-2">
<h3 className="font-semibold text-neutral-950">Configuration File</h3>
<Button onClick={downloadConfig} size="sm" variant='outline'>
Download .conf &nbsp; <FileDown size={18} />
</Button>
</div>
<pre className="whitespace-pre-wrap bg-gray-50 p-3 rounded text-sm overflow-x-auto text-neutral-950">{creds.output}</pre>
</div>
)}
</div>
)}
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
) : creds && creds.service === 'hestia' ? (
<div className="p-4 bg-gray-100 rounded shadow max-w-md mx-auto mt-10">
<h2 className="text-xl font-bold mb-4">Hestia Credentials</h2>
{dataLoading && <p>Loading credentials...</p>}
{creds && creds.service === 'hestia' && (
<div className="mt-4 bg-white p-4 rounded border">
<p><strong>Username:</strong> {creds.username}</p>
<p><strong>Password:</strong> {creds.password}</p>
{creds.output && (
<p><strong>Output:</strong> <pre className="whitespace-pre-wrap">{creds.output}</pre></p>
)}
</div>
)}
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
) : serviceName === 'streaming_api' ? (
<Card className='max-w-2xl mx-auto px-4 mt-8'>
<CardContent>
<CardHeader>
<CardTitle>STT Streaming API Purchase Succesfully</CardTitle>
<CardDescription>You can See Info on my Billing Section in Profile</CardDescription>
</CardHeader>
</CardContent>
</Card>
) : ''}
</div>
);
}

View File

@ -1,273 +0,0 @@
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>
);
}

View File

@ -1,99 +1,54 @@
import React, { useState } from 'react';
import React from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
export interface ServiceCardProps {
title: string;
description: string;
imageUrl: string;
features: [];
features: string[];
learnMoreUrl: string;
buyButtonText: string;
buyButtonUrl: string;
}
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl, buyButtonText, buyButtonUrl }: ServiceCardProps) {
// console.log('checkType', typeof features)
// console.log('checkType', features)
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
const [showLoginModal, setShowLoginModal] = useState(false);
const parsedFeatures = typeof features === 'string' ? JSON.parse(features) : features;
const handleBuyClick = () => {
if (!isLoggedIn) {
setShowLoginModal(true);
return;
}
window.location.href = buyButtonUrl;
console.log(buyButtonUrl)
};
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl }: ServiceCardProps) {
return (
<>
<Card className="overflow-hidden transition-all hover:shadow-lg dark:border-neutral-700 h-full flex flex-col">
<div className="relative h-48 w-full overflow-hidden bg-neutral-100 dark:bg-neutral-800">
<img
src={imageUrl}
alt={`${title}`}
className="object-cover w-full h-full transition-transform duration-300 hover:scale-105"
/>
</div>
<CardHeader className="pb-3">
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
<CardDescription className="text-neutral-400 text-justify">{description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<ul className="space-y-2 text-sm">
{
parsedFeatures && (
parsedFeatures.map((feature : string, index: number) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
<span className='text-zinc-600'>{feature}</span>
</li>
))
)
}
</ul>
</CardContent>
<CardFooter>
<div className='flex gap-4 w-full'>
{buyButtonText && buyButtonUrl && (
<Button className='w-full' variant='outline' onClick={handleBuyClick}>{buyButtonText}</Button>
)}
<a
href={learnMoreUrl}
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
>
Details
</a>
</div>
</CardFooter>
</Card>
{/* Login Required Modal */}
<Dialog open={showLoginModal} onOpenChange={setShowLoginModal}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-xl mb-2">Login Required</DialogTitle>
<DialogDescription>
You need to be logged in to purchase this service.
</DialogDescription>
</DialogHeader>
<div className="mt-4 flex flex-col space-y-2">
<Button onClick={() => window.location.href = '/login'} className="w-full bg-[#6d9e37] hover:bg-[#598035]">Login Now</Button>
<Button
variant="outline"
className="w-full"
onClick={() => setShowLoginModal(false)}
>
Continue Browsing
</Button>
</div>
</DialogContent>
</Dialog>
</>
<Card className="overflow-hidden transition-all hover:shadow-lg dark:border-neutral-700 h-full flex flex-col">
<div className="relative h-48 w-full overflow-hidden bg-neutral-100 dark:bg-neutral-800">
<img
src={imageUrl}
alt={`${title} illustration`}
className="object-cover w-full h-full transition-transform duration-300 hover:scale-105"
/>
</div>
<CardHeader className="pb-3">
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
<CardDescription className="text-neutral-400">{description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<ul className="space-y-2 text-sm">
{features.map((feature, index) => (
<li key={index} className="flex items-start">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span>{feature}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter>
<a
href={learnMoreUrl}
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
>
Learn More
</a>
</CardFooter>
</Card>
);
}
}

View File

@ -1,335 +0,0 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import PocketBase from 'pocketbase';
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Separator } from "./ui/separator";
import { Label } from "./ui/label";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from "./ui/loader";
interface AuthStatus {
message: string;
isError: boolean;
}
interface UserRecord {
id: string;
email: string;
name?: string;
type?: string;
avatar?: string;
[key: string]: any;
}
interface AuthResponse {
token: string;
record: UserRecord;
}
const SignupPage = () => {
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordVisible, setPasswordVisible] = useState(false);
const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
const [isLoading, setIsLoading] = useState(false);
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsLoading(true);
setStatus({ message: '', isError: false });
if (password !== confirmPassword) {
setStatus({ message: 'Passwords do not match', isError: true });
setIsLoading(false);
return;
}
try {
// Create the user
const userData = {
email,
name,
password,
passwordConfirm: confirmPassword,
emailVisibility: true,
};
const record = await pb.collection('users').create(userData);
// Automatically login after signup
const authData = await pb.collection('users').authWithPassword(email, password);
if (!authData?.token || !authData?.record) {
throw new Error("Authentication failed after signup");
}
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
const authResponse: AuthResponse = {
token: authData.token,
record: {
id: authData.record.id,
email: authData.record.email,
name: authData.record.name || authData.record.email.split('@')[0],
type: authData.record.type || 'user',
avatar: authData.record.avatar || '',
}
};
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error: any) {
console.error("Signup failed:", error);
setStatus({
message: error.message || "Signup failed. Please try again.",
isError: true
});
} finally {
setIsLoading(false);
}
};
const signupWithOAuth2 = async (provider: 'google' | 'facebook' | 'github') => {
try {
setIsLoading(true);
setStatus({ message: '', isError: false });
const authData = await pb.collection('users').authWithOAuth2({ provider });
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: {
id: authData.record.id,
email: authData.record.email || '',
name: authData.record.name || '',
type: authData.record.type || '',
avatar: authData.record.avatar || ''
}
};
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error) {
console.error(`${provider} Signup failed:`, error);
setStatus({
message: `${provider} signup failed. Please try again.`,
isError: true
});
} finally {
setIsLoading(false);
}
};
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
try {
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'new',
accessToken: authData.token,
email: authData.record.email,
name: authData.record.name,
type: authData.record.type,
avatar: avatarUrl,
isAuthenticated: true,
id: authData.record.id
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Session synced with backend:', data);
return data;
} catch (error: any) {
console.error('Error syncing session:', error);
throw new Error(`Session sync failed: ${error.message}`);
}
};
const { isLoggedIn, loading, error } = useIsLoggedIn();
if (loading) {
return <Loader />;
}
if (isLoggedIn) {
window.location.href = '/';
return null;
}
return (
<div className="min-h-screen flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg rounded-xl overflow-hidden">
<CardHeader className="text-center space-y-1">
<CardTitle className="text-2xl font-bold">Create an Account</CardTitle>
<CardDescription className="">
Join us to get started
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{status.message && (
<div className={`p-3 rounded-md text-sm ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{status.message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your Name"
required
className="focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Input
id="password"
type={passwordVisible ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setPasswordVisible(!passwordVisible)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{passwordVisible ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<div className="relative">
<Input
id="confirmPassword"
type={confirmPasswordVisible ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setConfirmPasswordVisible(!confirmPasswordVisible)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{confirmPasswordVisible ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating account...
</>
) : (
'Sign Up'
)}
</Button>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or sign up with
</span>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<Button
variant="outline"
onClick={() => signupWithOAuth2('google')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/google.svg" alt="Google" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => signupWithOAuth2('facebook')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/facebook.svg" alt="Facebook" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => signupWithOAuth2('github')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
</Button>
</div>
</CardContent>
<div className="px-6 pb-6 text-center text-sm text-gray-500">
Already have an account?{' '}
<a href="/login" className="font-medium text-[#6d9e37] hover:underline">
Sign in
</a>
</div>
</Card>
</div>
);
};
export default SignupPage;

View File

@ -1,171 +0,0 @@
import React, { useState } from "react";
export default function DeployWordPress() {
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const [debugInfo, setDebugInfo] = useState(null);
// Configuration - Replace with your actual values
const COOLIFY_API_URL = "http://192.168.1.197:8000/api/v1";
const TOKEN = "zXSR33z74eK26abbKyL9bz4d3PYouTSK8FSjOltv719c52d8";
const PROJECT_UUID = "wc40gg048gkwg0go80ggog44";
const SERVER_UUID = "sgswssowscc84o8sc8wockgc";
const ENVIRONMENT_NAME = "production"; // Changed from 'dev' to 'production' as default
const createWordPress = async () => {
setLoading(true);
setMessage("");
setDebugInfo(null);
try {
// 1. Create MySQL Service - Updated to Coolify's expected format
const mysqlPayload = {
name: "wordpress-db",
type: "mysql",
projectUuid: PROJECT_UUID,
serverUuid: SERVER_UUID,
version: "8.0",
destination: { // Coolify often requires this structure
serverUuid: SERVER_UUID,
environment: ENVIRONMENT_NAME
},
configuration: {
type: "mysql",
settings: { // Changed from environmentVariables to settings
MYSQL_ROOT_PASSWORD: "example",
MYSQL_DATABASE: "wordpress",
MYSQL_USER: "wordpress",
MYSQL_PASSWORD: "example",
MYSQL_ALLOW_EMPTY_PASSWORD: "no"
}
}
};
const mysqlRes = await fetch(`${COOLIFY_API_URL}/services`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(mysqlPayload),
});
const mysqlData = await mysqlRes.json();
setDebugInfo({ mysql: { request: mysqlPayload, response: mysqlData } });
if (!mysqlRes.ok) {
throw new Error(
mysqlData.message ||
mysqlData.error?.message ||
JSON.stringify(mysqlData.errors) ||
"MySQL validation failed"
);
}
// 2. Create WordPress Service
const wpPayload = {
name: "wordpress",
type: "wordpress",
projectUuid: PROJECT_UUID,
serverUuid: SERVER_UUID,
version: "latest",
destination: {
serverUuid: SERVER_UUID,
environment: ENVIRONMENT_NAME
},
configuration: {
type: "wordpress",
settings: {
WORDPRESS_DB_HOST: "wordpress-db",
WORDPRESS_DB_USER: "wordpress",
WORDPRESS_DB_PASSWORD: "example",
WORDPRESS_DB_NAME: "wordpress",
WORDPRESS_TABLE_PREFIX: "wp_"
}
}
};
const wpRes = await fetch(`${COOLIFY_API_URL}/services`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(wpPayload),
});
const wpData = await wpRes.json();
setDebugInfo(prev => ({ ...prev, wordpress: { request: wpPayload, response: wpData } }));
if (!wpRes.ok) {
throw new Error(
wpData.message ||
wpData.error?.message ||
JSON.stringify(wpData.errors) ||
"WordPress validation failed"
);
}
setMessage("🎉 WordPress + MySQL deployed successfully!");
} catch (err) {
setMessage(`❌ Deployment failed: ${err.message}`);
console.error("Deployment error:", err, debugInfo);
} finally {
setLoading(false);
}
};
return (
<div className="p-6 max-w-md mx-auto bg-white rounded-xl shadow-md">
<h2 className="text-2xl font-bold mb-4">Deploy WordPress</h2>
<button
onClick={createWordPress}
disabled={loading}
className={`w-full py-2 px-4 rounded-md text-white font-medium ${
loading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700"
}`}
>
{loading ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Deploying...
</span>
) : "Deploy WordPress"}
</button>
{message && (
<div className={`mt-4 p-3 rounded-md ${
message.includes("❌") ? "bg-red-50 text-red-700" : "bg-green-50 text-green-700"
}`}>
{message}
</div>
)}
{debugInfo && (
<div className="mt-4 space-y-4">
<details className="bg-gray-50 rounded-md p-2">
<summary className="font-medium cursor-pointer">Debug Details</summary>
<div className="mt-2 bg-white p-2 rounded border border-gray-200 overflow-auto max-h-60">
<pre>{JSON.stringify(debugInfo, null, 2)}</pre>
</div>
</details>
<div className="text-sm text-gray-600">
<p className="font-medium">Troubleshooting:</p>
<ul className="list-disc pl-5 space-y-1 mt-1">
<li>Verify <code>PROJECT_UUID</code> and <code>SERVER_UUID</code> are correct</li>
<li>Check if environment <code>{ENVIRONMENT_NAME}</code> exists</li>
<li>Ensure your Coolify version supports this API format</li>
</ul>
</div>
</div>
)}
</div>
);
}
// [{"uuid":"sgswssowscc84o8sc8wockgc","description":"This is the server where Coolify is running on. Don't delete this!","name":"localhost","ip":"host.docker.internal","is_coolify_host":true,"is_reachable":true,"is_usable":true,"port":22,"proxy":{"redirect_enabled":true},"settings":{"id":1,"concurrent_builds":2,"delete_unused_networks":false,"delete_unused_volumes":false,"docker_cleanup_frequency":"0 0 * * *","docker_cleanup_threshold":80,"dynamic_timeout":3600,"force_disabled":false,"force_docker_cleanup":true,"generate_exact_labels":false,"is_build_server":false,"is_cloudflare_tunnel":false,"is_jump_server":false,"is_logdrain_axiom_enabled":false,"is_logdrain_custom_enabled":false,"is_logdrain_highlight_enabled":false,"is_logdrain_newrelic_enabled":false,"is_metrics_enabled":false,"is_reachable":true,"is_sentinel_debug_enabled":false,"is_sentinel_enabled":false,"is_swarm_manager":false,"is_swarm_worker":false,"is_usable":true,"logdrain_axiom_api_key":null,"logdrain_axiom_dataset_name":null,"logdrain_custom_config":null,"logdrain_custom_config_parser":null,"logdrain_highlight_project_id":null,"logdrain_newrelic_base_uri":null,"logdrain_newrelic_license_key":null,"sentinel_custom_url":"http:\/\/host.docker.internal:8000","sentinel_metrics_history_days":7,"sentinel_metrics_refresh_rate_seconds":10,"sentinel_push_interval_seconds":60,"sentinel_token":"eyJpdiI6IllwMlBsOUtXODdUR0ZIbWtZenJRWFE9PSIsInZhbHVlIjoiZFUxcE9zSXFXdkVrN0tDUGdSbHpGVTE3cHVEeFlBM1hFWk56S05NVWVmVmxHV0tBQ2kra25uRnVzRzNHaFpBSGVTaGZLMGpqdS9nMU85MXhmS3VYMVE9PSIsIm1hYyI6ImY3ODQyNGI2OTVlMmRiMzFmNzJjZjRlYjNkNDIxOGVmZTAxOWMyNjY0ZTYxODE5MDIwY2FhMGUwYjU4ODMyN2MiLCJ0YWciOiIifQ==","server_disk_usage_check_frequency":"0 23 * * *","server_disk_usage_notification_threshold":80,"server_id":0,"server_timezone":"UTC","wildcard_domain":null,"created_at":"2025-04-03T17:12:45.000000Z","updated_at":"2025-04-03T17:16:25.000000Z"},"user":"root"}]

View File

@ -1,364 +0,0 @@
import React, { useState, useEffect } from "react";
import { Button } from "./ui/button";
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 = 'https://host-api.cs1.hz.siliconpin.com/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, index) => (
<th key={index} 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 key={index}>
<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>&nbsp;|&nbsp;
<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;

View File

@ -1,590 +0,0 @@
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>
);
}

View File

@ -1,132 +0,0 @@
import React, { useState, useEffect } from "react";
import { fromBlob, blobToURL } from "image-resize-compress";
import { Button } from "../ui/button";
import {
ImageIcon,
DownloadIcon,
SlidersHorizontal,
FileImage,
} from "lucide-react";
export default function ImageResize() {
const [previewUrl, setPreviewUrl] = useState(null);
const [resizedBlob, setResizedBlob] = useState(null);
const [imgQuality, setImgQuality] = useState(80);
const [selectedFile, setSelectedFile] = useState(null);
const [originalSize, setOriginalSize] = useState(0);
const [resizedSize, setResizedSize] = useState(0);
const [originalExtension, setOriginalExtension] = useState("webp");
useEffect(() => {
const resizeImage = async () => {
if (!selectedFile) return;
try {
const width = "auto";
const height = "auto";
const ext = selectedFile.name.split(".").pop().toLowerCase();
setOriginalExtension(ext);
setOriginalSize((selectedFile.size / 1024).toFixed(2)); // KB
// Only apply quality change for JPEG or WebP
const format = ["jpg", "jpeg", "webp"].includes(ext)
? ext
: "jpeg"; // fallback to jpeg for better quality control
const resized = await fromBlob(selectedFile, imgQuality, width, height, format);
const url = await blobToURL(resized);
setResizedBlob(resized);
setResizedSize((resized.size / 1024).toFixed(2)); // KB
setPreviewUrl(url);
} catch (err) {
console.error("Image resizing failed:", err);
}
};
resizeImage();
}, [selectedFile, imgQuality]);
const handleFileChange = (event) => {
const file = event.target.files[0];
if (file) {
setSelectedFile(file);
}
};
const handleDownload = () => {
if (!resizedBlob) return;
const link = document.createElement("a");
link.href = URL.createObjectURL(resizedBlob);
link.download = `resized-image.${originalExtension}`;
link.click();
};
return (
<div className="min-h-screen text-white p-6 flex items-center justify-center">
<div className="bg-gray-800 rounded-2xl shadow-lg w-full max-w-2xl p-6 space-y-6">
<h2 className="text-2xl font-bold flex items-center gap-2 text-[#6d9e37]">
<ImageIcon className="w-6 h-6" />
Image Resizer
</h2>
<label className="flex items-center gap-2 text-sm text-gray-300 font-medium">
<FileImage className="w-4 h-4" />
Choose an image
</label>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded-lg text-sm file:bg-[#6d9e37] file:text-white file:border-0 file:px-3 file:py-1"
/>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium flex items-center gap-1">
<SlidersHorizontal className="w-4 h-4" />
Quality: <span className="text-[#6d9e37]">{imgQuality}%</span>
</label>
<input
type="range"
min="1"
max="100"
value={imgQuality}
onChange={(e) => setImgQuality(e.target.value)}
className="w-full accent-[#6d9e37]"
/>
</div>
{previewUrl && (
<div className="border-t border-gray-700 pt-4 space-y-3">
<div className="text-sm text-gray-300 grid grid-cols-2 gap-2">
<p>
<strong>Original Size:</strong> {originalSize} KB
</p>
<p>
<strong>Resized Size:</strong> {resizedSize} KB
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-400 mb-2">Preview:</p>
<img
src={previewUrl}
alt="Resized Preview"
className="w-full h-auto max-h-96 object-contain rounded-lg border border-gray-700"
/>
</div>
<Button
onClick={handleDownload}
className="mt-4 w-full gap-2"
>
<DownloadIcon className="w-5 h-5" />
Download Resized Image
</Button>
</div>
)}
</div>
</div>
);
}

View File

@ -1,202 +0,0 @@
import React, { useState, useEffect } from "react";
import { marked } from 'marked';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { Button } from "./ui/button";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Comment from './Comment';
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
export default function TopicDetail(props) {
const [showCopied, setShowCopied] = useState(false);
if (!props.topic) {
return <div>Topic not found</div>;
}
const shareUrl = typeof window !== 'undefined' ? window.location.href : '';
const title = props.topic.title;
const text = `Check out this article: ${title}`;
const shareOnSocialMedia = (platform) => {
switch (platform){
case 'facebook':
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank');
break;
case 'twitter':
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`, '_blank');
break;
case 'linkedin':
window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
break;
case 'reddit':
window.open(`https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
break;
case 'whatsapp':
window.open(`https://wa.me/?text=${encodeURIComponent(`${text} ${shareUrl}`)}`, '_blank');
break;
case 'email':
window.open(`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(`${text}\n\n${shareUrl}`)}`);
break;
}
}
const copyToClipboard = () => {
navigator.clipboard.writeText(shareUrl);
setShowCopied(true);
setTimeout(() => setShowCopied(false), 2000);
};
// SVG Icons with improved styling
const SocialIcon = ({ children, className = "" }) => (
<span className={`w-5 h-5 flex items-center justify-center ${className}`}>
{children}
</span>
);
const FacebookIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/>
</svg>
);
const TwitterIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 30 30" fill="currentColor">
<path d="M26.37,26l-8.795-12.822l0.015,0.012L25.52,4h-2.65l-6.46,7.48L11.28,4H4.33l8.211,11.971L12.54,15.97L3.88,26h2.65 l7.182-8.322L19.42,26H26.37z M10.23,6l12.34,18h-2.1L8.12,6H10.23z"></path>
</svg>
);
const LinkedInIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
</svg>
);
const RedditIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.5.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.12-.07 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.963-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
</svg>
);
const WhatsAppIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
);
const MailIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-9.458l4.623 3.746zm-4.141-5.929h19.035l-9.517 7.713-9.518-7.713zm5.694 7.188l3.824 3.099 3.83-3.104 5.612 6.817h-18.779l5.513-6.812zm9.208-1.264l4.616-3.741v9.348l-4.616-5.607z"/>
</svg>
);
const LinkIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M6.188 8.719c.439-.439.926-.801 1.444-1.087 2.887-1.591 6.589-.745 8.445 2.069l-2.246 2.245c-.644-1.469-2.243-2.305-3.834-1.949-.599.134-1.168.433-1.633.898l-4.304 4.306c-1.307 1.307-1.307 3.433 0 4.74 1.307 1.307 3.433 1.307 4.74 0l1.327-1.327c1.207.479 2.501.67 3.779.575l-2.929 2.929c-2.511 2.511-6.582 2.511-9.093 0s-2.511-6.582 0-9.093l4.304-4.306zm6.836-6.836l-2.929 2.929c1.277-.096 2.572.096 3.779.574l1.326-1.326c1.307-1.307 3.433-1.307 4.74 0 1.307 1.307 1.307 3.433 0 4.74l-4.305 4.305c-1.311 1.311-3.44 1.3-4.74 0-.303-.303-.564-.68-.727-1.051l-2.246 2.245c.236.358.481.689.736 1.011.748.98 1.804 1.644 2.993 1.907 1.535.368 3.159.166 4.613-.617.518-.286 1.005-.648 1.444-1.087l4.304-4.305c2.512-2.511 2.512-6.582.001-9.093-2.511-2.51-6.581-2.51-9.092 0z"/>
</svg>
);
return (
<div className="container mx-auto px-4 py-12">
<article className="max-w-4xl mx-auto">
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
{/* Enhanced Social Share Buttons */}
<div className="mb-8">
<p className="text-sm text-gray-500 mb-3 font-medium">Share this article:</p>
<div className="flex flex-wrap gap-2">
<button
onClick={() => shareOnSocialMedia('facebook')}
className="flex items-center gap-1 px-2 py-2 bg-[#3b5998] hover:bg-[#2d4373] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on Facebook"
title="Share on Facebook"
>
<SocialIcon className="text-white"><FacebookIcon /></SocialIcon>
<span className="text-sm font-medium">Facebook</span>
</button>
<button
onClick={() => shareOnSocialMedia('whatsapp')}
className="flex items-center gap-1 px-2 py-2 bg-[#25D366] hover:bg-[#1da851] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on WhatsApp"
title="Share on WhatsApp"
>
<SocialIcon className="text-white"><WhatsAppIcon /></SocialIcon>
<span className="text-sm font-medium">WhatsApp</span>
</button>
<button
onClick={() => shareOnSocialMedia('twitter')}
className="flex items-center gap-1 px-2 py-2 bg-[#000000] hover:bg-[#000000] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on Twitter"
title="Share on Twitter"
>
<SocialIcon className="text-white"><TwitterIcon /></SocialIcon>
<span className="text-sm font-medium">Twitter</span>
</button>
<button
onClick={() => shareOnSocialMedia('linkedin')}
className="flex items-center gap-1 px-2 py-2 bg-[#0077b5] hover:bg-[#005582] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on LinkedIn"
title="Share on LinkedIn"
>
<SocialIcon className="text-white"><LinkedInIcon /></SocialIcon>
<span className="text-sm font-medium">LinkedIn</span>
</button>
<button
onClick={() => shareOnSocialMedia('reddit')}
className="flex items-center gap-1 px-2 py-2 bg-[#FF5700] hover:bg-[#e04e00] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on Reddit"
title="Share on Reddit"
>
<SocialIcon className="text-white"><RedditIcon /></SocialIcon>
<span className="text-sm font-medium">Reddit</span>
</button>
<button
onClick={() => shareOnSocialMedia('reddit')}
className="flex items-center gap-1 px-2 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share via Email"
title="Share via Email"
>
<SocialIcon className="text-white"><MailIcon /></SocialIcon>
<span className="text-sm font-medium">Email</span>
</button>
<div className="relative">
<button
onClick={copyToClipboard}
className="flex items-center gap-1 px-2 py-2 bg-gray-800 hover:bg-gray-900 text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Copy link"
title="Copy link to share"
>
<SocialIcon className="text-white"><LinkIcon /></SocialIcon>
<span className="text-sm font-medium">Copy Link</span>
</button>
{showCopied && (
<div className="absolute -bottom-8 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap shadow-md">
Link copied!
</div>
)}
</div>
</div>
</div>
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
<Comment topicId={props.topic.id}/>
</article>
</div>
);
}
// bg-[#6d9e37]

View File

@ -1,525 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import MDEditor, { commands } from '@uiw/react-md-editor';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
import Loader from "./ui/loader";
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
const urlParams = new URLSearchParams(window.location.search);
const slug = urlParams.get('slug');
// console.log('find slug from url', slug);
export default function EditTopic (){
// const { slug } = useParams();
// const navigate = useNavigate();
// Form state
const [formData, setFormData] = useState({ status: 'draft', category: '', slug: '', title: '', content: '', imageUrl: '' });
// UI state
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const [imageFile, setImageFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [imageDialogOpen, setImageDialogOpen] = useState(false);
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [editorMode, setEditorMode] = useState('edit');
// Fetch topic data on component mount
useEffect(() => {
const fetchTopic = async () => {
try {
const response = await fetch(`${TOPIC_API_URL}?query=get-single-topic&slug=${slug}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch topic');
}
const data = await response.json();
const topic = data.data[0]
// console.log('Single topic data', data)
setFormData({
status: topic.status || 'draft',
category: topic.category || '',
title: topic.title || '',
slug: topic.slug || '',
content: topic.content || '',
imageUrl: topic.imageUrl || ''
});
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchTopic();
}, [slug]);
// Upload file to MinIO (same as NewTopic)
const uploadToMinIO = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
formData.append('bucket', 'siliconpin-uploads');
formData.append('folder', 'topic-images');
try {
const response = await fetch(MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include',
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
return data.url;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// Generate slug from title (same as NewTopic)
// useEffect(() => {
// if (formData.title) {
// const newSlug = formData.title
// .toLowerCase()
// .replace(/[^\w\s]/g, '')
// .replace(/\s+/g, '-');
// setFormData(prev => ({ ...prev, slug: newSlug }));
// }
// }, [formData.title]);
// Custom image command for MDEditor (same as NewTopic)
const customImageCommand = {
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
</svg>
),
execute: () => {
setImageDialogOpen(true);
},
};
// Get all commands (same as NewTopic)
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
// Handle image URL insertion (same as NewTopic)
const handleInsertImageUrl = () => {
if (imageUrlInput) {
const imgMarkdown = `![Image](${imageUrlInput})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUrlInput('');
}
};
// Handle image file selection (same as NewTopic)
const handleImageFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setImageUploadFile(file);
const reader = new FileReader();
reader.onload = () => {
setImageUploadPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
// Upload image file (same as NewTopic)
const handleImageUpload = async () => {
if (!imageUploadFile) return;
try {
setIsSubmitting(true);
setUploadProgress(0);
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
setUploadProgress(progress);
});
const imgMarkdown = `![Image](${uploadedUrl})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUploadFile(null);
setImageUploadPreview('');
} catch (error) {
setError('Failed to upload image: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
// Form submission for EDIT
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
setUploadProgress(0);
try {
// Upload new featured image if selected
let imageUrl = formData.imageUrl;
if (imageFile) {
imageUrl = await uploadToMinIO(imageFile, (progress) => {
setUploadProgress(progress);
});
}
// Prepare payload
const payload = {
...formData,
imageUrl
};
// Submit to API (PUT request for update)
const response = await fetch(`${TOPIC_API_URL}?query=update-topic&slug=${formData.slug}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to update topic');
}
setSuccess(true);
setTimeout(() => {
navigate(`/topic/${formData.slug}`);
}, 1500);
} catch (err) {
setError(err.message);
} finally {
setIsSubmitting(false);
setUploadProgress(0);
}
};
// Handle form field changes (same as NewTopic)
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Handle featured image file selection (same as NewTopic)
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setImageFile(file);
const reader = new FileReader();
reader.onload = () => {
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
};
reader.readAsDataURL(file);
}
};
// Loading state
if (isLoading) {
return (
<Loader />
);
}
// Success state
if (success) {
return (
<div className="container mx-auto px-4 text-center py-8">
<h3 className="text-xl font-semibold mb-4">Topic has been successfully updated!</h3>
<p className="mb-4">You are being automatically redirected to the topic page...</p>
<div className="flex justify-center gap-4">
<Button onClick={() => window.location.href = `/topic/${formData.slug}`}>Preview this Topic</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Edit Topic</h2>
<p className="text-gray-600 mb-6">{formData.title}</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
name="status"
required
value={formData.status}
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
>
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="published">Publish</SelectItem>
<SelectItem value="archived">Archive</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category *</Label>
<Select
name="category"
required
value={formData.category}
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
>
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select Category" />
</SelectTrigger>
<SelectContent position="popper" className="z-50">
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Title and Slug */}
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Wriet Topic Title"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="slug">URL Slug</Label>
<input
type="hidden"
name="slug"
value={formData.slug}
onChange={handleChange}
className="bg-gray-50"
/>
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
</div>
{/* Content Editor */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="content">Content *</Label>
<button
type="button"
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
>
{editorMode === 'edit' ? 'Preview' : 'Edit'}
</button>
</div>
<div data-color-mode="light">
<MDEditor
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}
preview={editorMode}
commands={allCommands}
/>
</div>
</div>
{/* Image Upload Dialog (same as NewTopic) */}
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add Image</DialogTitle>
</DialogHeader>
<CustomTabs
tabs={[
{
label: "URL",
value: "url",
content: (
<div className="space-y-4">
<Input
type="text"
placeholder="Image URL"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
/>
<div className="flex justify-end">
<Button
type="button"
onClick={handleInsertImageUrl}
disabled={!imageUrlInput}
>
Upload Image
</Button>
</div>
</div>
)
},
{
label: "Upload",
value: "upload",
content: (
<div className="space-y-4">
<Input
type="file"
accept="image/*"
onChange={handleImageFileSelect}
/>
{imageUploadPreview && (
<div className="mt-2">
<img
src={imageUploadPreview}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
<div className="flex justify-end">
<Button
type="button"
onClick={handleImageUpload}
disabled={!imageUploadFile || isSubmitting}
>
{isSubmitting ? 'Uploading...' : 'Upload'}
</Button>
</div>
</div>
)
}
]}
/>
</DialogContent>
</Dialog>
{/* Featured Image Upload */}
<div className="space-y-2">
<Label htmlFor="image">Featured Image</Label>
<Input
type="file"
id="image"
onChange={handleFileChange}
accept="image/*"
className="cursor-pointer"
/>
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
{formData.imageUrl && (
<div className="mt-2">
<img
src={formData.imageUrl}
alt="Preview"
className="max-h-40 rounded-md border"
/>
<p className="text-xs text-gray-500 mt-1">Current Image</p>
</div>
)}
</div>
<Separator />
{/* Form Actions */}
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={() => navigate(-1)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="min-w-32 bg-[#6d9e37] hover:bg-[#5a8a2a]"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<span className="animate-spin"></span>
Uploading...
</span>
) : 'Update'}
</Button>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 rounded-md">
Error: {error}
</div>
)}
</form>
</div>
</div>
);
};

View File

@ -1,100 +0,0 @@
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { Pencil, Trash2, Search } from "lucide-react";
import { marked } from 'marked';
export default function TopicItems(props) {
// console.table(props.topics)
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [localSearchTerm, setLocalSearchTerm] = React.useState(props.searchTerm || '');
if (loading) {
return <div className="loading-indicator">Loading...</div>;
}
if (error) {
return <div className="error-message">Error loading authentication status</div>;
}
const handleSearchChange = (e) => {
setLocalSearchTerm(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
props.onSearch(e);
};
return (
<section className="container mx-auto px-4">
<div className="py-8 text-center">
<h2 className="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">{props.title}</h2>
<p className="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">{props.description}</p>
<form onSubmit={handleSubmit} className="flex gap-2 max-w-xl mx-auto mt-4">
<Input type="text" name="search" placeholder="Search Topic..." value={localSearchTerm} onChange={handleSearchChange}/>
<Button type="submit">
<Search className="h-4 w-4 mr-2" />
Search
</Button>
</form>
</div>
{
props.topics.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{
props.topics.map((topic) => (
<a href={`/topic/${topic.slug}`} key={topic.id} className="hover:scale-[1.02] transition-transform duration-200 ">
<Card className="h-full flex flex-col group relative">
<div className="">
<img src={topic.img ? topic.img : '/assets/images/thumb-place.jpg'} alt={topic.title} className="aspect-video object-cover rounded-t-lg" loading="lazy" />
</div>
<CardContent className="flex-1 p-6">
<CardTitle className="mb-2 line-clamp-1">{topic.title}</CardTitle>
<CardDescription className="line-clamp-4 mb-4 text-justify" dangerouslySetInnerHTML={{ __html: marked.parse(topic.content || '') }}></CardDescription>
<div className="flex justify-between items-center">
<p className="text-xs text-gray-500"><strong>Author: </strong>{topic.user.split('@')[0]}</p>
<p className="text-xs text-gray-500">
<strong>Date: </strong>
{new Date(topic.timestamp).toLocaleDateString('en-GB', {
day: '2-digit',
year: '2-digit',
month: 'short',
}).replace(',', '')}
</p>
</div>
{isLoggedIn && sessionData.user_email === topic.user && props.mytopic === true && (
<div className="flex justify-end gap-2 mt-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<Button variant="outline" size="sm" className="gap-1"
onClick={(e) => {
e.preventDefault();
window.location.href = `/topic/edit?slug=${topic.slug}`
// Handle edit action
}}
>
<Pencil className="h-4 w-4" />Edit
</Button>
<Button size="sm" className="gap-1 bg-red-500 hover:bg-red-600"
onClick={(e) => {
e.preventDefault();
// Handle delete action
}}
>
<Trash2 className="h-4 w-4" />Delete
</Button>
</div>
)}
</CardContent>
</Card>
</a>
))
}
</div>
) : (
<p className="text-center">No Topic Found <a href="/topic/new" className="text-[#6d9e37]"> Click Here</a> to Create Once</p>
)
}
</section>
);
}

View File

@ -1,232 +0,0 @@
import React, { useState, useEffect } from "react";
import TopicItems from "./TopicItem";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { Button } from "./ui/button";
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
export default function TopicCreation() {
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
const [topics, setTopics] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [pagination, setPagination] = useState({
current_page: 1,
last_page: 1,
per_page: 10,
total: 0
});
// Get current page from URL or default to 1
const getCurrentPage = () => {
const params = new URLSearchParams(window.location.search);
const page = parseInt(params.get('page')) || 1;
return Math.max(1, Math.min(page, pagination.last_page));
};
// Fetch topics data with abort controller
const fetchTopics = async (page) => {
setLoading(true);
try {
const response = await fetch(
`https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`,
{
method: 'GET',
credentials: 'include'
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setTopics(data.data || []);
setPagination(prev => ({
...data.pagination,
current_page: Math.min(data.pagination.current_page, data.pagination.last_page)
}));
} catch (err) {
setError(err.message);
console.error('Fetch error:', err);
} finally {
setLoading(false);
}
};
// Handle page change
const handlePageChange = (newPage) => {
const validatedPage = Math.max(1, Math.min(newPage, pagination.last_page));
window.history.pushState({}, '', `?page=${validatedPage}`);
fetchTopics(validatedPage);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// Initial load and URL change handling
useEffect(() => {
fetchTopics(getCurrentPage());
const handlePopState = () => {
fetchTopics(getCurrentPage());
};
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, []);
if (authLoading) {
return <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>;
}
if (authError) {
return <div className="error-message p-4 bg-red-100 text-red-700 rounded">Error loading authentication status</div>;
}
return (
<>
{isLoggedIn && (
<div className="container mx-auto flex justify-end gap-x-4 mb-4">
<a href="/topic/new" className="create-new-link">Create New</a>
<a href="/topic/my-topic">My Topics</a>
</div>
)}
{loading && !topics.length ? (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
) : error ? (
<div className="error-message p-4 bg-red-100 text-red-700 rounded">Error loading topics: {error}</div>
) : (
<>
<TopicItems topics={topics} title="SoliconPin Topics" description={topicPageDesc} />
{pagination.last_page > 1 && (
<div className="flex flex-col justify-between items-center mt-8 gap-4">
<div className="text-sm text-gray-600">
Showing {(pagination.current_page - 1) * pagination.per_page + 1}-
{Math.min(pagination.current_page * pagination.per_page, pagination.total)} of {pagination.total} topics
</div>
<div className="flex items-center gap-2">
{
pagination.current_page > 1 && (
<>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(1)}
disabled={pagination.current_page <= 1}
className="hidden sm:inline-flex"
>
First
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(pagination.current_page - 1)}
disabled={pagination.current_page <= 1}
>
Previous
</Button>
</>
)
}
<div className="flex items-center gap-1">
{generatePageNumbers(pagination.current_page, pagination.last_page).map((page, i) => (
page === '...' ? (
<span key={i} className="px-2">...</span>
) : (
<Button
key={i}
variant={page === pagination.current_page ? "default" : "outline"}
onClick={() => handlePageChange(page)}
className="min-w-10"
>
{page}
</Button>
)
))}
</div>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(pagination.current_page + 1)}
disabled={pagination.current_page >= pagination.last_page}
>
Next
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(pagination.last_page)}
disabled={pagination.current_page >= pagination.last_page}
className="hidden sm:inline-flex"
>
Last
</Button>
</div>
</div>
)}
</>
)}
</>
);
}
// Helper function to generate smart page numbers
function generatePageNumbers(currentPage, lastPage) {
const pages = [];
const maxVisible = 5; // Maximum visible page numbers
if (lastPage <= maxVisible) {
for (let i = 1; i <= lastPage; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
// Calculate start and end of middle pages
let start = Math.max(2, currentPage - 1);
let end = Math.min(lastPage - 1, currentPage + 1);
// Adjust if we're at the beginning
if (currentPage <= 3) {
end = maxVisible - 2;
}
// Adjust if we're at the end
if (currentPage >= lastPage - 2) {
start = lastPage - (maxVisible - 2);
}
// Add ellipsis if needed
if (start > 2) {
pages.push('...');
}
// Add middle pages
for (let i = start; i <= end; i++) {
pages.push(i);
}
// Add ellipsis if needed
if (end < lastPage - 1) {
pages.push('...');
}
// Always show last page
pages.push(lastPage);
}
return pages;
}

View File

@ -1,264 +0,0 @@
import React, { useState, useEffect } from "react";
import TopicItems from "./TopicItem";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { Button } from "./ui/button";
import Loader from "./ui/loader";
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
export default function TopicCreation() {
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
const [topics, setTopics] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [pagination, setPagination] = useState({
current_page: 1,
last_page: 1,
per_page: 10,
total: 0
});
// Get current page from URL or default to 1
const getCurrentPage = () => {
const params = new URLSearchParams(window.location.search);
const page = parseInt(params.get('page')) || 1;
return Math.max(1, Math.min(page, pagination.last_page));
};
// Fetch topics data
const fetchTopics = async (page, search = '') => {
setLoading(true);
try {
let url = `https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
const response = await fetch(url, {
method: 'GET',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setTopics(data.data || []);
setPagination(prev => ({
...data.pagination,
current_page: Math.min(data.pagination.current_page, data.pagination.last_page)
}));
setSearchTerm(data.search_term || '');
} catch (err) {
setError(err.message);
console.error('Fetch error:', err);
} finally {
setLoading(false);
}
};
// Handle page change
const handlePageChange = (newPage) => {
const validatedPage = Math.max(1, Math.min(newPage, pagination.last_page));
const params = new URLSearchParams(window.location.search);
params.set('page', validatedPage);
window.history.pushState({}, '', `?${params.toString()}`);
fetchTopics(validatedPage, searchTerm);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// Handle search
const handleSearch = (e) => {
e.preventDefault();
const newSearchTerm = e.target.elements.search.value.trim();
setSearchTerm(newSearchTerm);
// Reset to page 1 when searching
const params = new URLSearchParams(window.location.search);
params.set('page', 1);
if (newSearchTerm) {
params.set('search', newSearchTerm);
} else {
params.delete('search');
}
window.history.pushState({}, '', `?${params.toString()}`);
fetchTopics(1, newSearchTerm);
};
// Initial load and URL change handling
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const initialSearch = params.get('search') || '';
setSearchTerm(initialSearch);
fetchTopics(getCurrentPage(), initialSearch);
const handlePopState = () => {
const newParams = new URLSearchParams(window.location.search);
const newSearch = newParams.get('search') || '';
setSearchTerm(newSearch);
fetchTopics(getCurrentPage(), newSearch);
};
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, []);
// ... (keep existing authLoading and authError checks)
return (
<>
{isLoggedIn && (
<>
<div className="container mx-auto flex justify-end gap-x-4 my-4">
<Button className="hidden lg:block" onClick={() => window.location.href = '/topic/new'} variant="outline">Create New</Button>
<Button onClick={() => window.location.href = '/topic/my-topic'} variant="outline">My Creation</Button>
</div>
<button onClick={() => window.location.href = '/topic/new'} className="fixed z-10 bottom-20 lg:bottom-10 right-0 lg:right-10 rounded-full w-16 h-16 bg-[#6d9e37]">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#6d9e37] opacity-75 duration-[1s,15s]"></span>
<span class="relative inline-flex w-16 h-16 rounded-full bg-[#6d9e37]">
<svg className="border-[2px] border-[#fff] rounded-full" fill="#FFFFFF" viewBox="-5.76 -5.76 35.52 35.52" 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 fill-rule="evenodd" d="M12.3023235,7.94519388 L4.69610276,15.549589 C4.29095108,15.9079238 4.04030835,16.4092335 4,16.8678295 L4,20.0029438 L7.06398288,20.004826 C7.5982069,19.9670062 8.09548693,19.7183782 8.49479322,19.2616227 L16.0567001,11.6997158 L12.3023235,7.94519388 Z M13.7167068,6.53115006 L17.4709137,10.2855022 L19.8647941,7.89162181 C19.9513987,7.80501747 20.0000526,7.68755666 20.0000526,7.56507948 C20.0000526,7.4426023 19.9513987,7.32514149 19.8647932,7.23853626 L16.7611243,4.13485646 C16.6754884,4.04854589 16.5589355,4 16.43735,4 C16.3157645,4 16.1992116,4.04854589 16.1135757,4.13485646 L13.7167068,6.53115006 Z M16.43735,2 C17.0920882,2 17.7197259,2.26141978 18.1781068,2.7234227 L21.2790059,5.82432181 C21.7406843,6.28599904 22.0000526,6.91216845 22.0000526,7.56507948 C22.0000526,8.21799052 21.7406843,8.84415992 21.2790068,9.30583626 L9.95750718,20.6237545 C9.25902448,21.4294925 8.26890003,21.9245308 7.1346,22.0023295 L2,22.0023295 L2,21.0023295 L2.00324765,16.7873015 C2.08843822,15.7328366 2.57866679,14.7523321 3.32649633,14.0934196 L14.6953877,2.72462818 C15.1563921,2.2608295 15.7833514,2 16.43735,2 Z"></path> </g></svg>
</span>
</button>
</>
)}
{loading && !topics.length ? (
<Loader />
) : error ? (
<div className="error-message p-4 bg-red-100 text-red-700 rounded">Error loading topics: {error}</div>
) : (
<>
<TopicItems topics={topics} title="SoliconPin Topics" description={topicPageDesc} onSearch={handleSearch} searchTerm={searchTerm}/>
{pagination.last_page > 1 && (
<div className="flex flex-col justify-between items-center mt-8 gap-4">
<div className="text-sm text-gray-600">
Showing {(pagination.current_page - 1) * pagination.per_page + 1}-
{Math.min(pagination.current_page * pagination.per_page, pagination.total)} of {pagination.total} topics
{searchTerm && (
<span className="ml-2">matching "{searchTerm}"</span>
)}
</div>
<div className="flex items-center gap-2">
{
pagination.current_page > 1 && (
<>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(1)}
disabled={pagination.current_page <= 1}
className="hidden sm:inline-flex"
>
First
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(pagination.current_page - 1)}
disabled={pagination.current_page <= 1}
>
Previous
</Button>
</>
)
}
<div className="flex items-center gap-1">
{generatePageNumbers(pagination.current_page, pagination.last_page).map((page, i) => (
page === '...' ? (
<span key={i} className="px-2">...</span>
) : (
<Button
key={i}
variant={page === pagination.current_page ? "default" : "outline"}
onClick={() => handlePageChange(page)}
className="min-w-10"
>
{page}
</Button>
)
))}
</div>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(pagination.current_page + 1)}
disabled={pagination.current_page >= pagination.last_page}
>
Next
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(pagination.last_page)}
disabled={pagination.current_page >= pagination.last_page}
className="hidden sm:inline-flex"
>
Last
</Button>
</div>
</div>
)}
</>
)}
</>
);
}
// Helper function to generate smart page numbers
function generatePageNumbers(currentPage, lastPage) {
const pages = [];
const maxVisible = 5; // Maximum visible page numbers
if (lastPage <= maxVisible) {
for (let i = 1; i <= lastPage; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
// Calculate start and end of middle pages
let start = Math.max(2, currentPage - 1);
let end = Math.min(lastPage - 1, currentPage + 1);
// Adjust if we're at the beginning
if (currentPage <= 3) {
end = maxVisible - 2;
}
// Adjust if we're at the end
if (currentPage >= lastPage - 2) {
start = lastPage - (maxVisible - 2);
}
// Add ellipsis if needed
if (start > 2) {
pages.push('...');
}
// Add middle pages
for (let i = start; i <= end; i++) {
pages.push(i);
}
// Add ellipsis if needed
if (end < lastPage - 1) {
pages.push('...');
}
// Always show last page
pages.push(lastPage);
}
return pages;
}

View File

@ -1,372 +0,0 @@
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Separator } from "./ui/separator";
import { Textarea } from "./ui/textarea";
import React, { useState, useEffect } from 'react';
import {AvatarUpload} from './AvatarUpload';
import {localizeTime} from "../lib/localizeTime";
import Loader from "./ui/loader";
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search } from "lucide-react";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../lib/InvoicePDF";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import PasswordUpdateCard from './PasswordUpdateCard';
interface SessionData {
[key: string]: any;
}
interface UserData {
success: boolean;
session_data: SessionData;
user_avatar: string;
}
interface BillingItems {
[key: string]: any;
}
type ToastState = {
visible: boolean;
message: string;
};
type Invoice = {
billing_id: string;
};
type PaymentData = {
txn_id: string;
user_email: string;
};
export default function ProfilePage() {
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
const typedSessionData = sessionData as SessionData | null;
const [userData, setUserData] = useState<UserData | null>(null);
const [invoiceList, setInvoiceList] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedData, setSelectedData] = useState<BillingItems | null>(null);
const [toast, setToast] = useState<ToastState>({ visible: false, message: '' });
const [txnId, setTxnId] = useState<string>('');
const [userEmail, setUserEmail] = useState<string>('');
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/invoice/';
useEffect(() => {
const fetchSessionData = async () => {
try {
const response = await fetch(`${USER_API_URL}?query=get-user`, {
credentials: 'include', // Crucial for cookies
headers: { 'Accept': 'application/json' }
}
);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || 'Session fetch failed');
}
setUserData(data);
return data.session_data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
};
const getInvoiceListData = async () => {
try {
const response = await fetch(`${USER_API_URL}?query=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;
}
};
fetchSessionData();
getInvoiceListData();
}, []);
const showToast = (message: string) => {
setToast({ visible: true, message });
setTimeout(() => setToast({ visible: false, message: '' }), 3000);
};
const handlePanelBuyNow = (invoice: Invoice) => {
// setTxnId(data.txn_id);
// setUserEmail(data.user_email);
window.location.href = `/make-payment?query=get-initiated_payment&orderId=${invoice.billing_id}`;
showToast('Redirecting to payment page...');
};
const handleViewItems = (items: BillingItems) => {
setDialogOpen(true);
setSelectedData(items);
};
const sessionLogOut = () => {
fetch(`${USER_API_URL}?query=logout`, {
method: 'GET',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
if(data.success === true){
window.location.href = '/';
}
console.log('Logout Console', data.success);
})
}
if (loading) {
return <Loader />;
}
// Then handle not logged in state
if (!isLoggedIn) {
return (
<p className="text-center my-8">
You are not logged in! Please login first to access this page.{" "}
<a className="text-[#6d9e37]" href="/login">Click Here</a> to login
</p>
);
}
// Then handle error state
if (error) {
return <div>Error: {error}</div>;
}
// Then handle case where user data hasn't loaded yet
if (!userData) {
return <Loader />;
}
return (
<div className="space-y-6 container mx-auto">
<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-3xl">
<Card>
<CardHeader>
<CardTitle>Personal Information</CardTitle>
<CardDescription>
Update your personal information and avatar.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-16 w-16">
<AvatarImage src={userData.session_data?.user_avatar} />
<AvatarFallback>JP</AvatarFallback>
</Avatar>
{typedSessionData?.id && <AvatarUpload userId={typedSessionData.id} />}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="firstName">Full Name</Label>
<Input id="firstName" defaultValue={userData.session_data?.user_name || 'Jhon'} />
</div>
<div className="space-y-2">
<Label htmlFor="phone">Phone</Label>
<Input id="phone" defaultValue={userData.session_data?.user_vatar || ''} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
</div>
</CardContent>
</Card>
<Card className="mt-6">
<CardHeader>
<div className="flex flex-row justify-between">
<CardTitle>Billing Information</CardTitle>
<a href="/profile/billing-info" className="hover:bg-[#6d9e37] hover:text-white transtion duration-500 py-1 px-2 rounded">View All</a>
</div>
<CardDescription>
View your billing history.
</CardDescription>
<table className="w-full">
<thead>
<tr>
<th className="text-left">Order ID</th>
<th className="text-center">Invoice Date</th>
<th className="text-left">Description</th>
<th className="text-center">Amount</th>
<th className="text-center">Status</th>
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{
invoiceList.slice(0, 5).map((invoice, index) => (
<tr key={index} className="">
<td>{invoice.billing_id}</td>
<td className="text-center">{invoice?.created_at.split(' ')[0]}</td>
<td className=""><p className="line-clamp-1">{invoice.service}</p></td>
<td className="text-center">{invoice.amount}</td>
<td className={`text-center text-sm rounded-full h-fit ${invoice.status === 'pending' ? 'text-yellow-500' : invoice.status === 'completed' ? 'text-green-500' : 'text-red-500'}`}>{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}</td>
<td className="text-center flex justify-center items-center gap-2 p-2">
<button onClick={() => handleViewItems(invoice)}>
<Eye />
</button>
<PDFDownloadLink
title="Download PDF"
document={<InvoicePDF data={invoice} />}
fileName={`invoice_${invoice.billing_id}.pdf`}
className="text-[#6d9e37] hover:text-green-600"
>
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
</PDFDownloadLink>
{
invoice.status !== 'completed' ? (
<Button onClick={() => {handlePanelBuyNow(invoice)}} variant="outline" size="sm">
{
invoice.status === 'pending' ? 'Pay' : invoice.status === 'failed' ? 'Retry' : ''
}
</Button>
) : ''
}
</td>
</tr>
))
}
</tbody>
</table>
</CardHeader>
</Card>
</div>
<div className="flex-1 space-y-6 lg:max-w-xl">
<Card className="">
<CardHeader>
<div className="flex flex-row justify-between">
<CardTitle>My Services</CardTitle>
{/* <a href="/profile/billing-info" className="hover:bg-[#6d9e37] hover:text-white transtion duration-500 py-1 px-2 rounded">View All</a> */}
</div>
<CardDescription>
View your all Services.
</CardDescription>
<table className="w-full">
<thead>
<tr>
<th className="text-left">Order ID</th>
<th className="text-center">Date</th>
<th className="text-left">Description</th>
<th className="text-center">Status</th>
{/* <th className="text-center">Action</th> */}
</tr>
</thead>
<tbody>
{
invoiceList.slice(0, 5).map((invoice, index) => (
<tr key={index} className="">
<td>{invoice.billing_id}</td>
<td className="text-center">{invoice?.created_at.split(' ')[0]}</td>
<td className=""><p className="line-clamp-1">{invoice.service}</p></td>
<td className={`text-center text-sm rounded-full h-fit ${invoice.service_status === '0' ? 'text-yellow-500' : invoice.service_status === '1' ? 'text-green-500' : 'text-red-500'}`}>{invoice.service_status === '1' ? 'Active' : invoice.service_status === '0' ? 'Unactive' : ''}</td>
</tr>
))
}
</tbody>
</table>
</CardHeader>
</Card>
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut} />
<Card className="mt-6">
<CardHeader>
<CardTitle>Danger Zone</CardTitle>
<CardDescription>These actions are irreversible. Proceed with caution.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-4">
<Button onClick={sessionLogOut} className="bg-red-500 hover:bg-red-600">Logout</Button>
<Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
<Button variant="outline">Export data</Button>
</div>
</CardContent>
</Card>
</div>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="">Billing Details</DialogTitle>
<DialogDescription className="">Review the details for Billing ID: <span className="font-bold">{selectedData?.billing_id}</span></DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4 text-sm text-gray-700">
<div className="flex justify-between">
<span className="font-bold">Service:</span>
<span>{selectedData?.service}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">cycle:</span>
<span className="capitalize">{selectedData?.cycle}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Amount:</span>
<span>&#8377;{selectedData?.amount}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">User:</span>
<span>{selectedData?.user}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Status:</span>
<span className={`px-2 py-0.5 rounded text-white text-xs ${selectedData?.status === 'pending' ? 'bg-yellow-500' : selectedData?.status === 'completed' ? 'bg-green-500' : 'bg-red-500'}`}>
{selectedData?.status}
</span>
</div>
<hr className="my-2 border-gray-200" />
<div className="flex justify-between">
<span className="font-bold">Created At:</span>
<span>{localizeTime(selectedData?.created_at)}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Updated At:</span>
<span>{selectedData?.updated_at ? localizeTime(selectedData.updated_at) : '—'}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Silicon ID:</span>
<span>{selectedData?.siliconId}</span>
</div>
</div>
<DialogFooter className="mt-6">
{/* Optional: Add action buttons here */}
{/* <Button onClick={() => setDialogOpen(false)}>Close</Button> */}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,43 +0,0 @@
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>
);
}

View File

@ -1,57 +0,0 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Avatar = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement> & {
size?: "sm" | "md" | "lg"
}
>(({ className, size = "md", ...props }, ref) => {
const sizeClasses = {
sm: "h-8 w-8",
md: "h-10 w-10",
lg: "h-12 w-12",
};
return (
<span
ref={ref}
className={cn(
"relative flex shrink-0 overflow-hidden rounded-full",
sizeClasses[size],
className
)}
{...props}
/>
)
})
Avatar.displayName = "Avatar"
const AvatarImage = React.forwardRef<
HTMLImageElement,
React.ImgHTMLAttributes<HTMLImageElement>
>(({ className, ...props }, ref) => (
<img
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = "AvatarImage"
const AvatarFallback = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = "AvatarFallback"
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -1,40 +0,0 @@
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 };

View File

@ -5,14 +5,12 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "outline";
size?: "default" | "sm" | "lg";
type?: "submit" | "button";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", type, size = "default", ...props }, ref) => {
({ className, variant = "default", size = "default", ...props }, ref) => {
return (
<button
type={type}
className={cn(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-neutral-900",
{

View File

@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border border-[#6d9e37] bg-neutral-800 text-[#6d9e37] shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
"rounded-lg border border-neutral-200 bg-white text-neutral-950 shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className
)}
{...props}

View File

@ -1,16 +0,0 @@
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;

View File

@ -1,104 +0,0 @@
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 };

View File

@ -1,122 +0,0 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "../../lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-[#fff] p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-[#fff] transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4 text-black" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight text-[#6d9e37]",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-neutral-400", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -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-[#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-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
@ -22,4 +22,3 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
Input.displayName = "Input";
export { Input };

View File

@ -1,80 +0,0 @@
import React, { useEffect } from 'react';
const Loader = () => {
// CSS styles object
const styles = {
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
width: '100%',
},
loader: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginBottom: '16px',
},
dot: {
width: '12px',
height: '12px',
margin: '0 4px',
backgroundColor: '#6d9e37',
borderRadius: '50%',
display: 'inline-block',
animation: 'bounce 1.4s infinite ease-in-out both',
},
dot1: {
animationDelay: '-0.32s',
},
dot2: {
animationDelay: '-0.16s',
},
text: {
color: '#6d9e37',
fontSize: '1rem',
fontWeight: '500',
},
// Keyframes as a string to be injected
keyframes: `
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}
`,
};
useEffect(() => {
// Only run on client-side where document is available
if (typeof document !== 'undefined') {
const styleSheet = document.styleSheets[0];
try {
styleSheet.insertRule(styles.keyframes, styleSheet.cssRules.length);
} catch (e) {
// Fallback for browsers that might not support this
const styleElement = document.createElement('style');
styleElement.innerHTML = styles.keyframes;
document.head.appendChild(styleElement);
}
}
}, []); // Empty dependency array means this runs once on mount
return (
<div style={styles.container}>
<div style={styles.loader}>
<div style={{ ...styles.dot, ...styles.dot1 }}></div>
<div style={{ ...styles.dot, ...styles.dot2 }}></div>
<div style={styles.dot}></div>
</div>
<p style={styles.text}>Loading...</p>
</div>
);
};
export default Loader;

View File

@ -1,137 +0,0 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "../../lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef(
({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm",
"focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-900",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef(
({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-600 bg-neutral-800 shadow-md animate-in fade-in-80",
position === "popper" && "translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef(
({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
)
);
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef(
({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
"focus:bg-neutral-700 focus:text-white",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef(
({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-neutral-600", className)}
{...props}
/>
)
);
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
const ChevronDownIcon = React.forwardRef((props, ref) => (
<svg
ref={ref}
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
));
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};
export {
Select as SelectProps,
SelectGroup as SelectGroupProps,
SelectValue as SelectValueProps,
SelectTrigger as SelectTriggerProps,
SelectContent as SelectContentProps,
SelectLabel as SelectLabelProps,
SelectItem as SelectItemProps,
SelectSeparator as SelectSeparatorProps,
};

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface SelectProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<select
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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
>
{children}
</select>
);
}
);
Select.displayName = "Select";
export { Select };

View File

@ -1,23 +0,0 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Separator = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
orientation?: "horizontal" | "vertical"
}
>(({ className, orientation = "horizontal", ...props }, ref) => (
<div
ref={ref}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
role="separator"
{...props}
/>
))
Separator.displayName = "Separator"
export { Separator }

View File

@ -1,29 +0,0 @@
import React from "react";
export const Switch = ({
checked,
onCheckedChange,
id,
className = "",
...props
}) => {
return (
<button
type="button"
role="switch"
aria-checked={checked}
id={id}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#6d9e37] ${
checked ? "bg-[#6d9e37]" : "bg-gray-200"
} ${className}`}
onClick={() => onCheckedChange(!checked)}
{...props}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
);
};

View File

@ -1,62 +0,0 @@
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;

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