Compare commits
22 Commits
get-starte
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cbe98eff5 | ||
| dffdbc6e40 | |||
| 8f8c5f0d65 | |||
|
|
0438c30c97 | ||
| 0e030f0ce2 | |||
| 5cd02bfbde | |||
| 4b6e4af86d | |||
|
|
b58068d108 | ||
| 8b75fa057d | |||
|
|
c927fd6087 | ||
|
|
f9d1556ce9 | ||
| 1ed908b12e | |||
| ffae4acebd | |||
| daa4702904 | |||
|
|
c3faedf6f8 | ||
| a9eabcb683 | |||
| 66bd9ec890 | |||
| 9d3ef1a643 | |||
| 1661f7859d | |||
| 026c6c34b2 | |||
| 734fed06b8 | |||
| 0900d51954 |
107
package-lock.json
generated
107
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@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",
|
||||
@@ -21,6 +22,8 @@
|
||||
"lucide-react": "^0.484.0",
|
||||
"pocketbase": "^0.25.2",
|
||||
"postcss": "^8.5.3",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-to-print": "^3.0.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
@@ -1794,6 +1797,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -6007,6 +6044,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.1.tgz",
|
||||
"integrity": "sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.1.tgz",
|
||||
"integrity": "sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -6029,6 +6115,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-to-print": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.5.tgz",
|
||||
"integrity": "sha512-Z15MwMOzYCHWi26CZeFNwflAg7Nr8uWD6FTj+EkfIOjYyjr0MXGbI0c7rF4Fgrbj3XG9hFndb1ourxpPz2RAiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -6463,6 +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",
|
||||
@@ -7005,6 +7106,12 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@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",
|
||||
@@ -23,6 +24,8 @@
|
||||
"lucide-react": "^0.484.0",
|
||||
"pocketbase": "^0.25.2",
|
||||
"postcss": "^8.5.3",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-to-print": "^3.0.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
|
||||
8
public/.htaccess
Normal file
8
public/.htaccess
Normal file
@@ -0,0 +1,8 @@
|
||||
RewriteEngine On
|
||||
#RewriteCond %{HTTPS} !=on
|
||||
#RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,NE]
|
||||
#RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||
#RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*) data-not-updated-or-not-found
|
||||
39
public/.well-known/csaf/provider-metadata.json
Normal file
39
public/.well-known/csaf/provider-metadata.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"csaf_version": "2.0",
|
||||
"provider": {
|
||||
"name": "DWD Consultancy Services",
|
||||
"namespace": "com.siliconpin.dwd",
|
||||
"contact_details": [
|
||||
{
|
||||
"name": "Suvankar Sarkar",
|
||||
"email": "suvankar@siliconpin.com",
|
||||
"phone": "+91-700-160-1485",
|
||||
"website": "https://dwd.siliconpin.com/#about"
|
||||
}
|
||||
],
|
||||
"publisher": {
|
||||
"type": "vendor",
|
||||
"name": "SiliconPin",
|
||||
"namespace": "com.siliconpin"
|
||||
},
|
||||
"distribution": {
|
||||
"tlp_label": "WHITE",
|
||||
"url": "https://dwd.siliconpin.com/#about",
|
||||
"pgp_key": "https://siliconpin.com/pgp-key.asc"
|
||||
},
|
||||
"tracking": {
|
||||
"id": "EX-2025-0001",
|
||||
"status": "final",
|
||||
"initial_release_date": "2025-03-27T12:00:00Z",
|
||||
"current_release_date": "2025-03-27T12:00:00Z",
|
||||
"revision_history": [
|
||||
{
|
||||
"number": "1.0",
|
||||
"date": "2025-03-27T12:00:00Z",
|
||||
"summary": "Initial release"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
public/.well-known/security.txt
Normal file
7
public/.well-known/security.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Contact: mailto:suvankar@siliconpin.com
|
||||
Contact: https://siliconpin.com/contact/
|
||||
Expires: 2027-03-26T18:30:00.000Z
|
||||
Encryption: https://siliconpin.com/pgp-key.txt
|
||||
Acknowledgments: https://dwd.siliconpin.com
|
||||
Preferred-Languages: en,bn
|
||||
CSAF: https://siliconpin.com/.well-known/csaf/provider-metadata.json
|
||||
39
public/advisory.json
Normal file
39
public/advisory.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"csaf_version": "2.0",
|
||||
"provider": {
|
||||
"name": "DWD Consultancy Services",
|
||||
"namespace": "com.siliconpin.dwd",
|
||||
"contact_details": [
|
||||
{
|
||||
"name": "Suvankar Sarkar",
|
||||
"email": "suvankar@siliconpin.com",
|
||||
"phone": "+91-700-160-1485",
|
||||
"website": "https://dwd.siliconpin.com/#about"
|
||||
}
|
||||
],
|
||||
"publisher": {
|
||||
"type": "vendor",
|
||||
"name": "SiliconPin",
|
||||
"namespace": "com.siliconpin"
|
||||
},
|
||||
"distribution": {
|
||||
"tlp_label": "WHITE",
|
||||
"url": "https://dwd.siliconpin.com/#about",
|
||||
"pgp_key": "https://siliconpin.com/pgp-key.asc"
|
||||
},
|
||||
"tracking": {
|
||||
"id": "EX-2025-0001",
|
||||
"status": "final",
|
||||
"initial_release_date": "2025-03-27T12:00:00Z",
|
||||
"current_release_date": "2025-03-27T12:00:00Z",
|
||||
"revision_history": [
|
||||
{
|
||||
"number": "1.0",
|
||||
"date": "2025-03-27T12:00:00Z",
|
||||
"summary": "Initial release"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
public/advisory.json.asc
Normal file
14
public/advisory.json.asc
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCAAdFiEEL0GUnJpZXELqjphXMaCR/T68rscFAmflVEgACgkQMaCR/T68
|
||||
rse45Av6A0YL6qJSBvqN6tty7j0G68yfgsjeJxtuCxXiDwUjUIU4VwHORNeddZD/
|
||||
Lx5nmYf4BgszoFjgaix8dyu87bDn2QaA13+2/GMDkc3GT8yW8bzaR/saV1J1v070
|
||||
wVhButTIM48c4NpuqFX5Kytcvm632lZdeKBFF2rFfdZ3ajd7IJMr7CYVzziJdVa8
|
||||
pR00udgBYE05ewV9W8FLMmTwWqoOIQYtYR3+YP1rhuYInZ/7dobO5aI4/eliHIBo
|
||||
xe93UET9Zo19ywbnqhas+x4wJl8roI2qWDYsjp3+gRG4Ns5Myf2puhtVxexiY4xK
|
||||
6cvra2ujOFLksykPgy5lpw2WcpFFnm9K2s9NMvLAsv+3OXZHQANL7vS5ZFXxNxyU
|
||||
fm+F5uP8LiR1IXpFcJAP3MweaDdlWhL7TZp1tuA0J/i/oCFShZypzQLb9welYVbH
|
||||
a/CPm1pavGxEIlFkSOAlMmcp945s41qYia2BFOlW1ZjsDkrAR2561aLI/O7klpNw
|
||||
VLnngQsi
|
||||
=s4sX
|
||||
-----END PGP SIGNATURE-----
|
||||
5
public/data-not-updated-or-not-found/index.php
Normal file
5
public/data-not-updated-or-not-found/index.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
echo 'Data not updated or the page not found!';
|
||||
|
||||
?>
|
||||
1
public/humans.txt
Normal file
1
public/humans.txt
Normal file
@@ -0,0 +1 @@
|
||||
SiliconPin is a Decentralized NonProfit Organization / Group, Creating some digital freedom, if you want to join -welcome.
|
||||
41
public/pgp-key.txt
Normal file
41
public/pgp-key.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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-----
|
||||
41
public/public-key.asc
Normal file
41
public/public-key.asc
Normal file
@@ -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-----
|
||||
7
public/robots.txt
Normal file
7
public/robots.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
User-agent: *
|
||||
Disallow: /secret-location/
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://siliconpin.com/sitemap.xml
|
||||
7
public/security.txt
Normal file
7
public/security.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Contact: mailto:suvankar@siliconpin.com
|
||||
Contact: https://siliconpin.com/contact/
|
||||
Expires: 2027-03-26T18:30:00.000Z
|
||||
Encryption: https://siliconpin.com/pgp-key.txt
|
||||
Acknowledgments: https://dwd.siliconpin.com
|
||||
Preferred-Languages: en,bn
|
||||
CSAF: https://siliconpin.com/.well-known/csaf/provider-metadata.json
|
||||
72
public/sitemap.xml
Normal file
72
public/sitemap.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
||||
|
||||
|
||||
<url>
|
||||
<loc>https://siliconpin.com/</loc>
|
||||
<lastmod>2025-03-27T14:20:53+00:00</lastmod>
|
||||
<priority>1.00</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/services/</loc>
|
||||
<lastmod>2025-03-27T14:20:54+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/contact/</loc>
|
||||
<lastmod>2025-03-27T14:20:55+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/get-started/</loc>
|
||||
<lastmod>2025-03-27T14:20:56+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/about-us/</loc>
|
||||
<lastmod>2025-03-27T14:20:57+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/suggestion-or-report/</loc>
|
||||
<lastmod>2025-03-27T14:20:58+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/hire-developer/</loc>
|
||||
<lastmod>2025-03-27T14:21:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/hire-ai-agent/</loc>
|
||||
<lastmod>2025-03-27T14:21:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/privacy-policy/</loc>
|
||||
<lastmod>2025-03-27T14:21:02+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/terms-and-conditions/</loc>
|
||||
<lastmod>2025-03-27T14:21:03+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/refund-policy/</loc>
|
||||
<lastmod>2025-03-27T14:21:04+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://siliconpin.com/legal-agreement/</loc>
|
||||
<lastmod>2025-03-27T14:21:05+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
|
||||
|
||||
</urlset>
|
||||
@@ -1,776 +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,
|
||||
ip: false
|
||||
});
|
||||
|
||||
// Form validation
|
||||
const [formValid, setFormValid] = useState(true);
|
||||
|
||||
// Toast notification
|
||||
const [toast, setToast] = useState({ visible: false, message: '' });
|
||||
|
||||
// File upload reference
|
||||
const fileInputRef = React.useRef(null);
|
||||
|
||||
// Effect for handling domain type changes
|
||||
useEffect(() => {
|
||||
if (!useCustomDomain) {
|
||||
setShowDnsConfig(false);
|
||||
setIsValidDomain(false);
|
||||
setValidationMessage('');
|
||||
setIsValidating(false);
|
||||
}
|
||||
|
||||
validateForm();
|
||||
}, [useCustomDomain, dnsVerified.cname, dnsVerified.ns, dnsVerified.ns, domainType, dnsMethod]);
|
||||
|
||||
// Show toast notification
|
||||
const showToast = (message) => {
|
||||
setToast({ visible: true, message });
|
||||
setTimeout(() => setToast({ visible: false, message: '' }), 3000);
|
||||
};
|
||||
|
||||
// Handle deployment type change
|
||||
const handleDeploymentTypeChange = (e) => {
|
||||
setDeploymentType(e.target.value);
|
||||
};
|
||||
|
||||
// Handle source type change
|
||||
const handleSourceTypeChange = (e) => {
|
||||
setSourceType(e.target.value);
|
||||
};
|
||||
|
||||
// Handle file upload
|
||||
const handleFileChange = (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
setFileName(e.target.files[0].name);
|
||||
} else {
|
||||
setFileName('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle domain checkbox changes
|
||||
const handleUseSubdomainChange = (e) => {
|
||||
setUseSubdomain(e.target.checked);
|
||||
|
||||
// If CNAME record is selected, SiliconPin subdomain must be enabled
|
||||
if (useCustomDomain && dnsMethod === 'cname' && !e.target.checked) {
|
||||
setUseCustomDomain(false);
|
||||
}
|
||||
|
||||
validateForm();
|
||||
};
|
||||
|
||||
const handleUseCustomDomainChange = (e) => {
|
||||
setUseCustomDomain(e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
setShowDnsConfig(false);
|
||||
setIsValidDomain(false);
|
||||
setValidationMessage('');
|
||||
setIsValidating(false);
|
||||
setDnsVerified({
|
||||
cname: false,
|
||||
ns: false,
|
||||
a: false,
|
||||
ip: false
|
||||
});
|
||||
} else {
|
||||
// Force SiliconPin subdomain to be checked if custom domain is checked
|
||||
setUseSubdomain(true);
|
||||
}
|
||||
|
||||
validateForm();
|
||||
};
|
||||
|
||||
// Handle domain type change
|
||||
const handleDomainTypeChange = (e) => {
|
||||
setDomainType(e.target.value);
|
||||
|
||||
// Reset validation when changing domain type
|
||||
setIsValidDomain(false);
|
||||
setValidationMessage('');
|
||||
setDnsVerified({
|
||||
cname: false,
|
||||
ns: false,
|
||||
a: false
|
||||
});
|
||||
|
||||
validateForm();
|
||||
};
|
||||
|
||||
// Handle domain and subdomain input changes
|
||||
const handleDomainChange = (e) => {
|
||||
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?(www\.)?/i, '').replace(/\/+$/, '').trim();
|
||||
setCustomDomain(cleanedValue);
|
||||
};
|
||||
|
||||
const handleSubdomainChange = (e) => {
|
||||
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?/i, '').replace(/\/+$/, '').trim();
|
||||
setCustomSubdomain(cleanedValue);
|
||||
};
|
||||
|
||||
// Handle DNS method change
|
||||
const handleDnsMethodChange = (e) => {
|
||||
setDnsMethod(e.target.value);
|
||||
|
||||
// If changing to CNAME, ensure SiliconPin subdomain is enabled
|
||||
if (e.target.value === 'cname') {
|
||||
setUseSubdomain(true);
|
||||
}
|
||||
|
||||
// Reset DNS verification
|
||||
setDnsVerified(prev => ({
|
||||
...prev,
|
||||
[e.target.value]: false
|
||||
}));
|
||||
|
||||
validateForm();
|
||||
};
|
||||
|
||||
// Validate domain
|
||||
const validateDomain = () => {
|
||||
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
|
||||
// call /host-api/v1/domains/validate/?domain=domain.com
|
||||
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();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Check DNS configuration
|
||||
const checkSubDomainCname = () => {
|
||||
const domainToCheck = customDomain || customSubdomain;
|
||||
|
||||
fetch('http://localhost:2058/host-api/v1/check-c-name/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domainToCheck)}`
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
checkDnsConfig('cname');
|
||||
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
||||
console.log('CNAME record:', data.cname);
|
||||
// Handle success - update UI
|
||||
} else {
|
||||
console.error('Error:', data.message);
|
||||
// Handle error
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fetch error:', error);
|
||||
// Show error to user
|
||||
});
|
||||
};
|
||||
const checkDnsConfig = (type) => {
|
||||
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
||||
|
||||
// Simulate DNS check
|
||||
setTimeout(() => {
|
||||
setDnsVerified(prev => ({
|
||||
...prev,
|
||||
[type]: true
|
||||
}));
|
||||
|
||||
showToast(`${type} verified successfully!`);
|
||||
validateForm();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// Copy to clipboard
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => {
|
||||
showToast('Copied to clipboard!');
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('Failed to copy: ' + err);
|
||||
});
|
||||
};
|
||||
|
||||
// Validate form
|
||||
const validateForm = () => {
|
||||
// For custom domain, require DNS verification
|
||||
if (useCustomDomain) {
|
||||
if (dnsMethod === 'cname' && !dnsVerified.cname) {
|
||||
setFormValid(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dnsMethod === 'ns' && !dnsVerified.ns) {
|
||||
setFormValid(false);
|
||||
return;
|
||||
}
|
||||
if (dnsMethod === 'ip' && !dnsVerified.ip) {
|
||||
setFormValid(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setFormValid(true);
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formValid) {
|
||||
showToast('Please complete DNS verification before deploying.');
|
||||
return;
|
||||
}
|
||||
|
||||
// In a real app, this would submit the form data to the server
|
||||
console.log([{ deploymentType, appType, sampleWebAppType, sourceType, repoUrl, deploymentKey, useSubdomain, useCustomDomain, customDomain, customSubdomain, domainType, dnsMethod}]);
|
||||
|
||||
showToast('Form submitted successfully!');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Deployment Type Selection */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
|
||||
<select
|
||||
id="deployment-type"
|
||||
value={deploymentType}
|
||||
onChange={handleDeploymentTypeChange}
|
||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||
>
|
||||
<option value="app">💻 Deploy an App</option>
|
||||
<option value="source">⚙️ From Source</option>
|
||||
<option value="static">📄 Static Site Upload</option>
|
||||
<option value="sample-web-app">🌐 Sample Web App</option>
|
||||
</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
|
||||
disabled={!customDomain && !customSubdomain}
|
||||
type="button"
|
||||
onClick={validateDomain}
|
||||
className={`px-4 py-2 ${ !customDomain && !customSubdomain ? 'bg-neutral-600 cursor-not-allowed' : 'bg-[#6d9e37] focus:ring-[#6d9e37] transition-colors'} text-white font-medium rounded-md transition-colors focus:outline-none`}
|
||||
>
|
||||
Validate Domain
|
||||
</button>
|
||||
|
||||
{/* Validation Status */}
|
||||
{useCustomDomain && isValidating && (
|
||||
<div className="p-3 bg-neutral-700/50 rounded-md">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
<p className="text-white">Verifying domain registration and availability...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{useCustomDomain && !isValidating && validationMessage && (
|
||||
<div className={`p-3 rounded-md ${isValidDomain ? 'bg-green-900/20 border border-green-700/50' : 'bg-red-900/20 border border-red-700/50'}`}>
|
||||
<p className={isValidDomain ? 'text-green-400' : 'text-red-400'}>
|
||||
{validationMessage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* DNS Configuration Options */}
|
||||
{showDnsConfig && (
|
||||
<div className="mt-4 space-y-4 p-4 rounded-md bg-neutral-800/50 border border-neutral-700">
|
||||
<h4 className="text-white font-medium">Connect Your Domain</h4>
|
||||
|
||||
{/* CNAME Record Option */}
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<label for="dns-cname" className="flex items-start cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-cname"
|
||||
name="dns-method"
|
||||
value="cname"
|
||||
checked={dnsMethod === 'cname'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="dns-cname" className="block text-white font-medium">Use CNAME Record</label>
|
||||
<p className="text-sm text-neutral-300">Point your domain to our SiliconPin subdomain</p>
|
||||
|
||||
<div className="mt-3 flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">
|
||||
{defaultSubdomain}.subdomain.siliconpin.com
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(`${defaultSubdomain}.subdomain.siliconpin.com`)}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy CNAME value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (checkSubDomainCname())}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsVerified.cname
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
{dnsVerified.cname ? '✓ CNAME Verified' : 'Check CNAME'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Nameserver Option (only for full domains, not subdomains) */}
|
||||
{domainType === 'domain' && (
|
||||
<>
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<label for="dns-ns" className="flex items-start cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-ns"
|
||||
name="dns-method"
|
||||
value="ns"
|
||||
checked={dnsMethod === 'ns'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
|
||||
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('ns1.siliconpin.com')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('ns2.siliconpin.com')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkDnsConfig('ns')}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsVerified.ns
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
{dnsVerified.ns ? '✓ Nameservers Verified' : 'Check Nameservers'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<label for="dns-ip" className="flex items-start cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-ip"
|
||||
name="dns-method"
|
||||
value="ip"
|
||||
checked={dnsMethod === 'ip'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="dns-ip" className="block text-white font-medium">Use Our IP Address</label>
|
||||
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">xxx.xxx.x.xx</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('xxx.xxx.x.xx')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkDnsConfig('ip')}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsMethod === 'ip'
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
{/* {dnsVerified.ip ? '✓ IP Address Verified' : 'Check IP Address'} */}
|
||||
Proceed to Pay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Form Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={useCustomDomain && !formValid}
|
||||
className={`w-full mt-6 px-6 py-3 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-800
|
||||
${useCustomDomain && !formValid
|
||||
? 'bg-neutral-600 cursor-not-allowed'
|
||||
: 'bg-[#6d9e37] hover:bg-[#598035] focus:ring-[#6d9e37] transition-colors'
|
||||
}`}
|
||||
>
|
||||
Start the Deployment
|
||||
</button>
|
||||
</form>
|
||||
<Toast visible={toast.visible} message={toast.message} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
98
src/components/DomainSetupForm/DeploymentOptions/index.jsx
Normal file
98
src/components/DomainSetupForm/DeploymentOptions/index.jsx
Normal file
@@ -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;
|
||||
91
src/components/DomainSetupForm/DomainConfiguration/index.jsx
Normal file
91
src/components/DomainSetupForm/DomainConfiguration/index.jsx
Normal file
@@ -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;
|
||||
136
src/components/DomainSetupForm/index.jsx
Normal file
136
src/components/DomainSetupForm/index.jsx
Normal file
@@ -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;
|
||||
@@ -26,6 +26,7 @@ interface AuthResponse {
|
||||
record: UserRecord;
|
||||
}
|
||||
|
||||
|
||||
const LoginPage = () => {
|
||||
const [email, setEmail] = useState('suvodip@siliconpin.com');
|
||||
const [password, setPassword] = useState('Simple2pass');
|
||||
@@ -35,18 +36,30 @@ const LoginPage = () => {
|
||||
|
||||
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
record: {
|
||||
query: string;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setStatus({ message: '', isError: false });
|
||||
|
||||
|
||||
try {
|
||||
const authData = await pb.collection("users").authWithPassword(email, password);
|
||||
|
||||
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
|
||||
|
||||
const authResponse: AuthResponse = {
|
||||
token: authData.token,
|
||||
record: {
|
||||
query: 'new',
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name || '',
|
||||
@@ -54,8 +67,8 @@ const LoginPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
await syncSessionWithBackend(authResponse);
|
||||
// window.location.href = '/profile';
|
||||
await syncSessionWithBackend(authResponse, avatarUrl);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
setStatus({
|
||||
@@ -77,20 +90,21 @@ const LoginPage = () => {
|
||||
if (!authData?.record) {
|
||||
throw new Error("No user record found");
|
||||
}
|
||||
|
||||
|
||||
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
|
||||
const authResponse: AuthResponse = {
|
||||
token: authData.token,
|
||||
record: {
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email || '',
|
||||
name: authData.record.name || '',
|
||||
avatar: authData.record.avatar || ''
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email || '',
|
||||
name: authData.record.name || '',
|
||||
avatar: authData.record.avatar || ''
|
||||
}
|
||||
};
|
||||
|
||||
await syncSessionWithBackend(authResponse);
|
||||
// window.location.href = '/profile';
|
||||
await syncSessionWithBackend(authResponse, avatarUrl);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error(`${provider} Login failed:`, error);
|
||||
setStatus({
|
||||
@@ -101,22 +115,21 @@ const LoginPage = () => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const syncSessionWithBackend = async (authData: AuthResponse) => {
|
||||
|
||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Important for cookies
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: 'new',
|
||||
accessToken: authData.token,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name,
|
||||
avatar: authData.record.avatar
|
||||
? pb.files.getUrl(authData.record, authData.record.avatar)
|
||||
: '',
|
||||
isAuthenticated: true,
|
||||
id: authData.record.id
|
||||
query: 'new',
|
||||
accessToken: authData.token,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name,
|
||||
avatar: avatarUrl,
|
||||
isAuthenticated: true,
|
||||
id: authData.record.id
|
||||
})
|
||||
});
|
||||
|
||||
@@ -128,6 +141,7 @@ const LoginPage = () => {
|
||||
console.log('Session synced with backend:', data);
|
||||
} catch (error) {
|
||||
console.error('Error syncing session:', error);
|
||||
throw error; // Re-throw the error if you want calling functions to handle it
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
273
src/components/PrintInvoice.tsx
Normal file
273
src/components/PrintInvoice.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Invoice {
|
||||
invoice_id: number;
|
||||
invoice_number: string;
|
||||
customer_id: string;
|
||||
invoice_date: string;
|
||||
due_date: string;
|
||||
subtotal: number;
|
||||
tax_amount: number;
|
||||
total_amount: number;
|
||||
status: string;
|
||||
payment_terms: string | null;
|
||||
notes: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export default function PrintInvoice() {
|
||||
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const getInvoiceListData = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data);
|
||||
} catch (error: any) {
|
||||
console.error('Fetch error:', error);
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
const viewInvoiceDetails = (invoice: Invoice) => {
|
||||
setSelectedInvoice(invoice);
|
||||
}
|
||||
|
||||
const closeInvoiceDetails = () => {
|
||||
setSelectedInvoice(null);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="p-4 text-red-600">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading invoice data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-6xl mx-auto">
|
||||
{/* Invoice List */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Invoices</h1>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Invoice #</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Due Date</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{invoiceList.map((invoice) => (
|
||||
<tr key={invoice.invoice_id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">{invoice.invoice_number}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.invoice_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.due_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatCurrency(invoice.total_amount)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
${invoice.status === 'paid' ? 'bg-green-100 text-green-800' :
|
||||
invoice.status === 'draft' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'}`}>
|
||||
{invoice.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => viewInvoiceDetails(invoice)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-2"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Print
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Detail Modal */}
|
||||
{selectedInvoice && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Printable Invoice */}
|
||||
<div id="printable-invoice" className="p-8">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">INVOICE</h1>
|
||||
<p className="text-gray-600">{selectedInvoice.invoice_number}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-gray-600">Status: <span className={`font-semibold
|
||||
${selectedInvoice.status === 'paid' ? 'text-green-600' :
|
||||
selectedInvoice.status === 'draft' ? 'text-yellow-600' :
|
||||
'text-red-600'}`}>
|
||||
{selectedInvoice.status.toUpperCase()}
|
||||
</span></p>
|
||||
<p className="text-gray-600">Date: {formatDate(selectedInvoice.invoice_date)}</p>
|
||||
<p className="text-gray-600">Due: {formatDate(selectedInvoice.due_date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">From</h2>
|
||||
<p className="text-gray-800">Your Company Name</p>
|
||||
<p className="text-gray-600">123 Business Street</p>
|
||||
<p className="text-gray-600">City, State 12345</p>
|
||||
<p className="text-gray-600">contact@yourcompany.com</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Bill To</h2>
|
||||
<p className="text-gray-800">Customer ID: {selectedInvoice.customer_id}</p>
|
||||
<p className="text-gray-600">[Customer Name]</p>
|
||||
<p className="text-gray-600">[Customer Address]</p>
|
||||
<p className="text-gray-600">[Customer Email]</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-b border-gray-200 py-4 mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 font-semibold text-gray-700">
|
||||
<div className="col-span-6">Description</div>
|
||||
<div className="col-span-2 text-right">Subtotal</div>
|
||||
<div className="col-span-2 text-right">Tax</div>
|
||||
<div className="col-span-2 text-right">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 mb-2">
|
||||
<div className="col-span-6 text-gray-800">[Service/Product Description]</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.subtotal)}</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.tax_amount)}</div>
|
||||
<div className="col-span-2 text-right font-semibold">{formatCurrency(selectedInvoice.total_amount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-8">
|
||||
<div className="w-64">
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Subtotal:</span>
|
||||
<span>{formatCurrency(selectedInvoice.subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Tax:</span>
|
||||
<span>{formatCurrency(selectedInvoice.tax_amount)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 font-bold text-lg">
|
||||
<span>Total:</span>
|
||||
<span>{formatCurrency(selectedInvoice.total_amount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedInvoice.notes && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Notes</h2>
|
||||
<p className="text-gray-600">{selectedInvoice.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-8 border-t border-gray-200">
|
||||
<p className="text-gray-600 text-center">Thank you for your business!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-end">
|
||||
<button
|
||||
onClick={closeInvoiceDetails}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 mr-2"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Print Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Print Styles */}
|
||||
<style>
|
||||
{`
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
#printable-invoice, #printable-invoice * {
|
||||
visibility: visible;
|
||||
}
|
||||
#printable-invoice {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 20mm;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
src/components/UpdateAvatar.tsx
Normal file
52
src/components/UpdateAvatar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { X } from "lucide-react"; // Import an icon for the remove button
|
||||
|
||||
export function AvatarUpload() {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setSelectedFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
const handleRemoveFile = () => {
|
||||
setSelectedFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''; // Reset file input
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!selectedFile ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label
|
||||
htmlFor="avatar"
|
||||
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colorsfocus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
Change Avatar
|
||||
</Label>
|
||||
<Input type="file" id="avatar" ref={fileInputRef} accept="image/jpeg,image/png,image/gif" className="hidden" onChange={handleFileChange}/>
|
||||
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>Browse</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
JPG, GIF or PNG. 1MB max.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-3 space-x-2">
|
||||
<div className="truncate max-w-[200px]">
|
||||
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
|
||||
</div>
|
||||
<Button size="sm" className="text-xs p-1 h-fit">Update</Button>
|
||||
<Button size="sm" onClick={handleRemoveFile} className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit">Remove</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default AvatarUpload;
|
||||
@@ -7,6 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from ".
|
||||
import { Separator } from "./ui/separator";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import UpdateAvatar from './UpdateAvatar';
|
||||
|
||||
interface SessionData {
|
||||
[key: string]: any;
|
||||
@@ -21,37 +22,61 @@ interface UserData {
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
const [invoiceList, setInvoiceList] = useState<any[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const fetchSessionData = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'http://localhost:2058/host-api/v1/users/get-profile-data/',
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
credentials: 'include', // Crucial for cookies
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || 'Session fetch failed');
|
||||
}
|
||||
const data: UserData = await response.json();
|
||||
// console.log('success message', data.success);
|
||||
if(data.success === true){
|
||||
setUserData(data);
|
||||
// console.log('User Data', data);
|
||||
}
|
||||
|
||||
setUserData(data);
|
||||
return data.session_data;
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setError(error instanceof Error ? error.message : 'An unknown error occurred');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const getInvoiceListData = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include', // Crucial for cookies
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data); // Fix: Use `data.data` instead of `data`
|
||||
return data.data; // Fix: `session_data` does not exist in response
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fetchData();
|
||||
|
||||
fetchSessionData();
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
@@ -63,12 +88,6 @@ export default function ProfilePage() {
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6 container mx-auto">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Profile</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Update your profile settings.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||
<div className="flex-1 lg:max-w-2xl">
|
||||
@@ -85,14 +104,8 @@ export default function ProfilePage() {
|
||||
<AvatarImage src={userData.session_data?.user_avatar} />
|
||||
<AvatarFallback>JP</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<Button size="sm">
|
||||
Change avatar
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
JPG, GIF or PNG. 1MB max.
|
||||
</p>
|
||||
</div>
|
||||
<UpdateAvatar />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
@@ -111,56 +124,39 @@ export default function ProfilePage() {
|
||||
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio">Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
defaultValue="I'm a software developer based in New York."
|
||||
className="resize-none"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your application preferences.
|
||||
</CardDescription>
|
||||
<CardTitle>Billing Information</CardTitle>
|
||||
<CardDescription>
|
||||
View your billing history.
|
||||
</CardDescription>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Invoice ID</th>
|
||||
<th className="text-left">Date</th>
|
||||
<th className="text-left">Description</th>
|
||||
<th className="text-right">Amount</th>
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
invoiceList.map((invoice) => (
|
||||
<tr key={invoice.id}>
|
||||
<td>{invoice.invoice_id}</td>
|
||||
<td>{invoice.date}</td>
|
||||
<td>{invoice.description}</td>
|
||||
<td className="text-right">{invoice.amount}</td>
|
||||
<td className="text-center"><a href="">Print</a></td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">Language</Label>
|
||||
<Select defaultValue="english">
|
||||
<SelectTrigger id="language">
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="english">English</SelectItem>
|
||||
<SelectItem value="french">French</SelectItem>
|
||||
<SelectItem value="german">German</SelectItem>
|
||||
<SelectItem value="spanish">Spanish</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<Select defaultValue="est">
|
||||
<SelectTrigger id="timezone">
|
||||
<SelectValue placeholder="Select timezone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="est">Eastern Standard Time (EST)</SelectItem>
|
||||
<SelectItem value="cst">Central Standard Time (CST)</SelectItem>
|
||||
<SelectItem value="mst">Mountain Standard Time (MST)</SelectItem>
|
||||
<SelectItem value="pst">Pacific Standard Time (PST)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -188,8 +184,7 @@ export default function ProfilePage() {
|
||||
<Button className="mt-4">Update password</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Danger Zone</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -198,7 +193,7 @@ export default function ProfilePage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Button >Delete account</Button>
|
||||
<Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
|
||||
<Button variant="outline">Export data</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
22
src/components/atoms/SelectWithLabel/SelectWithLabel.tsx
Normal file
22
src/components/atoms/SelectWithLabel/SelectWithLabel.tsx
Normal file
@@ -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
src/components/atoms/SelectWithLabel/index.tsx
Normal file
0
src/components/atoms/SelectWithLabel/index.tsx
Normal file
80
src/components/shared/TemplatePreview.jsx
Normal file
80
src/components/shared/TemplatePreview.jsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
20
src/components/shared/Toast.jsx
Normal file
20
src/components/shared/Toast.jsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
16
src/components/ui/container.tsx
Normal file
16
src/components/ui/container.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<div className={`max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Container;
|
||||
38
src/components/ui/table.tsx
Normal file
38
src/components/ui/table.tsx
Normal file
@@ -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;
|
||||
42
src/components/ui/typography.tsx
Normal file
42
src/components/ui/typography.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { ElementType } from 'react';
|
||||
|
||||
interface TypographyProps {
|
||||
variant: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body1' | 'body2' | 'caption';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
color?: string; // Add color prop
|
||||
}
|
||||
|
||||
const Typography: React.FC<TypographyProps> = ({ variant, children, className = '', color }) => {
|
||||
const variantStyles: Record<string, React.CSSProperties> = {
|
||||
h1: { fontSize: '2.5rem', fontWeight: 'bold' },
|
||||
h2: { fontSize: '2rem', fontWeight: 'bold' },
|
||||
h3: { fontSize: '1.75rem', fontWeight: 'bold' },
|
||||
h4: { fontSize: '1.5rem', fontWeight: 'bold' },
|
||||
h5: { fontSize: '1.25rem', fontWeight: 'bold' },
|
||||
h6: { fontSize: '1rem', fontWeight: 'bold' },
|
||||
body1: { fontSize: '1rem', fontWeight: 'normal' },
|
||||
body2: { fontSize: '0.875rem', fontWeight: 'normal' },
|
||||
caption: { fontSize: '0.75rem', fontWeight: 'normal' },
|
||||
};
|
||||
|
||||
const combinedStyle = { ...variantStyles[variant], ...(color && { color }), ...(className ? { className } : {}) };
|
||||
|
||||
const variantToTag: Record<string, ElementType> = {
|
||||
h1: 'h1',
|
||||
h2: 'h2',
|
||||
h3: 'h3',
|
||||
h4: 'h4',
|
||||
h5: 'h5',
|
||||
h6: 'h6',
|
||||
body1: 'p',
|
||||
body2: 'p',
|
||||
caption: 'span',
|
||||
};
|
||||
|
||||
const Tag: ElementType = variantToTag[variant];
|
||||
|
||||
return <Tag style={combinedStyle}>{children}</Tag>;
|
||||
};
|
||||
|
||||
export default Typography;
|
||||
103
src/hooks/useDeploymentConfig.js
Normal file
103
src/hooks/useDeploymentConfig.js
Normal file
@@ -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;
|
||||
62
src/hooks/useDnsVerification.js
Normal file
62
src/hooks/useDnsVerification.js
Normal file
@@ -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;
|
||||
267
src/hooks/useDomainConfig.js
Normal file
267
src/hooks/useDomainConfig.js
Normal file
@@ -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;
|
||||
57
src/hooks/useFormValidation.js
Normal file
57
src/hooks/useFormValidation.js
Normal file
@@ -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;
|
||||
36
src/hooks/useToast.js
Normal file
36
src/hooks/useToast.js
Normal file
@@ -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;
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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";
|
||||
|
||||
7
src/pages/print-invoice.astro
Normal file
7
src/pages/print-invoice.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Invoice from "../components/PrintInvoice";
|
||||
---
|
||||
<Layout title="">
|
||||
<Invoice client:load/>
|
||||
</Layout>
|
||||
72
src/utils/domainUtils.js
Normal file
72
src/utils/domainUtils.js
Normal file
@@ -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 };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user