Compare commits
33 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
9cbe98eff5 | |
|
dffdbc6e40 | |
|
8f8c5f0d65 | |
![]() |
0438c30c97 | |
|
0e030f0ce2 | |
|
5cd02bfbde | |
|
4b6e4af86d | |
![]() |
b58068d108 | |
|
8b75fa057d | |
![]() |
c927fd6087 | |
![]() |
f9d1556ce9 | |
|
1ed908b12e | |
|
ffae4acebd | |
|
daa4702904 | |
![]() |
c3faedf6f8 | |
|
a9eabcb683 | |
|
66bd9ec890 | |
|
9d3ef1a643 | |
|
1661f7859d | |
|
026c6c34b2 | |
![]() |
beabcc7b67 | |
|
734fed06b8 | |
![]() |
d2f3576d10 | |
|
63c973f1ca | |
|
5c50e77a1c | |
![]() |
4e939b87fe | |
|
f7561801b7 | |
|
e8838d0277 | |
|
c9fc4ec8f9 | |
|
4f58b87f97 | |
|
937882fba2 | |
![]() |
6907bf2852 | |
|
fdad72889c |
|
@ -5,7 +5,7 @@ dist/
|
|||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
public/host-api/
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
14
README.md
14
README.md
|
@ -0,0 +1,14 @@
|
|||
### 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
|
|
@ -10,14 +10,23 @@
|
|||
"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-toast": "^1.2.6",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@types/react": "^19.0.12",
|
||||
"astro": "^5.5.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.484.0",
|
||||
"pocketbase": "^0.25.2",
|
||||
"postcss": "^8.5.3",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-to-print": "^3.0.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17"
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
@ -842,6 +851,44 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
|
@ -1359,6 +1406,577 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
|
||||
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
|
||||
"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
||||
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
|
||||
"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
|
||||
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
||||
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-rect": "1.1.0",
|
||||
"@radix-ui/react-use-size": "1.1.0",
|
||||
"@radix-ui/rect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
|
||||
"integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.2",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||
"integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
|
||||
"integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
|
||||
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
|
||||
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||
|
@ -1830,11 +2448,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.11.tgz",
|
||||
"integrity": "sha512-vrdxRZfo9ALXth6yPfV16PYTLZwsUWhVjjC+DkfE5t1suNSbBrWC9YqSuuxJZ8Ps6z1o2ycRpIqzZJIgklq4Tw==",
|
||||
"version": "19.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
|
||||
"integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
|
@ -2009,6 +2626,18 @@
|
|||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
|
||||
"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
|
@ -2639,8 +3268,7 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
|
@ -2724,6 +3352,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deterministic-object-hash": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz",
|
||||
|
@ -3138,6 +3772,15 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
|
@ -3807,6 +4450,15 @@
|
|||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.484.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.484.0.tgz",
|
||||
"integrity": "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
|
@ -5097,6 +5749,12 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pocketbase": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.25.2.tgz",
|
||||
"integrity": "sha512-ONZl1+qHJMnhR2uacBlBJ90lm7njtL/zy0606+1ROfK9hSL4LRBRc8r89rMcNRzPzRqCNyoFTh2Qg/lYXdEC1w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
|
@ -5339,6 +5997,133 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
||||
"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.1.tgz",
|
||||
"integrity": "sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.1.tgz",
|
||||
"integrity": "sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-to-print": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.5.tgz",
|
||||
"integrity": "sha512-Z15MwMOzYCHWi26CZeFNwflAg7Nr8uWD6FTj+EkfIOjYyjr0MXGbI0c7rF4Fgrbj3XG9hFndb1ourxpPz2RAiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -5773,6 +6558,12 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
|
@ -6139,6 +6930,15 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
|
||||
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
|
@ -6304,8 +7104,13 @@
|
|||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "4.37.0",
|
||||
|
@ -6624,6 +7429,49 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-node-es": "^1.1.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
14
package.json
14
package.json
|
@ -6,19 +6,29 @@
|
|||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
"astro": "astro",
|
||||
"push-s33": "rsync -rv --exclude .hta_config/conf.php dist/ dev2@siliconpin.s33.siliconpin.com:/home/dev2/domains/siliconpin.s33.siliconpin.com/public_html/"
|
||||
},
|
||||
"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-toast": "^1.2.6",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@types/react": "^19.0.12",
|
||||
"astro": "^5.5.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.484.0",
|
||||
"pocketbase": "^0.25.2",
|
||||
"postcss": "^8.5.3",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-to-print": "^3.0.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17"
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
RewriteEngine On
|
||||
#RewriteCond %{HTTPS} !=on
|
||||
#RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,NE]
|
||||
#RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||
#RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*) data-not-updated-or-not-found
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"csaf_version": "2.0",
|
||||
"provider": {
|
||||
"name": "DWD Consultancy Services",
|
||||
"namespace": "com.siliconpin.dwd",
|
||||
"contact_details": [
|
||||
{
|
||||
"name": "Suvankar Sarkar",
|
||||
"email": "suvankar@siliconpin.com",
|
||||
"phone": "+91-700-160-1485",
|
||||
"website": "https://dwd.siliconpin.com/#about"
|
||||
}
|
||||
],
|
||||
"publisher": {
|
||||
"type": "vendor",
|
||||
"name": "SiliconPin",
|
||||
"namespace": "com.siliconpin"
|
||||
},
|
||||
"distribution": {
|
||||
"tlp_label": "WHITE",
|
||||
"url": "https://dwd.siliconpin.com/#about",
|
||||
"pgp_key": "https://siliconpin.com/pgp-key.asc"
|
||||
},
|
||||
"tracking": {
|
||||
"id": "EX-2025-0001",
|
||||
"status": "final",
|
||||
"initial_release_date": "2025-03-27T12:00:00Z",
|
||||
"current_release_date": "2025-03-27T12:00:00Z",
|
||||
"revision_history": [
|
||||
{
|
||||
"number": "1.0",
|
||||
"date": "2025-03-27T12:00:00Z",
|
||||
"summary": "Initial release"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Contact: mailto:suvankar@siliconpin.com
|
||||
Contact: https://siliconpin.com/contact/
|
||||
Expires: 2027-03-26T18:30:00.000Z
|
||||
Encryption: https://siliconpin.com/pgp-key.txt
|
||||
Acknowledgments: https://dwd.siliconpin.com
|
||||
Preferred-Languages: en,bn
|
||||
CSAF: https://siliconpin.com/.well-known/csaf/provider-metadata.json
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"csaf_version": "2.0",
|
||||
"provider": {
|
||||
"name": "DWD Consultancy Services",
|
||||
"namespace": "com.siliconpin.dwd",
|
||||
"contact_details": [
|
||||
{
|
||||
"name": "Suvankar Sarkar",
|
||||
"email": "suvankar@siliconpin.com",
|
||||
"phone": "+91-700-160-1485",
|
||||
"website": "https://dwd.siliconpin.com/#about"
|
||||
}
|
||||
],
|
||||
"publisher": {
|
||||
"type": "vendor",
|
||||
"name": "SiliconPin",
|
||||
"namespace": "com.siliconpin"
|
||||
},
|
||||
"distribution": {
|
||||
"tlp_label": "WHITE",
|
||||
"url": "https://dwd.siliconpin.com/#about",
|
||||
"pgp_key": "https://siliconpin.com/pgp-key.asc"
|
||||
},
|
||||
"tracking": {
|
||||
"id": "EX-2025-0001",
|
||||
"status": "final",
|
||||
"initial_release_date": "2025-03-27T12:00:00Z",
|
||||
"current_release_date": "2025-03-27T12:00:00Z",
|
||||
"revision_history": [
|
||||
{
|
||||
"number": "1.0",
|
||||
"date": "2025-03-27T12:00:00Z",
|
||||
"summary": "Initial release"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCAAdFiEEL0GUnJpZXELqjphXMaCR/T68rscFAmflVEgACgkQMaCR/T68
|
||||
rse45Av6A0YL6qJSBvqN6tty7j0G68yfgsjeJxtuCxXiDwUjUIU4VwHORNeddZD/
|
||||
Lx5nmYf4BgszoFjgaix8dyu87bDn2QaA13+2/GMDkc3GT8yW8bzaR/saV1J1v070
|
||||
wVhButTIM48c4NpuqFX5Kytcvm632lZdeKBFF2rFfdZ3ajd7IJMr7CYVzziJdVa8
|
||||
pR00udgBYE05ewV9W8FLMmTwWqoOIQYtYR3+YP1rhuYInZ/7dobO5aI4/eliHIBo
|
||||
xe93UET9Zo19ywbnqhas+x4wJl8roI2qWDYsjp3+gRG4Ns5Myf2puhtVxexiY4xK
|
||||
6cvra2ujOFLksykPgy5lpw2WcpFFnm9K2s9NMvLAsv+3OXZHQANL7vS5ZFXxNxyU
|
||||
fm+F5uP8LiR1IXpFcJAP3MweaDdlWhL7TZp1tuA0J/i/oCFShZypzQLb9welYVbH
|
||||
a/CPm1pavGxEIlFkSOAlMmcp945s41qYia2BFOlW1ZjsDkrAR2561aLI/O7klpNw
|
||||
VLnngQsi
|
||||
=s4sX
|
||||
-----END PGP SIGNATURE-----
|
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 909 B |
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 901 B |
Binary file not shown.
After Width: | Height: | Size: 191 KiB |
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
echo 'Data not updated or the page not found!';
|
||||
|
||||
?>
|
|
@ -0,0 +1 @@
|
|||
SiliconPin is a Decentralized NonProfit Organization / Group, Creating some digital freedom, if you want to join -welcome.
|
|
@ -0,0 +1,48 @@
|
|||
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.
|
||||
.....................................................
|
|
@ -0,0 +1,41 @@
|
|||
-----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-----
|
|
@ -0,0 +1,41 @@
|
|||
-----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-----
|
|
@ -0,0 +1,7 @@
|
|||
User-agent: *
|
||||
Disallow: /secret-location/
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://siliconpin.com/sitemap.xml
|
|
@ -0,0 +1,7 @@
|
|||
Contact: mailto:suvankar@siliconpin.com
|
||||
Contact: https://siliconpin.com/contact/
|
||||
Expires: 2027-03-26T18:30:00.000Z
|
||||
Encryption: https://siliconpin.com/pgp-key.txt
|
||||
Acknowledgments: https://dwd.siliconpin.com
|
||||
Preferred-Languages: en,bn
|
||||
CSAF: https://siliconpin.com/.well-known/csaf/provider-metadata.json
|
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
||||
|
||||
|
||||
<url>
|
||||
<loc>https://siliconpin.com/</loc>
|
||||
<lastmod>2025-03-27T14:20:53+00:00</lastmod>
|
||||
<priority>1.00</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/services/</loc>
|
||||
<lastmod>2025-03-27T14:20:54+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/contact/</loc>
|
||||
<lastmod>2025-03-27T14:20:55+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/get-started/</loc>
|
||||
<lastmod>2025-03-27T14:20:56+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/about-us/</loc>
|
||||
<lastmod>2025-03-27T14:20:57+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/suggestion-or-report/</loc>
|
||||
<lastmod>2025-03-27T14:20:58+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/hire-developer/</loc>
|
||||
<lastmod>2025-03-27T14:21:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/hire-ai-agent/</loc>
|
||||
<lastmod>2025-03-27T14:21:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/privacy-policy/</loc>
|
||||
<lastmod>2025-03-27T14:21:02+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/terms-and-conditions/</loc>
|
||||
<lastmod>2025-03-27T14:21:03+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/refund-policy/</loc>
|
||||
<lastmod>2025-03-27T14:21:04+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/legal-agreement/</loc>
|
||||
<lastmod>2025-03-27T14:21:05+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
|
||||
|
||||
</urlset>
|
|
@ -1,713 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Toast } from './Toast';
|
||||
import { TemplatePreview } from './TemplatePreview';
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
// Form validation
|
||||
const [formValid, setFormValid] = useState(true);
|
||||
|
||||
// Toast notification
|
||||
const [toast, setToast] = useState({ visible: false, message: '' });
|
||||
|
||||
// File upload reference
|
||||
const fileInputRef = React.useRef(null);
|
||||
|
||||
// Function to clean domain input
|
||||
const cleanDomainInput = (input) => {
|
||||
// Remove http://, https://, www., and trailing slashes
|
||||
return input
|
||||
.replace(/^(https?:\/\/)?(www\.)?/i, '')
|
||||
.replace(/\/+$/, '')
|
||||
.trim();
|
||||
};
|
||||
|
||||
// Effect for handling domain type changes
|
||||
useEffect(() => {
|
||||
if (!useCustomDomain) {
|
||||
setShowDnsConfig(false);
|
||||
setIsValidDomain(false);
|
||||
setValidationMessage('');
|
||||
setIsValidating(false);
|
||||
}
|
||||
|
||||
validateForm();
|
||||
}, [useCustomDomain, dnsVerified.cname, 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
|
||||
});
|
||||
} 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 = cleanDomainInput(e.target.value);
|
||||
setCustomDomain(cleanedValue);
|
||||
};
|
||||
|
||||
const handleSubdomainChange = (e) => {
|
||||
const cleanedValue = cleanDomainInput(e.target.value);
|
||||
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 = () => {
|
||||
const domain = domainType === 'domain' ? customDomain : customSubdomain;
|
||||
|
||||
if (!domain) {
|
||||
setValidationMessage('Please enter a domain name.');
|
||||
setIsValidDomain(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial validation: check format with regex
|
||||
const validFormat = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(domain);
|
||||
if (!validFormat) {
|
||||
setValidationMessage('Domain format is invalid. Please check your entry.');
|
||||
setIsValidDomain(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
setValidationMessage('');
|
||||
setShowDnsConfig(false);
|
||||
|
||||
// Simulate an API call to validate the domain
|
||||
setTimeout(() => {
|
||||
// Simulate a real domain check - in a real app this would be an API call
|
||||
const checkResult = true; // Assume domain is valid for demo
|
||||
|
||||
setIsValidating(false);
|
||||
setIsValidDomain(checkResult);
|
||||
|
||||
if (checkResult) {
|
||||
setValidationMessage('Domain is valid and registered.');
|
||||
setShowDnsConfig(true);
|
||||
} else {
|
||||
setValidationMessage('Domain appears to be unregistered or unavailable.');
|
||||
}
|
||||
|
||||
validateForm();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// Check DNS configuration
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 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"
|
||||
/>
|
||||
<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"
|
||||
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"
|
||||
checked={useCustomDomain}
|
||||
onChange={handleUseCustomDomainChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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
|
||||
type="button"
|
||||
onClick={validateDomain}
|
||||
className="px-4 py-2 bg-neutral-600 text-white font-medium rounded-md hover:bg-neutral-500 transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-500"
|
||||
>
|
||||
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 border border-neutral-600 space-y-3">
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-cname"
|
||||
name="dns-method"
|
||||
value="cname"
|
||||
checked={dnsMethod === 'cname'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<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={() => checkDnsConfig('cname')}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nameserver Option (only for full domains, not subdomains) */}
|
||||
{domainType === 'domain' && (
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-ns"
|
||||
name="dns-method"
|
||||
value="ns"
|
||||
checked={dnsMethod === 'ns'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Component for app deployment options
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.appType - Selected app type
|
||||
* @param {Function} props.onAppTypeChange - Handler for app type change
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const AppDeployment = ({ appType, onAppTypeChange }) => {
|
||||
return (
|
||||
<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={onAppTypeChange}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDeployment;
|
|
@ -0,0 +1,79 @@
|
|||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Component for source code deployment options
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.sourceType - Source type (public/private)
|
||||
* @param {string} props.repoUrl - Repository URL
|
||||
* @param {string} props.deploymentKey - Deployment key for private repos
|
||||
* @param {Function} props.onSourceTypeChange - Handler for source type change
|
||||
* @param {Function} props.onRepoUrlChange - Handler for repo URL change
|
||||
* @param {Function} props.onDeploymentKeyChange - Handler for deployment key change
|
||||
* @param {Function} props.showToast - Function to show toast notifications
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const SourceDeployment = ({
|
||||
sourceType,
|
||||
repoUrl,
|
||||
deploymentKey,
|
||||
onSourceTypeChange,
|
||||
onRepoUrlChange,
|
||||
onDeploymentKeyChange,
|
||||
showToast
|
||||
}) => {
|
||||
return (
|
||||
<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={onSourceTypeChange}
|
||||
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={onRepoUrlChange}
|
||||
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={onDeploymentKeyChange}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourceDeployment;
|
|
@ -0,0 +1,53 @@
|
|||
import React, { useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Component for static site deployment options
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.fileName - Selected file name
|
||||
* @param {Function} props.onFileChange - Handler for file change
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const StaticDeployment = ({ fileName, onFileChange }) => {
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<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={onFileChange}
|
||||
className="hidden"
|
||||
accept=".zip,.tar"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{fileName && (
|
||||
<div className="mt-2 text-sm text-neutral-400">
|
||||
Selected file: {fileName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StaticDeployment;
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Component for sample web app deployment options
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.templateType - Selected template type
|
||||
* @param {Function} props.onTemplateTypeChange - Handler for template type change
|
||||
* @param {React.Component} props.TemplatePreview - Template preview component
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const TemplateDeployment = ({ templateType, onTemplateTypeChange, TemplatePreview }) => {
|
||||
return (
|
||||
<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={templateType}
|
||||
onChange={onTemplateTypeChange}
|
||||
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>
|
||||
|
||||
{/* Information box */}
|
||||
<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={templateType} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateDeployment;
|
|
@ -0,0 +1,98 @@
|
|||
import React from 'react';
|
||||
import AppDeployment from './AppDeployment';
|
||||
import SourceDeployment from './SourceDeployment';
|
||||
import StaticDeployment from './StaticDeployment';
|
||||
import TemplateDeployment from './TemplateDeployment';
|
||||
|
||||
/**
|
||||
* Main component for deployment options
|
||||
* @param {Object} props - Component props
|
||||
* @param {Object} props.deploymentConfig - Deployment configuration state
|
||||
* @param {Object} props.handlers - Event handlers for deployment options
|
||||
* @param {Function} props.showToast - Function to show toast notifications
|
||||
* @param {React.Component} props.TemplatePreview - Template preview component
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const DeploymentOptions = ({
|
||||
deploymentConfig,
|
||||
handlers,
|
||||
showToast,
|
||||
TemplatePreview
|
||||
}) => {
|
||||
const {
|
||||
type,
|
||||
appType,
|
||||
sampleWebAppType,
|
||||
sourceType,
|
||||
repoUrl,
|
||||
deploymentKey,
|
||||
fileName
|
||||
} = deploymentConfig;
|
||||
|
||||
const {
|
||||
handleDeploymentTypeChange,
|
||||
handleAppTypeChange,
|
||||
handleSampleWebAppTypeChange,
|
||||
handleSourceTypeChange,
|
||||
handleRepoUrlChange,
|
||||
handleDeploymentKeyChange,
|
||||
handleFileChange
|
||||
} = handlers;
|
||||
|
||||
return (
|
||||
<div 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={type}
|
||||
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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Render different components based on deployment type */}
|
||||
{type === 'app' && (
|
||||
<AppDeployment
|
||||
appType={appType}
|
||||
onAppTypeChange={handleAppTypeChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'sample-web-app' && (
|
||||
<TemplateDeployment
|
||||
templateType={sampleWebAppType}
|
||||
onTemplateTypeChange={handleSampleWebAppTypeChange}
|
||||
TemplatePreview={TemplatePreview}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'source' && (
|
||||
<SourceDeployment
|
||||
sourceType={sourceType}
|
||||
repoUrl={repoUrl}
|
||||
deploymentKey={deploymentKey}
|
||||
onSourceTypeChange={handleSourceTypeChange}
|
||||
onRepoUrlChange={handleRepoUrlChange}
|
||||
onDeploymentKeyChange={handleDeploymentKeyChange}
|
||||
showToast={showToast}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'static' && (
|
||||
<StaticDeployment
|
||||
fileName={fileName}
|
||||
onFileChange={handleFileChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeploymentOptions;
|
|
@ -0,0 +1,155 @@
|
|||
import React from 'react';
|
||||
import DnsConfiguration from './DnsConfiguration';
|
||||
|
||||
/**
|
||||
* Component for custom domain configuration
|
||||
* @param {Object} props - Component props
|
||||
* @param {Object} props.domainConfig - Domain configuration state
|
||||
* @param {Object} props.validation - Domain validation state
|
||||
* @param {Object} props.dnsVerified - DNS verification state
|
||||
* @param {Object} props.handlers - Event handlers for domain config
|
||||
* @param {Function} props.checkDnsConfig - Function to check DNS configuration
|
||||
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||
* @param {Function} props.showToast - Function to show toast notifications
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const CustomDomain = ({
|
||||
domainConfig,
|
||||
validation,
|
||||
dnsVerified,
|
||||
handlers,
|
||||
checkDnsConfig,
|
||||
defaultSubdomain,
|
||||
showToast
|
||||
}) => {
|
||||
const {
|
||||
domainType,
|
||||
customDomain,
|
||||
customSubdomain,
|
||||
dnsMethod
|
||||
} = domainConfig;
|
||||
|
||||
const {
|
||||
isValidating,
|
||||
isValidDomain,
|
||||
validationMessage,
|
||||
showDnsConfig
|
||||
} = validation;
|
||||
|
||||
const {
|
||||
handleDomainTypeChange,
|
||||
handleDomainChange,
|
||||
handleSubdomainChange,
|
||||
handleDnsMethodChange,
|
||||
validateDomain
|
||||
} = handlers;
|
||||
|
||||
return (
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
aria-label="Enter root domain"
|
||||
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"
|
||||
aria-label="Enter subdomain"
|
||||
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
|
||||
type="button"
|
||||
onClick={validateDomain}
|
||||
disabled={isValidating}
|
||||
className="px-4 py-2 bg-neutral-600 text-white font-medium rounded-md hover:bg-neutral-500 transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isValidating ? 'Validating...' : 'Validate Domain'}
|
||||
</button>
|
||||
|
||||
{/* Validation Status */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{!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 && (
|
||||
<DnsConfiguration
|
||||
domainType={domainType}
|
||||
dnsMethod={dnsMethod}
|
||||
defaultSubdomain={defaultSubdomain}
|
||||
dnsVerified={dnsVerified}
|
||||
onDnsMethodChange={handleDnsMethodChange}
|
||||
checkDnsConfig={checkDnsConfig}
|
||||
showToast={showToast}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomDomain;
|
|
@ -0,0 +1,161 @@
|
|||
import React from 'react';
|
||||
import { copyToClipboard } from '../../../utils/domainUtils';
|
||||
|
||||
/**
|
||||
* Component for DNS configuration options
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.domainType - Domain type ('domain' or 'subdomain')
|
||||
* @param {string} props.dnsMethod - DNS method ('cname' or 'ns')
|
||||
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||
* @param {Object} props.dnsVerified - DNS verification state
|
||||
* @param {Function} props.onDnsMethodChange - Handler for DNS method change
|
||||
* @param {Function} props.checkDnsConfig - Function to check DNS configuration
|
||||
* @param {Function} props.showToast - Function to show toast notifications
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const DnsConfiguration = ({
|
||||
domainType,
|
||||
dnsMethod,
|
||||
defaultSubdomain,
|
||||
dnsVerified,
|
||||
onDnsMethodChange,
|
||||
checkDnsConfig,
|
||||
showToast
|
||||
}) => {
|
||||
// Handle copy to clipboard with toast feedback
|
||||
const handleCopyToClipboard = async (text) => {
|
||||
const result = await copyToClipboard(text);
|
||||
if (result.success) {
|
||||
showToast('Copied to clipboard!');
|
||||
} else {
|
||||
showToast(`Failed to copy: ${result.error}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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 border border-neutral-600 space-y-3">
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-cname"
|
||||
name="dns-method"
|
||||
value="cname"
|
||||
checked={dnsMethod === 'cname'}
|
||||
onChange={onDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<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={() => handleCopyToClipboard(`${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={() => checkDnsConfig('cname')}
|
||||
disabled={dnsVerified.cname === 'checking'}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsVerified.cname === true
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: dnsVerified.cname === 'checking'
|
||||
? 'bg-neutral-500 cursor-not-allowed'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
{dnsVerified.cname === true
|
||||
? '✓ CNAME Verified'
|
||||
: dnsVerified.cname === 'checking'
|
||||
? 'Checking...'
|
||||
: 'Check CNAME'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nameserver Option (only for full domains, not subdomains) */}
|
||||
{domainType === 'domain' && (
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-ns"
|
||||
name="dns-method"
|
||||
value="ns"
|
||||
checked={dnsMethod === 'ns'}
|
||||
onChange={onDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<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={() => handleCopyToClipboard('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={() => handleCopyToClipboard('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')}
|
||||
disabled={dnsVerified.ns === 'checking'}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsVerified.ns === true
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: dnsVerified.ns === 'checking'
|
||||
? 'bg-neutral-500 cursor-not-allowed'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
{dnsVerified.ns === true
|
||||
? '✓ Nameservers Verified'
|
||||
: dnsVerified.ns === 'checking'
|
||||
? 'Checking...'
|
||||
: 'Check Nameservers'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DnsConfiguration;
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import CustomDomain from './CustomDomain';
|
||||
|
||||
/**
|
||||
* Main component for domain configuration
|
||||
* @param {Object} props - Component props
|
||||
* @param {Object} props.domainConfig - Domain configuration state
|
||||
* @param {Object} props.validation - Domain validation state
|
||||
* @param {Object} props.dnsVerified - DNS verification state
|
||||
* @param {Object} props.handlers - Event handlers for domain config
|
||||
* @param {Function} props.checkDnsConfig - Function to check DNS config
|
||||
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||
* @param {Function} props.showToast - Function to show toast notifications
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
const DomainConfiguration = ({
|
||||
domainConfig,
|
||||
validation,
|
||||
dnsVerified,
|
||||
handlers,
|
||||
checkDnsConfig,
|
||||
defaultSubdomain,
|
||||
showToast
|
||||
}) => {
|
||||
const { useSubdomain, useCustomDomain } = domainConfig;
|
||||
const {
|
||||
handleUseSubdomainChange,
|
||||
handleUseCustomDomainChange
|
||||
} = handlers;
|
||||
|
||||
return (
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
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
|
||||
aria-label="Default subdomain"
|
||||
/>
|
||||
<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"
|
||||
checked={useCustomDomain}
|
||||
onChange={handleUseCustomDomainChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="use-custom-domain" className="block text-white font-medium">Use Custom Domain</label>
|
||||
|
||||
{useCustomDomain && (
|
||||
<CustomDomain
|
||||
domainConfig={domainConfig}
|
||||
validation={validation}
|
||||
dnsVerified={dnsVerified}
|
||||
handlers={handlers}
|
||||
checkDnsConfig={checkDnsConfig}
|
||||
defaultSubdomain={defaultSubdomain}
|
||||
showToast={showToast}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainConfiguration;
|
|
@ -0,0 +1,136 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import DeploymentOptions from './DeploymentOptions';
|
||||
import DomainConfiguration from './DomainConfiguration';
|
||||
import { Toast } from '../shared/Toast';
|
||||
import { TemplatePreview } from '../shared/TemplatePreview';
|
||||
|
||||
// Custom hooks
|
||||
import useDeploymentConfig from '../../hooks/useDeploymentConfig';
|
||||
import useDomainConfig from '../../hooks/useDomainConfig';
|
||||
import useDnsVerification from '../../hooks/useDnsVerification';
|
||||
import useToast from '../../hooks/useToast';
|
||||
import useFormValidation from '../../hooks/useFormValidation';
|
||||
|
||||
/**
|
||||
* Main DomainSetupForm component
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||
* @returns {JSX.Element} - Rendered component
|
||||
*/
|
||||
export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
// Initialize hooks for different concerns
|
||||
const { toast, showToast } = useToast();
|
||||
|
||||
// Deployment configuration
|
||||
const deploymentConfig = useDeploymentConfig();
|
||||
|
||||
// DNS verification (depends on domain config for reset logic)
|
||||
const dnsVerificationHook = useDnsVerification(showToast);
|
||||
const { dnsVerified, checkDnsConfig, resetAllDnsVerification } = dnsVerificationHook;
|
||||
|
||||
// Domain configuration (needs DNS reset function)
|
||||
const domainConfig = useDomainConfig({}, resetAllDnsVerification);
|
||||
|
||||
// Pass the domain config to DNS verification hook for dependency tracking
|
||||
// This is done after initialization to avoid circular dependencies
|
||||
dnsVerificationHook.domainConfig = domainConfig.config;
|
||||
|
||||
// Form validation based on domain and DNS state
|
||||
const { formValid } = useFormValidation(
|
||||
domainConfig.config,
|
||||
domainConfig.validation,
|
||||
dnsVerified
|
||||
);
|
||||
|
||||
// Form submission handler
|
||||
const handleSubmit = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (domainConfig.useCustomDomain && !formValid) {
|
||||
showToast('Please complete domain validation and DNS verification before deploying.');
|
||||
return;
|
||||
}
|
||||
|
||||
// In a real app, this would submit the form data to the server
|
||||
console.log({
|
||||
deploymentType: deploymentConfig.type,
|
||||
appType: deploymentConfig.appType,
|
||||
sampleWebAppType: deploymentConfig.sampleWebAppType,
|
||||
sourceType: deploymentConfig.sourceType,
|
||||
repoUrl: deploymentConfig.repoUrl,
|
||||
deploymentKey: deploymentConfig.deploymentKey,
|
||||
useSubdomain: domainConfig.useSubdomain,
|
||||
useCustomDomain: domainConfig.useCustomDomain,
|
||||
customDomain: domainConfig.customDomain,
|
||||
customSubdomain: domainConfig.customSubdomain,
|
||||
domainType: domainConfig.domainType,
|
||||
dnsMethod: domainConfig.dnsMethod
|
||||
});
|
||||
|
||||
showToast('Form submitted successfully!');
|
||||
}, [
|
||||
formValid,
|
||||
showToast,
|
||||
deploymentConfig,
|
||||
domainConfig
|
||||
]);
|
||||
|
||||
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 Options Section */}
|
||||
<DeploymentOptions
|
||||
deploymentConfig={deploymentConfig}
|
||||
handlers={{
|
||||
handleDeploymentTypeChange: deploymentConfig.handleDeploymentTypeChange,
|
||||
handleAppTypeChange: deploymentConfig.handleAppTypeChange,
|
||||
handleSampleWebAppTypeChange: deploymentConfig.handleSampleWebAppTypeChange,
|
||||
handleSourceTypeChange: deploymentConfig.handleSourceTypeChange,
|
||||
handleRepoUrlChange: deploymentConfig.handleRepoUrlChange,
|
||||
handleDeploymentKeyChange: deploymentConfig.handleDeploymentKeyChange,
|
||||
handleFileChange: deploymentConfig.handleFileChange
|
||||
}}
|
||||
showToast={showToast}
|
||||
TemplatePreview={TemplatePreview}
|
||||
/>
|
||||
|
||||
{/* Domain Configuration Section */}
|
||||
<DomainConfiguration
|
||||
domainConfig={domainConfig.config}
|
||||
validation={domainConfig.validation}
|
||||
dnsVerified={dnsVerified}
|
||||
handlers={{
|
||||
handleUseSubdomainChange: domainConfig.handleUseSubdomainChange,
|
||||
handleUseCustomDomainChange: domainConfig.handleUseCustomDomainChange,
|
||||
handleDomainTypeChange: domainConfig.handleDomainTypeChange,
|
||||
handleDnsMethodChange: domainConfig.handleDnsMethodChange,
|
||||
handleDomainChange: domainConfig.handleDomainChange,
|
||||
handleSubdomainChange: domainConfig.handleSubdomainChange,
|
||||
validateDomain: domainConfig.validateDomain
|
||||
}}
|
||||
checkDnsConfig={checkDnsConfig}
|
||||
defaultSubdomain={defaultSubdomain}
|
||||
showToast={showToast}
|
||||
/>
|
||||
|
||||
{/* Form Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={domainConfig.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
|
||||
${domainConfig.useCustomDomain && !formValid
|
||||
? 'bg-neutral-600 cursor-not-allowed'
|
||||
: 'bg-[#6d9e37] hover:bg-[#598035] focus:ring-[#6d9e37] transition-colors'
|
||||
}`}
|
||||
aria-label="Start deployment"
|
||||
>
|
||||
Start the Deployment
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Toast visible={toast.visible} message={toast.message} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainSetupForm;
|
|
@ -0,0 +1,333 @@
|
|||
"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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
"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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
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";
|
||||
|
||||
interface AuthStatus {
|
||||
message: string;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
interface UserRecord {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
record: UserRecord;
|
||||
}
|
||||
|
||||
|
||||
const LoginPage = () => {
|
||||
const [email, setEmail] = useState('suvodip@siliconpin.com');
|
||||
const [password, setPassword] = useState('Simple2pass');
|
||||
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;
|
||||
avatar: string;
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setStatus({ message: '', isError: false });
|
||||
|
||||
try {
|
||||
const authData = await pb.collection("users").authWithPassword(email, password);
|
||||
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
|
||||
|
||||
const authResponse: AuthResponse = {
|
||||
token: authData.token,
|
||||
record: {
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name || '',
|
||||
avatar: authData.record.avatar || ''
|
||||
}
|
||||
};
|
||||
|
||||
await syncSessionWithBackend(authResponse, avatarUrl);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
setStatus({
|
||||
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 || '',
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Important for cookies
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: 'new',
|
||||
accessToken: authData.token,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name,
|
||||
avatar: avatarUrl,
|
||||
isAuthenticated: true,
|
||||
id: authData.record.id
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to sync session');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Session synced with backend:', data);
|
||||
} catch (error) {
|
||||
console.error('Error syncing session:', error);
|
||||
throw error; // Re-throw the error if you want calling functions to handle it
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
|
@ -0,0 +1,273 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Invoice {
|
||||
invoice_id: number;
|
||||
invoice_number: string;
|
||||
customer_id: string;
|
||||
invoice_date: string;
|
||||
due_date: string;
|
||||
subtotal: number;
|
||||
tax_amount: number;
|
||||
total_amount: number;
|
||||
status: string;
|
||||
payment_terms: string | null;
|
||||
notes: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export default function PrintInvoice() {
|
||||
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const getInvoiceListData = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data);
|
||||
} catch (error: any) {
|
||||
console.error('Fetch error:', error);
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
const viewInvoiceDetails = (invoice: Invoice) => {
|
||||
setSelectedInvoice(invoice);
|
||||
}
|
||||
|
||||
const closeInvoiceDetails = () => {
|
||||
setSelectedInvoice(null);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="p-4 text-red-600">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading invoice data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-6xl mx-auto">
|
||||
{/* Invoice List */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Invoices</h1>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Invoice #</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Due Date</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{invoiceList.map((invoice) => (
|
||||
<tr key={invoice.invoice_id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">{invoice.invoice_number}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.invoice_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.due_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatCurrency(invoice.total_amount)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
${invoice.status === 'paid' ? 'bg-green-100 text-green-800' :
|
||||
invoice.status === 'draft' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'}`}>
|
||||
{invoice.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => viewInvoiceDetails(invoice)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-2"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Print
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Detail Modal */}
|
||||
{selectedInvoice && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Printable Invoice */}
|
||||
<div id="printable-invoice" className="p-8">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">INVOICE</h1>
|
||||
<p className="text-gray-600">{selectedInvoice.invoice_number}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-gray-600">Status: <span className={`font-semibold
|
||||
${selectedInvoice.status === 'paid' ? 'text-green-600' :
|
||||
selectedInvoice.status === 'draft' ? 'text-yellow-600' :
|
||||
'text-red-600'}`}>
|
||||
{selectedInvoice.status.toUpperCase()}
|
||||
</span></p>
|
||||
<p className="text-gray-600">Date: {formatDate(selectedInvoice.invoice_date)}</p>
|
||||
<p className="text-gray-600">Due: {formatDate(selectedInvoice.due_date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">From</h2>
|
||||
<p className="text-gray-800">Your Company Name</p>
|
||||
<p className="text-gray-600">123 Business Street</p>
|
||||
<p className="text-gray-600">City, State 12345</p>
|
||||
<p className="text-gray-600">contact@yourcompany.com</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Bill To</h2>
|
||||
<p className="text-gray-800">Customer ID: {selectedInvoice.customer_id}</p>
|
||||
<p className="text-gray-600">[Customer Name]</p>
|
||||
<p className="text-gray-600">[Customer Address]</p>
|
||||
<p className="text-gray-600">[Customer Email]</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-b border-gray-200 py-4 mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 font-semibold text-gray-700">
|
||||
<div className="col-span-6">Description</div>
|
||||
<div className="col-span-2 text-right">Subtotal</div>
|
||||
<div className="col-span-2 text-right">Tax</div>
|
||||
<div className="col-span-2 text-right">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 mb-2">
|
||||
<div className="col-span-6 text-gray-800">[Service/Product Description]</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.subtotal)}</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.tax_amount)}</div>
|
||||
<div className="col-span-2 text-right font-semibold">{formatCurrency(selectedInvoice.total_amount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-8">
|
||||
<div className="w-64">
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Subtotal:</span>
|
||||
<span>{formatCurrency(selectedInvoice.subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Tax:</span>
|
||||
<span>{formatCurrency(selectedInvoice.tax_amount)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 font-bold text-lg">
|
||||
<span>Total:</span>
|
||||
<span>{formatCurrency(selectedInvoice.total_amount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedInvoice.notes && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Notes</h2>
|
||||
<p className="text-gray-600">{selectedInvoice.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-8 border-t border-gray-200">
|
||||
<p className="text-gray-600 text-center">Thank you for your business!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-end">
|
||||
<button
|
||||
onClick={closeInvoiceDetails}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 mr-2"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Print Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Print Styles */}
|
||||
<style>
|
||||
{`
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
#printable-invoice, #printable-invoice * {
|
||||
visibility: visible;
|
||||
}
|
||||
#printable-invoice {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 20mm;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -36,7 +36,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
|||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>{feature}</span>
|
||||
<span className='text-zinc-600'>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { X } from "lucide-react"; // Import an icon for the remove button
|
||||
|
||||
export function AvatarUpload() {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setSelectedFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
const handleRemoveFile = () => {
|
||||
setSelectedFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''; // Reset file input
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!selectedFile ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label
|
||||
htmlFor="avatar"
|
||||
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colorsfocus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
Change Avatar
|
||||
</Label>
|
||||
<Input type="file" id="avatar" ref={fileInputRef} accept="image/jpeg,image/png,image/gif" className="hidden" onChange={handleFileChange}/>
|
||||
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>Browse</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
JPG, GIF or PNG. 1MB max.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-3 space-x-2">
|
||||
<div className="truncate max-w-[200px]">
|
||||
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
|
||||
</div>
|
||||
<Button size="sm" className="text-xs p-1 h-fit">Update</Button>
|
||||
<Button size="sm" onClick={handleRemoveFile} className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit">Remove</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default AvatarUpload;
|
|
@ -0,0 +1,205 @@
|
|||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||
import { Button } from "./ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "./ui/select";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import UpdateAvatar from './UpdateAvatar';
|
||||
|
||||
interface SessionData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
success: boolean;
|
||||
session_data: SessionData;
|
||||
user_avatar: string;
|
||||
}
|
||||
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
const [invoiceList, setInvoiceList] = useState<any[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSessionData = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'http://localhost:2058/host-api/v1/users/get-profile-data/',
|
||||
{
|
||||
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('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include', // Crucial for cookies
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data); // Fix: Use `data.data` instead of `data`
|
||||
return data.data; // Fix: `session_data` does not exist in response
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
fetchSessionData();
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
return <div>Loading profile data...</div>;
|
||||
}
|
||||
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-2xl">
|
||||
<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>
|
||||
<UpdateAvatar />
|
||||
|
||||
</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>
|
||||
<CardTitle>Billing Information</CardTitle>
|
||||
<CardDescription>
|
||||
View your billing history.
|
||||
</CardDescription>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Invoice ID</th>
|
||||
<th className="text-left">Date</th>
|
||||
<th className="text-left">Description</th>
|
||||
<th className="text-right">Amount</th>
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
invoiceList.map((invoice) => (
|
||||
<tr key={invoice.id}>
|
||||
<td>{invoice.invoice_id}</td>
|
||||
<td>{invoice.date}</td>
|
||||
<td>{invoice.description}</td>
|
||||
<td className="text-right">{invoice.amount}</td>
|
||||
<td className="text-center"><a href="">Print</a></td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 lg:max-w-md">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security</CardTitle>
|
||||
<CardDescription>
|
||||
Update your password and security settings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currentPassword">Current password</Label>
|
||||
<Input id="currentPassword" type="password" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="newPassword">New password</Label>
|
||||
<Input id="newPassword" type="password" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm password</Label>
|
||||
<Input id="confirmPassword" type="password" />
|
||||
</div>
|
||||
<Button className="mt-4">Update password</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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 className="bg-red-500 hover:bg-red-600">Delete account</Button>
|
||||
<Button variant="outline">Export data</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import React from "react";
|
||||
|
||||
const SelectWithLabel = ({value, onChange}: any)=>{
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
|
||||
<select
|
||||
id="deployment-type"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
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>
|
||||
</select>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectWithLabel;
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
|
||||
export const TemplatePreview = ({ templateType }) => {
|
||||
// Template information for different types
|
||||
const templateInfo = {
|
||||
developer: {
|
||||
name: "Developer Portfolio",
|
||||
description: "A modern, responsive portfolio site with sections for projects, skills, and contact information. Perfect for developers to showcase their work.",
|
||||
features: ["Project showcase", "Skills section", "GitHub integration", "Contact form", "Blog ready"],
|
||||
image: "https://images.unsplash.com/photo-1498050108023-c5249f4df085?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2072&q=80"
|
||||
},
|
||||
designer: {
|
||||
name: "Designer Portfolio",
|
||||
description: "A visually stunning portfolio for designers with image galleries, case studies, and animations to showcase creative work.",
|
||||
features: ["Visual gallery", "Case studies", "Color scheme customization", "Smooth animations", "Design process showcase"],
|
||||
image: "https://images.unsplash.com/photo-1561070791-2526d30994b5?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2000&q=80"
|
||||
},
|
||||
photographer: {
|
||||
name: "Photographer Portfolio",
|
||||
description: "An elegant portfolio with fullscreen galleries, image zooming, and lightbox features designed for photographers to display their work.",
|
||||
features: ["Fullscreen galleries", "Image zoom", "Lightbox", "Category filtering", "Client proofing"],
|
||||
image: "https://images.unsplash.com/photo-1542038784456-1ea8e935640e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
|
||||
},
|
||||
documentation: {
|
||||
name: "Documentation Site",
|
||||
description: "A comprehensive documentation site with search, code snippets, and versioning support for technical documentation.",
|
||||
features: ["Search functionality", "Code snippets with syntax highlighting", "Versioning", "Sidebar navigation", "Mobile-friendly"],
|
||||
image: "https://images.unsplash.com/photo-1456406644174-8ddd4cd52a06?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2068&q=80"
|
||||
},
|
||||
business: {
|
||||
name: "Single Page Business Site",
|
||||
description: "A professional one-page website for businesses with sections for services, testimonials, team members, and contact information.",
|
||||
features: ["Single page layout", "Services section", "Testimonials", "Team profiles", "Contact form with map"],
|
||||
image: "https://images.unsplash.com/photo-1560179707-f14e90ef3623?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2073&q=80"
|
||||
}
|
||||
};
|
||||
|
||||
const template = templateInfo[templateType] || templateInfo.developer;
|
||||
|
||||
return (
|
||||
<div className="mt-6 bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
{/* Template preview image */}
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={template.image}
|
||||
alt={`${template.name} Preview`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex items-end">
|
||||
<div className="p-4">
|
||||
<h3 className="text-xl font-semibold text-white">{template.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template details */}
|
||||
<div className="p-5 space-y-4">
|
||||
<p className="text-neutral-300">{template.description}</p>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-medium mb-2">Features:</h4>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{template.features.map((feature, index) => (
|
||||
<li key={index} className="text-neutral-400 flex items-center">
|
||||
<span className="mr-2 text-[#6d9e37]">✓</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button className="px-4 py-2 bg-[#6d9e37] text-white rounded-md hover:bg-[#598035] transition-colors w-full">
|
||||
Select This Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Toast = ({ visible, message, type = 'success' }) => {
|
||||
// Color scheme based on type
|
||||
const bgColor = type === 'error' ? 'bg-red-900' : 'bg-green-900';
|
||||
const icon = type === 'error' ? '✕' : '✓';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-5 right-5 p-3 ${bgColor} text-white rounded-md shadow-lg transform transition-all duration-300 z-50 ${
|
||||
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{icon}</span>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
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 }
|
|
@ -8,7 +8,7 @@ export interface ButtonProps
|
|||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", ...props }, ref) => {
|
||||
({ className, variant = "default", type = '', size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
|
|
|
@ -8,7 +8,7 @@ const Card = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
"rounded-lg border border-[#6d9e37] bg-neutral-800 text-[#6d9e37] shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
interface ContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<div className={`max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Container;
|
|
@ -0,0 +1,122 @@
|
|||
"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,
|
||||
}
|
|
@ -1,25 +1,130 @@
|
|||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface SelectProps
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {}
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
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}
|
||||
>
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ 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<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, ...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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
);
|
||||
Select.displayName = "Select";
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
export { Select };
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ 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<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ 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<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ 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;
|
||||
|
||||
// ChevronDownIcon component
|
||||
const ChevronDownIcon = React.forwardRef<
|
||||
SVGSVGElement,
|
||||
React.SVGProps<SVGSVGElement>
|
||||
>((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"
|
||||
></path>
|
||||
</svg>
|
||||
));
|
||||
ChevronDownIcon.displayName = "ChevronDownIcon";
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
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 }
|
|
@ -0,0 +1,38 @@
|
|||
interface TableProps {
|
||||
headers: string[];
|
||||
data: any[];
|
||||
className?: string;
|
||||
children?: React.ReactNode; // Add children here
|
||||
}
|
||||
|
||||
const Table: React.FC<TableProps> = ({ headers, data, className = '', children }) => {
|
||||
return (
|
||||
<div className={`overflow-x-auto ${className}`}>
|
||||
<table className="min-w-full bg-white border border-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, index) => (
|
||||
<th key={index} className="px-6 py-3 text-left text-sm font-medium text-gray-600">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, index) => (
|
||||
<tr key={index} className="border-t">
|
||||
{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>
|
||||
))}
|
||||
{children} {/* This will allow you to pass additional content */}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
|
@ -0,0 +1,42 @@
|
|||
import type { ElementType } from 'react';
|
||||
|
||||
interface TypographyProps {
|
||||
variant: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body1' | 'body2' | 'caption';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
color?: string; // Add color prop
|
||||
}
|
||||
|
||||
const Typography: React.FC<TypographyProps> = ({ variant, children, className = '', color }) => {
|
||||
const variantStyles: Record<string, React.CSSProperties> = {
|
||||
h1: { fontSize: '2.5rem', fontWeight: 'bold' },
|
||||
h2: { fontSize: '2rem', fontWeight: 'bold' },
|
||||
h3: { fontSize: '1.75rem', fontWeight: 'bold' },
|
||||
h4: { fontSize: '1.5rem', fontWeight: 'bold' },
|
||||
h5: { fontSize: '1.25rem', fontWeight: 'bold' },
|
||||
h6: { fontSize: '1rem', fontWeight: 'bold' },
|
||||
body1: { fontSize: '1rem', fontWeight: 'normal' },
|
||||
body2: { fontSize: '0.875rem', fontWeight: 'normal' },
|
||||
caption: { fontSize: '0.75rem', fontWeight: 'normal' },
|
||||
};
|
||||
|
||||
const combinedStyle = { ...variantStyles[variant], ...(color && { color }), ...(className ? { className } : {}) };
|
||||
|
||||
const variantToTag: Record<string, ElementType> = {
|
||||
h1: 'h1',
|
||||
h2: 'h2',
|
||||
h3: 'h3',
|
||||
h4: 'h4',
|
||||
h5: 'h5',
|
||||
h6: 'h6',
|
||||
body1: 'p',
|
||||
body2: 'p',
|
||||
caption: 'span',
|
||||
};
|
||||
|
||||
const Tag: ElementType = variantToTag[variant];
|
||||
|
||||
return <Tag style={combinedStyle}>{children}</Tag>;
|
||||
};
|
||||
|
||||
export default Typography;
|
|
@ -0,0 +1,103 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for managing deployment configuration state
|
||||
* @param {Object} initialConfig - Initial deployment configuration
|
||||
* @returns {Object} - Deployment configuration state and updater functions
|
||||
*/
|
||||
const useDeploymentConfig = (initialConfig = {}) => {
|
||||
const [config, setConfig] = useState({
|
||||
type: 'app',
|
||||
appType: 'wordpress',
|
||||
sampleWebAppType: 'developer',
|
||||
sourceType: 'public',
|
||||
repoUrl: '',
|
||||
deploymentKey: '',
|
||||
fileName: '',
|
||||
...initialConfig
|
||||
});
|
||||
|
||||
// Extract values for easier access
|
||||
const {
|
||||
type,
|
||||
appType,
|
||||
sampleWebAppType,
|
||||
sourceType,
|
||||
repoUrl,
|
||||
deploymentKey,
|
||||
fileName
|
||||
} = config;
|
||||
|
||||
// Update a single field
|
||||
const updateField = useCallback((field, value) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Handler for deployment type change
|
||||
const handleDeploymentTypeChange = useCallback((e) => {
|
||||
updateField('type', e.target.value);
|
||||
}, [updateField]);
|
||||
|
||||
// Handler for app type change
|
||||
const handleAppTypeChange = useCallback((e) => {
|
||||
updateField('appType', e.target.value);
|
||||
}, [updateField]);
|
||||
|
||||
// Handler for sample web app type change
|
||||
const handleSampleWebAppTypeChange = useCallback((e) => {
|
||||
updateField('sampleWebAppType', e.target.value);
|
||||
}, [updateField]);
|
||||
|
||||
// Handler for source type change
|
||||
const handleSourceTypeChange = useCallback((e) => {
|
||||
updateField('sourceType', e.target.value);
|
||||
}, [updateField]);
|
||||
|
||||
// Handler for repo URL change
|
||||
const handleRepoUrlChange = useCallback((e) => {
|
||||
updateField('repoUrl', e.target.value);
|
||||
}, [updateField]);
|
||||
|
||||
// Handler for deployment key change
|
||||
const handleDeploymentKeyChange = useCallback((e) => {
|
||||
updateField('deploymentKey', e.target.value);
|
||||
}, [updateField]);
|
||||
|
||||
// Handler for file change
|
||||
const handleFileChange = useCallback((e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
updateField('fileName', e.target.files[0].name);
|
||||
} else {
|
||||
updateField('fileName', '');
|
||||
}
|
||||
}, [updateField]);
|
||||
|
||||
return {
|
||||
// State values
|
||||
config,
|
||||
type,
|
||||
appType,
|
||||
sampleWebAppType,
|
||||
sourceType,
|
||||
repoUrl,
|
||||
deploymentKey,
|
||||
fileName,
|
||||
|
||||
// Update functions
|
||||
updateField,
|
||||
|
||||
// Event handlers
|
||||
handleDeploymentTypeChange,
|
||||
handleAppTypeChange,
|
||||
handleSampleWebAppTypeChange,
|
||||
handleSourceTypeChange,
|
||||
handleRepoUrlChange,
|
||||
handleDeploymentKeyChange,
|
||||
handleFileChange
|
||||
};
|
||||
};
|
||||
|
||||
export default useDeploymentConfig;
|
|
@ -0,0 +1,62 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for managing DNS verification
|
||||
* @param {Function} showToast - Function to display toast notifications
|
||||
* @returns {Object} - DNS verification state and methods
|
||||
*/
|
||||
const useDnsVerification = (showToast) => {
|
||||
// DNS verification state
|
||||
const [dnsVerified, setDnsVerified] = useState({
|
||||
cname: false,
|
||||
ns: false,
|
||||
a: false
|
||||
});
|
||||
|
||||
// Check DNS configuration
|
||||
const checkDnsConfig = useCallback((type) => {
|
||||
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
||||
|
||||
// Set type to 'checking' state
|
||||
setDnsVerified(prev => ({
|
||||
...prev,
|
||||
[type]: 'checking'
|
||||
}));
|
||||
|
||||
// Simulate DNS check with a delay
|
||||
setTimeout(() => {
|
||||
setDnsVerified(prev => ({
|
||||
...prev,
|
||||
[type]: true
|
||||
}));
|
||||
|
||||
showToast(`${type} verified successfully!`);
|
||||
}, 1500);
|
||||
}, [showToast]);
|
||||
|
||||
// Reset DNS verification for a specific type
|
||||
const resetDnsVerification = useCallback((type) => {
|
||||
setDnsVerified(prev => ({
|
||||
...prev,
|
||||
[type]: false
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Reset all DNS verification
|
||||
const resetAllDnsVerification = useCallback(() => {
|
||||
setDnsVerified({
|
||||
cname: false,
|
||||
ns: false,
|
||||
a: false
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dnsVerified,
|
||||
checkDnsConfig,
|
||||
resetDnsVerification,
|
||||
resetAllDnsVerification
|
||||
};
|
||||
};
|
||||
|
||||
export default useDnsVerification;
|
|
@ -0,0 +1,267 @@
|
|||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { cleanDomainInput, isValidDomainFormat, validateDomainAvailability } from '../utils/domainUtils';
|
||||
|
||||
/**
|
||||
* Custom hook for managing domain configuration
|
||||
* @param {Object} initialConfig - Initial domain configuration
|
||||
* @param {Function} resetAllDnsVerification - Function to reset all DNS verification
|
||||
* @returns {Object} - Domain configuration state and methods
|
||||
*/
|
||||
const useDomainConfig = (initialConfig = {}, resetAllDnsVerification) => {
|
||||
// Domain configuration state
|
||||
const [config, setConfig] = useState({
|
||||
useSubdomain: true,
|
||||
useCustomDomain: false,
|
||||
customDomain: '',
|
||||
customSubdomain: '',
|
||||
domainType: 'domain',
|
||||
dnsMethod: 'cname',
|
||||
...initialConfig
|
||||
});
|
||||
|
||||
// Domain validation state
|
||||
const [validation, setValidation] = useState({
|
||||
isValidating: false,
|
||||
isValidDomain: false,
|
||||
validationMessage: '',
|
||||
showDnsConfig: false
|
||||
});
|
||||
|
||||
// Extract values for easier access
|
||||
const {
|
||||
useSubdomain,
|
||||
useCustomDomain,
|
||||
customDomain,
|
||||
customSubdomain,
|
||||
domainType,
|
||||
dnsMethod
|
||||
} = config;
|
||||
|
||||
const {
|
||||
isValidating,
|
||||
isValidDomain,
|
||||
validationMessage,
|
||||
showDnsConfig
|
||||
} = validation;
|
||||
|
||||
// Update a single field in domain config
|
||||
const updateField = useCallback((field, value) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Update validation state
|
||||
const updateValidation = useCallback((updates) => {
|
||||
setValidation(prev => ({
|
||||
...prev,
|
||||
...updates
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Handler for use subdomain checkbox
|
||||
const handleUseSubdomainChange = useCallback((e) => {
|
||||
const newValue = e.target.checked;
|
||||
|
||||
updateField('useSubdomain', newValue);
|
||||
|
||||
// CRITICAL RULE: If CNAME record is selected, SiliconPin subdomain must be enabled
|
||||
// If user tries to uncheck SiliconPin subdomain while using CNAME, disable custom domain
|
||||
if (useCustomDomain && dnsMethod === 'cname' && !newValue) {
|
||||
updateField('useCustomDomain', false);
|
||||
|
||||
// Since we're disabling custom domain, reset validation
|
||||
updateValidation({
|
||||
showDnsConfig: false,
|
||||
isValidDomain: false,
|
||||
validationMessage: '',
|
||||
isValidating: false
|
||||
});
|
||||
|
||||
// Also reset DNS verification
|
||||
if (resetAllDnsVerification) {
|
||||
resetAllDnsVerification();
|
||||
}
|
||||
}
|
||||
}, [useCustomDomain, dnsMethod, updateField, updateValidation, resetAllDnsVerification]);
|
||||
|
||||
// Handler for use custom domain checkbox
|
||||
const handleUseCustomDomainChange = useCallback((e) => {
|
||||
const newValue = e.target.checked;
|
||||
|
||||
updateField('useCustomDomain', newValue);
|
||||
|
||||
if (!newValue) {
|
||||
// Reset validation when disabling custom domain
|
||||
updateValidation({
|
||||
showDnsConfig: false,
|
||||
isValidDomain: false,
|
||||
validationMessage: '',
|
||||
isValidating: false
|
||||
});
|
||||
|
||||
// Reset DNS verification
|
||||
if (resetAllDnsVerification) {
|
||||
resetAllDnsVerification();
|
||||
}
|
||||
} else {
|
||||
// CRITICAL RULE: Force SiliconPin subdomain to be checked if custom domain is checked
|
||||
updateField('useSubdomain', true);
|
||||
}
|
||||
}, [updateField, updateValidation, resetAllDnsVerification]);
|
||||
|
||||
// Handler for domain type change
|
||||
const handleDomainTypeChange = useCallback((e) => {
|
||||
updateField('domainType', e.target.value);
|
||||
|
||||
// Reset validation when changing domain type
|
||||
updateValidation({
|
||||
isValidDomain: false,
|
||||
validationMessage: '',
|
||||
showDnsConfig: false
|
||||
});
|
||||
|
||||
// Reset DNS verification
|
||||
if (resetAllDnsVerification) {
|
||||
resetAllDnsVerification();
|
||||
}
|
||||
}, [updateField, updateValidation, resetAllDnsVerification]);
|
||||
|
||||
// Handler for DNS method change
|
||||
const handleDnsMethodChange = useCallback((e) => {
|
||||
const newValue = e.target.value;
|
||||
|
||||
updateField('dnsMethod', newValue);
|
||||
|
||||
// CRITICAL RULE: If changing to CNAME, ensure SiliconPin subdomain is enabled
|
||||
if (newValue === 'cname') {
|
||||
updateField('useSubdomain', true);
|
||||
}
|
||||
}, [updateField]);
|
||||
|
||||
// Handler for domain input change
|
||||
const handleDomainChange = useCallback((e) => {
|
||||
const cleanedValue = cleanDomainInput(e.target.value);
|
||||
updateField('customDomain', cleanedValue);
|
||||
|
||||
// Reset domain validation when input changes
|
||||
if (isValidDomain) {
|
||||
updateValidation({
|
||||
isValidDomain: false,
|
||||
validationMessage: '',
|
||||
showDnsConfig: false
|
||||
});
|
||||
}
|
||||
}, [updateField, updateValidation, isValidDomain]);
|
||||
|
||||
// Handler for subdomain input change
|
||||
const handleSubdomainChange = useCallback((e) => {
|
||||
const cleanedValue = cleanDomainInput(e.target.value);
|
||||
updateField('customSubdomain', cleanedValue);
|
||||
|
||||
// Reset domain validation when input changes
|
||||
if (isValidDomain) {
|
||||
updateValidation({
|
||||
isValidDomain: false,
|
||||
validationMessage: '',
|
||||
showDnsConfig: false
|
||||
});
|
||||
}
|
||||
}, [updateField, updateValidation, isValidDomain]);
|
||||
|
||||
// Validate domain
|
||||
const validateDomain = useCallback(async () => {
|
||||
const domain = domainType === 'domain' ? customDomain : customSubdomain;
|
||||
|
||||
if (!domain) {
|
||||
updateValidation({
|
||||
validationMessage: 'Please enter a domain name.',
|
||||
isValidDomain: false,
|
||||
showDnsConfig: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial validation: check format with regex
|
||||
if (!isValidDomainFormat(domain)) {
|
||||
updateValidation({
|
||||
validationMessage: 'Domain format is invalid. Please check your entry.',
|
||||
isValidDomain: false,
|
||||
showDnsConfig: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show validation in progress
|
||||
updateValidation({
|
||||
isValidating: true,
|
||||
validationMessage: '',
|
||||
showDnsConfig: false
|
||||
});
|
||||
|
||||
// Validate with API
|
||||
const result = await validateDomainAvailability(domain, domainType);
|
||||
|
||||
updateValidation({
|
||||
isValidating: false,
|
||||
isValidDomain: result.isValid,
|
||||
validationMessage: result.message,
|
||||
showDnsConfig: result.isValid
|
||||
});
|
||||
|
||||
// Reset DNS verification when domain is validated
|
||||
if (result.isValid && resetAllDnsVerification) {
|
||||
resetAllDnsVerification();
|
||||
}
|
||||
}, [
|
||||
domainType,
|
||||
customDomain,
|
||||
customSubdomain,
|
||||
updateValidation,
|
||||
resetAllDnsVerification
|
||||
]);
|
||||
|
||||
// Reset validation when disabling custom domain
|
||||
useEffect(() => {
|
||||
if (!useCustomDomain) {
|
||||
updateValidation({
|
||||
showDnsConfig: false,
|
||||
isValidDomain: false,
|
||||
validationMessage: '',
|
||||
isValidating: false
|
||||
});
|
||||
}
|
||||
}, [useCustomDomain, updateValidation]);
|
||||
|
||||
return {
|
||||
// State values
|
||||
config,
|
||||
validation,
|
||||
useSubdomain,
|
||||
useCustomDomain,
|
||||
customDomain,
|
||||
customSubdomain,
|
||||
domainType,
|
||||
dnsMethod,
|
||||
isValidating,
|
||||
isValidDomain,
|
||||
validationMessage,
|
||||
showDnsConfig,
|
||||
|
||||
// Update functions
|
||||
updateField,
|
||||
updateValidation,
|
||||
|
||||
// Event handlers
|
||||
handleUseSubdomainChange,
|
||||
handleUseCustomDomainChange,
|
||||
handleDomainTypeChange,
|
||||
handleDnsMethodChange,
|
||||
handleDomainChange,
|
||||
handleSubdomainChange,
|
||||
validateDomain
|
||||
};
|
||||
};
|
||||
|
||||
export default useDomainConfig;
|
|
@ -0,0 +1,57 @@
|
|||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for form validation based on critical business rules
|
||||
* @param {Object} domainConfig - Domain configuration state
|
||||
* @param {Object} validation - Domain validation state
|
||||
* @param {Object} dnsVerified - DNS verification state
|
||||
* @returns {Object} - Form validation state and methods
|
||||
*/
|
||||
const useFormValidation = (domainConfig, validation, dnsVerified) => {
|
||||
const [formValid, setFormValid] = useState(true);
|
||||
|
||||
const { useCustomDomain, dnsMethod } = domainConfig;
|
||||
const { isValidDomain } = validation;
|
||||
|
||||
// Validate form
|
||||
const validateForm = useCallback(() => {
|
||||
let valid = true;
|
||||
|
||||
// For custom domain, require domain validation and DNS verification
|
||||
if (useCustomDomain) {
|
||||
// First requirement: domain must be validated successfully
|
||||
if (!isValidDomain) {
|
||||
valid = false;
|
||||
}
|
||||
// Second requirement: appropriate DNS verification must pass
|
||||
else if (dnsMethod === 'cname' && dnsVerified.cname !== true) {
|
||||
valid = false;
|
||||
}
|
||||
else if (dnsMethod === 'ns' && dnsVerified.ns !== true) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
setFormValid(valid);
|
||||
return valid;
|
||||
}, [useCustomDomain, isValidDomain, dnsMethod, dnsVerified.cname, dnsVerified.ns]);
|
||||
|
||||
// Validate form when relevant state changes
|
||||
useEffect(() => {
|
||||
validateForm();
|
||||
}, [
|
||||
useCustomDomain,
|
||||
isValidDomain,
|
||||
dnsMethod,
|
||||
dnsVerified.cname,
|
||||
dnsVerified.ns,
|
||||
validateForm
|
||||
]);
|
||||
|
||||
return {
|
||||
formValid,
|
||||
validateForm
|
||||
};
|
||||
};
|
||||
|
||||
export default useFormValidation;
|
|
@ -0,0 +1,36 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for managing toast notifications
|
||||
* @param {number} duration - Duration in ms to show the toast (default: 3000)
|
||||
* @returns {Object} - Toast state and methods
|
||||
*/
|
||||
const useToast = (duration = 3000) => {
|
||||
const [toast, setToast] = useState({
|
||||
visible: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
// Show a toast notification
|
||||
const showToast = useCallback((message) => {
|
||||
setToast({ visible: true, message });
|
||||
|
||||
// Auto-hide the toast after the specified duration
|
||||
setTimeout(() => {
|
||||
setToast({ visible: false, message: '' });
|
||||
}, duration);
|
||||
}, [duration]);
|
||||
|
||||
// Hide the toast notification
|
||||
const hideToast = useCallback(() => {
|
||||
setToast({ visible: false, message: '' });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
toast,
|
||||
showToast,
|
||||
hideToast
|
||||
};
|
||||
};
|
||||
|
||||
export default useToast;
|
|
@ -174,8 +174,8 @@ const organizationSchema = {
|
|||
<ul class="space-y-2 text-neutral-400">
|
||||
<li class="flex items-center gap-2">📦 <a href="/services" class="hover:text-[#6d9e37] transition-colors">All Services</a></li>
|
||||
<li class="flex items-center gap-2">🚀 <a href="/get-started" class="hover:text-[#6d9e37] transition-colors">Get Started</a></li>
|
||||
<li class="flex items-center gap-2">👨💻 <a href="/services/hire-developer" class="hover:text-[#6d9e37] transition-colors">Hire a Human Developer</a></li>
|
||||
<li class="flex items-center gap-2">🤖 <a href="/services/hire-ai-agent" class="hover:text-[#6d9e37] transition-colors">Hire an AI Agent</a></li>
|
||||
<li class="flex items-center gap-2">👨💻 <a href="/hire-developer" class="hover:text-[#6d9e37] transition-colors">Hire a Human Developer</a></li>
|
||||
<li class="flex items-center gap-2">🤖 <a href="/hire-ai-agent" class="hover:text-[#6d9e37] transition-colors">Hire an AI Agent</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ const contactSchema = {
|
|||
<span class="text-neutral-300 font-medium">Sunday:</span>
|
||||
<span class="text-white">Closed</span>
|
||||
</div>
|
||||
<div class="pt-3 border-t border-neutral-700">
|
||||
<div class="pt-3 border-t border-neutral-700 flex items-center justify-between">
|
||||
<span class="text-neutral-300">Technical Support:</span>
|
||||
<span class="text-[#6d9e37] font-semibold block mt-1">24/7</span>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro"
|
||||
---
|
||||
<Layout title="">
|
||||
<div>
|
||||
<pre id="get-response">Adding domain, please wait...</pre>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
fetch('http://localhost:2058/host-api/v1/add-domain/')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Network error');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const resultElement = document.getElementById('get-response');
|
||||
if (data.status === 'success') {
|
||||
resultElement.textContent = `✅ ${data.message}\n${data.output || ''}`;
|
||||
} else {
|
||||
resultElement.textContent = `❌ ${data.message}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('get-response').textContent = `❌ Fetch Error: ${error.message}`;
|
||||
});
|
||||
</script>
|
||||
</Layout>
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro"
|
||||
---
|
||||
<Layout title="">
|
||||
<div>
|
||||
<h2>Delete Domain</h2>
|
||||
<form id="deleteDomainForm">
|
||||
<input type="text" id="domainName" placeholder="example.com" required>
|
||||
<button type="submit">Delete Domain</button>
|
||||
</form>
|
||||
<pre id="deleteResponse"></pre>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
document.getElementById('deleteDomainForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const domain = document.getElementById('domainName').value;
|
||||
const responseElement = document.getElementById('deleteResponse');
|
||||
|
||||
responseElement.textContent = "Deleting domain...";
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/delete-domain/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domain)}`
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
responseElement.textContent = `✅ ${data.message}\nDomain: ${data.domain}`;
|
||||
// Refresh domain list if needed
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
responseElement.textContent = `❌ ${data.message}\nError: ${data.error || 'Unknown error'}`;
|
||||
}
|
||||
} catch (error) {
|
||||
responseElement.textContent = `❌ Network Error: ${error.message}`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</Layout>
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro"
|
||||
---
|
||||
<Layout title="">
|
||||
<div>
|
||||
<pre id="get-response">Loading domains...</pre>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
fetch('http://localhost:2058/host-api/v1/list-domain/')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Network error');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const pre = document.getElementById('get-response');
|
||||
if (data.status === 'success') {
|
||||
// Format domains as a table
|
||||
let output = 'DOMAINS LIST:\n\n';
|
||||
output += 'Domain'.padEnd(40) + 'IP'.padEnd(15) + 'SSL'.padEnd(5) + 'Status\n';
|
||||
output += '-'.repeat(70) + '\n';
|
||||
|
||||
if (Object.keys(data.domains).length > 0) {
|
||||
Object.entries(data.domains).forEach(([domain, info]) => {
|
||||
output += `${domain.padEnd(40)}${info.IP.padEnd(15)}${info.SSL.padEnd(5)}${info.SUSPENDED === 'no' ? 'Active' : 'Suspended'}\n`;
|
||||
});
|
||||
} else {
|
||||
output += 'No domains found for this user\n';
|
||||
}
|
||||
|
||||
pre.textContent = output;
|
||||
} else {
|
||||
pre.textContent = `❌ Error: ${data.message}\nDebug: ${data.debug || ''}\nJSON Error: ${data.json_error || ''}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('get-response').textContent = `❌ Fetch Error: ${error.message}`;
|
||||
});
|
||||
</script>
|
||||
</Layout>
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import { DomainSetupForm } from '../components/DomainSetupForm';
|
||||
import { DomainSetupForm } from '../components/DomainSetupForm/index';
|
||||
|
||||
// Page-specific SEO metadata
|
||||
const pageTitle = "Get Started | SiliconPin";
|
||||
const pageDescription = "Start your project with SiliconPin's hosting services. Deploy a web app, from source code, or upload a static site.";
|
||||
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
||||
const pageImage = "https://siliconpin.com/assets/images/get-started-og-image.png";
|
||||
|
||||
// Generate a random subdomain for server side rendering
|
||||
const randomString = () => {
|
||||
|
@ -24,7 +24,7 @@ const defaultSubdomain = randomString();
|
|||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">Get Started</h1>
|
||||
<p class="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">
|
||||
Launch your project with SiliconPin's high-performance hosting services
|
||||
Launch your project with SiliconPin's easy and reliable hosting services
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import HireAIAgent from "../components/HireAIAgent";
|
||||
---
|
||||
<Layout title="">
|
||||
<div>
|
||||
<HireAIAgent client:load />
|
||||
</div>
|
||||
</Layout>
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Developer from "../components/HireDeveloper";
|
||||
---
|
||||
<Layout title="">
|
||||
<div>
|
||||
<Developer client:load />
|
||||
</div>
|
||||
</Layout>
|
|
@ -2,8 +2,8 @@
|
|||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
// Page-specific SEO metadata
|
||||
const pageTitle = "SiliconPin - High-Performance Hosting Solutions";
|
||||
const pageDescription = "SiliconPin offers reliable, high-performance hosting solutions for PHP, Node.js, Python, Kubernetes (K8s), and K3s with 24/7 technical support.";
|
||||
const pageTitle = "SiliconPin - Lets create some digital freedom";
|
||||
const pageDescription = "SiliconPin - easy to deploy apps and tools, freedom oriented apps and tools, high-performance, hosting solutions for PHP, Node.js, Python, Kubernetes (K8s), and K3s, and technical support.";
|
||||
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
||||
---
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import Layout from '../layouts/Layout.astro';
|
|||
|
||||
// Page-specific SEO metadata
|
||||
const pageTitle = "Legal Agreement | SiliconPin";
|
||||
const pageDescription = "Review SiliconPin's legal agreement for using our hosting services. Find information about your legal rights and obligations.";
|
||||
const pageDescription = "Review SiliconPin's legal agreement for using our services. Find information about your legal rights and obligations.";
|
||||
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
||||
---
|
||||
|
||||
|
@ -23,6 +23,20 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
|||
|
||||
<div class="max-w-4xl mx-auto space-y-8">
|
||||
<section class="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
|
||||
<h3 class="text-xl font-medium text-[#6d9e37]">TLDR;</h3>
|
||||
<p class="text-neutral-300">
|
||||
We are trying to create some digital freedom together. We avoid any vender locking - for you and us.
|
||||
some of us from DWD Consultancy Services, having a quite experience manipulating data and data resilience.
|
||||
Still we advise to keep a backup of your data periodically and use our automated backup and snapshot services (even AWS/GCP dont take data responsibility).
|
||||
You are responsible for your content, and we are responsible for our services.
|
||||
|
||||
If something goes wrong, we will try to fix it. For SLA we will refund or we will part ways.
|
||||
We are based in India, and Indian law applies to this agreement. We find some law funny and useless like the cookie acknowledgement.
|
||||
<br>
|
||||
we strongly feel just to be reasonable and fair to each other.
|
||||
</p>
|
||||
<hr>
|
||||
<br>
|
||||
<p class="text-neutral-300">
|
||||
This Legal Agreement ("Agreement") is a binding contract between you ("Customer" or "you") and SiliconPin ("Company", "we", or "us") governing your use of our hosting services and related products (collectively, the "Services"). By using our Services, you acknowledge that you have read, understood, and agree to be bound by this Agreement.
|
||||
</p>
|
||||
|
@ -168,7 +182,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
|||
If you have any questions about this Legal Agreement, please contact us:
|
||||
</p>
|
||||
<div class="text-neutral-300">
|
||||
<p>Email: <a href="mailto:legal@siliconpin.com" class="text-[#6d9e37] hover:underline">legal@siliconpin.com</a></p>
|
||||
<p>Email: <a href="mailto:contact@siliconpin.com" class="text-[#6d9e37] hover:underline">contact@siliconpin.com</a></p>
|
||||
<p>Phone: +91-700-160-1485</p>
|
||||
<p>Address: 121 Lalbari, GourBongo Road, Habra, W.B. 743271, India</p>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import LoginPage from "../components/Login";
|
||||
---
|
||||
<Layout title="">
|
||||
<LoginPage client:load />
|
||||
</Layout>
|
|
@ -0,0 +1,265 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PocketBase Sign-In</title>
|
||||
</head>
|
||||
<style>
|
||||
.login-main-conatiner{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.login-container{
|
||||
max-width: 380px;
|
||||
width: 100%;
|
||||
background: #222;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 100px 0;
|
||||
}
|
||||
#loginForm{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.auth-title {
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.auth-input {
|
||||
background: #333;
|
||||
border: 1px solid #444;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
transition: border 0.3s;
|
||||
}
|
||||
.auth-input:focus {
|
||||
border-color: #00bcd4;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
background: linear-gradient(135deg, #00bcd4, #0288d1);
|
||||
color: white;
|
||||
padding: 14px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-top: 10px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.auth-btn:hover {
|
||||
background: linear-gradient(135deg, #0288d1, #00bcd4);
|
||||
}
|
||||
|
||||
.auth-social {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.auth-social-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
transition: opacity 0.3s;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.auth-social-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.auth-google {
|
||||
background: #db4437;
|
||||
}
|
||||
.auth-facebook {
|
||||
background: #1877f2;
|
||||
}
|
||||
.auth-github {
|
||||
background: #333;
|
||||
}
|
||||
.auth-divider {
|
||||
margin: 20px 0;
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 37%;
|
||||
height: 1px;
|
||||
background: #444;
|
||||
top: 50%;
|
||||
}
|
||||
.auth-divider::before {
|
||||
left: 0;
|
||||
}
|
||||
.auth-divider::after {
|
||||
right: 0;
|
||||
}
|
||||
.auth-error {
|
||||
color: #ff4c4c;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.auth-footer {
|
||||
margin-top: 15px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
.auth-footer a {
|
||||
color: #00bcd4;
|
||||
text-decoration: none;
|
||||
}
|
||||
.auth-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.pass-view-button{
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<body>
|
||||
<div class="login-main-conatiner">
|
||||
<div class="login-container">
|
||||
<h2 class="auth-title">Login to Your Account</h2>
|
||||
<form id="loginForm">
|
||||
<input class="auth-input" id="email" type="email" name="email" placeholder="Email Address" required value="suvodip@siliconpin.com">
|
||||
<div class="auth-input" style="display: flex; position: relative;">
|
||||
<input class="auth-input" id="password" type="text" name="password" placeholder="Password" required style="width: 100%;" value="Simple2pass" />
|
||||
<button onclick="toggleInputType();" type="button" class="pass-view-button" style="position: absolute; right: 0; margin-top: 3px;">
|
||||
<img src="/assets/eye.svg" id="eyeToggle" alt="">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="auth-btn" type="submit">Sign In</button>
|
||||
</form>
|
||||
<p class="auth-divider">Or continue with</p>
|
||||
<div class="">
|
||||
<button id="loginGoogleBtn" onclick="loginWithOAuth2('google')" class="auth-social-btn auth-google">Login with Google</button>
|
||||
<button id="loginFacebookBtn" onclick="loginWithOAuth2('facebook')" class="auth-social-btn auth-facebook">Login with Facebook</button>
|
||||
<button id="loginGitHubBtn" onclick="loginWithOAuth2('github')" class="auth-social-btn auth-github">Login with GitHub</button>
|
||||
</div>
|
||||
|
||||
<p id="status" style="display: none;"></p>
|
||||
<p class="auth-footer">Don't have an account? <a href="/sign-up">Sign up</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<script is:inline type="module">
|
||||
import PocketBase from 'https://cdn.jsdelivr.net/npm/pocketbase@0.19.0/+esm';
|
||||
|
||||
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
||||
let isAuthenticated = false;
|
||||
|
||||
document.getElementById("loginForm").addEventListener("submit", async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const email = document.getElementById("email").value;
|
||||
const password = document.getElementById("password").value;
|
||||
const status = document.getElementById("status");
|
||||
|
||||
try {
|
||||
const authData = await pb.collection("users").authWithPassword(email, password);
|
||||
console.log("User signed in:", authData);
|
||||
status.style.display = 'block';
|
||||
status.textContent = "Login successful!";
|
||||
status.style.color = "green";
|
||||
isAuthenticated = true;
|
||||
updateUI(authData.record, authData.token);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
status.style.display = 'block';
|
||||
status.textContent = "Login failed. Please check your credentials.";
|
||||
status.style.color = "red";
|
||||
}
|
||||
});
|
||||
|
||||
async function loginWithOAuth2(provider) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: provider });
|
||||
|
||||
if (!authData || !authData.record) {
|
||||
console.error("Login failed: No user record found.");
|
||||
return;
|
||||
}
|
||||
let accessToken = authData.token;
|
||||
isAuthenticated = true;
|
||||
console.log("Google Auth Response:", authData);
|
||||
updateUI(authData.record, authData.token);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error("Google Login failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(user, token) {
|
||||
if (!user || !user.email) {
|
||||
console.error("User data is missing:", user);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send user data to PHP session
|
||||
fetch('http://localhost:2058/host-api/v1/users/session/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
accessToken: token,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar: user.avatar ? pb.files.getUrl(user, user.avatar) : '',
|
||||
isAuthenticated : true,
|
||||
id: user.id
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log('Session updated:', data))
|
||||
.catch(error => console.error('Error saving session:', error));
|
||||
}
|
||||
|
||||
window.loginWithOAuth2 = loginWithOAuth2;
|
||||
|
||||
if (pb.authStore.isValid) {
|
||||
updateUI(pb.authStore.model);
|
||||
}
|
||||
|
||||
const inputType = document.getElementById('password');
|
||||
const eyeToggle = document.getElementById('eyeToggle');
|
||||
function toggleInputType(){
|
||||
if(inputType.type === 'password'){
|
||||
inputType.type = 'text';
|
||||
eyeToggle.src = '/assets/eye-close.svg';
|
||||
}else{
|
||||
inputType.type = 'password';
|
||||
eyeToggle.src = '/assets/eye.svg';
|
||||
}
|
||||
}
|
||||
window.toggleInputType = toggleInputType;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Invoice from "../components/PrintInvoice";
|
||||
---
|
||||
<Layout title="">
|
||||
<Invoice client:load/>
|
||||
</Layout>
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
// const phpHello = `$_SESSION['userName']`;
|
||||
import UserProfile from "../components/UserProfile";
|
||||
---
|
||||
|
||||
<Layout title="Profile Page">
|
||||
<UserProfile client:load />
|
||||
<!-- <div class="flex items-center justify-center min-h-screen bg-gray-700">
|
||||
<div class="w-96 p-6 shadow-lg rounded-lg bg-white text-center">
|
||||
<img class="w-24 h-24 rounded-full border-4 border-blue-500 mx-auto" src="/profile.jpg" alt="Profile Picture" />
|
||||
<h2 class="text-2xl font-semibold mt-4" set:html={phpHello ? phpHello : 'User Name'}></h2>
|
||||
<p class="text-gray-600">Frontend Developer</p>
|
||||
<p class="text-gray-500 text-sm mt-2">"Building amazing UI experiences one component at a time."</p>
|
||||
<div class="flex justify-center space-x-4 mt-4">
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg">Follow</button>
|
||||
<button class="bg-gray-200 text-black px-4 py-2 rounded-lg">Message</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</Layout>
|
|
@ -10,43 +10,25 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
|||
// Service data
|
||||
const services = [
|
||||
{
|
||||
title: 'PHP Hosting',
|
||||
description: 'Fast, secure, and reliable PHP hosting solutions for your web applications.',
|
||||
title: 'Deploy an App',
|
||||
description: 'WordPress, Joomla, Drupal, PrestaShop, Wiki, Moodle, Directus, PocketBase, StarAPI and more.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1599507593499-a3f7d7d97667?q=80&w=2000&auto=format&fit=crop',
|
||||
features: [
|
||||
'PHP 8.x support',
|
||||
'One-click installation of popular CMS',
|
||||
'Free SSL certificates',
|
||||
'Optimized for WordPress, Laravel, etc.',
|
||||
'24/7 Technical support'
|
||||
],
|
||||
learnMoreUrl: '/services/php'
|
||||
features: [ 'WordPress', 'Joomla', 'Drupal', 'PrestaShop', 'Wiki', 'Moodle', 'Directus', 'PocketBase', 'StarAPI' ],
|
||||
learnMoreUrl: '/services/deploy-an-app'
|
||||
},
|
||||
{
|
||||
title: 'Node.js Hosting',
|
||||
description: 'High-performance Node.js hosting with seamless deployment pipelines.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1570063578733-6a33b69d1538?q=80&w=2000&auto=format&fit=crop',
|
||||
features: [
|
||||
'Latest Node.js versions',
|
||||
'NPM/Yarn support',
|
||||
'Express, Next.js, and more',
|
||||
'Managed SSL certificates',
|
||||
'Automatic scaling'
|
||||
],
|
||||
learnMoreUrl: '/services/nodejs'
|
||||
title: 'Deploy From Source Code',
|
||||
description: 'Node.js, Python, Ruby, Go, Rust, and more. Deploy your custom applications with ease.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1599507593499-a3f7d7d97667?q=80&w=2000&auto=format&fit=crop',
|
||||
features: ['Node.js', 'Python', 'Ruby', 'Go', 'Rust', 'Docker', 'Kubernetes', 'JAMstack', 'Serverless'],
|
||||
learnMoreUrl: '/services/deploy-from-source-code'
|
||||
},
|
||||
{
|
||||
title: 'Python Hosting',
|
||||
description: 'Scalable Python hosting for web applications, APIs, and data science projects.',
|
||||
title: 'Static Site Hosting',
|
||||
description: 'Secure and scalable hosting for static websites and JAMstack applications.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1526379879527-8559ecfcaec0?q=80&w=2000&auto=format&fit=crop',
|
||||
features: [
|
||||
'Python 3.x support',
|
||||
'Django, Flask, and FastAPI ready',
|
||||
'Virtual environments',
|
||||
'Jupyter notebook integration',
|
||||
'Seamless deployment'
|
||||
],
|
||||
learnMoreUrl: '/services/python'
|
||||
features: ['JAMstack', 'Gatsby', 'Hugo', 'Next.js', 'Nuxt.js', 'VuePress', 'Eleventy', 'SvelteKit', 'Astro'],
|
||||
learnMoreUrl: '/services/static-site-hosting'
|
||||
},
|
||||
{
|
||||
title: 'Kubernetes (K8s)',
|
||||
|
@ -73,6 +55,20 @@ const services = [
|
|||
'Simplified management'
|
||||
],
|
||||
learnMoreUrl: '/services/k3s'
|
||||
},
|
||||
{
|
||||
title: 'Hire a Human Developer',
|
||||
description: 'Need a custom solution? Our experts can design a tailored App or WebApp for your specific needs.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
||||
features: ['Node.js', 'Python', 'Ruby', 'Go', 'Rust', 'Docker', 'Kubernetes', 'JAMstack', 'Serverless'],
|
||||
learnMoreUrl: '/services/hire-a-human-developer'
|
||||
},
|
||||
{
|
||||
title: 'Hire an AI Agent',
|
||||
description: 'Need a custom solution? Our experts can design a tailored AI Agent for your specific needs.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
||||
features: ['Reactive Agents (Stateless, Rule-Based)', 'Proactive Agents (Stateful, Machine Learning)', 'Hybrid Agents (Reactive + Proactive)', 'Model-Based Agents (Stateful, Uses Memory)', 'Goal-Based Agents (Optimizes for an Objective)', 'Utility-Based Agents', 'Self Learning Agents', 'Autonomous AI Agents'],
|
||||
learnMoreUrl: '/services/hire-an-ai-agent'
|
||||
}
|
||||
];
|
||||
---
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Clean domain input by removing http://, https://, www., and trailing slashes
|
||||
* @param {string} input - The domain input string to clean
|
||||
* @returns {string} - The cleaned domain string
|
||||
*/
|
||||
export const cleanDomainInput = (input) => {
|
||||
return input
|
||||
.replace(/^(https?:\/\/)?(www\.)?/i, '')
|
||||
.replace(/\/+$/, '')
|
||||
.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate domain format using regex
|
||||
* @param {string} domain - The domain to validate
|
||||
* @returns {boolean} - Whether the domain format is valid
|
||||
*/
|
||||
export const isValidDomainFormat = (domain) => {
|
||||
if (!domain) return false;
|
||||
return /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(domain);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate domain availability with API
|
||||
* @param {string} domain - The domain to validate
|
||||
* @param {string} type - The domain type ('domain' or 'subdomain')
|
||||
* @returns {Promise} - Promise resolving to validation result
|
||||
*/
|
||||
export const validateDomainAvailability = async (domain, type) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/validate-domain/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ domain, type })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Network error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
isValid: data.status === "success",
|
||||
message: data.status === "success"
|
||||
? 'Domain is valid and registered.'
|
||||
: 'Domain appears to be unregistered or unavailable.'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error validating domain:', error);
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Error checking domain: ${error.message}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {Promise} - Promise resolving to success or error
|
||||
*/
|
||||
export const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue