feat: add Phase 3 provision UI and dashboard pages
Wallet-connected provisioning wizard at /provision with SIWE auth, subdomain picker, and multi-step deploy flow. Dashboard at /dashboard shows instance list with status and management controls. Adds wagmi + viem + react-query for wallet integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dca3140065
commit
908e2257e4
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
|
|
@ -21,6 +22,8 @@
|
|||
"sharp": "^0.34.5",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"viem": "^2.46.3",
|
||||
"wagmi": "^3.5.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -35,6 +38,12 @@
|
|||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@adraffy/ens-normalize": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
|
||||
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
|
|
@ -1236,6 +1245,45 @@
|
|||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
|
||||
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -2789,6 +2837,42 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip32": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
|
||||
"integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/curves": "~1.9.0",
|
||||
"@noble/hashes": "~1.8.0",
|
||||
"@scure/base": "~1.2.5"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip39": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
|
||||
"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "~1.8.0",
|
||||
"@scure/base": "~1.2.5"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
|
|
@ -3069,6 +3153,32 @@
|
|||
"tailwindcss": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.90.20",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
|
||||
"integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.90.21",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
||||
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.20"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
|
|
@ -3682,6 +3792,58 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@wagmi/core": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.0.tgz",
|
||||
"integrity": "sha512-EU5gDsUp5t7+cuLv12/L8hfyWfCIKsBNiiBqpOqxZJxvAcAiQk4xFe2jMgaQPqApc3Omvxrk032M8AQ4N0cQeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "5.0.1",
|
||||
"mipd": "0.0.7",
|
||||
"zustand": "5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/query-core": ">=5.0.0",
|
||||
"ox": ">=0.11.1",
|
||||
"typescript": ">=5.7.3",
|
||||
"viem": "2.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@tanstack/query-core": {
|
||||
"optional": true
|
||||
},
|
||||
"ox": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/abitype": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
|
||||
"integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4",
|
||||
"zod": "^3.22.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
|
|
@ -5060,6 +5222,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -5996,6 +6164,21 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isows": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
|
||||
"integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||
|
|
@ -6534,6 +6717,26 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mipd": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz",
|
||||
"integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wagmi-dev"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -6876,6 +7079,36 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ox": {
|
||||
"version": "0.12.4",
|
||||
"resolved": "https://registry.npmjs.org/ox/-/ox-0.12.4.tgz",
|
||||
"integrity": "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adraffy/ens-normalize": "^1.11.0",
|
||||
"@noble/ciphers": "^1.3.0",
|
||||
"@noble/curves": "1.9.1",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@scure/bip32": "^1.7.0",
|
||||
"@scure/bip39": "^1.6.0",
|
||||
"abitype": "^1.2.3",
|
||||
"eventemitter3": "5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
|
@ -8114,7 +8347,7 @@
|
|||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -8302,6 +8535,117 @@
|
|||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/viem": {
|
||||
"version": "2.46.3",
|
||||
"resolved": "https://registry.npmjs.org/viem/-/viem-2.46.3.tgz",
|
||||
"integrity": "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/curves": "1.9.1",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@scure/bip32": "1.7.0",
|
||||
"@scure/bip39": "1.6.0",
|
||||
"abitype": "1.2.3",
|
||||
"isows": "1.0.7",
|
||||
"ox": "0.12.4",
|
||||
"ws": "8.18.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wagmi": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.5.0.tgz",
|
||||
"integrity": "sha512-39uiY6Vkc28NiAHrxJzVTodoRgSVGG97EewwUxRf+jcFMTe8toAnaM8pJZA3Zw/6snMg4tSgWLJAtMnOacLe7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@wagmi/connectors": "7.2.1",
|
||||
"@wagmi/core": "3.4.0",
|
||||
"use-sync-external-store": "1.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": ">=5.0.0",
|
||||
"react": ">=18",
|
||||
"typescript": ">=5.7.3",
|
||||
"viem": "2.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wagmi/node_modules/@wagmi/connectors": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-7.2.1.tgz",
|
||||
"integrity": "sha512-/tyDepUMDM8eNzNX3ofjqHNRFZ6XcZ3u0+cQp5x0/LHCpMA8tRh7A1/e7dTrYiIJeL7iLgHzfHUXCsU02OKMLQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wevm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@base-org/account": "^2.5.1",
|
||||
"@coinbase/wallet-sdk": "^4.3.6",
|
||||
"@metamask/sdk": "~0.33.1",
|
||||
"@safe-global/safe-apps-provider": "~0.18.6",
|
||||
"@safe-global/safe-apps-sdk": "^9.1.0",
|
||||
"@wagmi/core": "3.4.0",
|
||||
"@walletconnect/ethereum-provider": "^2.21.1",
|
||||
"porto": "~0.2.35",
|
||||
"typescript": ">=5.7.3",
|
||||
"viem": "2.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@base-org/account": {
|
||||
"optional": true
|
||||
},
|
||||
"@coinbase/wallet-sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@metamask/sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@safe-global/safe-apps-provider": {
|
||||
"optional": true
|
||||
},
|
||||
"@safe-global/safe-apps-sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@walletconnect/ethereum-provider": {
|
||||
"optional": true
|
||||
},
|
||||
"porto": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wagmi/node_modules/use-sync-external-store": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
@ -8417,6 +8761,27 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
|
@ -8458,6 +8823,35 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz",
|
||||
"integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
|
|
@ -22,6 +23,8 @@
|
|||
"sharp": "^0.34.5",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"viem": "^2.46.3",
|
||||
"wagmi": "^3.5.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useAccount, useConnect } from "wagmi";
|
||||
import { injected } from "wagmi/connectors";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { InstanceCard, type Instance } from "@/components/InstanceCard";
|
||||
import {
|
||||
Wallet,
|
||||
Plus,
|
||||
Loader2,
|
||||
ServerOff,
|
||||
} from "lucide-react";
|
||||
import { apiFetch, apiHeaders } from "@/lib/api";
|
||||
import { getNonce, createSiweMessage } from "@/lib/siwe";
|
||||
import { useSignMessage } from "wagmi";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { address, isConnected, chain } = useAccount();
|
||||
const { connect } = useConnect();
|
||||
const { signMessageAsync } = useSignMessage();
|
||||
|
||||
const [siweToken, setSiweToken] = useState("");
|
||||
const [instances, setInstances] = useState<Instance[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [signingIn, setSigningIn] = useState(false);
|
||||
|
||||
const signIn = useCallback(async () => {
|
||||
if (!address || !chain) return;
|
||||
setSigningIn(true);
|
||||
try {
|
||||
const nonce = await getNonce();
|
||||
const message = createSiweMessage(address, nonce, chain.id);
|
||||
const signature = await signMessageAsync({ message });
|
||||
const token = btoa(JSON.stringify({ message, signature }));
|
||||
setSiweToken(token);
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
`Sign-in failed: ${e instanceof Error ? e.message : "Unknown error"}`
|
||||
);
|
||||
} finally {
|
||||
setSigningIn(false);
|
||||
}
|
||||
}, [address, chain, signMessageAsync]);
|
||||
|
||||
const fetchInstances = useCallback(async () => {
|
||||
if (!siweToken) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await apiFetch<{ instances: Instance[] }>("/v1/spaces", {
|
||||
headers: apiHeaders(siweToken),
|
||||
});
|
||||
setInstances(data.instances);
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
`Failed to load spaces: ${e instanceof Error ? e.message : "Unknown"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [siweToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (siweToken) fetchInstances();
|
||||
}, [siweToken, fetchInstances]);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (slug: string) => {
|
||||
if (!confirm(`Remove space "${slug}"? This will destroy all data.`))
|
||||
return;
|
||||
try {
|
||||
await apiFetch(`/v1/spaces/${slug}`, {
|
||||
method: "DELETE",
|
||||
headers: apiHeaders(siweToken),
|
||||
});
|
||||
toast.success(`Space "${slug}" is being removed`);
|
||||
fetchInstances();
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
`Delete failed: ${e instanceof Error ? e.message : "Unknown"}`
|
||||
);
|
||||
}
|
||||
},
|
||||
[siweToken, fetchInstances]
|
||||
);
|
||||
|
||||
// Not connected
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-16 text-center space-y-6">
|
||||
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto">
|
||||
<Wallet className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Connect your wallet to view and manage your Postiz spaces.
|
||||
</p>
|
||||
<Button onClick={() => connect({ connector: injected() })} size="lg">
|
||||
<Wallet className="mr-2 h-5 w-5" />
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Connected but not signed in
|
||||
if (!siweToken) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-16 text-center space-y-6">
|
||||
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto">
|
||||
<Wallet className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Sign in with your wallet to access your spaces.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-mono">
|
||||
{address?.slice(0, 6)}...{address?.slice(-4)}
|
||||
</span>
|
||||
<Badge variant="secondary">{chain?.name}</Badge>
|
||||
</div>
|
||||
<Button onClick={signIn} size="lg" disabled={signingIn}>
|
||||
{signingIn ? (
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<Wallet className="mr-2 h-5 w-5" />
|
||||
)}
|
||||
Sign Message to Continue
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Signed in
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Your Spaces</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage your deployed Postiz instances
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="secondary" className="font-mono">
|
||||
{address?.slice(0, 6)}...{address?.slice(-4)}
|
||||
</Badge>
|
||||
<Button asChild>
|
||||
<a href="/provision">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Space
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<Loader2 className="h-8 w-8 text-primary animate-spin mx-auto" />
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Loading your spaces...
|
||||
</p>
|
||||
</div>
|
||||
) : instances.length === 0 ? (
|
||||
<Card className="border-dashed border-2">
|
||||
<CardContent className="py-12 text-center space-y-4">
|
||||
<ServerOff className="h-12 w-12 text-muted-foreground/40 mx-auto" />
|
||||
<div>
|
||||
<h3 className="font-semibold">No spaces yet</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Deploy your first Postiz instance to get started with social
|
||||
media scheduling.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<a href="/provision">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Deploy Your First Space
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{instances.map((instance) => (
|
||||
<InstanceCard
|
||||
key={instance.id}
|
||||
instance={instance}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ export default function HomePage() {
|
|||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="text-lg px-8 border-primary/30 hover:bg-primary/5">
|
||||
<a href="#deploy">Deploy Your Own</a>
|
||||
<a href="/provision">Deploy Your Own</a>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -315,7 +315,7 @@ export default function HomePage() {
|
|||
Postiz handles scheduling, Temporal handles workflows, Traefik handles routing.
|
||||
</p>
|
||||
<div className="bg-card/80 backdrop-blur border rounded-lg p-4 max-w-lg mx-auto text-left font-mono text-sm">
|
||||
<p className="text-muted-foreground"># Clone and deploy</p>
|
||||
<p className="text-muted-foreground"># Or self-host manually</p>
|
||||
<p>git clone https://gitea.jeffemmett.com/jeffemmett/rsocials-online</p>
|
||||
<p>cd rsocials-online/postiz</p>
|
||||
<p>cp .env.example .env</p>
|
||||
|
|
@ -324,14 +324,14 @@ export default function HomePage() {
|
|||
</div>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<Button asChild size="lg" className="text-lg px-8 bg-gradient-to-r from-primary to-accent hover:opacity-90">
|
||||
<a href="https://socials.crypto-commons.org">
|
||||
Try the Live Instance
|
||||
<a href="/provision">
|
||||
Deploy with 1 Click
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="text-lg px-8 border-primary/30 hover:bg-primary/5">
|
||||
<a href="https://github.com/gitroomhq/postiz-app" target="_blank" rel="noopener noreferrer">
|
||||
Postiz on GitHub
|
||||
<a href="https://socials.crypto-commons.org" target="_blank" rel="noopener noreferrer">
|
||||
Try the Live Instance
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { ProvisionForm } from "@/components/ProvisionForm";
|
||||
|
||||
export const metadata = {
|
||||
title: "Provision a Space - rSocials",
|
||||
description:
|
||||
"Deploy your own Postiz social media scheduling instance for your community.",
|
||||
};
|
||||
|
||||
export default function ProvisionPage() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-8 py-8">
|
||||
<div className="text-center space-y-3">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-primary/10 text-primary border-primary/20"
|
||||
>
|
||||
Self-Service Provisioning
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-bold">Deploy Your Postiz Space</h1>
|
||||
<p className="text-muted-foreground max-w-md mx-auto">
|
||||
Connect your wallet, choose a subdomain, and get a fully managed
|
||||
Postiz instance in under 2 minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProvisionForm />
|
||||
|
||||
<div className="text-center text-xs text-muted-foreground space-y-1">
|
||||
<p>
|
||||
Each space includes Postiz, PostgreSQL, Redis, and Temporal — all
|
||||
managed for you.
|
||||
</p>
|
||||
<p>
|
||||
Your data stays on our EU-based server. Cancel anytime from the{" "}
|
||||
<a href="/dashboard" className="text-primary hover:underline">
|
||||
dashboard
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Globe,
|
||||
ExternalLink,
|
||||
Trash2,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface Instance {
|
||||
id: string;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
primaryDomain: string;
|
||||
fallbackDomain: string;
|
||||
owner: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const statusConfig: Record<
|
||||
string,
|
||||
{ label: string; color: string; icon: React.ReactNode }
|
||||
> = {
|
||||
active: {
|
||||
label: "Active",
|
||||
color: "bg-green-500/10 text-green-600",
|
||||
icon: <CheckCircle2 className="h-3 w-3" />,
|
||||
},
|
||||
provisioning: {
|
||||
label: "Deploying",
|
||||
color: "bg-yellow-500/10 text-yellow-600",
|
||||
icon: <Loader2 className="h-3 w-3 animate-spin" />,
|
||||
},
|
||||
failed: {
|
||||
label: "Failed",
|
||||
color: "bg-destructive/10 text-destructive",
|
||||
icon: <AlertCircle className="h-3 w-3" />,
|
||||
},
|
||||
destroying: {
|
||||
label: "Removing",
|
||||
color: "bg-orange-500/10 text-orange-600",
|
||||
icon: <Loader2 className="h-3 w-3 animate-spin" />,
|
||||
},
|
||||
destroyed: {
|
||||
label: "Removed",
|
||||
color: "bg-muted text-muted-foreground",
|
||||
icon: <Clock className="h-3 w-3" />,
|
||||
},
|
||||
};
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export function InstanceCard({
|
||||
instance,
|
||||
onDelete,
|
||||
}: {
|
||||
instance: Instance;
|
||||
onDelete?: (slug: string) => void;
|
||||
}) {
|
||||
const status = statusConfig[instance.status] ?? statusConfig.active;
|
||||
|
||||
return (
|
||||
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
||||
<CardContent className="pt-5 pb-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<Globe className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">{instance.displayName}</h3>
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{instance.fallbackDomain}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={`${status.color} flex items-center gap-1`}>
|
||||
{status.icon}
|
||||
{status.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||
<div>
|
||||
<span className="block font-medium text-foreground/80">
|
||||
Created
|
||||
</span>
|
||||
{formatDate(instance.createdAt)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="block font-medium text-foreground/80">
|
||||
Primary Domain
|
||||
</span>
|
||||
<span className="font-mono">{instance.primaryDomain}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button asChild variant="outline" size="sm" className="flex-1">
|
||||
<a
|
||||
href={`https://${instance.fallbackDomain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="mr-1 h-3 w-3" />
|
||||
Open
|
||||
</a>
|
||||
</Button>
|
||||
{onDelete && instance.status === "active" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => onDelete(instance.slug)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -28,11 +28,17 @@ export function Navbar() {
|
|||
Platforms
|
||||
</Link>
|
||||
<Link
|
||||
href="#deploy"
|
||||
href="/provision"
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Deploy
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="/zine"
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { WagmiProvider } from "wagmi";
|
||||
import { Toaster } from "sonner";
|
||||
import { wagmiConfig } from "@/lib/wagmi-config";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Toaster position="bottom-right" />
|
||||
</>
|
||||
<WagmiProvider config={wagmiConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Toaster position="bottom-right" />
|
||||
</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,459 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useAccount, useConnect, useDisconnect, useSignMessage } from "wagmi";
|
||||
import { injected } from "wagmi/connectors";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
Wallet,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
Globe,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
import { apiFetch, apiHeaders } from "@/lib/api";
|
||||
import { getNonce, createSiweMessage } from "@/lib/siwe";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type Step = "connect" | "configure" | "confirm" | "provisioning" | "done";
|
||||
|
||||
interface ProvisionResult {
|
||||
instance: {
|
||||
slug: string;
|
||||
primaryDomain: string;
|
||||
fallbackDomain: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
const RESERVED_SLUGS = new Set([
|
||||
"www",
|
||||
"api",
|
||||
"admin",
|
||||
"mail",
|
||||
"app",
|
||||
"staging",
|
||||
"test",
|
||||
"dev",
|
||||
"socials",
|
||||
"cc",
|
||||
"crypto-commons",
|
||||
"votc",
|
||||
"p2pf",
|
||||
"bcrg",
|
||||
]);
|
||||
|
||||
const SLUG_RE = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/;
|
||||
|
||||
export function ProvisionForm() {
|
||||
const { address, isConnected, chain } = useAccount();
|
||||
const { connect } = useConnect();
|
||||
const { disconnect } = useDisconnect();
|
||||
const { signMessageAsync } = useSignMessage();
|
||||
|
||||
const [step, setStep] = useState<Step>("connect");
|
||||
const [slug, setSlug] = useState("");
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [slugError, setSlugError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<ProvisionResult | null>(null);
|
||||
const [provisionError, setProvisionError] = useState("");
|
||||
const [siweToken, setSiweToken] = useState("");
|
||||
|
||||
const validateSlug = useCallback((value: string) => {
|
||||
const v = value.toLowerCase();
|
||||
if (!v) {
|
||||
setSlugError("");
|
||||
return false;
|
||||
}
|
||||
if (v.length < 3) {
|
||||
setSlugError("Must be at least 3 characters");
|
||||
return false;
|
||||
}
|
||||
if (!SLUG_RE.test(v)) {
|
||||
setSlugError(
|
||||
"Only lowercase letters, numbers, and hyphens. Must start/end with alphanumeric."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (RESERVED_SLUGS.has(v)) {
|
||||
setSlugError("This subdomain is reserved");
|
||||
return false;
|
||||
}
|
||||
setSlugError("");
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const handleConnect = useCallback(() => {
|
||||
connect({ connector: injected() });
|
||||
}, [connect]);
|
||||
|
||||
const handleSignIn = useCallback(async () => {
|
||||
if (!address || !chain) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const nonce = await getNonce();
|
||||
const message = createSiweMessage(address, nonce, chain.id);
|
||||
const signature = await signMessageAsync({ message });
|
||||
// The signature + message together serve as the bearer token
|
||||
const token = btoa(JSON.stringify({ message, signature }));
|
||||
setSiweToken(token);
|
||||
setStep("configure");
|
||||
toast.success("Wallet connected and signed in");
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
`Sign-in failed: ${e instanceof Error ? e.message : "Unknown error"}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [address, chain, signMessageAsync]);
|
||||
|
||||
const handleProvision = useCallback(async () => {
|
||||
if (!validateSlug(slug) || !displayName.trim()) return;
|
||||
setLoading(true);
|
||||
setProvisionError("");
|
||||
setStep("provisioning");
|
||||
|
||||
try {
|
||||
const res = await apiFetch<ProvisionResult>("/v1/spaces", {
|
||||
method: "POST",
|
||||
headers: apiHeaders(siweToken),
|
||||
body: JSON.stringify({
|
||||
slug: slug.toLowerCase(),
|
||||
displayName: displayName.trim(),
|
||||
}),
|
||||
});
|
||||
setResult(res);
|
||||
setStep("done");
|
||||
toast.success("Your space is live!");
|
||||
} catch (e) {
|
||||
setProvisionError(
|
||||
e instanceof Error ? e.message : "Provisioning failed"
|
||||
);
|
||||
setStep("confirm");
|
||||
toast.error("Provisioning failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [slug, displayName, siweToken, validateSlug]);
|
||||
|
||||
const progress =
|
||||
step === "connect"
|
||||
? 0
|
||||
: step === "configure"
|
||||
? 33
|
||||
: step === "confirm"
|
||||
? 66
|
||||
: step === "provisioning"
|
||||
? 80
|
||||
: 100;
|
||||
|
||||
return (
|
||||
<div className="max-w-lg mx-auto space-y-6">
|
||||
<Progress value={progress} className="h-2" />
|
||||
|
||||
{/* Step 1: Connect Wallet */}
|
||||
{step === "connect" && (
|
||||
<Card className="border-2 border-primary/30">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto">
|
||||
<Wallet className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">Connect Your Wallet</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Sign in with your Ethereum wallet to provision a Postiz instance
|
||||
for your community.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isConnected ? (
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
<Wallet className="mr-2 h-5 w-5" />
|
||||
Connect Wallet
|
||||
</Button>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Connected</p>
|
||||
<p className="font-mono text-sm">
|
||||
{address?.slice(0, 6)}...{address?.slice(-4)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="bg-green-500/10 text-green-600">
|
||||
{chain?.name ?? "Unknown"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => disconnect()}
|
||||
className="flex-1"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSignIn}
|
||||
disabled={loading}
|
||||
className="flex-1"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowRight className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Sign In
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 2: Configure Space */}
|
||||
{step === "configure" && (
|
||||
<Card className="border-2 border-primary/30">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="h-16 w-16 rounded-full bg-accent/10 flex items-center justify-center mx-auto">
|
||||
<Globe className="h-8 w-8 text-accent" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">Configure Your Space</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose a name and subdomain for your Postiz instance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Community Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="My Community"
|
||||
className="w-full px-3 py-2 rounded-md border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Subdomain
|
||||
</label>
|
||||
<div className="flex items-center gap-0">
|
||||
<input
|
||||
type="text"
|
||||
value={slug}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
||||
setSlug(v);
|
||||
validateSlug(v);
|
||||
}}
|
||||
placeholder="my-community"
|
||||
className="flex-1 px-3 py-2 rounded-l-md border border-r-0 bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
maxLength={30}
|
||||
/>
|
||||
<span className="px-3 py-2 bg-muted border rounded-r-md text-sm text-muted-foreground whitespace-nowrap">
|
||||
.rsocials.online
|
||||
</span>
|
||||
</div>
|
||||
{slugError && (
|
||||
<p className="text-xs text-destructive mt-1">{slugError}</p>
|
||||
)}
|
||||
{slug && !slugError && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Your space will be at{" "}
|
||||
<span className="font-mono text-foreground">
|
||||
{slug}.rsocials.online
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setStep("connect")}
|
||||
className="flex-1"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setStep("confirm")}
|
||||
disabled={!slug || !!slugError || !displayName.trim()}
|
||||
className="flex-1"
|
||||
>
|
||||
Review
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 3: Confirm */}
|
||||
{step === "confirm" && (
|
||||
<Card className="border-2 border-primary/30">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="h-16 w-16 rounded-full bg-green-500/10 flex items-center justify-center mx-auto">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">Confirm Provisioning</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 p-4 bg-muted rounded-lg text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Name</span>
|
||||
<span className="font-medium">{displayName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Subdomain</span>
|
||||
<span className="font-mono">{slug}.rsocials.online</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Owner</span>
|
||||
<span className="font-mono text-xs">
|
||||
{address?.slice(0, 6)}...{address?.slice(-4)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
This will deploy a new Postiz instance with its own database,
|
||||
scheduler, and domain routing. Takes about 2 minutes.
|
||||
</p>
|
||||
|
||||
{provisionError && (
|
||||
<div className="flex items-center gap-2 p-3 bg-destructive/10 rounded-lg text-sm text-destructive">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
{provisionError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setStep("configure")}
|
||||
className="flex-1"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleProvision}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-gradient-to-r from-primary to-accent hover:opacity-90"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Deploy Space
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 4: Provisioning */}
|
||||
{step === "provisioning" && (
|
||||
<Card className="border-2 border-primary/30">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="text-center space-y-3">
|
||||
<Loader2 className="h-12 w-12 text-primary animate-spin mx-auto" />
|
||||
<h2 className="text-xl font-bold">Deploying Your Space</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Setting up containers, database, and routing...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Generating compose configuration...
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Starting Postiz containers...
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Configuring domain routing...
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 5: Done */}
|
||||
{step === "done" && result && (
|
||||
<Card className="border-2 border-green-500/30 bg-gradient-to-br from-green-500/5 to-transparent">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="h-16 w-16 rounded-full bg-green-500/10 flex items-center justify-center mx-auto">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">Your Space is Live!</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{displayName} is now deployed and ready to use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 p-4 bg-muted rounded-lg text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">URL</span>
|
||||
<a
|
||||
href={`https://${result.instance.fallbackDomain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline font-mono"
|
||||
>
|
||||
{result.instance.fallbackDomain}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Status</span>
|
||||
<Badge className="bg-green-500/10 text-green-600">
|
||||
{result.instance.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button asChild className="flex-1">
|
||||
<a
|
||||
href={`https://${result.instance.fallbackDomain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Open Postiz
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" asChild className="flex-1">
|
||||
<a href="/dashboard">View Dashboard</a>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
const API_BASE =
|
||||
process.env.NEXT_PUBLIC_API_URL ?? "https://api.rsocials.online";
|
||||
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
opts?: RequestInit
|
||||
): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...opts?.headers,
|
||||
},
|
||||
...opts,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ error: res.statusText }));
|
||||
throw new Error(body.error ?? `API error ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function apiHeaders(token: string) {
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { apiFetch } from "./api";
|
||||
|
||||
export async function getNonce(): Promise<string> {
|
||||
const data = await apiFetch<{ nonce: string }>("/v1/auth/nonce");
|
||||
return data.nonce;
|
||||
}
|
||||
|
||||
export function createSiweMessage(
|
||||
address: string,
|
||||
nonce: string,
|
||||
chainId: number
|
||||
): string {
|
||||
const domain = window.location.host;
|
||||
const origin = window.location.origin;
|
||||
const issuedAt = new Date().toISOString();
|
||||
|
||||
return [
|
||||
`${domain} wants you to sign in with your Ethereum account:`,
|
||||
address,
|
||||
"",
|
||||
"Sign in to rSocials to provision your social media instance.",
|
||||
"",
|
||||
`URI: ${origin}`,
|
||||
`Version: 1`,
|
||||
`Chain ID: ${chainId}`,
|
||||
`Nonce: ${nonce}`,
|
||||
`Issued At: ${issuedAt}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { http, createConfig } from "wagmi";
|
||||
import { base, baseSepolia } from "wagmi/chains";
|
||||
import { injected, walletConnect } from "wagmi/connectors";
|
||||
|
||||
const projectId = process.env.NEXT_PUBLIC_WC_PROJECT_ID ?? "";
|
||||
|
||||
export const wagmiConfig = createConfig({
|
||||
chains: [base, baseSepolia],
|
||||
connectors: [
|
||||
injected(),
|
||||
...(projectId ? [walletConnect({ projectId })] : []),
|
||||
],
|
||||
transports: {
|
||||
[base.id]: http(),
|
||||
[baseSepolia.id]: http(),
|
||||
},
|
||||
});
|
||||
|
|
@ -30,5 +30,5 @@
|
|||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "api"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue