unify-0.0.1
1
.gitignore
vendored
@ -13,6 +13,7 @@ wit/
|
||||
packages/**/pkg/*.wasm
|
||||
packages/**/wit
|
||||
kinode/packages/**/pkg/ui
|
||||
*/**/node_modules
|
||||
.env
|
||||
kinode/src/bootstrapped_processes.rs
|
||||
kinode/packages/**/wasi_snapshot_preview1.wasm
|
||||
|
@ -15,8 +15,8 @@
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-CPkF34RS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-jnOcECnM.css">
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-p7W2iPaq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-7ftanDAW.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
18
kinode/packages/app_store/ui/.eslintrc.cjs
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
26
kinode/packages/app_store/ui/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
src/abis/types
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
65
kinode/packages/app_store/ui/README.md
Normal file
@ -0,0 +1,65 @@
|
||||
# App Store UI
|
||||
|
||||
This UI template uses the React framework compiled with Vite.
|
||||
It is based on the Vite React Typescript template.
|
||||
|
||||
## Setup
|
||||
|
||||
There are 2 ways to set up this repo:
|
||||
1. Place this repo next to the `pkg` repo of your Kinode project (usually the top level).
|
||||
2. Set `BASE_URL` in `vite.config.ts` to the URL of your Kinode project (i.e. `/chess:chess:sys`) and comment out lines 8 and 9.
|
||||
|
||||
## Development
|
||||
|
||||
Run `npm i`, `npm run tc`, and then `npm start` to start working on the UI.
|
||||
By default, the dev server will proxy requests to `http://localhost:8080`.
|
||||
You can change the proxy target by setting `VITE_NODE_URL` like so:
|
||||
`VITE_NODE_URL=http://localhost:8081 npm start`
|
||||
|
||||
You may see an error:
|
||||
|
||||
```
|
||||
[vite] Pre-transform error: Failed to load url /our.js (resolved id: /our.js). Does the file exist?
|
||||
```
|
||||
|
||||
You can safely ignore this error. The file will be served by the node via the proxy.
|
||||
|
||||
#### public vs assets
|
||||
|
||||
The `public/assets` folder contains files that are referenced in `index.html`, `src/assets` is for asset files that are only referenced in `src` code.
|
||||
|
||||
## Building
|
||||
|
||||
Run `npm run build`, the build will be generated in the `dist` directory.
|
||||
If this repo is next to your Kinode `pkg` directory then you can `npm run build:copy` to build and copy it for installation.
|
||||
|
||||
## About Vite + React
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
22
kinode/packages/app_store/ui/chat_metadata.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Chat Template",
|
||||
"subtitle": "The chat template from kit",
|
||||
"description": "The kit chat template is the default app when starting a new kit project. This app is the basic version of that, packaged for the app store.",
|
||||
"image": "https://st4.depositphotos.com/7662228/30134/v/450/depositphotos_301343880-stock-illustration-best-chat-speech-bubble-icon.jpg",
|
||||
"version": "0.1.2",
|
||||
"license": "MIT",
|
||||
"website": "https://kinode.org",
|
||||
"screenshots": [
|
||||
"https://pongo-uploads.s3.us-east-2.amazonaws.com/Screenshot+2024-01-30+at+10.01.46+PM.png",
|
||||
"https://pongo-uploads.s3.us-east-2.amazonaws.com/Screenshot+2024-01-30+at+10.01.52+PM.png"
|
||||
],
|
||||
"mirrors": [
|
||||
"odinsbadeye.os"
|
||||
],
|
||||
"versions": [
|
||||
"a2c584bf63a730efdc79ec0a3c93bc97eba4e8745c633e3abe090b4f7e270e92",
|
||||
"c13f7ae39fa7f652164cfc1db305cd864cc1dc5f33827a2d74f7dde70ef36662",
|
||||
"09d24205d8e1f3634448e881db200b88ad691bbdaabbccb885b225147ba4a93e",
|
||||
"733be24324802a35944a73f355595f781de65d9d6e393bdabe879edcb77dfb62"
|
||||
]
|
||||
}
|
25
kinode/packages/app_store/ui/index.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!-- This sets window.our.node -->
|
||||
<script src="/our.js"></script>
|
||||
|
||||
<title>Package Store</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<link rel="icon"
|
||||
href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzc5IiBoZWlnaHQ9IjUxNCIgdmlld0JveD0iMCAwIDc3OSA1MTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8c3R5bGU+CiAgICAgICAgQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykgewogICAgICAgICAgICBzdmcgeyBmaWxsOiB3aGl0ZTsgfQogICAgICAgIH0KICAgICAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBsaWdodCkgewogICAgICAgICAgICBzdmcgeyBmaWxsOiBibGFjazsgfQogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8cGF0aCBkPSJNNzUzLjA5MiA1LjkxOTMyQzc1Ni41NTcgNS4wOTk3NiA3NTUuOTYyIC0wLjAwMDEyMjA3IDc1Mi40MDEgLTAuMDAwMTIyMDdINDI2LjAwMUM0MjQuNzU1IC0wLjAwMDEyMjA3IDQyMy42MzkgMC43NzAyNyA0MjMuMTk3IDEuOTM1MzVMMjM2Ljk2OCA0OTIuNkMyMzUuNzI5IDQ5NS44NjUgMjQwLjEyMyA0OTguMjU1IDI0Mi4xOTEgNDk1LjQ0MUw1NjkuMzU3IDUwLjExMzJDNTY5Ljc3OCA0OS41MzkyIDU3MC4zOTEgNDkuMTMzOSA1NzEuMDg0IDQ4Ljk3TDc1My4wOTIgNS45MTkzMloiLz4KICAgIDxwYXRoIGQ9Ik0xMS45NjY1IDQwLjIyODhDOS4xMDk0OSAzOC43NzcgMTAuMjEzNSAzNC40NTgzIDEzLjQxNjcgMzQuNTU1N0w0MDQuMjczIDQ2LjQzNjdDNDA2LjMzNCA0Ni40OTkzIDQwNy43MTkgNDguNTc0OSA0MDYuOTg2IDUwLjUwMjNMMzQ3LjQzOCAyMDYuOTgxQzM0Ni44MDQgMjA4LjY0NyAzNDQuODY1IDIwOS4zOTYgMzQzLjI3NSAyMDguNTg4TDExLjk2NjUgNDAuMjI4OFoiLz4KPC9zdmc+Cg==">
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
9057
kinode/packages/app_store/ui/package-lock.json
generated
Normal file
53
kinode/packages/app_store/ui/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "kit-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"start": "vite --port 3000",
|
||||
"build": "tsc && vite build",
|
||||
"copy": "mkdir -p ../pkg/ui && rm -rf ../pkg/ui/* && cp -r dist/* ../pkg/ui/",
|
||||
"build:copy": "npm run build && npm run copy",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"tc": "typechain --target ethers-v5 --out-dir src/abis/types/ \"./src/abis/**/*.json\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@ethersproject/hash": "^5.7.0",
|
||||
"@kinode/client-api": "^0.1.0",
|
||||
"@szhsin/react-menu": "^4.1.0",
|
||||
"@web3-react/coinbase-wallet": "^8.2.3",
|
||||
"@web3-react/core": "^8.2.2",
|
||||
"@web3-react/gnosis-safe": "^8.2.4",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/metamask": "^8.2.3",
|
||||
"@web3-react/network": "^8.2.3",
|
||||
"@web3-react/types": "^8.2.2",
|
||||
"@web3-react/walletconnect": "^8.2.3",
|
||||
"@web3-react/walletconnect-connector": "^6.2.13",
|
||||
"@web3-react/walletconnect-v2": "^8.5.1",
|
||||
"ethers": "^5.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-router-dom": "^6.21.3",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typechain/ethers-v5": "^11.1.1",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"typechain": "^8.3.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
1
kinode/packages/app_store/ui/public/assets/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
353
kinode/packages/app_store/ui/src/App.css
Normal file
@ -0,0 +1,353 @@
|
||||
#root {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
width: 75%;
|
||||
max-height: calc(100vh - 64px);
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
/* General */
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.row.center {
|
||||
justify-content: center;
|
||||
}
|
||||
.row.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.row.around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.col.center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--input-background);
|
||||
border-radius: 0.75em;
|
||||
border: 1px solid var(--orange-medium);
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
button.action-btn {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
button.small {
|
||||
padding: 0.25em 0.5em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Specific */
|
||||
|
||||
.searchbar {
|
||||
height: 2.25em;
|
||||
padding: .5em 1em;
|
||||
border-radius: 16px;
|
||||
flex: 1;
|
||||
background-color: var(--input-background);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.searchbar > input {
|
||||
border: none;
|
||||
height: 1.5em;
|
||||
margin-left: 0.5em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button.connect-wallet {
|
||||
margin: 1em auto 0;
|
||||
}
|
||||
|
||||
.my-pkg-btn {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.my-pkg-btn.selected {
|
||||
background-color: var(--bg-gray-medium);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
cursor: pointer;
|
||||
width: calc(100% - 10.3em);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.app-header:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.app-header.large:hover {
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.app-header.small > img {
|
||||
height: 3em;
|
||||
margin-right: 1em;
|
||||
border-radius: 0.375em;
|
||||
}
|
||||
|
||||
.app-header > img {
|
||||
height: 3em;
|
||||
margin-right: 1em;
|
||||
border-radius: 0.375em;
|
||||
}
|
||||
|
||||
.app-header.large > img {
|
||||
height: 5em;
|
||||
margin-right: 1em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
.app-header.large .app-name {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.app-entry {
|
||||
padding: 0.125em;
|
||||
width: 100%;
|
||||
}
|
||||
.app-actions {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.dropdown > ul {
|
||||
background-color: var(--orange-medium);
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 0.5em;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
border: 1px solid var(--orange-medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dropdown .dropdown-header {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.dropdown .dropdown-list {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: -0.5em;
|
||||
}
|
||||
|
||||
.page-selector {
|
||||
margin: 0.25em 0.5em;
|
||||
}
|
||||
.page-selector.selected {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-right: 1em;
|
||||
justify-content: center;
|
||||
width: 2.5em;
|
||||
}
|
||||
|
||||
.app-details {
|
||||
margin-top: 0.5em;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.app-details .title {
|
||||
width: 8em;
|
||||
text-align: left;
|
||||
}
|
||||
.app-details .value {
|
||||
margin-bottom: 0.5em;
|
||||
text-align: left;
|
||||
max-width: calc(100% - 8em);
|
||||
}
|
||||
.app-details .value.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.app-details .value.permission {
|
||||
background-color: var(--bg-gray-medium);
|
||||
border-radius: 2em;
|
||||
padding: 0.25em 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.app-screenshots {
|
||||
margin-top: 0.5em;
|
||||
overflow-x: scroll;
|
||||
max-width: 100%;
|
||||
}
|
||||
.app-screenshots > img {
|
||||
margin-right: 1em;
|
||||
max-height: 10em;
|
||||
max-width: 100%;
|
||||
border-radius: 0.5em;
|
||||
border: 1px solid var(--bg-gray-medium);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: var(--bg-gray-solid);
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.f-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#loading h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#loader {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
#loader div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 6px;
|
||||
border: 6px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
#loader div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
#loader div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
#loader div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes loader {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.action-entry {
|
||||
margin-bottom: 0.25em;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.action-entry:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.action-entry:first-child {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.my-apps-list {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
max-height: calc(100vh - 10em);
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: calc(100% - 6em);
|
||||
}
|
||||
|
||||
.title > div {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
min-height: 10em;
|
||||
min-width: 20em;
|
||||
}
|
||||
.modal-backdrop .close {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
font-size: 18px;
|
||||
font-weight: 200;
|
||||
cursor: pointer;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.modal {
|
||||
position: relative;
|
||||
background-color: var(--dark-background);
|
||||
color: black;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
line-height: 24px;
|
||||
max-width: 500px;
|
||||
min-width: 300px;
|
||||
color: var(--text-light);
|
||||
}
|
||||
.modal .modal-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.modal .modal-content {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
form.new {
|
||||
gap: 1em;
|
||||
}
|
||||
form.metadata {
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form.metadata input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form.metadata .row {
|
||||
margin-top: 1em;
|
||||
}
|
||||
form.metadata .col.label {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
align-items: center;
|
||||
margin: 1em 0;
|
||||
}
|
125
kinode/packages/app_store/ui/src/App.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import { ethers } from "ethers";
|
||||
import { Web3ReactProvider, Web3ReactHooks } from '@web3-react/core';
|
||||
import type { MetaMask } from '@web3-react/metamask'
|
||||
|
||||
import { PackageStore, PackageStore__factory } from "./abis/types";
|
||||
import StorePage from "./pages/StorePage";
|
||||
import MyAppsPage from "./pages/MyAppsPage";
|
||||
import AppPage from "./pages/AppPage";
|
||||
import { MY_APPS_PATH } from "./constants/path";
|
||||
import { ChainId, PACKAGE_STORE_ADDRESSES } from "./constants/chain";
|
||||
import PublishPage from "./pages/PublishPage";
|
||||
import { hooks as metaMaskHooks, metaMask } from './utils/metamask'
|
||||
import "./App.css";
|
||||
|
||||
const connectors: [MetaMask, Web3ReactHooks][] = [
|
||||
[metaMask, metaMaskHooks],
|
||||
]
|
||||
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
env: {
|
||||
VITE_SEPOLIA_RPC_URL: string;
|
||||
BASE_URL: string;
|
||||
VITE_NODE_URL?: string;
|
||||
DEV: boolean;
|
||||
};
|
||||
}
|
||||
interface Window {
|
||||
our: {
|
||||
node: string;
|
||||
process: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
useProvider,
|
||||
} = metaMaskHooks;
|
||||
|
||||
const RPC_URL = import.meta.env.VITE_SEPOLIA_RPC_URL;
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
if (window.our) window.our.process = BASE_URL?.replace("/", "");
|
||||
|
||||
const PROXY_TARGET = `${
|
||||
import.meta.env.VITE_NODE_URL || "http://localhost:8080"
|
||||
}${BASE_URL}`;
|
||||
|
||||
// This env also has BASE_URL which should match the process + package name
|
||||
const WEBSOCKET_URL = import.meta.env.DEV // eslint-disable-line
|
||||
? `${PROXY_TARGET.replace("http", "ws")}`
|
||||
: undefined;
|
||||
|
||||
function App() {
|
||||
const provider = useProvider();
|
||||
const [nodeConnected, setNodeConnected] = useState(true); // eslint-disable-line
|
||||
|
||||
const [packageAbi, setPackageAbi] = useState<PackageStore>(
|
||||
PackageStore__factory.connect(
|
||||
PACKAGE_STORE_ADDRESSES[ChainId.SEPOLIA],
|
||||
new ethers.providers.JsonRpcProvider(RPC_URL)) // TODO: get the RPC URL from the wallet
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
provider?.getNetwork().then(network => {
|
||||
if (network.chainId === ChainId.SEPOLIA) {
|
||||
setPackageAbi(PackageStore__factory.connect(
|
||||
PACKAGE_STORE_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner())
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [provider])
|
||||
|
||||
useEffect(() => {
|
||||
// if (window.our?.node && window.our?.process) {
|
||||
// const api = new KinodeClientApi({
|
||||
// uri: WEBSOCKET_URL,
|
||||
// nodeId: window.our.node,
|
||||
// processId: window.our.process,
|
||||
// onOpen: (_event, _api) => {
|
||||
// console.log("Connected to Kinode");
|
||||
// // api.send({ data: "Hello World" });
|
||||
// },
|
||||
// onMessage: (json, _api) => {
|
||||
// console.log('UNEXPECTED WEBSOCKET MESSAGE', json)
|
||||
// },
|
||||
// });
|
||||
|
||||
// setApi(api);
|
||||
// } else {
|
||||
// setNodeConnected(false);
|
||||
// }
|
||||
}, []);
|
||||
|
||||
if (!nodeConnected) {
|
||||
return (
|
||||
<div className="node-not-connected">
|
||||
<h2 style={{ color: "red" }}>Node not connected</h2>
|
||||
<h4>
|
||||
You need to start a node at {PROXY_TARGET} before you can use this UI
|
||||
in development.
|
||||
</h4>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const props = { provider, packageAbi };
|
||||
|
||||
return (
|
||||
<Web3ReactProvider connectors={connectors}>
|
||||
<Router basename={BASE_URL}>
|
||||
<Routes>
|
||||
<Route path="/" element={<StorePage {...props} />} />
|
||||
<Route path={MY_APPS_PATH} element={<MyAppsPage {...props} />} />
|
||||
<Route path="/app-details/:id" element={<AppPage {...props} />} />
|
||||
<Route path="/publish" element={<PublishPage {...props} />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Web3ReactProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
978
kinode/packages/app_store/ui/src/abis/PackageStore.json
Normal file
@ -0,0 +1,978 @@
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"name": "UPGRADE_INTERFACE_VERSION",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "approve",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "to",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "apps",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "packageName",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "publisherKnsNodeId",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "metadataUrl",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadataHash",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "balanceOf",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "contractURI",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "getApproved",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "getInitializedVersion",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint64",
|
||||
"internalType": "uint64"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "getPackageId",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "packageName",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "publisherName",
|
||||
"type": "bytes",
|
||||
"internalType": "bytes"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "pure"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "getPackageInfo",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "package",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "tuple",
|
||||
"internalType": "struct IKinodeAppStore.PackageInfo",
|
||||
"components": [
|
||||
{
|
||||
"name": "packageName",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "publisherKnsNodeId",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "metadataUrl",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadataHash",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "getPackageInfo",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "packageName",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "publisherName",
|
||||
"type": "bytes",
|
||||
"internalType": "bytes"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "tuple",
|
||||
"internalType": "struct IKinodeAppStore.PackageInfo",
|
||||
"components": [
|
||||
{
|
||||
"name": "packageName",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "publisherKnsNodeId",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "metadataUrl",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadataHash",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "initialize",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_knsResolver",
|
||||
"type": "address",
|
||||
"internalType": "contract KNSRegistryResolver"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "isApprovedForAll",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "operator",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
"internalType": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "knsResolver",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address",
|
||||
"internalType": "contract KNSRegistryResolver"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "name",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "owner",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "ownerOf",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "proxiableUUID",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "registerApp",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "packageName",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "publisherName",
|
||||
"type": "bytes",
|
||||
"internalType": "bytes"
|
||||
},
|
||||
{
|
||||
"name": "metadataUrl",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadataHash",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "renounceOwnership",
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "safeTransferFrom",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "from",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "safeTransferFrom",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "from",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "bytes",
|
||||
"internalType": "bytes"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "setApprovalForAll",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "operator",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"type": "bool",
|
||||
"internalType": "bool"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "supportsInterface",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "interfaceId",
|
||||
"type": "bytes4",
|
||||
"internalType": "bytes4"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
"internalType": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "symbol",
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "tokenURI",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "transferFrom",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "from",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "transferOwnership",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "newOwner",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "unlistPacakge",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "package",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "updateContractURI",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "uri",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "updateMetadata",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "package",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "metadataUrl",
|
||||
"type": "string",
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadataHash",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "upgradeToAndCall",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "newImplementation",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "bytes",
|
||||
"internalType": "bytes"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"stateMutability": "payable"
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "AppMetadataUpdated",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "package",
|
||||
"type": "uint256",
|
||||
"indexed": true,
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "metadataUrl",
|
||||
"type": "string",
|
||||
"indexed": false,
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadataHash",
|
||||
"type": "bytes32",
|
||||
"indexed": false,
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "AppRegistered",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "package",
|
||||
"type": "uint256",
|
||||
"indexed": true,
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "packageName",
|
||||
"type": "string",
|
||||
"indexed": false,
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "publisherName",
|
||||
"type": "bytes",
|
||||
"indexed": false,
|
||||
"internalType": "bytes"
|
||||
},
|
||||
{
|
||||
"name": "metadataUrl",
|
||||
"type": "string",
|
||||
"indexed": false,
|
||||
"internalType": "string"
|
||||
},
|
||||
{
|
||||
"name": "metadataHash",
|
||||
"type": "bytes32",
|
||||
"indexed": false,
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "Approval",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"indexed": true,
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "ApprovalForAll",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "operator",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"type": "bool",
|
||||
"indexed": false,
|
||||
"internalType": "bool"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "Initialized",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "version",
|
||||
"type": "uint64",
|
||||
"indexed": false,
|
||||
"internalType": "uint64"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "OwnershipTransferred",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "previousOwner",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "newOwner",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "Transfer",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "from",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"indexed": true,
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"name": "Upgraded",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "implementation",
|
||||
"type": "address",
|
||||
"indexed": true,
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"anonymous": false
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "AddressEmptyCode",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC1967InvalidImplementation",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "implementation",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC1967NonPayable",
|
||||
"inputs": []
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721IncorrectOwner",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "sender",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721InsufficientApproval",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "operator",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721InvalidApprover",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "approver",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721InvalidOperator",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "operator",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721InvalidOwner",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721InvalidReceiver",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "receiver",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721InvalidSender",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "sender",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "ERC721NonexistentToken",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "tokenId",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "FailedInnerCall",
|
||||
"inputs": []
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "InvalidInitialization",
|
||||
"inputs": []
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "NotInitializing",
|
||||
"inputs": []
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "OwnableInvalidOwner",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "OwnableUnauthorizedAccount",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "account",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "UUPSUnauthorizedCallContext",
|
||||
"inputs": []
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "UUPSUnsupportedProxiableUUID",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "slot",
|
||||
"type": "bytes32",
|
||||
"internalType": "bytes32"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "error",
|
||||
"name": "Unauthorized",
|
||||
"inputs": []
|
||||
}
|
||||
]
|
BIN
kinode/packages/app_store/ui/src/assets/background.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
18
kinode/packages/app_store/ui/src/assets/kinode.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="580" height="72" viewBox="0 0 580 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_641)">
|
||||
<path d="M0.824922 1.07031L0.794922 70.0703H14.7949L14.8049 1.07031H0.824922Z" fill="#FFF5D9"/>
|
||||
<path d="M16.5947 36.8803L41.2547 1.07031H58.2447L33.1647 36.8803L61.2447 70.0703H42.9947L16.5947 36.8803Z" fill="#FFF5D9"/>
|
||||
<path d="M119.885 1.07031H105.765V70.0703H119.885V1.07031Z" fill="#FFF5D9"/>
|
||||
<path d="M173.185 1.07031V70.0703H186.775V26.8303L224.045 70.0703H234.825V1.07031H221.325V45.6803L183.445 1.07031H173.185Z" fill="#FFF5D9"/>
|
||||
<path d="M342.465 8.86C333.025 0.15 321.645 0 318.535 0C315.475 0 303.575 0.22 294.005 9.52C283.845 19.4 283.805 32.24 283.795 35.66C283.785 39.3 283.895 49.03 290.805 57.99C300.855 71.02 316.695 71.31 318.535 71.32C321.375 71.32 334.185 71 343.965 60.66C353.065 51.04 353.265 39.4 353.275 35.66C353.275 32.49 353.305 18.86 342.455 8.86H342.465ZM318.435 58.01C307.095 58.01 297.895 47.95 297.895 35.54C297.895 23.13 307.085 13.07 318.435 13.07C329.785 13.07 338.975 23.13 338.975 35.54C338.975 47.95 329.785 58.01 318.435 58.01Z" fill="#FFF5D9"/>
|
||||
<path d="M450.495 12.0802C444.975 5.46023 437.135 0.990234 427.955 0.990234C417.555 0.990234 405.295 1.07023 402.295 1.07023V69.9802C405.285 69.9802 417.555 70.0602 427.955 70.0602C445.525 70.0602 458.445 53.4102 459.065 36.8602C459.395 28.0102 456.185 18.9002 450.495 12.0802ZM440.085 49.9502C436.895 53.8702 432.705 56.6902 427.665 57.5602C424.025 58.1902 420.095 57.8302 416.405 57.8302C416.405 50.4002 416.405 42.9802 416.405 35.5502V13.2202C423.795 13.2202 430.525 12.7002 436.605 17.6002C440.275 20.5602 442.925 24.7102 444.165 29.2402C444.525 30.5402 444.765 31.8802 444.875 33.2302C445.395 39.3702 443.995 45.1402 440.085 49.9502Z" fill="#FFF5D9"/>
|
||||
<path d="M508.135 0.990234V70.0602H552.715V57.9302H522.035V40.4202H547.125V28.0702H521.995V13.3202H552.715V0.990234H508.135Z" fill="#FFF5D9"/>
|
||||
<path d="M574.835 66.0398H572.745L571.015 63.0698H569.845V66.0398H567.805V57.5498H571.765C572.845 57.5498 573.865 57.9298 574.425 58.9398C575.205 60.3698 574.665 62.3798 573.105 63.0298C573.725 64.1198 574.225 64.9498 574.845 66.0398H574.835ZM570.375 61.0798H570.845C571.335 61.0798 572.365 61.0798 572.365 60.2898C572.365 59.5598 571.335 59.5598 570.845 59.5598H570.375V61.0798Z" fill="#FFF5D9"/>
|
||||
<path d="M570.964 69.0002C574.913 69.0002 578.114 65.799 578.114 61.8502C578.114 57.9014 574.913 54.7002 570.964 54.7002C567.016 54.7002 563.814 57.9014 563.814 61.8502C563.814 65.799 567.016 69.0002 570.964 69.0002Z" stroke="#FFF5D9" stroke-width="2.2" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6_641">
|
||||
<rect width="578.41" height="71.32" fill="white" transform="translate(0.794922)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
10
kinode/packages/app_store/ui/src/assets/logo.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="122" height="81" viewBox="0 0 122 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_651)">
|
||||
<path d="M89.3665 8.06803L121.5 0.35155L66.5111 0.320312L63.7089 7.69502L0.5 5.7032L54.0253 32.9925L36.1529 80.3203L89.3665 8.06803Z" fill="#FFF5D9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6_651">
|
||||
<rect width="121" height="80" fill="white" transform="translate(0.5 0.320312)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 431 B |
1
kinode/packages/app_store/ui/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFF5D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
After Width: | Height: | Size: 188 B |
1
kinode/packages/app_store/ui/src/assets/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
221
kinode/packages/app_store/ui/src/components/ActionButton.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import Modal from "./Modal";
|
||||
import { getAppName } from "../utils/app";
|
||||
import Loader from "./Loader";
|
||||
|
||||
interface ActionButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
}
|
||||
|
||||
export default function ActionButton({ app, ...props }: ActionButtonProps) {
|
||||
const { updateApp, downloadApp, installApp, getCaps, getMyApp } =
|
||||
useAppsStore();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [mirror, setMirror] = useState(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
const [customMirror, setCustomMirror] = useState("");
|
||||
const [caps, setCaps] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState("");
|
||||
|
||||
const { clean, installed, downloaded, updatable } = useMemo(() => {
|
||||
const versions = Object.entries(app?.metadata?.properties?.code_hashes || {});
|
||||
const latestHash = (versions.find(([v]) => v === app.metadata?.properties?.current_version) || [])[1];
|
||||
|
||||
const installed = app.installed;
|
||||
const downloaded = Boolean(app.state);
|
||||
|
||||
const updatable =
|
||||
Boolean(app.state?.our_version && latestHash) &&
|
||||
app.state?.our_version !== latestHash &&
|
||||
app.publisher !== window.our.node;
|
||||
return {
|
||||
clean: !installed && !downloaded && !updatable,
|
||||
installed,
|
||||
downloaded,
|
||||
updatable,
|
||||
};
|
||||
}, [app]);
|
||||
|
||||
useEffect(() => {
|
||||
setMirror(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
}, [app.metadata?.properties?.mirrors]);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (installed && !updatable) {
|
||||
window.alert("App is installed");
|
||||
} else {
|
||||
if (downloaded) {
|
||||
getCaps(app).then((manifest) => {
|
||||
setCaps(manifest.request_capabilities);
|
||||
});
|
||||
}
|
||||
setShowModal(true);
|
||||
}
|
||||
}, [app, installed, downloaded, updatable, setShowModal, getCaps]);
|
||||
|
||||
const download = useCallback(async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const targetMirror = mirror === "Other" ? customMirror : mirror;
|
||||
|
||||
if (!targetMirror) {
|
||||
window.alert("Please select a mirror");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(`Downloading ${getAppName(app)}...`);
|
||||
await downloadApp(app, targetMirror);
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then(() => {
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(
|
||||
`Failed to download app from ${targetMirror}, please try a different mirror.`
|
||||
);
|
||||
setLoading("");
|
||||
}
|
||||
}, [mirror, customMirror, app, downloadApp, getMyApp]);
|
||||
|
||||
const install = useCallback(async () => {
|
||||
try {
|
||||
setLoading(`Installing ${getAppName(app)}...`);
|
||||
await installApp(app);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then((app) => {
|
||||
if (!app.installed) return;
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to install, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, installApp, getMyApp]);
|
||||
|
||||
const update = useCallback(async () => {
|
||||
try {
|
||||
setLoading(`Updating ${getAppName(app)}...`);
|
||||
await updateApp(app);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then((app) => {
|
||||
if (!app.installed) return;
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to update, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, updateApp, getMyApp]);
|
||||
|
||||
const appName = getAppName(app);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={`small action-btn ${props.className || ""}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{installed && updatable
|
||||
? "Update"
|
||||
: installed
|
||||
? "Installed"
|
||||
: downloaded
|
||||
? "Install"
|
||||
: "Download"}
|
||||
</button>
|
||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
||||
{loading ? (
|
||||
<Loader msg={loading} />
|
||||
) : clean ? (
|
||||
<form className="col" style={{alignItems: "center", gap: "1em"}} onSubmit={download}>
|
||||
<h4>Download '{appName}'</h4>
|
||||
<h5 style={{ margin: 0 }}>Select Mirror</h5>
|
||||
<select value={mirror} onChange={(e) => setMirror(e.target.value)}>
|
||||
{((app.metadata?.properties?.mirrors || []).concat(["Other"])).map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{mirror === "Other" && (
|
||||
<input
|
||||
type="text"
|
||||
value={customMirror}
|
||||
onChange={(e) => setCustomMirror(e.target.value)}
|
||||
placeholder="Mirror, i.e. 'template.os'"
|
||||
style={{ padding: "0.5em", maxWidth: 240, width: "100%" }}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<button type="submit">
|
||||
Download
|
||||
</button>
|
||||
</form>
|
||||
) : downloaded ? (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 style={{ margin: 0 }}>
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
<ul className="col" style={{ alignItems: "flex-start" }}>
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button type="button" onClick={install}>
|
||||
Approve & Install
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 style={{ margin: 0 }}>
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
{/* <h5>Send Messages:</h5> */}
|
||||
<br />
|
||||
<ul className="col" style={{ alignItems: "flex-start" }}>
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* <h5>Receive Messages:</h5>
|
||||
<ul>
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul> */}
|
||||
<button type="button" onClick={update}>
|
||||
Approve & Update
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
23
kinode/packages/app_store/ui/src/components/AppEntry.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import AppHeader from "./AppHeader";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import MoreActions from "./MoreActions";
|
||||
|
||||
interface AppEntryProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
}
|
||||
|
||||
export default function AppEntry({ app, ...props }: AppEntryProps) {
|
||||
return (
|
||||
<div {...props} key={appId(app)} className="app-entry row between">
|
||||
<AppHeader app={app} size="small" />
|
||||
<div className="app-actions row">
|
||||
{!app.state?.caps_approved && <ActionButton app={app} style={{ marginRight: "1em" }} />}
|
||||
<MoreActions app={app} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
39
kinode/packages/app_store/ui/src/components/AppHeader.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
export default function AppHeader({
|
||||
app,
|
||||
size = "medium",
|
||||
...props
|
||||
}: AppHeaderProps) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={`app-header row ${size} ${props.className || ""}`}
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
app.metadata?.image ||
|
||||
"https://png.pngtree.com/png-vector/20190215/ourmid/pngtree-vector-question-mark-icon-png-image_515448.jpg"
|
||||
}
|
||||
alt="app icon"
|
||||
/>
|
||||
<div className="col title">
|
||||
<div className="app-name ellipsis">{app.metadata?.name || appId(app)}</div>
|
||||
{app.metadata?.description && size !== "large" && (
|
||||
<div className="ellipsis">{app.metadata?.description?.slice(0, 100)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
30
kinode/packages/app_store/ui/src/components/Checkbox.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Checkbox({
|
||||
readOnly = false,
|
||||
checked,
|
||||
setChecked,
|
||||
}: {
|
||||
readOnly?: boolean;
|
||||
checked: boolean;
|
||||
setChecked?: (checked: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checked"
|
||||
name="checked"
|
||||
checked={checked}
|
||||
onChange={(e) => setChecked && setChecked(e.target.checked)}
|
||||
autoFocus
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{checked && (
|
||||
<span onClick={() => setChecked && setChecked(false)} className="checkmark">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
48
kinode/packages/app_store/ui/src/components/Dropdown.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { FaEllipsisH } from 'react-icons/fa';
|
||||
import { Menu, MenuButton } from '@szhsin/react-menu';
|
||||
|
||||
interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
export default function Dropdown({ ...props }: DropdownProps) {
|
||||
return (
|
||||
<Menu {...props} className={"dropdown " + props.className} menuButton={<MenuButton className="small">
|
||||
<FaEllipsisH style={{ marginBottom: '-0.125em' }} />
|
||||
</MenuButton>}>
|
||||
{props.children}
|
||||
</Menu>
|
||||
)
|
||||
// const [isOpen, setIsOpen] = useState(false);
|
||||
// // const [selectedOption, setSelectedOption] = useState(null);
|
||||
// const dropdownRef = useRef(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleClickOutside = (event) => {
|
||||
// if (dropdownRef.current && !dropdownRef?.current?.contains(event.target)) {
|
||||
// setIsOpen(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
// return () => {
|
||||
// document.removeEventListener('mousedown', handleClickOutside);
|
||||
// };
|
||||
// }, [dropdownRef]);
|
||||
|
||||
// const toggleDropdown = () => setIsOpen(!isOpen);
|
||||
|
||||
// return (
|
||||
// <div className="dropdown col" ref={dropdownRef}>
|
||||
// <div className="dropdown-header row" onClick={toggleDropdown} style={displayStyle}>
|
||||
// {display || <FaEllipsisH style={{ marginBottom: '-0.125em' }} />}
|
||||
// </div>
|
||||
// {isOpen && (
|
||||
// <div className="dropdown-list col">
|
||||
// {props.children}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
}
|
14
kinode/packages/app_store/ui/src/components/Loader.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
type LoaderProps = {
|
||||
msg: string
|
||||
}
|
||||
|
||||
export default function Loader({ msg } : LoaderProps) {
|
||||
return (
|
||||
<div id="loading" className="col center">
|
||||
<h4>{msg}</h4>
|
||||
<div id="loader"> <div/> <div/> <div/> <div/> </div>
|
||||
</div>
|
||||
)
|
||||
}
|
293
kinode/packages/app_store/ui/src/components/MetadataForm.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
|
||||
interface Props {
|
||||
app?: AppInfo;
|
||||
packageName: string;
|
||||
publisherId: string;
|
||||
goBack: () => void;
|
||||
}
|
||||
|
||||
const VALID_VERSION_REGEX = /^\d+\.\d+\.\d+$/;
|
||||
|
||||
const MetadataForm = ({ app, packageName, publisherId, goBack }: Props) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: app?.metadata?.name || "",
|
||||
description: app?.metadata?.description || "",
|
||||
image: app?.metadata?.image || "",
|
||||
external_url: app?.metadata?.external_url || "",
|
||||
animation_url: app?.metadata?.animation_url || "",
|
||||
// properties, which can come from the app itself
|
||||
package_name: packageName,
|
||||
current_version: "",
|
||||
publisher: publisherId,
|
||||
mirrors: [publisherId],
|
||||
});
|
||||
|
||||
const [codeHashes, setCodeHashes] = useState<[string, string][]>(
|
||||
Object.entries(app?.metadata?.properties?.code_hashes || {}).concat([
|
||||
["", app?.state?.our_version || ""],
|
||||
])
|
||||
);
|
||||
|
||||
const handleFieldChange = (field, value) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleFieldChange("package_name", packageName);
|
||||
}, [packageName]);
|
||||
|
||||
useEffect(() => {
|
||||
handleFieldChange("publisher", publisherId);
|
||||
}, [publisherId]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const code_hashes = codeHashes.reduce((acc, [version, hash]) => {
|
||||
acc[version] = hash;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!VALID_VERSION_REGEX.test(formData.current_version)) {
|
||||
window.alert("Current version must be in the format x.y.z");
|
||||
return;
|
||||
} else if (!code_hashes[formData.current_version]) {
|
||||
window.alert(
|
||||
`Code hashes must include current version (${formData.current_version})`
|
||||
);
|
||||
return;
|
||||
} else if (
|
||||
!Object.keys(code_hashes).reduce(
|
||||
(valid, version) => valid && VALID_VERSION_REGEX.test(version),
|
||||
true
|
||||
)
|
||||
) {
|
||||
window.alert("Code hashes must be a JSON object with valid version keys");
|
||||
return;
|
||||
}
|
||||
|
||||
const jsonData = JSON.stringify({
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
image: formData.image,
|
||||
external_url: formData.external_url,
|
||||
animation_url: formData.animation_url,
|
||||
properties: {
|
||||
package_name: formData.package_name,
|
||||
current_version: formData.current_version,
|
||||
publisher: formData.publisher,
|
||||
mirrors: formData.mirrors,
|
||||
code_hashes,
|
||||
},
|
||||
});
|
||||
|
||||
const blob = new Blob([jsonData], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download =
|
||||
formData.package_name + "_" + formData.publisher + "_metadata.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}, [formData, codeHashes]);
|
||||
|
||||
const handleClearForm = () => {
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
image: "",
|
||||
external_url: "",
|
||||
animation_url: "",
|
||||
|
||||
package_name: "",
|
||||
current_version: "",
|
||||
publisher: "",
|
||||
mirrors: [],
|
||||
});
|
||||
setCodeHashes([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="col card metadata" style={{ gap: "0.5em" }}>
|
||||
<h4>Fill out metadata</h4>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleFieldChange("name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleFieldChange("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Image URL</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Image URL"
|
||||
value={formData.image}
|
||||
onChange={(e) => handleFieldChange("image", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">External URL</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="External URL"
|
||||
value={formData.external_url}
|
||||
onChange={(e) => handleFieldChange("external_url", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Animation URL</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Animation URL"
|
||||
value={formData.animation_url}
|
||||
onChange={(e) => handleFieldChange("animation_url", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Package Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Package Name"
|
||||
value={formData.package_name}
|
||||
onChange={(e) => handleFieldChange("package_name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Current Version</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Current Version"
|
||||
value={formData.current_version}
|
||||
onChange={(e) => handleFieldChange("current_version", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Publisher</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Publisher"
|
||||
value={formData.publisher}
|
||||
onChange={(e) => handleFieldChange("publisher", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Mirrors (separated by commas)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Mirrors (separated by commas)"
|
||||
value={formData.mirrors.join(",")}
|
||||
onChange={(e) =>
|
||||
handleFieldChange(
|
||||
"mirrors",
|
||||
e.target.value.split(",").map((m) => m.trim())
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col label"
|
||||
style={{
|
||||
gap: "0.5em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="row"
|
||||
style={{
|
||||
gap: "0.5em",
|
||||
marginTop: 0,
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h5 style={{ margin: 0 }}>Code Hashes</h5>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeHashes([...codeHashes, ["", ""]])}
|
||||
className="small"
|
||||
>
|
||||
Add code hash
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{codeHashes.map(([version, hash], ind, arr) => (
|
||||
<div
|
||||
key={ind + "_code_hash"}
|
||||
className="row"
|
||||
style={{ gap: "0.5em", marginTop: 0, width: "100%" }}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Version"
|
||||
value={version}
|
||||
onChange={(e) =>
|
||||
setCodeHashes((prev) => {
|
||||
const newHashes = [...prev];
|
||||
newHashes[ind][0] = e.target.value;
|
||||
return newHashes;
|
||||
})
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Hash"
|
||||
value={hash}
|
||||
onChange={(e) =>
|
||||
setCodeHashes((prev) => {
|
||||
const newHashes = [...prev];
|
||||
newHashes[ind][1] = e.target.value;
|
||||
return newHashes;
|
||||
})
|
||||
}
|
||||
style={{ flex: 5 }}
|
||||
/>
|
||||
{arr.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCodeHashes((prev) => prev.filter((_, i) => i !== ind))
|
||||
}
|
||||
style={{
|
||||
fontSize: "2em",
|
||||
height: 32,
|
||||
lineHeight: "1em",
|
||||
padding: "0 0.2em",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="row" style={{ gap: "0.5em", margin: "1em 0" }}>
|
||||
<button type="button" onClick={handleSubmit}>
|
||||
Download JSON
|
||||
</button>
|
||||
<button type="button" onClick={handleClearForm}>
|
||||
Clear Form
|
||||
</button>
|
||||
<button type="button" onClick={goBack}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetadataForm;
|
42
kinode/packages/app_store/ui/src/components/Modal.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { MouseEvent } from 'react'
|
||||
import { FaPlus } from 'react-icons/fa'
|
||||
|
||||
export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
show: boolean
|
||||
hide: () => void
|
||||
hideClose?: boolean
|
||||
children: React.ReactNode,
|
||||
title?: string
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
show,
|
||||
hide,
|
||||
hideClose = false,
|
||||
title,
|
||||
...props
|
||||
}) => {
|
||||
const dontHide = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (!show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`modal-backdrop ${show ? 'show' : ''}`} onClick={hide}>
|
||||
<div {...props} className={`col modal ${props.className || ''}`} onClick={dontHide}>
|
||||
{Boolean(title) && <h4 className='modal-title'>{title}</h4>}
|
||||
{!hideClose && (
|
||||
<FaPlus className='close' onClick={hide} />
|
||||
)}
|
||||
<div className='col modal-content' onClick={dontHide}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
82
kinode/packages/app_store/ui/src/components/MoreActions.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MenuItem } from "@szhsin/react-menu";
|
||||
|
||||
import Dropdown from "./Dropdown";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
|
||||
interface MoreActionsProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
}
|
||||
|
||||
export default function MoreActions({ app }: MoreActionsProps) {
|
||||
const { uninstallApp, setMirroring, setAutoUpdate } = useAppsStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const downloaded = Boolean(app.state);
|
||||
|
||||
if (!downloaded) {
|
||||
if (!app.metadata) return <div style={{ width: 38 }} />;
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
{app.metadata?.description && (
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
>
|
||||
View Details
|
||||
</MenuItem>
|
||||
)}
|
||||
{app.metadata?.external_url && (
|
||||
<MenuItem>
|
||||
<a
|
||||
style={{
|
||||
color: "inherit",
|
||||
whiteSpace: "nowrap",
|
||||
cursor: "pointer",
|
||||
marginTop: "0.25em",
|
||||
}}
|
||||
target="_blank"
|
||||
href={app.metadata?.external_url}
|
||||
>
|
||||
View Site
|
||||
</a>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
>
|
||||
View Details
|
||||
</MenuItem>
|
||||
{app.installed && (
|
||||
<>
|
||||
<MenuItem className="action-entry" onClick={() => uninstallApp(app)}>
|
||||
Uninstall
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => setMirroring(app, !app.state?.mirroring)}
|
||||
>
|
||||
{app.state?.mirroring ? "Stop" : "Start"} Mirroring
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => setAutoUpdate(app, !app.state?.auto_update)}
|
||||
>
|
||||
{app.state?.auto_update ? "Disable" : "Enable"} Auto Update
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
86
kinode/packages/app_store/ui/src/components/SearchHeader.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
FaArrowLeft,
|
||||
FaDownload,
|
||||
FaRegTimesCircle,
|
||||
FaSearch,
|
||||
FaUpload,
|
||||
} from "react-icons/fa";
|
||||
|
||||
import { MY_APPS_PATH } from "../constants/path";
|
||||
|
||||
interface SearchHeaderProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onBack?: () => void;
|
||||
onlyMyApps?: boolean;
|
||||
hideSearch?: boolean;
|
||||
}
|
||||
|
||||
export default function SearchHeader({
|
||||
value = "",
|
||||
onChange = () => null,
|
||||
onBack,
|
||||
hideSearch = false,
|
||||
}: SearchHeaderProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const canGoBack = location.key !== "default";
|
||||
const isMyAppsPage = location.pathname === MY_APPS_PATH;
|
||||
|
||||
return (
|
||||
<div className="search-header row between">
|
||||
{location.pathname !== '/' ? (
|
||||
<button className="back-btn col center" onClick={() => {
|
||||
if (onBack) {
|
||||
onBack()
|
||||
} else {
|
||||
canGoBack ? navigate(-1) : navigate('/')
|
||||
}
|
||||
}}>
|
||||
<FaArrowLeft />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="back-btn col center"
|
||||
onClick={() => navigate("/publish")}
|
||||
>
|
||||
<FaUpload />
|
||||
</button>
|
||||
)}
|
||||
{!hideSearch && (
|
||||
<div className="searchbar row">
|
||||
<FaSearch
|
||||
className="search-icon"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={value}
|
||||
placeholder="Search for apps..."
|
||||
/>
|
||||
{value.length > 0 && (
|
||||
<FaRegTimesCircle
|
||||
className="search-icon"
|
||||
style={{ margin: "0 -0.25em 0 0.25em" }}
|
||||
onClick={() => onChange("")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="row">
|
||||
<button
|
||||
className={`my-pkg-btn row ${isMyAppsPage ? "selected" : ""}`}
|
||||
onClick={() => (isMyAppsPage ? navigate(-1) : navigate(MY_APPS_PATH))}
|
||||
>
|
||||
<FaDownload style={{ marginRight: "0.5em" }} />
|
||||
My Packages
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
16
kinode/packages/app_store/ui/src/constants/chain.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export enum ChainId {
|
||||
SEPOLIA = 11155111,
|
||||
OPTIMISM = 10,
|
||||
OPTIMISM_GOERLI = 420,
|
||||
LOCAL = 1337,
|
||||
}
|
||||
|
||||
export const SEPOLIA_OPT_HEX = '0xaa36a7';
|
||||
export const OPTIMISM_OPT_HEX = '0xa';
|
||||
export const SEPOLIA_OPT_INT = '11155111';
|
||||
|
||||
// Sepolia (for now)
|
||||
export const PACKAGE_STORE_ADDRESSES = {
|
||||
[ChainId.SEPOLIA]: '0x18c39eB547A0060C6034f8bEaFB947D1C16eADF1',
|
||||
// [ChainId.OPTIMISM]: '0x8f6e1c9C5a0fE0A7f9Cf0e9b3aF1A9c4f5c6A9e0',
|
||||
};
|
22
kinode/packages/app_store/ui/src/constants/http.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export enum HTTP_STATUS {
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
NO_CONTENT = 204,
|
||||
MOVED_PERMANENTLY = 301,
|
||||
FOUND = 302,
|
||||
SEE_OTHER = 303,
|
||||
NOT_MODIFIED = 304,
|
||||
TEMPORARY_REDIRECT = 307,
|
||||
PERMANENT_REDIRECT = 308,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
PAYLOAD_TOO_LARGE = 413,
|
||||
UNSUPPORTED_MEDIA_TYPE = 415,
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
BAD_GATEWAY = 502,
|
||||
SERVICE_UNAVAILABLE = 503
|
||||
}
|
1
kinode/packages/app_store/ui/src/constants/path.ts
Normal file
@ -0,0 +1 @@
|
||||
export const MY_APPS_PATH = '/my-apps';
|
217
kinode/packages/app_store/ui/src/index.css
Normal file
@ -0,0 +1,217 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--text-light: #FFF5D9;
|
||||
--text-dark: #22211F;
|
||||
--text-orange: #FF7533;
|
||||
--orange-light: #F36822;
|
||||
--orange-medium: #F35422;
|
||||
--orange-burnt: #E25F35;
|
||||
|
||||
--medium-gray: #7E7E7E;
|
||||
--gray-button: rgba(253, 245, 220, 0.25);
|
||||
--dark-background: rgb(130, 59, 28);
|
||||
--input-background: rgba(243, 84, 34, 0.25); /* orange-medium */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2em;
|
||||
font-size: 16px;
|
||||
color: var(--text-light);
|
||||
font-weight: 400;
|
||||
background: url('./assets/background.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
background-color: var(--dark-background);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, a, button, input {
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 64px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 48px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 36px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 24px;
|
||||
}
|
||||
h5 {
|
||||
font-size: 20px;
|
||||
}
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input, button {
|
||||
all: unset;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="checkbox"] {
|
||||
padding: 1em;
|
||||
border: 1px solid var(--orange-medium);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
font-size: 1em;
|
||||
background-color: var(--input-background);
|
||||
color: var(--text-light);
|
||||
text-align: left;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
padding: 0.25em 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: var(--orange-medium);
|
||||
}
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
font-size: 24px;
|
||||
top: -5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
::placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
::-webkit-input-placeholder::placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
::-moz-placeholder::placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
::-ms-input-placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
label {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
button, [type='button'], [type='reset'], [type='submit'] {
|
||||
padding: 0.75em 1em;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--orange-medium);
|
||||
/* border-image: linear-gradient(to right, var(--orange-medium), var(--orange-light)); */
|
||||
border-radius: 8px;
|
||||
background: var(--orange-medium);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 1.125em;
|
||||
transition: all 0.1s;
|
||||
box-shadow: 0 1px 2px var(--orange-light);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
button.alt {
|
||||
background-color: var(--text-light);
|
||||
color: var(--text-dark);
|
||||
border-color: var(--text-light);
|
||||
box-shadow: 0 1px 2px var(--text-light);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: var(--medium-gray);
|
||||
border: 1px solid var(--medium-gray);
|
||||
box-shadow: 0 1px 2px var(--medium-gray);
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
ul, li {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.25em 0.5em;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--orange-medium);
|
||||
background-color: var(--input-background);
|
||||
color: var(--text-light);
|
||||
border-radius: 8px;
|
||||
|
||||
/* Use a custom chevron image */
|
||||
background-image: url('./assets/select-chevron.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center; /* Adjust the horizontal position to control padding */
|
||||
background-size: 16px; /* Adjust size of the chevron */
|
||||
padding-right: 2em; /* Adjust the padding to make room for the chevron */
|
||||
|
||||
-webkit-appearance: none; /* Removes default styling on WebKit browsers like Safari */
|
||||
-moz-appearance: none; /* Removes default styling on Firefox */
|
||||
appearance: none; /* Standard property, currently not fully supported */
|
||||
}
|
||||
|
||||
|
||||
/* @media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: var(--bg-black);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
color: var(--bg-black);
|
||||
background-color: var(--bg-gray-medium);
|
||||
}
|
||||
button:hover {
|
||||
background-color: var(--bg-gray-light);
|
||||
}
|
||||
input:focus-visible {
|
||||
outline: -webkit-focus-ring-color auto 1px;
|
||||
}
|
||||
|
||||
select {
|
||||
border: 1px solid var(--bg-gray-solid);
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
input:focus-visible {
|
||||
outline: var(--text-dark) auto 1px;
|
||||
}
|
||||
} */
|
10
kinode/packages/app_store/ui/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
127
kinode/packages/app_store/ui/src/pages/AppPage.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import ActionButton from "../components/ActionButton";
|
||||
import AppHeader from "../components/AppHeader";
|
||||
import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { appId } from "../utils/app";
|
||||
|
||||
interface AppPageProps extends PageProps {}
|
||||
|
||||
export default function AppPage(props: AppPageProps) {
|
||||
// eslint-disable-line
|
||||
const { myApps, listedApps, getListedApp } = useAppsStore();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const [app, setApp] = useState<AppInfo | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const myApp = myApps.local.find((a) => appId(a) === params.id);
|
||||
if (myApp) return setApp(myApp);
|
||||
|
||||
if (params.id) {
|
||||
const app = listedApps.find((a) => appId(a) === params.id);
|
||||
if (app) {
|
||||
setApp(app);
|
||||
} else {
|
||||
getListedApp(params.id)
|
||||
.then((app) => setApp(app))
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
}, [params.id]);
|
||||
|
||||
const goToPublish = useCallback(() => {
|
||||
navigate("/publish", { state: { app } });
|
||||
}, [app, navigate]);
|
||||
|
||||
const version = useMemo(
|
||||
() => app?.metadata?.properties?.current_version || "Unknown",
|
||||
[app]
|
||||
);
|
||||
const versions = Object.entries(app?.metadata?.properties?.code_hashes || {});
|
||||
const hash =
|
||||
app?.state?.our_version ||
|
||||
(versions[(versions.length || 1) - 1] || ["", ""])[1];
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<SearchHeader value="" onChange={() => null} hideSearch />
|
||||
<div className="card" style={{ marginTop: "1em" }}>
|
||||
{app ? (
|
||||
<>
|
||||
<div className="row between">
|
||||
<AppHeader app={app} size="large" />
|
||||
<ActionButton app={app} style={{ marginRight: "0.5em" }} />
|
||||
</div>
|
||||
<div className="col" style={{ marginTop: "1em" }}>
|
||||
<div className="app-details row">
|
||||
<div className="title">Description</div>
|
||||
<div className="value">
|
||||
{(app.metadata?.description || "No description given").slice(
|
||||
0,
|
||||
2000
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-details row">
|
||||
<div className="title">Publisher</div>
|
||||
<div className="value underline">{app.publisher}</div>
|
||||
</div>
|
||||
<div className="app-details row">
|
||||
<div className="title">Version</div>
|
||||
<div className="value">{version}</div>
|
||||
</div>
|
||||
<div className="app-details row">
|
||||
<div className="title">Mirrors</div>
|
||||
<div className="col">
|
||||
{(app.metadata?.properties?.mirrors || []).map(
|
||||
(mirror, index) => (
|
||||
<div key={index + mirror} className="value underline">
|
||||
{mirror}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="app-details row">
|
||||
<div className="title">Permissions</div>
|
||||
<div className="col">
|
||||
{app.permissions?.map((permission, index) => (
|
||||
<div key={index + permission} className="value permission">{permission}</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="app-details row">
|
||||
<div className="title">Hash</div>
|
||||
<div className="value" style={{ wordBreak: "break-all" }}>
|
||||
{hash}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-screenshots row">
|
||||
{(app.metadata?.properties?.screenshots || []).map(
|
||||
(screenshot, index) => (
|
||||
<img key={index + screenshot} src={screenshot} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{app.installed && (
|
||||
<button type="button" onClick={goToPublish}>
|
||||
Publish
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h4>App details not found for </h4>
|
||||
<h4>{params.id}</h4>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
80
kinode/packages/app_store/ui/src/pages/MyAppsPage.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { FaUpload } from "react-icons/fa";
|
||||
|
||||
import { AppInfo, MyApps } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import AppEntry from "../components/AppEntry";
|
||||
import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { appId } from "../utils/app";
|
||||
|
||||
interface MyAppsPageProps extends PageProps {}
|
||||
|
||||
export default function MyAppsPage(props: MyAppsPageProps) { // eslint-disable-line
|
||||
const { myApps, getMyApps } = useAppsStore()
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [displayedApps, setDisplayedApps] = useState<MyApps>(myApps);
|
||||
|
||||
useEffect(() => {
|
||||
getMyApps()
|
||||
.then(setDisplayedApps)
|
||||
.catch((error) => console.error(error));
|
||||
}, []); // eslint-disable-line
|
||||
|
||||
const searchMyApps = useCallback((query: string) => {
|
||||
setSearchQuery(query);
|
||||
const filteredApps = Object.keys(myApps).reduce((acc, key) => {
|
||||
acc[key] = myApps[key].filter((app) => {
|
||||
return app.package.toLowerCase().includes(query.toLowerCase())
|
||||
|| app.metadata?.description?.toLowerCase().includes(query.toLowerCase())
|
||||
|| app.metadata?.description?.toLowerCase().includes(query.toLowerCase());
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {
|
||||
downloaded: [] as AppInfo[],
|
||||
installed: [] as AppInfo[],
|
||||
local: [] as AppInfo[],
|
||||
system: [] as AppInfo[],
|
||||
} as MyApps)
|
||||
|
||||
setDisplayedApps(filteredApps);
|
||||
}, [myApps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery) {
|
||||
searchMyApps(searchQuery);
|
||||
} else {
|
||||
setDisplayedApps(myApps);
|
||||
}
|
||||
}, [myApps]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: '100%' }}>
|
||||
<SearchHeader value={searchQuery} onChange={searchMyApps} />
|
||||
<div className="row between page-title">
|
||||
<h4 style={{ marginBottom: "0.5em" }}>My Packages</h4>
|
||||
<button className="row" onClick={() => navigate('/publish')}>
|
||||
<FaUpload style={{ marginRight: "0.5em" }} />
|
||||
Publish Package
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="my-apps-list">
|
||||
<div className="new card col" style={{ gap: "1em" }}>
|
||||
<h4>Downloaded</h4>
|
||||
{(displayedApps.downloaded || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
<h4>Installed</h4>
|
||||
{(displayedApps.installed || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
<h4>Local</h4>
|
||||
{(displayedApps.local || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
<h4>System</h4>
|
||||
{(displayedApps.system || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
305
kinode/packages/app_store/ui/src/pages/PublishPage.tsx
Normal file
@ -0,0 +1,305 @@
|
||||
import React, { useState, useCallback, FormEvent, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { BigNumber, utils } from "ethers";
|
||||
import { useWeb3React } from "@web3-react/core";
|
||||
|
||||
import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { setChain } from "../utils/chain";
|
||||
import { SEPOLIA_OPT_HEX } from "../constants/chain";
|
||||
import { hooks, metaMask } from "../utils/metamask";
|
||||
import Loader from "../components/Loader";
|
||||
import { toDNSWireFormat } from "../utils/dnsWire";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import MetadataForm from "../components/MetadataForm";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
|
||||
const { useIsActivating } = hooks;
|
||||
|
||||
interface PublishPageProps extends PageProps {}
|
||||
|
||||
export default function PublishPage({
|
||||
provider,
|
||||
packageAbi,
|
||||
}: PublishPageProps) {
|
||||
// get state from router
|
||||
const { state } = useLocation();
|
||||
const { listedApps } = useAppsStore();
|
||||
// TODO: figure out how to handle provider
|
||||
const { account, isActive } = useWeb3React();
|
||||
const isActivating = useIsActivating();
|
||||
|
||||
const [loading, setLoading] = useState("");
|
||||
const [publishSuccess, setPublishSuccess] = useState<
|
||||
{ packageName: string; publisherId: string } | undefined
|
||||
>();
|
||||
const [showMetadataForm, setShowMetadataForm] = useState<boolean>(false);
|
||||
const [packageName, setPackageName] = useState<string>("");
|
||||
const [publisherId, setPublisherId] = useState<string>(
|
||||
window.our?.node || ""
|
||||
); // BytesLike
|
||||
const [metadataUrl, setMetadataUrl] = useState<string>("");
|
||||
const [metadataHash, setMetadataHash] = useState<string>(""); // BytesLike
|
||||
const [isUpdate, setIsUpdate] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const app: AppInfo | undefined = state?.app;
|
||||
if (app) {
|
||||
setPackageName(app.package);
|
||||
setPublisherId(app.publisher);
|
||||
setIsUpdate(true);
|
||||
}
|
||||
}, [state])
|
||||
|
||||
const connectWallet = useCallback(async () => {
|
||||
await metaMask.activate().catch(() => {});
|
||||
|
||||
try {
|
||||
setChain(SEPOLIA_OPT_HEX);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const calculateMetadataHash = useCallback(async () => {
|
||||
if (!metadataUrl) {
|
||||
setMetadataHash("");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const metadataResponse = await fetch(metadataUrl);
|
||||
const metadataText = await metadataResponse.text();
|
||||
JSON.parse(metadataText); // confirm it's valid JSON
|
||||
const metadataHash = utils.keccak256(utils.toUtf8Bytes(metadataText));
|
||||
setMetadataHash(metadataHash);
|
||||
} catch (error) {
|
||||
window.alert(
|
||||
"Error calculating metadata hash. Please ensure the URL is valid and the metadata is in JSON format."
|
||||
);
|
||||
}
|
||||
}, [metadataUrl]);
|
||||
|
||||
const publishPackage = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let metadata = metadataHash;
|
||||
|
||||
try {
|
||||
if (!metadata) {
|
||||
// https://pongo-uploads.s3.us-east-2.amazonaws.com/chat_metadata.json
|
||||
const metadataResponse = await fetch(metadataUrl);
|
||||
await metadataResponse.json(); // confirm it's valid JSON
|
||||
const metadataText = await metadataResponse.text(); // hash as text
|
||||
metadata = utils.keccak256(utils.toUtf8Bytes(metadataText));
|
||||
}
|
||||
|
||||
setLoading("Please confirm the transaction in your wallet");
|
||||
const publisherIdDnsWireFormat = toDNSWireFormat(publisherId);
|
||||
await setChain(SEPOLIA_OPT_HEX);
|
||||
|
||||
// TODO: have a checkbox to show if it's an update of an existing package
|
||||
|
||||
const tx = await (isUpdate
|
||||
? packageAbi.updateMetadata(
|
||||
BigNumber.from(
|
||||
utils.solidityKeccak256(
|
||||
["string", "bytes"],
|
||||
[packageName, publisherIdDnsWireFormat]
|
||||
)
|
||||
),
|
||||
metadataUrl,
|
||||
metadata
|
||||
)
|
||||
: packageAbi.registerApp(
|
||||
packageName,
|
||||
publisherIdDnsWireFormat,
|
||||
metadataUrl,
|
||||
metadata
|
||||
));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
setLoading("Publishing package...");
|
||||
await tx.wait();
|
||||
setPublishSuccess({ packageName, publisherId });
|
||||
setPackageName("");
|
||||
setPublisherId(window.our?.node || publisherId);
|
||||
setMetadataUrl("");
|
||||
setMetadataHash("");
|
||||
setIsUpdate(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.alert(
|
||||
"Error publishing package. Please ensure the package name and publisher ID are valid, and the metadata is in JSON format."
|
||||
);
|
||||
} finally {
|
||||
setLoading("");
|
||||
}
|
||||
},
|
||||
[
|
||||
packageName,
|
||||
isUpdate,
|
||||
publisherId,
|
||||
metadataUrl,
|
||||
metadataHash,
|
||||
packageAbi,
|
||||
setPublishSuccess,
|
||||
setPackageName,
|
||||
setPublisherId,
|
||||
setMetadataUrl,
|
||||
setMetadataHash,
|
||||
setIsUpdate,
|
||||
]
|
||||
);
|
||||
|
||||
const checkIfUpdate = useCallback(async () => {
|
||||
if (isUpdate) return;
|
||||
|
||||
if (
|
||||
packageName &&
|
||||
publisherId &&
|
||||
listedApps.find(
|
||||
(app) => app.package === packageName && app.publisher === publisherId
|
||||
)
|
||||
) {
|
||||
setIsUpdate(true);
|
||||
}
|
||||
}, [listedApps, packageName, publisherId, isUpdate, setIsUpdate]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<SearchHeader hideSearch onBack={showMetadataForm ? () => setShowMetadataForm(false) : undefined} />
|
||||
<div className="row between page-title">
|
||||
<h4>Publish Package</h4>
|
||||
{Boolean(account) && (
|
||||
<div style={{ textAlign: "right", lineHeight: 1.5 }}>
|
||||
{" "}
|
||||
Connected as{" "}
|
||||
{account?.slice(0, 6) + "..." + account?.slice(account.length - 6)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="col center">
|
||||
<Loader msg={loading} />
|
||||
</div>
|
||||
) : publishSuccess ? (
|
||||
<div className="col center">
|
||||
<h4 style={{ marginBottom: "0.5em" }}>Package Published!</h4>
|
||||
<div style={{ marginBottom: "0.5em" }}>
|
||||
<strong>Package Name:</strong> {publishSuccess.packageName}
|
||||
</div>
|
||||
<div style={{ marginBottom: "0.5em" }}>
|
||||
<strong>Publisher ID:</strong> {publishSuccess.publisherId}
|
||||
</div>
|
||||
<button
|
||||
className={`my-pkg-btn row`}
|
||||
style={{ marginTop: "1em" }}
|
||||
onClick={() => setPublishSuccess(undefined)}
|
||||
>
|
||||
Publish Another Package
|
||||
</button>
|
||||
</div>
|
||||
) : showMetadataForm ? (
|
||||
<MetadataForm {...{packageName, publisherId, app: state?.app}} goBack={() => setShowMetadataForm(false)} />
|
||||
) : !account || !isActive ? (
|
||||
<>
|
||||
<h4 style={{}}>Please connect your wallet to publish a package</h4>
|
||||
<button className={`connect-wallet row`} onClick={connectWallet}>
|
||||
Connect Wallet
|
||||
</button>
|
||||
</>
|
||||
) : isActivating ? (
|
||||
<Loader msg="Approve connection in your wallet" />
|
||||
) : (
|
||||
<form
|
||||
className="new card col"
|
||||
style={{ flex: 1, overflowY: "scroll" }}
|
||||
onSubmit={publishPackage}
|
||||
>
|
||||
<div
|
||||
className="row between"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
padding: "0.5em",
|
||||
margin: "0 0 0 -0.5em",
|
||||
}}
|
||||
onClick={() => setIsUpdate(!isUpdate)}
|
||||
>
|
||||
<Checkbox checked={isUpdate} readOnly />
|
||||
<label htmlFor="update" style={{ cursor: "pointer", marginLeft: 8 }}>
|
||||
Update existing package
|
||||
</label>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="package-name">Package Name</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
id="package-name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="my-package"
|
||||
value={packageName}
|
||||
onChange={(e) => setPackageName(e.target.value)}
|
||||
onBlur={checkIfUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="publisher-id">Publisher ID</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
id="publisher-id"
|
||||
type="text"
|
||||
required
|
||||
value={publisherId}
|
||||
onChange={(e) => setPublisherId(e.target.value)}
|
||||
onBlur={checkIfUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="metadata-url">
|
||||
Metadata URL
|
||||
</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
id="metadata-url"
|
||||
type="text"
|
||||
required
|
||||
value={metadataUrl}
|
||||
onChange={(e) => setMetadataUrl(e.target.value)}
|
||||
onBlur={calculateMetadataHash}
|
||||
placeholder="https://github/my-org/my-repo/metadata.json"
|
||||
/>
|
||||
<div style={{ textAlign: "left", margin: "0.5em 0 0" }}>
|
||||
Metadata is a JSON file that describes your package.
|
||||
<br /> You can{" "}
|
||||
<a onClick={() => setShowMetadataForm(true)} style={{ cursor: "pointer", textDecoration: "underline" }}>
|
||||
fill out a template here
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="metadata-hash">Metadata Hash</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
readOnly
|
||||
id="metadata-hash"
|
||||
type="text"
|
||||
value={metadataHash}
|
||||
onChange={(e) => setMetadataHash(e.target.value)}
|
||||
placeholder="Calculated automatically from metadata URL"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="primary">
|
||||
Publish
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
182
kinode/packages/app_store/ui/src/pages/StorePage.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
|
||||
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import AppEntry from "../components/AppEntry";
|
||||
import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { appId } from "../utils/app";
|
||||
|
||||
interface StorePageProps extends PageProps {}
|
||||
|
||||
export default function StorePage(props: StorePageProps) {
|
||||
// eslint-disable-line
|
||||
const { listedApps, getListedApps } = useAppsStore();
|
||||
|
||||
const [resultsSort, setResultsSort] = useState<string>("Recently published");
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [displayedApps, setDisplayedApps] = useState<AppInfo[]>(listedApps);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const pages = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
{ length: Math.ceil(displayedApps.length / 10) },
|
||||
(_, index) => index + 1
|
||||
),
|
||||
[displayedApps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const start = (page - 1) * 10;
|
||||
const end = start + 10;
|
||||
setDisplayedApps(listedApps.slice(start, end));
|
||||
}, [listedApps]);
|
||||
|
||||
// GET on load
|
||||
useEffect(() => {
|
||||
getListedApps()
|
||||
.then((apps) => {
|
||||
setDisplayedApps(Object.values(apps));
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
}, []); // eslint-disable-line
|
||||
|
||||
// const pages = useMemo(
|
||||
// () => {
|
||||
// const displayedApps = query ? searchResults : latestApps;
|
||||
|
||||
// return Array.from(
|
||||
// { length: Math.ceil((displayedApps.length - 2) / 10) },
|
||||
// (_, index) => index + 1
|
||||
// )
|
||||
// },
|
||||
// [query, searchResults, latestApps]
|
||||
// );
|
||||
|
||||
// const featuredApps = useMemo(() => latestApps.slice(0, 2), [latestApps]);
|
||||
// const displayedApps = useMemo(
|
||||
// () => {
|
||||
// const displayedApps = query ? searchResults : latestApps.slice(2);
|
||||
// return displayedApps.slice((page - 1) * 10, page * 10)
|
||||
// },
|
||||
// [latestApps, searchResults, page, query]
|
||||
// );
|
||||
|
||||
const sortApps = useCallback(async (sort: string) => {
|
||||
switch (sort) {
|
||||
case "Recently published":
|
||||
break;
|
||||
case "Most popular":
|
||||
break;
|
||||
case "Best rating":
|
||||
break;
|
||||
case "Recently updated":
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// const viewDetails = useCallback(
|
||||
// (app: AppInfo) => () => {
|
||||
// navigate(`/app-details/${appId(app)}`);
|
||||
// },
|
||||
// [navigate]
|
||||
// );
|
||||
|
||||
const searchApps = useCallback(
|
||||
(query: string) => {
|
||||
setSearchQuery(query);
|
||||
const filteredApps = listedApps.filter(
|
||||
(app) => {
|
||||
return (
|
||||
app.package.toLowerCase().includes(query.toLowerCase()) ||
|
||||
app.metadata?.description
|
||||
?.toLowerCase()
|
||||
.includes(query.toLowerCase()) ||
|
||||
app.metadata?.description
|
||||
?.toLowerCase()
|
||||
.includes(query.toLowerCase())
|
||||
);
|
||||
},
|
||||
[listedApps]
|
||||
);
|
||||
setDisplayedApps(filteredApps);
|
||||
},
|
||||
[listedApps]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
{/* <div style={{ position: "absolute", top: 4, left: 8 }}>
|
||||
ID: <strong>{window.our?.node}</strong>
|
||||
</div> */}
|
||||
<SearchHeader value={searchQuery} onChange={searchApps} />
|
||||
{/* <h3 style={{ marginBottom: "0.5em" }}>Featured</h3>
|
||||
<div className="featured row">
|
||||
{featuredApps.map((app, i) => (
|
||||
<div
|
||||
key={app.name + app.metadata_hash}
|
||||
className="card col"
|
||||
style={{
|
||||
marginLeft: i === 1 ? "1em" : 0,
|
||||
flex: 1,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={viewDetails(app)}
|
||||
>
|
||||
<AppHeader app={app} />
|
||||
<div style={{ marginTop: "0.25em" }}>
|
||||
{app.metadata?.description || "No description provided."}
|
||||
</div>
|
||||
<ActionButton style={{ marginTop: "0.5em" }} />
|
||||
</div>
|
||||
))}
|
||||
</div> */}
|
||||
<div className="row between page-title">
|
||||
<h4>New</h4>
|
||||
|
||||
<select
|
||||
value={resultsSort}
|
||||
onChange={(e) => {
|
||||
setResultsSort(e.target.value);
|
||||
sortApps(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option>Recently published</option>
|
||||
<option>Most popular</option>
|
||||
<option>Best rating</option>
|
||||
<option>Recently updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="new card col" style={{ flex: 1, overflowY: "scroll", gap: "1em" }}>
|
||||
{displayedApps.map((app) => (
|
||||
<AppEntry
|
||||
key={appId(app) + (app.state?.our_version || "")}
|
||||
app={app}
|
||||
/>
|
||||
))}
|
||||
{pages.length > 1 && (
|
||||
<div className="row" style={{ alignSelf: "center" }}>
|
||||
{page !== pages[0] && (
|
||||
<FaChevronLeft onClick={() => setPage(page - 1)} />
|
||||
)}
|
||||
{pages.map((p) => (
|
||||
<div
|
||||
key={`page-${p}`}
|
||||
className={`page-selector ${p === page ? "selected" : ""}`}
|
||||
onClick={() => setPage(p)}
|
||||
>
|
||||
{p}
|
||||
</div>
|
||||
))}
|
||||
{page !== pages[pages.length - 1] && (
|
||||
<FaChevronRight onClick={() => setPage(page + 1)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
204
kinode/packages/app_store/ui/src/store/apps-store.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
import { MyApps, AppInfo, PackageManifest } from '../types/Apps'
|
||||
import { HTTP_STATUS } from '../constants/http';
|
||||
import { appId, getAppType } from '../utils/app';
|
||||
|
||||
const BASE_URL = (import.meta as any).env.BASE_URL; // eslint-disable-line
|
||||
|
||||
const isApp = (a1: AppInfo, a2: AppInfo) => a1.package === a2.package && a1.publisher === a2.publisher
|
||||
|
||||
export interface AppsStore {
|
||||
myApps: MyApps
|
||||
listedApps: AppInfo[]
|
||||
searchResults: AppInfo[]
|
||||
query: string
|
||||
|
||||
getMyApps: () => Promise<MyApps>
|
||||
getListedApps: () => Promise<AppInfo[]>
|
||||
getMyApp: (app: AppInfo) => Promise<AppInfo>
|
||||
installApp: (app: AppInfo) => Promise<void>
|
||||
updateApp: (app: AppInfo) => Promise<void>
|
||||
uninstallApp: (app: AppInfo) => Promise<void>
|
||||
getListedApp: (packageName: string) => Promise<AppInfo>
|
||||
downloadApp: (app: AppInfo, download_from: string) => Promise<void>
|
||||
getCaps: (app: AppInfo) => Promise<PackageManifest>
|
||||
approveCaps: (app: AppInfo) => Promise<void>
|
||||
setMirroring: (info: AppInfo, mirroring: boolean) => Promise<void>
|
||||
setAutoUpdate: (app: AppInfo, autoUpdate: boolean) => Promise<void>
|
||||
|
||||
// searchApps: (query: string, onlyMyApps?: boolean) => Promise<AppInfo[]>
|
||||
|
||||
get: () => AppsStore
|
||||
set: (partial: AppsStore | Partial<AppsStore>) => void
|
||||
}
|
||||
|
||||
const useAppsStore = create<AppsStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
myApps: {
|
||||
downloaded: [] as AppInfo[],
|
||||
installed: [] as AppInfo[],
|
||||
local: [] as AppInfo[],
|
||||
system: [] as AppInfo[],
|
||||
},
|
||||
listedApps: [] as AppInfo[],
|
||||
searchResults: [] as AppInfo[],
|
||||
query: '',
|
||||
getMyApps: async () => {
|
||||
const res = await fetch(`${BASE_URL}/apps`)
|
||||
const apps = await res.json() as AppInfo[]
|
||||
|
||||
const myApps = apps.reduce((acc, app) => {
|
||||
const appType = getAppType(app)
|
||||
|
||||
acc[appType].push(app)
|
||||
return acc
|
||||
}, {
|
||||
downloaded: [],
|
||||
installed: [],
|
||||
local: [],
|
||||
system: [],
|
||||
} as MyApps)
|
||||
|
||||
set(() => ({ myApps }))
|
||||
return myApps
|
||||
},
|
||||
getListedApps: async () => {
|
||||
const res = await fetch(`${BASE_URL}/apps/listed`)
|
||||
const apps = await res.json() as AppInfo[]
|
||||
set({ listedApps: apps })
|
||||
return apps
|
||||
},
|
||||
getMyApp: async (info: AppInfo) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}`)
|
||||
const app = await res.json() as AppInfo
|
||||
const appType = getAppType(app)
|
||||
const myApps = get().myApps
|
||||
myApps[appType] = myApps[appType].map((a) => isApp(a, app) ? app : a)
|
||||
const listedApps = [...get().listedApps].map((a) => isApp(a, app) ? app : a)
|
||||
set({ myApps, listedApps })
|
||||
return app
|
||||
},
|
||||
installApp: async (info: AppInfo) => {
|
||||
const approveRes = await fetch(`${BASE_URL}/apps/${appId(info)}/caps`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (approveRes.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to approve caps for app: ${appId(info)}`)
|
||||
}
|
||||
|
||||
const installRes = await fetch(`${BASE_URL}/apps/${appId(info)}`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (installRes.status !== HTTP_STATUS.CREATED) {
|
||||
throw new Error(`Failed to install app: ${appId(info)}`)
|
||||
}
|
||||
},
|
||||
updateApp: async (app: AppInfo) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.NO_CONTENT) {
|
||||
throw new Error(`Failed to update app: ${appId(app)}`)
|
||||
}
|
||||
|
||||
// TODO: get the app from the server instead of updating locally
|
||||
},
|
||||
uninstallApp: async (app: AppInfo) => {
|
||||
if (!confirm(`Are you sure you want to remove ${appId(app)}?`)) return
|
||||
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.NO_CONTENT) {
|
||||
throw new Error(`Failed to remove app: ${appId(app)}`)
|
||||
}
|
||||
|
||||
const myApps = { ...get().myApps }
|
||||
const appType = getAppType(app)
|
||||
myApps[appType] = myApps[appType].filter((a) => !isApp(a, app))
|
||||
const listedApps = get().listedApps.map((a) => isApp(a, app) ? { ...a, state: undefined, installed: false } : a)
|
||||
set({ myApps, listedApps })
|
||||
},
|
||||
getListedApp: async (packageName: string) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/listed/${packageName}`)
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to get app: ${packageName}`)
|
||||
}
|
||||
const app = await res.json() as AppInfo
|
||||
return app
|
||||
},
|
||||
downloadApp: async (info: AppInfo, download_from: string) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/listed/${appId(info)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ download_from }),
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.CREATED) {
|
||||
throw new Error(`Failed to get app: ${appId(info)}`)
|
||||
}
|
||||
},
|
||||
getCaps: async (info: AppInfo) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/caps`)
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to get app: ${appId(info)}`)
|
||||
}
|
||||
|
||||
const caps = await res.json() as PackageManifest[]
|
||||
return caps[0]
|
||||
},
|
||||
approveCaps: async (info: AppInfo) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/caps`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to get app: ${appId(info)}`)
|
||||
}
|
||||
},
|
||||
setMirroring: async (info: AppInfo, mirroring: boolean) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/mirror`, {
|
||||
method: mirroring ? 'PUT' : 'DELETE',
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to start mirror: ${appId(info)}`)
|
||||
}
|
||||
get().getMyApp(info)
|
||||
},
|
||||
setAutoUpdate: async (info: AppInfo, autoUpdate: boolean) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/auto-update`, {
|
||||
method: autoUpdate ? 'PUT' : 'DELETE',
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to change auto update: ${appId(info)}`)
|
||||
}
|
||||
get().getMyApp(info)
|
||||
},
|
||||
|
||||
// searchApps: async (query: string, onlyMyApps = true) => {
|
||||
// if (onlyMyApps) {
|
||||
// const searchResults = get().myApps.filter((app) =>
|
||||
// app.name.toLowerCase().includes(query.toLowerCase())
|
||||
// || app.publisher.toLowerCase().includes(query.toLowerCase())
|
||||
// || app.metadata?.name?.toLowerCase()?.includes(query.toLowerCase())
|
||||
// )
|
||||
// set(() => ({ searchResults }))
|
||||
// return searchResults
|
||||
// } else {
|
||||
// const res = await fetch(`${BASE_URL}/apps/search/${encodeURIComponent(query)}`)
|
||||
// const searchResults = await res.json() as AppInfo[]
|
||||
// set(() => ({ searchResults }))
|
||||
// return searchResults
|
||||
// }
|
||||
// },
|
||||
|
||||
get,
|
||||
set,
|
||||
}),
|
||||
{
|
||||
name: 'app_store', // unique name
|
||||
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export default useAppsStore
|
101
kinode/packages/app_store/ui/src/types/Apps.ts
Normal file
@ -0,0 +1,101 @@
|
||||
export interface MyApps {
|
||||
downloaded: AppInfo[]
|
||||
installed: AppInfo[]
|
||||
local: AppInfo[]
|
||||
system: AppInfo[]
|
||||
}
|
||||
|
||||
export interface AppListing {
|
||||
owner?: string
|
||||
package: string
|
||||
publisher: string
|
||||
metadata_hash: string
|
||||
metadata?: OnchainPackageMetadata
|
||||
installed: boolean
|
||||
state?: PackageState
|
||||
}
|
||||
|
||||
export interface Erc721Properties {
|
||||
package_name: string;
|
||||
publisher: string;
|
||||
current_version: string;
|
||||
mirrors: string[];
|
||||
code_hashes: Record<string, string>;
|
||||
license?: string;
|
||||
screenshots?: string[];
|
||||
wit_version?: [number, number, number];
|
||||
}
|
||||
|
||||
export interface OnchainPackageMetadata {
|
||||
name?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
external_url?: string;
|
||||
animation_url?: string;
|
||||
properties: Erc721Properties;
|
||||
}
|
||||
|
||||
export interface PackageState {
|
||||
mirrored_from: string;
|
||||
our_version: string;
|
||||
installed: boolean;
|
||||
verified: boolean;
|
||||
caps_approved: boolean;
|
||||
manifest_hash?: string;
|
||||
mirroring: boolean;
|
||||
auto_update: boolean;
|
||||
// source_zip?: Uint8Array, // bytes
|
||||
}
|
||||
|
||||
export interface AppInfo extends AppListing {
|
||||
permissions?: string[]
|
||||
}
|
||||
|
||||
export interface PackageManifest {
|
||||
process_name: string
|
||||
process_wasm_path: string
|
||||
on_exit: string
|
||||
request_networking: boolean
|
||||
request_capabilities: string[]
|
||||
grant_capabilities: string[]
|
||||
public: boolean
|
||||
}
|
||||
|
||||
[
|
||||
{
|
||||
"installed": false,
|
||||
"metadata": null,
|
||||
"metadata_hash": "0xf244e4e227494c6a0716597f0c405284eb53f7916427d48ceb03a24ed5b52b5d",
|
||||
"owner": "0x7Bf904E36715B650Fb1F99113cb4A2B2FfE22392",
|
||||
"package": "sdapi",
|
||||
"publisher": "mothu-et-doria.os",
|
||||
"state": null
|
||||
},
|
||||
{
|
||||
"installed": false,
|
||||
|
||||
"metadata_hash": "0xe43f616b39f2511f2c3c29c801a0993de5a74ab1fc4382ff7c68aad50f0242f3",
|
||||
"owner": "0xDe12193c037F768fDC0Db0B77B7E70de723b95E7",
|
||||
"package": "chat",
|
||||
"publisher": "mythicengineer.os",
|
||||
"state": null
|
||||
},
|
||||
{
|
||||
"installed": false,
|
||||
"metadata": null,
|
||||
"metadata_hash": "0x4385b4b9ddddcc25ce99d6ae1542b1362c0e7f41abf1385cd9eda4d39ced6e39",
|
||||
"owner": "0x7213aa2A6581b37506C035b387b4Bf2Fb93E2f88",
|
||||
"package": "chat_template",
|
||||
"publisher": "odinsbadeye.os",
|
||||
"state": null
|
||||
},
|
||||
{
|
||||
"installed": false,
|
||||
"metadata": null,
|
||||
"metadata_hash": "0x0f4c02462407d88fb43a0e24df7e36b7be4a09f2fc27bb690e5b76c8d21088ef",
|
||||
"owner": "0x958946dEcCfe3546fE7F3f98eb07c100E472F09D",
|
||||
"package": "kino_files",
|
||||
"publisher": "gloriainexcelsisdeo.os",
|
||||
"state": null
|
||||
}
|
||||
]
|
7
kinode/packages/app_store/ui/src/types/Page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { ethers } from "ethers";
|
||||
import { PackageStore } from "../abis/types";
|
||||
|
||||
export interface PageProps {
|
||||
provider?: ethers.providers.Web3Provider;
|
||||
packageAbi: PackageStore
|
||||
}
|
24
kinode/packages/app_store/ui/src/utils/app.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { AppInfo } from "../types/Apps";
|
||||
|
||||
export const appId = (app: AppInfo) => `${app.package}:${app.publisher}`
|
||||
|
||||
export const getAppName = (app: AppInfo) => app.metadata?.name || appId(app)
|
||||
|
||||
export enum AppType {
|
||||
Downloaded = 'downloaded',
|
||||
Installed = 'installed',
|
||||
Local = 'local',
|
||||
System = 'system',
|
||||
}
|
||||
|
||||
export const getAppType = (app: AppInfo) => {
|
||||
if (app.publisher === 'sys') {
|
||||
return AppType.System
|
||||
} else if (app.state?.our_version && !app.state?.capsApproved) {
|
||||
return AppType.Downloaded
|
||||
} else if (!app.metadata) {
|
||||
return AppType.Local
|
||||
} else {
|
||||
return AppType.Installed
|
||||
}
|
||||
}
|
88
kinode/packages/app_store/ui/src/utils/chain.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { SEPOLIA_OPT_HEX, OPTIMISM_OPT_HEX } from "../constants/chain";
|
||||
const CHAIN_NOT_FOUND = "4902"
|
||||
|
||||
export interface Chain {
|
||||
chainId: string, // Replace with the correct chainId for Sepolia
|
||||
chainName: string,
|
||||
nativeCurrency: {
|
||||
name: string,
|
||||
symbol: string,
|
||||
decimals: number
|
||||
},
|
||||
rpcUrls: string[],
|
||||
blockExplorerUrls: string[]
|
||||
}
|
||||
|
||||
export const CHAIN_DETAILS: { [key: string]: Chain } = {
|
||||
[SEPOLIA_OPT_HEX]: {
|
||||
chainId: SEPOLIA_OPT_HEX,
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: ['https://rpc.sepolia.org'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||
},
|
||||
[OPTIMISM_OPT_HEX]: {
|
||||
chainId: OPTIMISM_OPT_HEX,
|
||||
chainName: 'Optimism',
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: ['https://mainnet.optimism.io'],
|
||||
blockExplorerUrls: ['https://optimistic.etherscan.io']
|
||||
}
|
||||
}
|
||||
|
||||
export const getNetworkName = (networkId: string) => {
|
||||
switch (networkId) {
|
||||
case '1':
|
||||
case '0x1':
|
||||
return 'Ethereum'; // Ethereum Mainnet
|
||||
case '10':
|
||||
case 'a':
|
||||
case '0xa':
|
||||
return 'Optimism'; // Optimism
|
||||
case '42161':
|
||||
return 'Arbitrum'; // Arbitrum One
|
||||
case '11155111':
|
||||
case 'aa36a7':
|
||||
case '0xaa36a7':
|
||||
return 'Sepolia'; // Sepolia Testnet
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const setChain = async (chainId: string) => {
|
||||
let networkId = await (window.ethereum as any)?.request({ method: 'net_version' }).catch(() => '1') // eslint-disable-line
|
||||
networkId = '0x' + (typeof networkId === 'string' ? networkId.replace(/^0x/, '') : networkId.toString(16))
|
||||
|
||||
if (!CHAIN_DETAILS[chainId]) {
|
||||
console.error(`Invalid chain ID: ${chainId}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (chainId !== networkId) {
|
||||
try {
|
||||
await (window.ethereum as any)?.request({ // eslint-disable-line
|
||||
method: "wallet_switchEthereumChain",
|
||||
params: [{ chainId }]
|
||||
});
|
||||
} catch (err) {
|
||||
if (String(err).includes(CHAIN_NOT_FOUND)) {
|
||||
await (window.ethereum as any)?.request({ // eslint-disable-line
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [CHAIN_DETAILS[chainId]]
|
||||
})
|
||||
} else {
|
||||
window.alert(`You must enable the ${getNetworkName(chainId)} network in your wallet.`)
|
||||
throw new Error(`User cancelled connection to ${chainId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
kinode/packages/app_store/ui/src/utils/dnsWire.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export function toDNSWireFormat(domain: string) {
|
||||
const parts = domain.split('.');
|
||||
const result = new Uint8Array(domain.length + parts.length);
|
||||
let idx = 0;
|
||||
|
||||
for (const part of parts) {
|
||||
const len = part.length;
|
||||
result[idx] = len; // write length byte
|
||||
idx++;
|
||||
for (let j = 0; j < len; j++) {
|
||||
result[idx] = part.charCodeAt(j); // write ASCII bytes of the label
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
// result[idx] = 0; // TODO do you need null byte at the end?
|
||||
|
||||
return `0x${Array.from(result).map(byte => byte.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
4
kinode/packages/app_store/ui/src/utils/metamask.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { initializeConnector } from '@web3-react/core'
|
||||
import { MetaMask } from '@web3-react/metamask'
|
||||
|
||||
export const [metaMask, hooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions }))
|
1
kinode/packages/app_store/ui/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
27
kinode/packages/app_store/ui/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// "strict": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"../src",
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
kinode/packages/app_store/ui/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
68
kinode/packages/app_store/ui/vite.config.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
/*
|
||||
If you are developing a UI outside of a Kinode project,
|
||||
comment out the following 2 lines:
|
||||
*/
|
||||
// import manifest from '../pkg/manifest.json'
|
||||
// import metadata from '../pkg/metadata.json'
|
||||
|
||||
/*
|
||||
IMPORTANT:
|
||||
This must match the process name from pkg/manifest.json + pkg/metadata.json
|
||||
The format is "/" + "process_name:package_name:publisher_node"
|
||||
*/
|
||||
const BASE_URL = `/main:app_store:sys`;
|
||||
// const BASE_URL = `/${manifest[0].process_name}:${metadata.package}:${metadata.publisher}`;
|
||||
|
||||
// This is the proxy URL, it must match the node you are developing against
|
||||
const PROXY_URL = (process.env.VITE_NODE_URL || 'http://127.0.0.1:8080').replace('localhost', '127.0.0.1');
|
||||
|
||||
console.log('process.env.VITE_NODE_URL', process.env.VITE_NODE_URL, PROXY_URL);
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: BASE_URL,
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['/our.js']
|
||||
}
|
||||
},
|
||||
server: {
|
||||
open: true,
|
||||
proxy: {
|
||||
'/our': {
|
||||
target: PROXY_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
[`${BASE_URL}/our.js`]: {
|
||||
target: PROXY_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(BASE_URL, ''),
|
||||
},
|
||||
// This route will match all other HTTP requests to the backend
|
||||
[`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|$))`]: {
|
||||
target: PROXY_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
// '/example': {
|
||||
// target: PROXY_URL,
|
||||
// changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(BASE_URL, ''),
|
||||
// // This is only for debugging purposes
|
||||
// configure: (proxy, _options) => {
|
||||
// proxy.on('error', (err, _req, _res) => {
|
||||
// console.log('proxy error', err);
|
||||
// });
|
||||
// proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||
// console.log('Sending Request to the Target:', req.method, req.url);
|
||||
// });
|
||||
// proxy.on('proxyRes', (proxyRes, req, _res) => {
|
||||
// console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
}
|
||||
}
|
||||
});
|
5036
kinode/packages/app_store/ui/yarn.lock
Normal file
25
kinode/packages/homepage/ui/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
src/abis/types/*
|
1
kinode/packages/homepage/ui/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v18.18.0
|
18
kinode/packages/homepage/ui/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Register
|
||||
This app is compiled and put into the root directory of every Kinode node for login and registration. It handles all on-chain KNS registration flows
|
||||
|
||||
## Development
|
||||
|
||||
1. Run `yarn` to install dependencies
|
||||
2. Run `yarn run tc` to generate ABIs
|
||||
3. Start a kinode locally on port 8080 (default)
|
||||
3. Run `yarn start` to serve the UI at http://localhost:3000 (proxies requests to local kinode)
|
||||
|
||||
If you would like to proxy requests to a kinode that is not at http://localhost:8080, change the `proxy` field in `package.json`.
|
||||
|
||||
## Building
|
||||
|
||||
1. Run `yarn` to install dependencies
|
||||
2. Run `yarn run tc` to generate ABIs
|
||||
3. Run `yarn build` to generate the `./build` folder
|
||||
4. Overwrite `kinode/kinode/src/register-ui/build` with `./build`
|
19
kinode/packages/homepage/ui/add-inline-tags.js
Normal file
@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const indexPath = path.join(__dirname, 'build', 'index.html');
|
||||
|
||||
fs.readFile(indexPath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let modifiedHtml = data
|
||||
.replace(/<script src="(.*?)"><\/script>/g, '<script src="$1" inline></script>')
|
||||
.replace(/<link href="(.*?)" rel="stylesheet">/g, '<link href="$1" rel="stylesheet" inline>');
|
||||
|
||||
fs.writeFile(indexPath, modifiedHtml, 'utf8', (err) => {
|
||||
if (err) return console.log(err);
|
||||
});
|
||||
});
|
6
kinode/packages/homepage/ui/build_all.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
source ~/.nvm/nvm.sh
|
||||
nvm use
|
||||
npm install
|
||||
npm run tc
|
||||
npm run build
|
21429
kinode/packages/homepage/ui/package-lock.json
generated
Normal file
71
kinode/packages/homepage/ui/package.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "register",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "http://127.0.0.1:8080",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@ethersproject/hash": "^5.7.0",
|
||||
"@typechain/ethers-v5": "^11.1.1",
|
||||
"@types/node": "^16.18.50",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@web3-react/coinbase-wallet": "^8.2.3",
|
||||
"@web3-react/core": "^8.2.2",
|
||||
"@web3-react/gnosis-safe": "^8.2.4",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/metamask": "^8.2.3",
|
||||
"@web3-react/network": "^8.2.3",
|
||||
"@web3-react/types": "^8.2.2",
|
||||
"@web3-react/walletconnect": "^8.2.3",
|
||||
"@web3-react/walletconnect-connector": "^6.2.13",
|
||||
"@web3-react/walletconnect-v2": "^8.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eth-ens-namehash": "^2.0.8",
|
||||
"ethers": "^5.7.2",
|
||||
"idna-uts46-hx": "^2.3.1",
|
||||
"is-valid-domain": "^0.1.6",
|
||||
"jazzicon": "^1.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typechain": "^8.3.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "GENERATE_SOURCEMAP=false react-scripts build",
|
||||
"build:copy": "npm run build && npm run copy",
|
||||
"copy": "mkdir -p ../../../src/register-ui/build && rm -rf ../../../src/register-ui/build/* && cp -r build/* ../../../src/register-ui/build/",
|
||||
"inline": "node ./add-inline-tags.js && cd build && inline-source ./index.html > ./inline-index.html && cd ..",
|
||||
"build-inline": "npm run build && npm run inline",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"tc": "typechain --target ethers-v5 --out-dir src/abis/types/ \"./src/abis/**/*.json\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-modal": "^3.16.2",
|
||||
"inline-source-cli": "^2.0.0"
|
||||
}
|
||||
}
|
23
kinode/packages/homepage/ui/public/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Welcome - Kinode</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="icon"
|
||||
href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzc5IiBoZWlnaHQ9IjUxNCIgdmlld0JveD0iMCAwIDc3OSA1MTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8c3R5bGU+CiAgICAgICAgQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykgewogICAgICAgICAgICBzdmcgeyBmaWxsOiB3aGl0ZTsgfQogICAgICAgIH0KICAgICAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBsaWdodCkgewogICAgICAgICAgICBzdmcgeyBmaWxsOiBibGFjazsgfQogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8cGF0aCBkPSJNNzUzLjA5MiA1LjkxOTMyQzc1Ni41NTcgNS4wOTk3NiA3NTUuOTYyIC0wLjAwMDEyMjA3IDc1Mi40MDEgLTAuMDAwMTIyMDdINDI2LjAwMUM0MjQuNzU1IC0wLjAwMDEyMjA3IDQyMy42MzkgMC43NzAyNyA0MjMuMTk3IDEuOTM1MzVMMjM2Ljk2OCA0OTIuNkMyMzUuNzI5IDQ5NS44NjUgMjQwLjEyMyA0OTguMjU1IDI0Mi4xOTEgNDk1LjQ0MUw1NjkuMzU3IDUwLjExMzJDNTY5Ljc3OCA0OS41MzkyIDU3MC4zOTEgNDkuMTMzOSA1NzEuMDg0IDQ4Ljk3TDc1My4wOTIgNS45MTkzMloiLz4KICAgIDxwYXRoIGQ9Ik0xMS45NjY1IDQwLjIyODhDOS4xMDk0OSAzOC43NzcgMTAuMjEzNSAzNC40NTgzIDEzLjQxNjcgMzQuNTU1N0w0MDQuMjczIDQ2LjQzNjdDNDA2LjMzNCA0Ni40OTkzIDQwNy43MTkgNDguNTc0OSA0MDYuOTg2IDUwLjUwMjNMMzQ3LjQzOCAyMDYuOTgxQzM0Ni44MDQgMjA4LjY0NyAzNDQuODY1IDIwOS4zOTYgMzQzLjI3NSAyMDguNTg4TDExLjk2NjUgNDAuMjI4OFoiLz4KPC9zdmc+Cg==">
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
25
kinode/packages/homepage/ui/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
kinode/packages/homepage/ui/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
256
kinode/packages/homepage/ui/src/App.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Navigate, BrowserRouter as Router, Route, Routes, useParams } from 'react-router-dom';
|
||||
import { hooks } from "./connectors/metamask";
|
||||
import {
|
||||
KNS_REGISTRY_ADDRESSES,
|
||||
DOT_OS_ADDRESSES,
|
||||
ENS_REGISTRY_ADDRESSES,
|
||||
NAMEWRAPPER_ADDRESSES,
|
||||
KNS_ENS_ENTRY_ADDRESSES,
|
||||
KNS_ENS_EXIT_ADDRESSES,
|
||||
} from "./constants/addresses";
|
||||
import { ChainId } from "./constants/chainId";
|
||||
import {
|
||||
KNSRegistryResolver,
|
||||
KNSRegistryResolver__factory,
|
||||
DotOsRegistrar,
|
||||
DotOsRegistrar__factory,
|
||||
KNSEnsEntry,
|
||||
KNSEnsEntry__factory,
|
||||
KNSEnsExit,
|
||||
KNSEnsExit__factory,
|
||||
NameWrapper,
|
||||
NameWrapper__factory,
|
||||
ENSRegistry,
|
||||
ENSRegistry__factory
|
||||
} from "./abis/types";
|
||||
import { ethers } from "ethers";
|
||||
import ConnectWallet from "./components/ConnectWallet";
|
||||
import RegisterEthName from "./pages/RegisterEthName";
|
||||
import RegisterOsName from "./pages/RegisterKnsName";
|
||||
import ClaimOsInvite from "./pages/ClaimKnsInvite";
|
||||
import SetPassword from "./pages/SetPassword";
|
||||
import Login from './pages/Login'
|
||||
import Reset from './pages/ResetKnsName'
|
||||
import OsHome from "./pages/KinodeHome"
|
||||
import ResetNode from "./pages/ResetNode";
|
||||
import ImportKeyfile from "./pages/ImportKeyfile";
|
||||
import { UnencryptedIdentity } from "./lib/types";
|
||||
|
||||
const {
|
||||
useProvider,
|
||||
} = hooks;
|
||||
|
||||
function App() {
|
||||
const provider = useProvider();
|
||||
const params = useParams()
|
||||
|
||||
const [pw, setPw] = useState<string>('');
|
||||
const [key, setKey] = useState<string>('');
|
||||
const [keyFileName, setKeyFileName] = useState<string>('');
|
||||
const [reset, setReset] = useState<boolean>(false);
|
||||
const [direct, setDirect] = useState<boolean>(false);
|
||||
const [knsName, setOsName] = useState<string>('');
|
||||
const [appSizeOnLoad, setAppSizeOnLoad] = useState<number>(0);
|
||||
const [networkingKey, setNetworkingKey] = useState<string>('');
|
||||
const [ipAddress, setIpAddress] = useState<number>(0);
|
||||
const [port, setPort] = useState<number>(0);
|
||||
const [routers, setRouters] = useState<string[]>([]);
|
||||
const [nodeChainId, setNodeChainId] = useState('')
|
||||
|
||||
const [navigateToLogin, setNavigateToLogin] = useState<boolean>(false)
|
||||
const [initialVisit, setInitialVisit] = useState<boolean>(!params?.initial)
|
||||
|
||||
const [connectOpen, setConnectOpen] = useState<boolean>(false);
|
||||
const openConnect = () => setConnectOpen(true)
|
||||
const closeConnect = () => setConnectOpen(false)
|
||||
|
||||
const rpcUrl = useMemo(() => provider?.network?.chainId === ChainId.SEPOLIA ? process.env.REACT_APP_SEPOLIA_RPC_URL : process.env.REACT_APP_OPTIMISM_RPC_URL, [provider])
|
||||
|
||||
const [dotOs, setDotOs] = useState<DotOsRegistrar>(
|
||||
DotOsRegistrar__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? DOT_OS_ADDRESSES[ChainId.SEPOLIA] : DOT_OS_ADDRESSES[ChainId.OPTIMISM],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [kns, setKns] = useState<KNSRegistryResolver>(
|
||||
KNSRegistryResolver__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? KNS_REGISTRY_ADDRESSES[ChainId.SEPOLIA] : KNS_REGISTRY_ADDRESSES[ChainId.OPTIMISM],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [knsEnsEntry, setKnsEnsEntry] = useState<KNSEnsEntry>(
|
||||
KNSEnsEntry__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? KNS_ENS_ENTRY_ADDRESSES[ChainId.SEPOLIA] : KNS_ENS_ENTRY_ADDRESSES[ChainId.MAINNET],
|
||||
// set rpc url based on chain id
|
||||
new ethers.providers.JsonRpcProvider(provider?.network?.chainId === ChainId.SEPOLIA ? process.env.REACT_APP_SEPOLIA_RPC_URL : process.env.REACT_APP_MAINNET_RPC_URL))
|
||||
);
|
||||
|
||||
const [knsEnsExit, setKnsEnsExit] = useState<KNSEnsExit>(
|
||||
KNSEnsExit__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? KNS_ENS_EXIT_ADDRESSES[ChainId.SEPOLIA] : KNS_ENS_EXIT_ADDRESSES[ChainId.OPTIMISM],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [nameWrapper, setNameWrapper] = useState<NameWrapper>(
|
||||
NameWrapper__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? NAMEWRAPPER_ADDRESSES[ChainId.SEPOLIA] : NAMEWRAPPER_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [ensRegistry, setEnsRegistry] = useState<ENSRegistry>(
|
||||
ENSRegistry__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? ENS_REGISTRY_ADDRESSES[ChainId.SEPOLIA] : ENS_REGISTRY_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
useEffect(() => setAppSizeOnLoad(
|
||||
(window.performance.getEntriesByType('navigation') as any)[0].transferSize
|
||||
), []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const infoResponse = await fetch('/info', { method: 'GET' })
|
||||
|
||||
if (infoResponse.status > 399) {
|
||||
console.log('no info, unbooted')
|
||||
} else {
|
||||
const info: UnencryptedIdentity = await infoResponse.json()
|
||||
|
||||
if (initialVisit) {
|
||||
setOsName(info.name)
|
||||
setRouters(info.allowed_routers)
|
||||
setNavigateToLogin(true)
|
||||
setInitialVisit(false)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.log('no info, unbooted')
|
||||
}
|
||||
|
||||
try {
|
||||
const currentChainResponse = await fetch('/current-chain', { method: 'GET' })
|
||||
|
||||
if (currentChainResponse.status < 400) {
|
||||
const nodeChainId = await currentChainResponse.json()
|
||||
setNodeChainId(nodeChainId.toLowerCase())
|
||||
console.log('Node Chain ID:', nodeChainId)
|
||||
}
|
||||
} catch {
|
||||
console.log('error getting current chain')
|
||||
}
|
||||
})()
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => setNavigateToLogin(false), [initialVisit])
|
||||
|
||||
useEffect(() => {
|
||||
provider?.getNetwork().then(network => {
|
||||
if (network.chainId === ChainId.SEPOLIA) {
|
||||
setDotOs(DotOsRegistrar__factory.connect(
|
||||
DOT_OS_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKns(KNSRegistryResolver__factory.connect(
|
||||
KNS_REGISTRY_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKnsEnsEntry(KNSEnsEntry__factory.connect(
|
||||
KNS_ENS_ENTRY_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKnsEnsExit(KNSEnsExit__factory.connect(
|
||||
KNS_ENS_EXIT_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setNameWrapper(NameWrapper__factory.connect(
|
||||
NAMEWRAPPER_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setEnsRegistry(ENSRegistry__factory.connect(
|
||||
ENS_REGISTRY_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
|
||||
} else if (network.chainId === ChainId.OPTIMISM || network.chainId === ChainId.MAINNET) {
|
||||
setDotOs(DotOsRegistrar__factory.connect(
|
||||
DOT_OS_ADDRESSES[ChainId.OPTIMISM],
|
||||
provider!.getSigner())
|
||||
)
|
||||
setKns(KNSRegistryResolver__factory.connect(
|
||||
KNS_REGISTRY_ADDRESSES[ChainId.OPTIMISM],
|
||||
provider!.getSigner())
|
||||
)
|
||||
setKnsEnsExit(KNSEnsExit__factory.connect(
|
||||
KNS_ENS_EXIT_ADDRESSES[ChainId.OPTIMISM],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKnsEnsEntry(KNSEnsEntry__factory.connect(
|
||||
KNS_ENS_ENTRY_ADDRESSES[ChainId.MAINNET],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setNameWrapper(NameWrapper__factory.connect(
|
||||
NAMEWRAPPER_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(process.env.REACT_APP_MAINNET_RPC_URL)
|
||||
))
|
||||
setEnsRegistry(ENSRegistry__factory.connect(
|
||||
ENS_REGISTRY_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(process.env.REACT_APP_MAINNET_RPC_URL)
|
||||
))
|
||||
}
|
||||
})
|
||||
}, [provider])
|
||||
|
||||
const knsEnsEntryNetwork = ChainId.SEPOLIA;
|
||||
const knsEnsExitNetwork = ChainId.SEPOLIA;
|
||||
|
||||
// just pass all the props each time since components won't mind extras
|
||||
const props = {
|
||||
direct, setDirect,
|
||||
key,
|
||||
keyFileName, setKeyFileName,
|
||||
reset, setReset,
|
||||
pw, setPw,
|
||||
knsName, setOsName,
|
||||
dotOs, kns,
|
||||
knsEnsEntryNetwork, knsEnsExitNetwork,
|
||||
knsEnsEntry, knsEnsExit,
|
||||
nameWrapper, ensRegistry,
|
||||
connectOpen, openConnect, closeConnect,
|
||||
provider, appSizeOnLoad,
|
||||
networkingKey, setNetworkingKey,
|
||||
ipAddress, setIpAddress,
|
||||
port, setPort,
|
||||
routers, setRouters,
|
||||
nodeChainId,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<>
|
||||
<ConnectWallet {...props} />
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={navigateToLogin
|
||||
? <Navigate to="/login" replace />
|
||||
: <OsHome {...props} />
|
||||
} />
|
||||
<Route path="/claim-invite" element={<ClaimOsInvite {...props} />} />
|
||||
<Route path="/register-name" element={<RegisterOsName {...props} />} />
|
||||
<Route path="/register-eth-name" element={<RegisterEthName {...props} />} />
|
||||
<Route path="/set-password" element={<SetPassword {...props} />} />
|
||||
<Route path="/reset" element={<Reset {...props} />} />
|
||||
<Route path="/reset-node" element={<ResetNode {...props} />} />
|
||||
<Route path="/import-keyfile" element={<ImportKeyfile {...props} />} />
|
||||
<Route path="/login" element={<Login {...props} />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
7741
kinode/packages/homepage/ui/src/abis/DotOsRegistrar.json
Normal file
1336
kinode/packages/homepage/ui/src/abis/DotOsRegistrar.ts
Normal file
1
kinode/packages/homepage/ui/src/abis/ENSRegistry.json
Normal file
1
kinode/packages/homepage/ui/src/abis/KNSEnsEntry.json
Normal file
1
kinode/packages/homepage/ui/src/abis/KNSEnsExit.json
Normal file
11870
kinode/packages/homepage/ui/src/abis/KNSRegistryResolver.json
Normal file
1
kinode/packages/homepage/ui/src/abis/NameWrapper.json
Normal file
BIN
kinode/packages/homepage/ui/src/assets/arbitrum.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
kinode/packages/homepage/ui/src/assets/background.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
kinode/packages/homepage/ui/src/assets/eth.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
18
kinode/packages/homepage/ui/src/assets/kinode.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="580" height="72" viewBox="0 0 580 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_641)">
|
||||
<path d="M0.824922 1.07031L0.794922 70.0703H14.7949L14.8049 1.07031H0.824922Z" fill="#FFF5D9"/>
|
||||
<path d="M16.5947 36.8803L41.2547 1.07031H58.2447L33.1647 36.8803L61.2447 70.0703H42.9947L16.5947 36.8803Z" fill="#FFF5D9"/>
|
||||
<path d="M119.885 1.07031H105.765V70.0703H119.885V1.07031Z" fill="#FFF5D9"/>
|
||||
<path d="M173.185 1.07031V70.0703H186.775V26.8303L224.045 70.0703H234.825V1.07031H221.325V45.6803L183.445 1.07031H173.185Z" fill="#FFF5D9"/>
|
||||
<path d="M342.465 8.86C333.025 0.15 321.645 0 318.535 0C315.475 0 303.575 0.22 294.005 9.52C283.845 19.4 283.805 32.24 283.795 35.66C283.785 39.3 283.895 49.03 290.805 57.99C300.855 71.02 316.695 71.31 318.535 71.32C321.375 71.32 334.185 71 343.965 60.66C353.065 51.04 353.265 39.4 353.275 35.66C353.275 32.49 353.305 18.86 342.455 8.86H342.465ZM318.435 58.01C307.095 58.01 297.895 47.95 297.895 35.54C297.895 23.13 307.085 13.07 318.435 13.07C329.785 13.07 338.975 23.13 338.975 35.54C338.975 47.95 329.785 58.01 318.435 58.01Z" fill="#FFF5D9"/>
|
||||
<path d="M450.495 12.0802C444.975 5.46023 437.135 0.990234 427.955 0.990234C417.555 0.990234 405.295 1.07023 402.295 1.07023V69.9802C405.285 69.9802 417.555 70.0602 427.955 70.0602C445.525 70.0602 458.445 53.4102 459.065 36.8602C459.395 28.0102 456.185 18.9002 450.495 12.0802ZM440.085 49.9502C436.895 53.8702 432.705 56.6902 427.665 57.5602C424.025 58.1902 420.095 57.8302 416.405 57.8302C416.405 50.4002 416.405 42.9802 416.405 35.5502V13.2202C423.795 13.2202 430.525 12.7002 436.605 17.6002C440.275 20.5602 442.925 24.7102 444.165 29.2402C444.525 30.5402 444.765 31.8802 444.875 33.2302C445.395 39.3702 443.995 45.1402 440.085 49.9502Z" fill="#FFF5D9"/>
|
||||
<path d="M508.135 0.990234V70.0602H552.715V57.9302H522.035V40.4202H547.125V28.0702H521.995V13.3202H552.715V0.990234H508.135Z" fill="#FFF5D9"/>
|
||||
<path d="M574.835 66.0398H572.745L571.015 63.0698H569.845V66.0398H567.805V57.5498H571.765C572.845 57.5498 573.865 57.9298 574.425 58.9398C575.205 60.3698 574.665 62.3798 573.105 63.0298C573.725 64.1198 574.225 64.9498 574.845 66.0398H574.835ZM570.375 61.0798H570.845C571.335 61.0798 572.365 61.0798 572.365 60.2898C572.365 59.5598 571.335 59.5598 570.845 59.5598H570.375V61.0798Z" fill="#FFF5D9"/>
|
||||
<path d="M570.964 69.0002C574.913 69.0002 578.114 65.799 578.114 61.8502C578.114 57.9014 574.913 54.7002 570.964 54.7002C567.016 54.7002 563.814 57.9014 563.814 61.8502C563.814 65.799 567.016 69.0002 570.964 69.0002Z" stroke="#FFF5D9" stroke-width="2.2" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6_641">
|
||||
<rect width="578.41" height="71.32" fill="white" transform="translate(0.794922)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
10
kinode/packages/homepage/ui/src/assets/logo.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="122" height="81" viewBox="0 0 122 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_651)">
|
||||
<path d="M89.3665 8.06803L121.5 0.35155L66.5111 0.320312L63.7089 7.69502L0.5 5.7032L54.0253 32.9925L36.1529 80.3203L89.3665 8.06803Z" fill="#FFF5D9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6_651">
|
||||
<rect width="121" height="80" fill="white" transform="translate(0.5 0.320312)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 431 B |
BIN
kinode/packages/homepage/ui/src/assets/optimism.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
kinode/packages/homepage/ui/src/assets/sepolia.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
kinode/packages/homepage/ui/src/assets/unknown.png
Normal file
After Width: | Height: | Size: 14 KiB |
100
kinode/packages/homepage/ui/src/components/ChainInfo.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { useCallback } from 'react';
|
||||
import ethLogo from '../assets/eth.png';
|
||||
import sepoliaLogo from '../assets/sepolia.png';
|
||||
import optimismLogo from '../assets/optimism.png';
|
||||
import arbitrumLogo from '../assets/arbitrum.png';
|
||||
import unknownLogo from '../assets/unknown.png';
|
||||
import Jazzicon from "./Jazzicon";
|
||||
import { hooks } from "../connectors/metamask";
|
||||
import { KNS_REGISTRY_ADDRESSES } from '../constants/addresses';
|
||||
|
||||
const { useChainId } = hooks;
|
||||
|
||||
interface ChainInfoProps {
|
||||
account: string;
|
||||
networkName: string;
|
||||
changeConnectedAccount: () => void;
|
||||
changeToNodeChain: () => void;
|
||||
}
|
||||
|
||||
function ChainInfo({
|
||||
account,
|
||||
networkName,
|
||||
changeConnectedAccount,
|
||||
changeToNodeChain,
|
||||
}: ChainInfoProps) {
|
||||
const chainId = useChainId();
|
||||
|
||||
const formatAddress = (address: string) => {
|
||||
return `${address.substring(0, 6)}...${address.substring(
|
||||
address.length - 4
|
||||
)}`;
|
||||
};
|
||||
|
||||
const generateNetworkIcon = (networkName: string) => {
|
||||
switch (networkName) {
|
||||
case "Ethereum":
|
||||
return <img className="network-icon" src={ethLogo} alt={networkName} />;
|
||||
case "Optimism":
|
||||
return (
|
||||
<img className="network-icon" src={optimismLogo} alt={networkName} />
|
||||
);
|
||||
case "Arbitrum":
|
||||
return (
|
||||
<img className="network-icon" src={arbitrumLogo} alt={networkName} />
|
||||
);
|
||||
case "Sepolia":
|
||||
return (
|
||||
<img
|
||||
className="network-icon"
|
||||
src={sepoliaLogo}
|
||||
alt={networkName}
|
||||
style={{ filter: "grayscale(100%)" }}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<img
|
||||
className="network-icon"
|
||||
src={unknownLogo}
|
||||
alt={networkName}
|
||||
style={{ filter: "grayscale(100%)" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const showKnsAddress = useCallback(() => {
|
||||
window.alert(`The KNS Contract Address is: ${KNS_REGISTRY_ADDRESSES[chainId || ''] || 'unavailable on ' + networkName}`)
|
||||
}, [chainId, networkName])
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 10, maxWidth: 500 }}>
|
||||
{/* TODO: prompt to change address */}
|
||||
<button
|
||||
onClick={changeConnectedAccount}
|
||||
className="chain-button monospace"
|
||||
>
|
||||
<Jazzicon
|
||||
address={account || ""}
|
||||
diameter={24}
|
||||
style={{ marginRight: "0.5em" }}
|
||||
/>{" "}
|
||||
{formatAddress(account || "")}
|
||||
</button>
|
||||
<button
|
||||
onClick={changeToNodeChain}
|
||||
className="chain-button"
|
||||
style={{ maxWidth: "27%" }}
|
||||
>
|
||||
{generateNetworkIcon(networkName)} {networkName}
|
||||
</button>
|
||||
{/* TODO: show KNS contract ID in modal */}
|
||||
<button onClick={showKnsAddress} className="chain-button" style={{ maxWidth: "27%" }}>
|
||||
KNS Contract
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChainInfo;
|
63
kinode/packages/homepage/ui/src/components/ConnectWallet.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useCallback } from 'react';
|
||||
import { hooks, metaMask } from "../connectors/metamask";
|
||||
import Modal from "react-modal"
|
||||
import { SEPOLIA_OPT_HEX, SEPOLIA_OPT_INT } from '../constants/chainId';
|
||||
|
||||
const {
|
||||
useChainId,
|
||||
useIsActivating,
|
||||
} = hooks;
|
||||
|
||||
type ConnectWalletProps = {
|
||||
connectOpen: boolean,
|
||||
closeConnect: () => void
|
||||
}
|
||||
|
||||
export default function ConnectWallet({ connectOpen, closeConnect }: ConnectWalletProps) {
|
||||
const isActivating = useIsActivating();
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
closeConnect()
|
||||
await metaMask.activate().catch(() => { })
|
||||
|
||||
try {
|
||||
const networkId = String(await (window.ethereum as any)?.request({ method: 'net_version' }).catch(() => '0x1'))
|
||||
|
||||
if (networkId !== SEPOLIA_OPT_HEX && networkId !== SEPOLIA_OPT_INT) {
|
||||
const SEPOLIA_DETAILS = {
|
||||
chainId: '0xaa36a7',
|
||||
chainName: 'Sepolia Test Network',
|
||||
nativeCurrency: {
|
||||
name: 'Ethereum',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: ['https://sepolia-infura.brave.com/'], // Replace with Sepolia's RPC URL
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io'] // Replace with Sepolia's block explorer URL
|
||||
};
|
||||
|
||||
await (window.ethereum as any)?.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [SEPOLIA_DETAILS]
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('FAILED TO ADD SEPOLIA:', err)
|
||||
}
|
||||
}, [closeConnect]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={connectOpen}
|
||||
onRequestClose={closeConnect}
|
||||
className="connect-modal"
|
||||
overlayClassName="overlay-modal"
|
||||
>
|
||||
<div className="connect-modal-content">
|
||||
<button onClick={connect} disabled={isActivating} >
|
||||
Connect to Wallet
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
interface Props {
|
||||
direct: boolean;
|
||||
setDirect: (direct: boolean) => void;
|
||||
}
|
||||
|
||||
export default function DirectCheckbox({ direct, setDirect }: Props) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div style={{ position: "relative" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="direct"
|
||||
name="direct"
|
||||
checked={direct}
|
||||
onChange={(e) => setDirect(e.target.checked)}
|
||||
autoFocus
|
||||
/>
|
||||
{direct && (
|
||||
<span onClick={() => setDirect(false)} className="checkmark">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<label htmlFor="direct" className="direct-node-message">
|
||||
Register as a direct node. If you are unsure leave unchecked.
|
||||
</label>
|
||||
<div className="tooltip-container">
|
||||
<div className="tooltip-button">ℹ</div>
|
||||
<div className="tooltip-content">
|
||||
A direct node publishes its own networking information on-chain: IP,
|
||||
port, so on. An indirect node relies on the service of routers, which
|
||||
are themselves direct nodes. Only register a direct node if you know
|
||||
what you’re doing and have a public, static IP address.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
119
kinode/packages/homepage/ui/src/components/EnterEthName.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { hooks } from "../connectors/metamask";
|
||||
import { NameWrapper, ENSRegistry } from "../abis/types";
|
||||
import isValidDomain from 'is-valid-domain'
|
||||
import { hash } from 'eth-ens-namehash'
|
||||
import { toAscii } from 'idna-uts46-hx'
|
||||
|
||||
global.Buffer = global.Buffer || require('buffer').Buffer;
|
||||
|
||||
const {
|
||||
useChainId,
|
||||
useProvider,
|
||||
useAccount,
|
||||
} = hooks;
|
||||
|
||||
type ClaimOsNameProps = {
|
||||
name: string,
|
||||
setName: React.Dispatch<React.SetStateAction<string>>
|
||||
nameValidities: string[],
|
||||
setNameValidities: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
nameWrapper: NameWrapper,
|
||||
ensRegistry: ENSRegistry,
|
||||
triggerNameCheck: boolean
|
||||
}
|
||||
|
||||
function EnterEthName({
|
||||
name,
|
||||
setName,
|
||||
nameValidities,
|
||||
setNameValidities,
|
||||
nameWrapper,
|
||||
ensRegistry,
|
||||
triggerNameCheck
|
||||
}: ClaimOsNameProps) {
|
||||
|
||||
const userAddress = useAccount()
|
||||
|
||||
console.log("userAddress", userAddress)
|
||||
|
||||
const NAME_URL = "Name must be a valid URL without subdomains (A-Z, a-z, 0-9, and punycode)"
|
||||
const NAME_NOT_OWNED = "Name is not owned by your wallet"
|
||||
const NAME_INVALID_PUNY = "Unsupported punycode character"
|
||||
|
||||
const debouncer = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (debouncer.current)
|
||||
clearTimeout(debouncer.current);
|
||||
|
||||
debouncer.current = setTimeout(async () => {
|
||||
|
||||
if (name.length == 0) return
|
||||
|
||||
let index: number
|
||||
let validities = [...nameValidities]
|
||||
|
||||
let normalized: string
|
||||
index = validities.indexOf(NAME_INVALID_PUNY)
|
||||
try {
|
||||
normalized = toAscii(name + ".eth")
|
||||
if (index != -1) validities.splice(index, 1)
|
||||
} catch (e) {
|
||||
if (index == -1) validities.push(NAME_INVALID_PUNY)
|
||||
}
|
||||
|
||||
// only check if name is valid punycode
|
||||
if (normalized! !== undefined) {
|
||||
|
||||
index = validities.indexOf(NAME_URL)
|
||||
if (name != "" && !isValidDomain(normalized)) {
|
||||
if (index == -1) validities.push(NAME_URL)
|
||||
} else if (index != -1) validities.splice(index, 1)
|
||||
|
||||
index = validities.indexOf(NAME_NOT_OWNED)
|
||||
if (validities.length == 0 || index != -1) {
|
||||
|
||||
let owner = await ensRegistry.owner(hash(normalized))
|
||||
|
||||
if (owner == nameWrapper.address)
|
||||
owner = await nameWrapper.ownerOf(hash(normalized))
|
||||
|
||||
if (owner != userAddress) {
|
||||
if (index == -1) validities.push(NAME_NOT_OWNED)
|
||||
} else {
|
||||
validities.splice(index, 1)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
setNameValidities(validities)
|
||||
|
||||
}, 500)
|
||||
}, [name, triggerNameCheck])
|
||||
|
||||
const noDots = (e: any) => e.target.value.indexOf('.') == -1
|
||||
&& setName(e.target.value)
|
||||
|
||||
return (
|
||||
<div className="col" style={{ width: '100%' }}>
|
||||
<div className="row" style={{ width: '100%' }}>
|
||||
<input
|
||||
value={name}
|
||||
onChange={noDots}
|
||||
type="text"
|
||||
required
|
||||
name="dot-os-name"
|
||||
placeholder="e.g. myname"
|
||||
/>
|
||||
<div className="os">.eth</div>
|
||||
</div>
|
||||
{nameValidities.map((x, i) => <div key={i}><br /><span className="name-validity">{x}</span></div>)}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default EnterEthName;
|
111
kinode/packages/homepage/ui/src/components/EnterKnsName.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { hooks } from "../connectors/metamask";
|
||||
import { DotOsRegistrar } from "../abis/types";
|
||||
import isValidDomain from 'is-valid-domain'
|
||||
import { hash } from 'eth-ens-namehash'
|
||||
import { toAscii } from 'idna-uts46-hx'
|
||||
|
||||
global.Buffer = global.Buffer || require('buffer').Buffer;
|
||||
|
||||
const {
|
||||
useChainId,
|
||||
useProvider,
|
||||
} = hooks;
|
||||
|
||||
type ClaimOsNameProps = {
|
||||
name: string,
|
||||
setName: React.Dispatch<React.SetStateAction<string>>
|
||||
nameValidities: string[],
|
||||
setNameValidities: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
dotOs: DotOsRegistrar,
|
||||
triggerNameCheck: boolean
|
||||
}
|
||||
|
||||
function EnterOsName({
|
||||
name,
|
||||
setName,
|
||||
nameValidities,
|
||||
setNameValidities,
|
||||
dotOs,
|
||||
triggerNameCheck
|
||||
}: ClaimOsNameProps) {
|
||||
|
||||
const NAME_URL = "Name must be a valid URL without subdomains (A-Z, a-z, 0-9, and punycode)"
|
||||
const NAME_LENGTH = "Name must be 9 characters or more"
|
||||
const NAME_CLAIMED = "Name is already claimed"
|
||||
const NAME_INVALID_PUNY = "Unsupported punycode character"
|
||||
|
||||
const debouncer = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (debouncer.current)
|
||||
clearTimeout(debouncer.current);
|
||||
|
||||
debouncer.current = setTimeout(async () => {
|
||||
|
||||
let index: number
|
||||
let validities = [...nameValidities]
|
||||
|
||||
const len = [...name].length
|
||||
index = validities.indexOf(NAME_LENGTH)
|
||||
if (len < 9 && len != 0) {
|
||||
if (index == -1) validities.push(NAME_LENGTH)
|
||||
} else if (index != -1) validities.splice(index, 1)
|
||||
|
||||
let normalized: string
|
||||
index = validities.indexOf(NAME_INVALID_PUNY)
|
||||
try {
|
||||
normalized = toAscii(name + ".os")
|
||||
if (index != -1) validities.splice(index, 1)
|
||||
} catch (e) {
|
||||
if (index == -1) validities.push(NAME_INVALID_PUNY)
|
||||
}
|
||||
|
||||
// only check if name is valid punycode
|
||||
if (normalized! !== undefined) {
|
||||
|
||||
index = validities.indexOf(NAME_URL)
|
||||
if (name != "" && !isValidDomain(normalized)) {
|
||||
if (index == -1) validities.push(NAME_URL)
|
||||
} else if (index != -1) validities.splice(index, 1)
|
||||
|
||||
index = validities.indexOf(NAME_CLAIMED)
|
||||
if (validities.length == 0 || index != -1) {
|
||||
try {
|
||||
await dotOs.ownerOf(hash(normalized))
|
||||
if (index == -1) validities.push(NAME_CLAIMED)
|
||||
} catch (e) {
|
||||
if (index != -1) validities.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setNameValidities(validities)
|
||||
|
||||
}, 500)
|
||||
}, [name, triggerNameCheck])
|
||||
|
||||
const noDots = (e: any) => e.target.value.indexOf('.') == -1
|
||||
&& setName(e.target.value)
|
||||
|
||||
return (
|
||||
<div className="col" style={{ width: '100%' }}>
|
||||
<div className="row" style={{ width: '100%' }}>
|
||||
<input
|
||||
value={name}
|
||||
onChange={noDots}
|
||||
type="text"
|
||||
required
|
||||
name="dot-os-name"
|
||||
placeholder="e.g. myname"
|
||||
/>
|
||||
<div className="os">.os</div>
|
||||
</div>
|
||||
{nameValidities.map((x, i) => <div key={i}><br /><span className="name-validity">{x}</span></div>)}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default EnterOsName;
|
27
kinode/packages/homepage/ui/src/components/Jazzicon.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import jazzicon from 'jazzicon';
|
||||
|
||||
interface JazziconProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
address: string;
|
||||
diameter?: number;
|
||||
}
|
||||
|
||||
const Jazzicon: React.FC<JazziconProps> = ({ address, diameter = 40, ...props }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (address && ref.current) {
|
||||
const seed = parseInt(address.slice(2, 10), 16); // Derive a seed from Ethereum address
|
||||
const icon = jazzicon(diameter, seed);
|
||||
|
||||
// Clear the current icon
|
||||
ref.current.innerHTML = '';
|
||||
// Append the new icon
|
||||
ref.current.appendChild(icon);
|
||||
}
|
||||
}, [address, diameter]);
|
||||
|
||||
return <div {...props} ref={ref} />;
|
||||
};
|
||||
|
||||
export default Jazzicon;
|
176
kinode/packages/homepage/ui/src/components/KnsHeader.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import { useWeb3React } from "@web3-react/core";
|
||||
import { hooks, metaMask } from "../connectors/metamask";
|
||||
import { ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import Loader from "./Loader";
|
||||
import { getNetworkName, setChain } from "../utils/chain";
|
||||
import ChainInfo from "./ChainInfo";
|
||||
import { OPTIMISM_OPT_HEX, SEPOLIA_OPT_HEX } from "../constants/chainId";
|
||||
|
||||
import sepoliaLogo from "../assets/sepolia.png";
|
||||
import optimismLogo from "../assets/optimism.png";
|
||||
|
||||
const { useIsActivating, useChainId } = hooks;
|
||||
|
||||
type OsHeaderProps = {
|
||||
header: ReactNode;
|
||||
nameLogo?: boolean;
|
||||
nodeChainId: string;
|
||||
openConnect: () => void;
|
||||
closeConnect: () => void;
|
||||
hideConnect?: boolean;
|
||||
};
|
||||
|
||||
function OsHeader({
|
||||
header,
|
||||
openConnect,
|
||||
nameLogo = false,
|
||||
closeConnect,
|
||||
nodeChainId,
|
||||
hideConnect = false,
|
||||
}: OsHeaderProps) {
|
||||
const { account, isActive } = useWeb3React();
|
||||
const isActivating = useIsActivating();
|
||||
const chainId = useChainId();
|
||||
|
||||
const [networkName, setNetworkName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setNetworkName(getNetworkName((chainId || 1).toString()));
|
||||
}, [chainId]);
|
||||
|
||||
const connectWallet = useCallback(async () => {
|
||||
closeConnect();
|
||||
await metaMask.activate().catch(() => {});
|
||||
|
||||
try {
|
||||
setChain(nodeChainId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [closeConnect, nodeChainId]);
|
||||
|
||||
const changeToNodeChain = useCallback(async () => {
|
||||
// If correct ndetwork is set, just say that
|
||||
if (chainId) {
|
||||
const hexChainId = "0x" + chainId.toString(16);
|
||||
if (hexChainId === nodeChainId) {
|
||||
return alert(
|
||||
`You are already connected to ${getNetworkName(chainId.toString())}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
setChain(nodeChainId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}, [chainId, nodeChainId]);
|
||||
|
||||
const changeConnectedAccount = useCallback(async () => {
|
||||
alert("You can change your connected account in your wallet.");
|
||||
}, []);
|
||||
|
||||
// <div style={{ textAlign: 'center', lineHeight: 1.5 }}> Connected as {account?.slice(0,6) + '...' + account?.slice(account.length - 6)}</div>
|
||||
return (
|
||||
<>
|
||||
<div id="signup-form-header" className="col">
|
||||
{(nodeChainId === SEPOLIA_OPT_HEX ||
|
||||
nodeChainId === OPTIMISM_OPT_HEX) && (
|
||||
<div
|
||||
className="tooltip-container"
|
||||
style={{ position: "absolute", top: 32, right: 32 }}
|
||||
>
|
||||
<div className="tooltip-button chain">
|
||||
{nodeChainId === SEPOLIA_OPT_HEX ? (
|
||||
<img alt="sepolia" src={sepoliaLogo} className="sepolia" />
|
||||
) : nodeChainId === OPTIMISM_OPT_HEX ? (
|
||||
<img alt="optimism" src={optimismLogo} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="tooltip-content left">
|
||||
{nodeChainId === SEPOLIA_OPT_HEX ? (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: "1.5em",
|
||||
maxWidth: 450,
|
||||
}}
|
||||
>
|
||||
Your Kinode is currently pointed at Sepolia. To point at
|
||||
Optimism, boot without the "--testnet" flag.
|
||||
</div>
|
||||
) : nodeChainId === OPTIMISM_OPT_HEX ? (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: "1.5em",
|
||||
maxWidth: 450,
|
||||
}}
|
||||
>
|
||||
Your Kinode is currently pointed at Optimism. To point at
|
||||
Sepolia, boot with the "--testnet" flag.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="col" style={{ gap: 16, marginBottom: 32 }}>
|
||||
{header}
|
||||
</div>
|
||||
{!hideConnect && (
|
||||
<div
|
||||
style={{
|
||||
minWidth: "50vw",
|
||||
width: 400,
|
||||
justifyContent: "center",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{isActive && account ? (
|
||||
<ChainInfo
|
||||
account={account}
|
||||
networkName={networkName}
|
||||
changeToNodeChain={changeToNodeChain}
|
||||
changeConnectedAccount={changeConnectedAccount}
|
||||
/>
|
||||
) : (
|
||||
<div className="col" style={{ gap: 32, marginTop: 16 }}>
|
||||
<h5 style={{ textAlign: "center", lineHeight: "1.5em" }}>
|
||||
You must connect to a browser wallet to continue
|
||||
</h5>
|
||||
{/* <div style={{ textAlign: 'center', lineHeight: '1.5em' }}>We recommend <a href="https://metamask.io/download.html" target="_blank" rel="noreferrer">MetaMask</a></div> */}
|
||||
{isActivating ? (
|
||||
<Loader msg="Approve connection in your wallet" />
|
||||
) : (
|
||||
<button onClick={connectWallet}> Connect Wallet </button>
|
||||
)}
|
||||
{nodeChainId === SEPOLIA_OPT_HEX && (
|
||||
<h5
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: "1.5em",
|
||||
maxWidth: 450,
|
||||
}}
|
||||
>
|
||||
Kinode is currently on the Sepolia Testnet, if you need
|
||||
testnet ETH, you can get some from the{" "}
|
||||
<a
|
||||
href="https://sepoliafaucet.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Sepolia Faucet
|
||||
</a>
|
||||
</h5>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OsHeader;
|
12
kinode/packages/homepage/ui/src/components/Loader.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
type LoaderProps = {
|
||||
msg: string
|
||||
}
|
||||
|
||||
export default function Loader({ msg } : LoaderProps) {
|
||||
return (
|
||||
<div id="loading" className="col">
|
||||
<h3>{msg}</h3>
|
||||
<div id="loader"> <div/> <div/> <div/> <div/> </div>
|
||||
</div>
|
||||
)
|
||||
}
|
4
kinode/packages/homepage/ui/src/connectors/metamask.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { initializeConnector } from '@web3-react/core'
|
||||
import { MetaMask } from '@web3-react/metamask'
|
||||
|
||||
export const [metaMask, hooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions }))
|
34
kinode/packages/homepage/ui/src/constants/addresses.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { ChainId } from './chainId'
|
||||
|
||||
type AddressMap = { [chainId: string]: string }
|
||||
|
||||
export const KNS_REGISTRY_ADDRESSES: AddressMap = {
|
||||
[ChainId.SEPOLIA]: '0x3807fBD692Aa5c96F1D8D7c59a1346a885F40B1C',
|
||||
[ChainId.OPTIMISM]: '0xca5b5811c0C40aAB3295f932b1B5112Eb7bb4bD6',
|
||||
}
|
||||
|
||||
export const DOT_OS_ADDRESSES: AddressMap = {
|
||||
[ChainId.SEPOLIA]: '0xC5a939923E0B336642024b479502E039338bEd00',
|
||||
[ChainId.OPTIMISM]: '0x66929F55Ea1E38591f9430E5013C92cdC01F6cAd',
|
||||
}
|
||||
|
||||
export const NAMEWRAPPER_ADDRESSES: AddressMap = {
|
||||
[ChainId.SEPOLIA]: '0x0635513f179D50A207757E05759CbD106d7dFcE8',
|
||||
[ChainId.MAINNET]: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401',
|
||||
}
|
||||
|
||||
export const ENS_REGISTRY_ADDRESSES: AddressMap = {
|
||||
[ChainId.SEPOLIA]: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
|
||||
[ChainId.MAINNET]: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
|
||||
}
|
||||
|
||||
export const KNS_ENS_ENTRY_ADDRESSES: AddressMap = {
|
||||
[ChainId.SEPOLIA]: '0xD4583DFd73B382B7e3230aa29Be774C1843FB7d2',
|
||||
[ChainId.GOERLI]: '0xD4583DFd73B382B7e3230aa29Be774C1843FB7d2',
|
||||
[ChainId.MAINNET]: '0xa1F47fBBa93574DB4a049C1c5bA03471A21EE01D',
|
||||
}
|
||||
|
||||
export const KNS_ENS_EXIT_ADDRESSES: AddressMap = {
|
||||
[ChainId.SEPOLIA]: '0x528bA1BA3186d8CABD2c4E8758a98fAf64eD8Af0',
|
||||
[ChainId.OPTIMISM]: '0x0b35664aB5950cE92bce7222be165BB575D9b7c5',
|
||||
}
|
13
kinode/packages/homepage/ui/src/constants/chainId.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export enum ChainId {
|
||||
LOCAL = 1337,
|
||||
MAINNET = 1,
|
||||
SEPOLIA = 11155111,
|
||||
OPTIMISM = 10,
|
||||
OPTIMISM_GOERLI = 420,
|
||||
GOERLI = 5,
|
||||
}
|
||||
|
||||
export const SEPOLIA_OPT_HEX = '0xaa36a7';
|
||||
export const OPTIMISM_OPT_HEX = '0xa';
|
||||
export const MAINNET_OPT_HEX = '0x1';
|
||||
export const SEPOLIA_OPT_INT = '11155111';
|
2
kinode/packages/homepage/ui/src/constants/errors.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const KEY_WRONG_NET_KEY = "Keyfile does not match public key";
|
||||
export const KEY_WRONG_IP = "IP Address does not match records";
|
7
kinode/packages/homepage/ui/src/declarations.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
declare module 'eth-ens-namehash' {
|
||||
export function hash(name: string): string;
|
||||
export function normalize(name: string): string;
|
||||
}
|
||||
declare module 'idna-uts46-hx' {
|
||||
export function toAscii(domain: string, options?: object): string;
|
||||
}
|