mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-17 14:24:26 +01:00
format everything with prettier
This commit is contained in:
@@ -1,48 +1,50 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"env": {
|
env: {
|
||||||
"browser": true,
|
browser: true,
|
||||||
"es2021": true
|
es2021: true
|
||||||
},
|
},
|
||||||
"extends": [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:solid/typescript",
|
"plugin:solid/typescript",
|
||||||
"plugin:import/typescript",
|
"plugin:import/typescript",
|
||||||
"plugin:import/recommended"
|
"plugin:import/recommended"
|
||||||
],
|
],
|
||||||
"overrides": [
|
overrides: [],
|
||||||
],
|
parser: "@typescript-eslint/parser",
|
||||||
"parser": "@typescript-eslint/parser",
|
parserOptions: {
|
||||||
"parserOptions": {
|
tsconfigRootDir: "./",
|
||||||
"tsconfigRootDir": "./",
|
project: ["./tsconfig.json"],
|
||||||
"project": ["./tsconfig.json"],
|
ecmaVersion: "latest",
|
||||||
"ecmaVersion": "latest",
|
sourceType: "module",
|
||||||
"sourceType": "module",
|
ecmaFeatures: {
|
||||||
"ecmaFeatures": {
|
jsx: true
|
||||||
"jsx": true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": [
|
plugins: ["@typescript-eslint", "solid", "import"],
|
||||||
"@typescript-eslint",
|
rules: {
|
||||||
"solid",
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"import"
|
"warn",
|
||||||
],
|
{
|
||||||
"rules": {
|
argsIgnorePattern: "^_",
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
destructuredArrayIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
"solid/reactivity": "warn",
|
"solid/reactivity": "warn",
|
||||||
"solid/no-destructure": "warn",
|
"solid/no-destructure": "warn",
|
||||||
"solid/jsx-no-undef": "error",
|
"solid/jsx-no-undef": "error",
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off"
|
||||||
},
|
},
|
||||||
"settings": {
|
settings: {
|
||||||
"import/parsers": {
|
"import/parsers": {
|
||||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||||
},
|
},
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
"typescript": {
|
typescript: {
|
||||||
"project": ["./tsconfig.json"],
|
project: ["./tsconfig.json"],
|
||||||
"alwaysTryTypes": true
|
alwaysTryTypes: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
32
.prettierignore
Normal file
32
.prettierignore
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# PWA dev stuff
|
||||||
|
dev-dist
|
||||||
|
.solid
|
||||||
|
/test-results/
|
||||||
|
/tests-examples/
|
||||||
|
/playwright-report/
|
||||||
|
/playwright/.cache/
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"tabWidth": 2,
|
"tabWidth": 4,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"printWidth": 100,
|
"printWidth": 80,
|
||||||
"useTabs": false
|
"useTabs": false
|
||||||
}
|
}
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -9,9 +9,31 @@ pnpm install
|
|||||||
pnpm run dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Env
|
||||||
|
|
||||||
|
The easiest way to get start with development is to create a file called `.env.local` and copy the contents of `.env.example` into it. This is basically identical to the env that `signet-app.mutinywallet.com` uses.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
We have a couple Playwright e2e tests in the e2e folder. You can run these with:
|
||||||
|
|
||||||
|
```
|
||||||
|
just test
|
||||||
|
```
|
||||||
|
|
||||||
|
Or get a visual look into what's happening:
|
||||||
|
|
||||||
|
```
|
||||||
|
just test-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
Hopefully your editor picks up on the `.prettirrc` file and auto formats accordingly. If you want to format everything in the project run `pnpm run format`.
|
||||||
|
|
||||||
### Local
|
### Local
|
||||||
|
|
||||||
To make local development easier with a latest local version of [the node manager](https://github.com/MutinyWallet/mutiny-node), you may want to `pnpm link` it.
|
If you want to develop against a local version of [the node manager](https://github.com/MutinyWallet/mutiny-node), you may want to `pnpm link` it.
|
||||||
|
|
||||||
Due to how [Vite's dev server works](https://vitejs.dev/config/server-options.html#server-fs-allow), the linked `mutiny-node` project folder should be a sibling of this `mutiny-web` folder. Alternatively you can change the allow path in `vite.config.ts`.
|
Due to how [Vite's dev server works](https://vitejs.dev/config/server-options.html#server-fs-allow), the linked `mutiny-node` project folder should be a sibling of this `mutiny-web` folder. Alternatively you can change the allow path in `vite.config.ts`.
|
||||||
|
|
||||||
|
|||||||
6
justfile
6
justfile
@@ -8,3 +8,9 @@ local:
|
|||||||
|
|
||||||
remote:
|
remote:
|
||||||
pnpm unlink "@mutinywallet/mutiny-wasm" && pnpm install
|
pnpm unlink "@mutinywallet/mutiny-wasm" && pnpm install
|
||||||
|
|
||||||
|
test:
|
||||||
|
pnpm exec playwright test
|
||||||
|
|
||||||
|
test-ui:
|
||||||
|
pnpm exec playwright test --ui
|
||||||
|
|||||||
110
package.json
110
package.json
@@ -1,56 +1,58 @@
|
|||||||
{
|
{
|
||||||
"name": "mws",
|
"name": "mws",
|
||||||
"version": "0.3.7",
|
"version": "0.3.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@8.3.1",
|
"packageManager": "pnpm@8.3.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "solid-start dev",
|
"dev": "solid-start dev",
|
||||||
"host": "solid-start dev --host",
|
"host": "solid-start dev --host",
|
||||||
"build": "solid-start build",
|
"build": "solid-start build",
|
||||||
"start": "solid-start start",
|
"start": "solid-start start",
|
||||||
"lint": "eslint src --ext .ts,.tsx,.js"
|
"lint": "eslint src --ext .ts,.tsx,.js",
|
||||||
},
|
"format": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\""
|
||||||
"type": "module",
|
},
|
||||||
"devDependencies": {
|
"type": "module",
|
||||||
"@playwright/test": "^1.34.3",
|
"devDependencies": {
|
||||||
"@types/node": "^18.16.15",
|
"@playwright/test": "^1.34.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
"@types/node": "^18.16.15",
|
||||||
"@typescript-eslint/parser": "^5.59.7",
|
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
||||||
"autoprefixer": "^10.4.14",
|
"@typescript-eslint/parser": "^5.59.7",
|
||||||
"esbuild": "^0.14.54",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.41.0",
|
"esbuild": "^0.14.54",
|
||||||
"eslint-import-resolver-typescript": "2.7.1",
|
"eslint": "^8.41.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-import-resolver-typescript": "2.7.1",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-solid": "0.11.0",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"postcss": "^8.4.23",
|
"eslint-plugin-solid": "0.11.0",
|
||||||
"solid-start-node": "^0.2.26",
|
"postcss": "^8.4.23",
|
||||||
"tailwindcss": "^3.3.2",
|
"prettier": "^2.8.8",
|
||||||
"typescript": "^4.9.5",
|
"solid-start-node": "^0.2.26",
|
||||||
"vite": "^4.3.9",
|
"tailwindcss": "^3.3.2",
|
||||||
"vite-plugin-pwa": "^0.14.7",
|
"typescript": "^4.9.5",
|
||||||
"vite-plugin-wasm": "^3.2.2",
|
"vite": "^4.3.9",
|
||||||
"workbox-window": "^6.5.4"
|
"vite-plugin-pwa": "^0.14.7",
|
||||||
},
|
"vite-plugin-wasm": "^3.2.2",
|
||||||
"dependencies": {
|
"workbox-window": "^6.5.4"
|
||||||
"@kobalte/core": "^0.9.6",
|
},
|
||||||
"@kobalte/tailwindcss": "^0.5.0",
|
"dependencies": {
|
||||||
"@modular-forms/solid": "^0.13.2",
|
"@kobalte/core": "^0.9.6",
|
||||||
"@mutinywallet/mutiny-wasm": "0.3.7",
|
"@kobalte/tailwindcss": "^0.5.0",
|
||||||
"@mutinywallet/waila-wasm": "^0.2.0",
|
"@modular-forms/solid": "^0.13.2",
|
||||||
"@solid-primitives/upload": "^0.0.111",
|
"@mutinywallet/mutiny-wasm": "0.3.7",
|
||||||
"@solidjs/meta": "^0.28.5",
|
"@mutinywallet/waila-wasm": "^0.2.0",
|
||||||
"@solidjs/router": "^0.8.2",
|
"@solid-primitives/upload": "^0.0.111",
|
||||||
"@thisbeyond/solid-select": "^0.14.0",
|
"@solidjs/meta": "^0.28.5",
|
||||||
"class-variance-authority": "^0.4.0",
|
"@solidjs/router": "^0.8.2",
|
||||||
"nostr-tools": "^1.11.1",
|
"@thisbeyond/solid-select": "^0.14.0",
|
||||||
"qr-scanner": "^1.4.2",
|
"class-variance-authority": "^0.4.0",
|
||||||
"solid-js": "^1.7.5",
|
"nostr-tools": "^1.11.1",
|
||||||
"solid-qr-code": "^0.0.8",
|
"qr-scanner": "^1.4.2",
|
||||||
"solid-start": "^0.2.26",
|
"solid-js": "^1.7.5",
|
||||||
"undici": "^5.22.1"
|
"solid-qr-code": "^0.0.8",
|
||||||
},
|
"solid-start": "^0.2.26",
|
||||||
"engines": {
|
"undici": "^5.22.1"
|
||||||
"node": ">=16.8"
|
},
|
||||||
}
|
"engines": {
|
||||||
|
"node": ">=16.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -87,6 +87,9 @@ devDependencies:
|
|||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.23
|
specifier: ^8.4.23
|
||||||
version: 8.4.23
|
version: 8.4.23
|
||||||
|
prettier:
|
||||||
|
specifier: ^2.8.8
|
||||||
|
version: 2.8.8
|
||||||
solid-start-node:
|
solid-start-node:
|
||||||
specifier: ^0.2.26
|
specifier: ^0.2.26
|
||||||
version: 0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.9)
|
version: 0.2.26(solid-start@0.2.26)(undici@5.22.1)(vite@4.3.9)
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
export function Back() {
|
export function Back() {
|
||||||
return (<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
return (
|
||||||
<path d="M17.546 8 8 17.546l9.546 9.546" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
<svg
|
||||||
</svg>
|
width="36"
|
||||||
)
|
height="36"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.546 8 8 17.546l9.546 9.546"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
export function Paste() {
|
export function Paste() {
|
||||||
return (<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
return (
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z" fill="currentColor" />
|
<svg
|
||||||
<path d="M12.75 3h10.5v4.5h-10.5V3Z" fill="currentColor" />
|
width="36"
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z" fill="currentColor" />
|
height="36"
|
||||||
</svg>)
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M12.75 3h10.5v4.5h-10.5V3Z" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
export function Scan() {
|
export function Scan() {
|
||||||
return (<svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
return (
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z" fill="currentColor" />
|
<svg
|
||||||
</svg>)
|
width="37"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 37 36"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,112 +6,125 @@ import { ActivityItem, HackActivityType } from "./ActivityItem";
|
|||||||
import { DetailsIdModal } from "./DetailsModal";
|
import { DetailsIdModal } from "./DetailsModal";
|
||||||
|
|
||||||
export const THREE_COLUMNS =
|
export const THREE_COLUMNS =
|
||||||
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0";
|
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0";
|
||||||
export const CENTER_COLUMN = "min-w-0 overflow-hidden max-w-full";
|
export const CENTER_COLUMN = "min-w-0 overflow-hidden max-w-full";
|
||||||
export const MISSING_LABEL = "py-1 px-2 bg-white/10 rounded inline-block text-sm";
|
export const MISSING_LABEL =
|
||||||
export const REDSHIFT_LABEL = "py-1 px-2 bg-white text-m-red rounded inline-block text-sm";
|
"py-1 px-2 bg-white/10 rounded inline-block text-sm";
|
||||||
|
export const REDSHIFT_LABEL =
|
||||||
|
"py-1 px-2 bg-white text-m-red rounded inline-block text-sm";
|
||||||
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]";
|
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]";
|
||||||
|
|
||||||
export type OnChainTx = {
|
export type OnChainTx = {
|
||||||
txid: string;
|
txid: string;
|
||||||
received: number;
|
received: number;
|
||||||
sent: number;
|
sent: number;
|
||||||
fee?: number;
|
fee?: number;
|
||||||
confirmation_time?: {
|
confirmation_time?: {
|
||||||
Confirmed?: {
|
Confirmed?: {
|
||||||
height: number;
|
height: number;
|
||||||
time: number;
|
time: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
labels: string[];
|
||||||
labels: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UtxoItem = {
|
export type UtxoItem = {
|
||||||
outpoint: string;
|
outpoint: string;
|
||||||
txout: {
|
txout: {
|
||||||
value: number;
|
value: number;
|
||||||
script_pubkey: string;
|
script_pubkey: string;
|
||||||
};
|
};
|
||||||
keychain: string;
|
keychain: string;
|
||||||
is_spent: boolean;
|
is_spent: boolean;
|
||||||
redshifted?: boolean;
|
redshifted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function UnifiedActivityItem(props: {
|
function UnifiedActivityItem(props: {
|
||||||
item: MutinyActivity;
|
item: MutinyActivity;
|
||||||
onClick: (id: string, kind: HackActivityType) => void;
|
onClick: (id: string, kind: HackActivityType) => void;
|
||||||
}) {
|
}) {
|
||||||
const click = () => {
|
const click = () => {
|
||||||
props.onClick(props.item.id, props.item.kind as unknown as HackActivityType);
|
props.onClick(
|
||||||
};
|
props.item.id,
|
||||||
|
props.item.kind as unknown as HackActivityType
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActivityItem
|
<ActivityItem
|
||||||
// This is actually the ActivityType enum but wasm is hard
|
// This is actually the ActivityType enum but wasm is hard
|
||||||
kind={props.item.kind as unknown as HackActivityType}
|
kind={props.item.kind as unknown as HackActivityType}
|
||||||
labels={props.item.labels}
|
labels={props.item.labels}
|
||||||
contacts={props.item.contacts}
|
contacts={props.item.contacts}
|
||||||
// FIXME: is this something we can put into node logic?
|
// FIXME: is this something we can put into node logic?
|
||||||
amount={props.item.amount_sats || 0}
|
amount={props.item.amount_sats || 0}
|
||||||
date={props.item.last_updated}
|
date={props.item.last_updated}
|
||||||
positive={props.item.inbound}
|
positive={props.item.inbound}
|
||||||
onClick={click}
|
onClick={click}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CombinedActivity(props: { limit?: number }) {
|
export function CombinedActivity(props: { limit?: number }) {
|
||||||
const [state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
|
|
||||||
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
||||||
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
||||||
const [detailsId, setDetailsId] = createSignal("");
|
const [detailsId, setDetailsId] = createSignal("");
|
||||||
|
|
||||||
function openDetailsModal(id: string, kind: HackActivityType) {
|
function openDetailsModal(id: string, kind: HackActivityType) {
|
||||||
console.log("Opening details modal: ", id, kind);
|
console.log("Opening details modal: ", id, kind);
|
||||||
|
|
||||||
setDetailsId(id);
|
setDetailsId(id);
|
||||||
setDetailsKind(kind);
|
setDetailsKind(kind);
|
||||||
setDetailsOpen(true);
|
setDetailsOpen(true);
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!state.wallet_loading && !state.is_syncing) {
|
|
||||||
actions.syncActivity();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
createEffect(() => {
|
||||||
<>
|
if (!state.wallet_loading && !state.is_syncing) {
|
||||||
<Show when={detailsId() && detailsKind()}>
|
actions.syncActivity();
|
||||||
<DetailsIdModal
|
}
|
||||||
open={detailsOpen()}
|
});
|
||||||
kind={detailsKind()}
|
|
||||||
id={detailsId()}
|
return (
|
||||||
setOpen={setDetailsOpen}
|
<>
|
||||||
/>
|
<Show when={detailsId() && detailsKind()}>
|
||||||
</Show>
|
<DetailsIdModal
|
||||||
<Switch>
|
open={detailsOpen()}
|
||||||
<Match when={state.activity.length === 0}>
|
kind={detailsKind()}
|
||||||
<div class="w-full text-center pb-4">
|
id={detailsId()}
|
||||||
<NiceP>Receive some sats to get started</NiceP>
|
setOpen={setDetailsOpen}
|
||||||
</div>
|
/>
|
||||||
</Match>
|
</Show>
|
||||||
<Match when={props.limit && state.activity.length > props.limit}>
|
<Switch>
|
||||||
<For each={state.activity.slice(0, props.limit)}>
|
<Match when={state.activity.length === 0}>
|
||||||
{(activityItem) => (
|
<div class="w-full text-center pb-4">
|
||||||
<UnifiedActivityItem item={activityItem} onClick={openDetailsModal} />
|
<NiceP>Receive some sats to get started</NiceP>
|
||||||
)}
|
</div>
|
||||||
</For>
|
</Match>
|
||||||
</Match>
|
<Match
|
||||||
<Match when={state.activity.length >= 0}>
|
when={props.limit && state.activity.length > props.limit}
|
||||||
<For each={state.activity}>
|
>
|
||||||
{(activityItem) => (
|
<For each={state.activity.slice(0, props.limit)}>
|
||||||
<UnifiedActivityItem item={activityItem} onClick={openDetailsModal} />
|
{(activityItem) => (
|
||||||
)}
|
<UnifiedActivityItem
|
||||||
</For>
|
item={activityItem}
|
||||||
</Match>
|
onClick={openDetailsModal}
|
||||||
</Switch>
|
/>
|
||||||
</>
|
)}
|
||||||
);
|
</For>
|
||||||
|
</Match>
|
||||||
|
<Match when={state.activity.length >= 0}>
|
||||||
|
<For each={state.activity}>
|
||||||
|
{(activityItem) => (
|
||||||
|
<UnifiedActivityItem
|
||||||
|
item={activityItem}
|
||||||
|
onClick={openDetailsModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { Match, ParentComponent, Switch, createMemo, createResource } from "solid-js";
|
import {
|
||||||
|
Match,
|
||||||
|
ParentComponent,
|
||||||
|
Switch,
|
||||||
|
createMemo,
|
||||||
|
createResource
|
||||||
|
} from "solid-js";
|
||||||
import { satsToUsd } from "~/utils/conversions";
|
import { satsToUsd } from "~/utils/conversions";
|
||||||
import bolt from "~/assets/icons/bolt.svg";
|
import bolt from "~/assets/icons/bolt.svg";
|
||||||
import chain from "~/assets/icons/chain.svg";
|
import chain from "~/assets/icons/chain.svg";
|
||||||
@@ -9,137 +15,161 @@ import { useMegaStore } from "~/state/megaStore";
|
|||||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||||
|
|
||||||
export const ActivityAmount: ParentComponent<{
|
export const ActivityAmount: ParentComponent<{
|
||||||
amount: string;
|
amount: string;
|
||||||
price: number;
|
price: number;
|
||||||
positive?: boolean;
|
positive?: boolean;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const amountInUsd = createMemo(() => {
|
const amountInUsd = createMemo(() => {
|
||||||
const parsed = Number(props.amount);
|
const parsed = Number(props.amount);
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
return props.amount;
|
return props.amount;
|
||||||
} else {
|
} else {
|
||||||
return satsToUsd(props.price, parsed, true);
|
return satsToUsd(props.price, parsed, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const prettyPrint = createMemo(() => {
|
const prettyPrint = createMemo(() => {
|
||||||
const parsed = Number(props.amount);
|
const parsed = Number(props.amount);
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
return props.amount;
|
return props.amount;
|
||||||
} else {
|
} else {
|
||||||
return parsed.toLocaleString();
|
return parsed.toLocaleString();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="flex flex-col"
|
class="flex flex-col"
|
||||||
classList={{ "items-end": !props.center, "items-center": props.center }}
|
classList={{
|
||||||
>
|
"items-end": !props.center,
|
||||||
<div class="text-base" classList={{ "text-m-green": props.positive }}>
|
"items-center": props.center
|
||||||
{props.positive && "+ "}
|
}}
|
||||||
{prettyPrint()} <span class="text-sm">SATS</span>
|
>
|
||||||
</div>
|
<div
|
||||||
<div class="text-sm text-neutral-500">
|
class="text-base"
|
||||||
≈ {amountInUsd()} <span class="text-sm">USD</span>
|
classList={{ "text-m-green": props.positive }}
|
||||||
</div>
|
>
|
||||||
</div>
|
{props.positive && "+ "}
|
||||||
);
|
{prettyPrint()} <span class="text-sm">SATS</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-neutral-500">
|
||||||
|
≈ {amountInUsd()}
|
||||||
|
<span class="text-sm">USD</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function LabelCircle(props: { name?: string; contact: boolean; label: boolean }) {
|
function LabelCircle(props: {
|
||||||
// TODO: don't need to run this if it's not a contact
|
name?: string;
|
||||||
const [gradient] = createResource(async () => {
|
contact: boolean;
|
||||||
return generateGradient(props.name || "?");
|
label: boolean;
|
||||||
});
|
}) {
|
||||||
|
// TODO: don't need to run this if it's not a contact
|
||||||
|
const [gradient] = createResource(async () => {
|
||||||
|
return generateGradient(props.name || "?");
|
||||||
|
});
|
||||||
|
|
||||||
const text = () =>
|
const text = () =>
|
||||||
props.contact && props.name && props.name.length ? props.name[0] : props.label ? "≡" : "?";
|
props.contact && props.name && props.name.length
|
||||||
const bg = () => (props.name && props.contact ? gradient() : "gray");
|
? props.name[0]
|
||||||
|
: props.label
|
||||||
|
? "≡"
|
||||||
|
: "?";
|
||||||
|
const bg = () => (props.name && props.contact ? gradient() : "gray");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="flex-none h-[3rem] w-[3rem] rounded-full flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
class="flex-none h-[3rem] w-[3rem] rounded-full flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||||
style={{ background: bg() }}
|
style={{ background: bg() }}
|
||||||
>
|
>
|
||||||
{text()}
|
{text()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HackActivityType = "Lightning" | "OnChain" | "ChannelOpen";
|
export type HackActivityType = "Lightning" | "OnChain" | "ChannelOpen";
|
||||||
|
|
||||||
export function ActivityItem(props: {
|
export function ActivityItem(props: {
|
||||||
// This is actually the ActivityType enum but wasm is hard
|
// This is actually the ActivityType enum but wasm is hard
|
||||||
kind: HackActivityType;
|
kind: HackActivityType;
|
||||||
contacts: Contact[];
|
contacts: Contact[];
|
||||||
labels: string[];
|
labels: string[];
|
||||||
amount: number | bigint;
|
amount: number | bigint;
|
||||||
date?: number | bigint;
|
date?: number | bigint;
|
||||||
positive?: boolean;
|
positive?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const firstContact = () => (props.contacts?.length ? props.contacts[0] : null);
|
const firstContact = () =>
|
||||||
|
props.contacts?.length ? props.contacts[0] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => props.onClick && props.onClick()}
|
onClick={() => props.onClick && props.onClick()}
|
||||||
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0"
|
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0"
|
||||||
classList={{ "cursor-pointer": !!props.onClick }}
|
classList={{ "cursor-pointer": !!props.onClick }}
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 md:gap-4 items-center">
|
<div class="flex gap-2 md:gap-4 items-center">
|
||||||
<div class="">
|
<div class="">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.kind === "Lightning"}>
|
<Match when={props.kind === "Lightning"}>
|
||||||
<img class="w-[1rem]" src={bolt} alt="lightning" />
|
<img class="w-[1rem]" src={bolt} alt="lightning" />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.kind === "OnChain"}>
|
<Match when={props.kind === "OnChain"}>
|
||||||
<img class="w-[1rem]" src={chain} alt="onchain" />
|
<img class="w-[1rem]" src={chain} alt="onchain" />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.kind === "ChannelOpen"}>
|
<Match when={props.kind === "ChannelOpen"}>
|
||||||
<img class="w-[1rem]" src={shuffle} alt="swap" />
|
<img class="w-[1rem]" src={shuffle} alt="swap" />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<LabelCircle
|
||||||
|
name={firstContact()?.name}
|
||||||
|
contact={props.contacts?.length > 0}
|
||||||
|
label={props.labels?.length > 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Switch>
|
||||||
|
<Match when={firstContact()?.name}>
|
||||||
|
<span class="text-base font-semibold truncate">
|
||||||
|
{firstContact()?.name}
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
<Match when={props.labels.length > 0}>
|
||||||
|
<span class="text-base font-semibold truncate">
|
||||||
|
{props.labels[0]}
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<span class="text-base font-semibold text-neutral-500">
|
||||||
|
Unknown
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
<Switch>
|
||||||
|
<Match when={props.date && props.date > 2147483647}>
|
||||||
|
<time class="text-sm text-neutral-500">Pending</time>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<time class="text-sm text-neutral-500">
|
||||||
|
{timeAgo(props.date)}
|
||||||
|
</time>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<ActivityAmount
|
||||||
|
amount={props.amount.toString()}
|
||||||
|
price={state.price}
|
||||||
|
positive={props.positive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
);
|
||||||
<LabelCircle
|
|
||||||
name={firstContact()?.name}
|
|
||||||
contact={props.contacts?.length > 0}
|
|
||||||
label={props.labels?.length > 0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<Switch>
|
|
||||||
<Match when={firstContact()?.name}>
|
|
||||||
<span class="text-base font-semibold truncate">{firstContact()?.name}</span>
|
|
||||||
</Match>
|
|
||||||
<Match when={props.labels.length > 0}>
|
|
||||||
<span class="text-base font-semibold truncate">{props.labels[0]}</span>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<span class="text-base font-semibold text-neutral-500">Unknown</span>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
<Switch>
|
|
||||||
<Match when={props.date && props.date > 2147483647}>
|
|
||||||
<time class="text-sm text-neutral-500">Pending</time>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
<ActivityAmount
|
|
||||||
amount={props.amount.toString()}
|
|
||||||
price={state.price}
|
|
||||||
positive={props.positive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,56 @@
|
|||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js";
|
||||||
import { useMegaStore } from "~/state/megaStore"
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { satsToUsd } from "~/utils/conversions"
|
import { satsToUsd } from "~/utils/conversions";
|
||||||
|
|
||||||
function prettyPrintAmount(n?: number | bigint): string {
|
function prettyPrintAmount(n?: number | bigint): string {
|
||||||
if (!n || n.valueOf() === 0) {
|
if (!n || n.valueOf() === 0) {
|
||||||
return "0"
|
return "0";
|
||||||
}
|
}
|
||||||
return n.toLocaleString()
|
return n.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Amount(props: {
|
export function Amount(props: {
|
||||||
amountSats: bigint | number | undefined;
|
amountSats: bigint | number | undefined;
|
||||||
showFiat?: boolean;
|
showFiat?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
centered?: boolean;
|
centered?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true);
|
const amountInUsd = () =>
|
||||||
|
satsToUsd(state.price, Number(props.amountSats) || 0, true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2" classList={{ "items-center": props.centered }}>
|
<div
|
||||||
<h1 class="text-4xl font-light">
|
class="flex flex-col gap-2"
|
||||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
classList={{ "items-center": props.centered }}
|
||||||
<span class="text-xl">SATS</span>
|
>
|
||||||
</h1>
|
<h1 class="text-4xl font-light">
|
||||||
<Show when={props.showFiat}>
|
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
||||||
<h2 class="text-xl font-light text-white/70">
|
|
||||||
≈ {props.loading ? "..." : amountInUsd()}
|
<span class="text-xl">SATS</span>
|
||||||
<span class="text-sm">USD</span>
|
</h1>
|
||||||
</h2>
|
<Show when={props.showFiat}>
|
||||||
</Show>
|
<h2 class="text-xl font-light text-white/70">
|
||||||
</div>
|
≈ {props.loading ? "..." : amountInUsd()}
|
||||||
);
|
<span class="text-sm">USD</span>
|
||||||
|
</h2>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AmountSmall(props: {
|
export function AmountSmall(props: {
|
||||||
amountSats: bigint | number | undefined
|
amountSats: bigint | number | undefined;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<span class="font-light">
|
<span class="font-light">
|
||||||
{prettyPrintAmount(props.amountSats)}
|
{prettyPrintAmount(props.amountSats)}
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
{props.amountSats === 1 || props.amountSats === 1n ? "SAT" : "SATS"}
|
{props.amountSats === 1 || props.amountSats === 1n
|
||||||
</span>
|
? "SAT"
|
||||||
</span>
|
: "SATS"}
|
||||||
);
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,132 +5,172 @@ import { satsToUsd } from "~/utils/conversions";
|
|||||||
import { AmountEditable } from "./AmountEditable";
|
import { AmountEditable } from "./AmountEditable";
|
||||||
|
|
||||||
const noop = () => {
|
const noop = () => {
|
||||||
// do nothing
|
// do nothing
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => {
|
const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="flex justify-between items-center" classList={{ "text-neutral-400": props.gray }}>
|
<div
|
||||||
<div class="font-semibold uppercase">{props.key}</div>
|
class="flex justify-between items-center"
|
||||||
<div class="font-light">{props.children}</div>
|
classList={{ "text-neutral-400": props.gray }}
|
||||||
</div>
|
>
|
||||||
);
|
<div class="font-semibold uppercase">{props.key}</div>
|
||||||
|
<div class="font-light">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InlineAmount: ParentComponent<{ amount: string; sign?: string; fiat?: boolean }> = (
|
export const InlineAmount: ParentComponent<{
|
||||||
props
|
amount: string;
|
||||||
) => {
|
sign?: string;
|
||||||
const prettyPrint = createMemo(() => {
|
fiat?: boolean;
|
||||||
const parsed = Number(props.amount);
|
}> = (props) => {
|
||||||
if (isNaN(parsed)) {
|
const prettyPrint = createMemo(() => {
|
||||||
return props.amount;
|
const parsed = Number(props.amount);
|
||||||
} else {
|
if (isNaN(parsed)) {
|
||||||
return parsed.toLocaleString();
|
return props.amount;
|
||||||
}
|
} else {
|
||||||
});
|
return parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="inline-block text-lg">
|
<div class="inline-block text-lg">
|
||||||
{props.sign ? `${props.sign} ` : ""}
|
{props.sign ? `${props.sign} ` : ""}
|
||||||
{props.fiat ? "$" : ""}
|
{props.fiat ? "$" : ""}
|
||||||
{prettyPrint()} <span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
{prettyPrint()}{" "}
|
||||||
</div>
|
<span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function USDShower(props: { amountSats: string; fee?: string }) {
|
function USDShower(props: { amountSats: string; fee?: string }) {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
const amountInUsd = () => satsToUsd(state.price, add(props.amountSats, props.fee), true);
|
const amountInUsd = () =>
|
||||||
|
satsToUsd(state.price, add(props.amountSats, props.fee), true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={!(props.amountSats === "0")}>
|
<Show when={!(props.amountSats === "0")}>
|
||||||
<KeyValue gray key="">
|
<KeyValue gray key="">
|
||||||
<div class="self-end">
|
<div class="self-end">
|
||||||
≈ {amountInUsd()} <span class="text-sm">USD</span>
|
≈ {amountInUsd()}
|
||||||
</div>
|
<span class="text-sm">USD</span>
|
||||||
</KeyValue>
|
</div>
|
||||||
</Show>
|
</KeyValue>
|
||||||
);
|
</Show>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(a: string, b?: string) {
|
function add(a: string, b?: string) {
|
||||||
return Number(a || 0) + Number(b || 0);
|
return Number(a || 0) + Number(b || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AmountCard(props: {
|
export function AmountCard(props: {
|
||||||
amountSats: string;
|
amountSats: string;
|
||||||
fee?: string;
|
fee?: string;
|
||||||
reserve?: string;
|
reserve?: string;
|
||||||
initialOpen?: boolean;
|
initialOpen?: boolean;
|
||||||
isAmountEditable?: boolean;
|
isAmountEditable?: boolean;
|
||||||
setAmountSats?: (amount: bigint) => void;
|
setAmountSats?: (amount: bigint) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<VStack>
|
<VStack>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.fee}>
|
<Match when={props.fee}>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Amount">
|
<KeyValue key="Amount">
|
||||||
<Show
|
<Show
|
||||||
when={props.isAmountEditable}
|
when={props.isAmountEditable}
|
||||||
fallback={<InlineAmount amount={props.amountSats} />}
|
fallback={
|
||||||
>
|
<InlineAmount
|
||||||
<AmountEditable
|
amount={props.amountSats}
|
||||||
initialOpen={props.initialOpen ?? false}
|
/>
|
||||||
initialAmountSats={props.amountSats.toString()}
|
}
|
||||||
setAmountSats={props.setAmountSats ? props.setAmountSats : noop}
|
>
|
||||||
/>
|
<AmountEditable
|
||||||
</Show>
|
initialOpen={props.initialOpen ?? false}
|
||||||
</KeyValue>
|
initialAmountSats={props.amountSats.toString()}
|
||||||
<KeyValue gray key="+ Fee">
|
setAmountSats={
|
||||||
<InlineAmount amount={props.fee || "0"} />
|
props.setAmountSats
|
||||||
</KeyValue>
|
? props.setAmountSats
|
||||||
</div>
|
: noop
|
||||||
<hr class="border-white/20" />
|
}
|
||||||
<div class="flex flex-col gap-1">
|
/>
|
||||||
<KeyValue key="Total">
|
</Show>
|
||||||
<InlineAmount amount={add(props.amountSats, props.fee).toString()} />
|
</KeyValue>
|
||||||
</KeyValue>
|
<KeyValue gray key="+ Fee">
|
||||||
<USDShower amountSats={props.amountSats} fee={props.fee} />
|
<InlineAmount amount={props.fee || "0"} />
|
||||||
</div>
|
</KeyValue>
|
||||||
</Match>
|
</div>
|
||||||
<Match when={props.reserve}>
|
<hr class="border-white/20" />
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Channel size">
|
<KeyValue key="Total">
|
||||||
<InlineAmount amount={add(props.amountSats, props.reserve).toString()} />
|
<InlineAmount
|
||||||
</KeyValue>
|
amount={add(
|
||||||
<KeyValue gray key="- Channel Reserve">
|
props.amountSats,
|
||||||
<InlineAmount amount={props.reserve || "0"} />
|
props.fee
|
||||||
</KeyValue>
|
).toString()}
|
||||||
</div>
|
/>
|
||||||
<hr class="border-white/20" />
|
</KeyValue>
|
||||||
<div class="flex flex-col gap-1">
|
<USDShower
|
||||||
<KeyValue key="Spendable">
|
amountSats={props.amountSats}
|
||||||
<InlineAmount amount={props.amountSats} />
|
fee={props.fee}
|
||||||
</KeyValue>
|
/>
|
||||||
<USDShower amountSats={props.amountSats} fee={props.reserve} />
|
</div>
|
||||||
</div>
|
</Match>
|
||||||
</Match>
|
<Match when={props.reserve}>
|
||||||
<Match when={!props.fee && !props.reserve}>
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex flex-col gap-1">
|
<KeyValue key="Channel size">
|
||||||
<KeyValue key="Amount">
|
<InlineAmount
|
||||||
<Show
|
amount={add(
|
||||||
when={props.isAmountEditable}
|
props.amountSats,
|
||||||
fallback={<InlineAmount amount={props.amountSats} />}
|
props.reserve
|
||||||
>
|
).toString()}
|
||||||
<AmountEditable
|
/>
|
||||||
initialOpen={props.initialOpen ?? false}
|
</KeyValue>
|
||||||
initialAmountSats={props.amountSats.toString()}
|
<KeyValue gray key="- Channel Reserve">
|
||||||
setAmountSats={props.setAmountSats ? props.setAmountSats : noop}
|
<InlineAmount amount={props.reserve || "0"} />
|
||||||
/>
|
</KeyValue>
|
||||||
</Show>
|
</div>
|
||||||
</KeyValue>
|
<hr class="border-white/20" />
|
||||||
<USDShower amountSats={props.amountSats} />
|
<div class="flex flex-col gap-1">
|
||||||
</div>
|
<KeyValue key="Spendable">
|
||||||
</Match>
|
<InlineAmount amount={props.amountSats} />
|
||||||
</Switch>
|
</KeyValue>
|
||||||
</VStack>
|
<USDShower
|
||||||
</Card>
|
amountSats={props.amountSats}
|
||||||
);
|
fee={props.reserve}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={!props.fee && !props.reserve}>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<KeyValue key="Amount">
|
||||||
|
<Show
|
||||||
|
when={props.isAmountEditable}
|
||||||
|
fallback={
|
||||||
|
<InlineAmount
|
||||||
|
amount={props.amountSats}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AmountEditable
|
||||||
|
initialOpen={props.initialOpen ?? false}
|
||||||
|
initialAmountSats={props.amountSats.toString()}
|
||||||
|
setAmountSats={
|
||||||
|
props.setAmountSats
|
||||||
|
? props.setAmountSats
|
||||||
|
: noop
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</KeyValue>
|
||||||
|
<USDShower amountSats={props.amountSats} />
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { For, ParentComponent, Show, createResource, createSignal, onMount } from "solid-js";
|
import {
|
||||||
|
For,
|
||||||
|
ParentComponent,
|
||||||
|
Show,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
onMount
|
||||||
|
} from "solid-js";
|
||||||
import { Button } from "~/components/layout";
|
import { Button } from "~/components/layout";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { satsToUsd, usdToSats } from "~/utils/conversions";
|
import { satsToUsd, usdToSats } from "~/utils/conversions";
|
||||||
@@ -10,328 +17,393 @@ import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
|||||||
import { InfoBox } from "./InfoBox";
|
import { InfoBox } from "./InfoBox";
|
||||||
import { Network } from "~/logic/mutinyWalletSetup";
|
import { Network } from "~/logic/mutinyWalletSetup";
|
||||||
|
|
||||||
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "DEL"];
|
const CHARACTERS = [
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
".",
|
||||||
|
"0",
|
||||||
|
"DEL"
|
||||||
|
];
|
||||||
|
|
||||||
const FIXED_AMOUNTS_SATS = [
|
const FIXED_AMOUNTS_SATS = [
|
||||||
{ label: "10k", amount: "10000" },
|
{ label: "10k", amount: "10000" },
|
||||||
{ label: "100k", amount: "100000" },
|
{ label: "100k", amount: "100000" },
|
||||||
{ label: "1m", amount: "1000000" }
|
{ label: "1m", amount: "1000000" }
|
||||||
];
|
];
|
||||||
|
|
||||||
const FIXED_AMOUNTS_USD = [
|
const FIXED_AMOUNTS_USD = [
|
||||||
{ label: "$1", amount: "1" },
|
{ label: "$1", amount: "1" },
|
||||||
{ label: "$10", amount: "10" },
|
{ label: "$10", amount: "10" },
|
||||||
{ label: "$100", amount: "100" }
|
{ label: "$100", amount: "100" }
|
||||||
];
|
];
|
||||||
|
|
||||||
function fiatInputSanitizer(input: string): string {
|
function fiatInputSanitizer(input: string): string {
|
||||||
// Make sure only numbers and a single decimal point are allowed
|
// Make sure only numbers and a single decimal point are allowed
|
||||||
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
||||||
|
|
||||||
// Remove leading zeros if not a decimal, add 0 if starts with a decimal
|
// Remove leading zeros if not a decimal, add 0 if starts with a decimal
|
||||||
const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0.");
|
const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0.");
|
||||||
|
|
||||||
// If there are three characters after the decimal, shift the decimal
|
// If there are three characters after the decimal, shift the decimal
|
||||||
const shifted = cleaned.match(/(\.[0-9]{3}).*/g) ? (parseFloat(cleaned) * 10).toFixed(2) : cleaned;
|
const shifted = cleaned.match(/(\.[0-9]{3}).*/g)
|
||||||
|
? (parseFloat(cleaned) * 10).toFixed(2)
|
||||||
|
: cleaned;
|
||||||
|
|
||||||
// Truncate any numbers two past the decimal
|
// Truncate any numbers two past the decimal
|
||||||
const twoDecimals = shifted.replace(/(\.[0-9]{2}).*/g, "$1");
|
const twoDecimals = shifted.replace(/(\.[0-9]{2}).*/g, "$1");
|
||||||
|
|
||||||
return twoDecimals;
|
return twoDecimals;
|
||||||
}
|
}
|
||||||
|
|
||||||
function satsInputSanitizer(input: string): string {
|
function satsInputSanitizer(input: string): string {
|
||||||
// Make sure only numbers are allowed
|
// Make sure only numbers are allowed
|
||||||
const numeric = input.replace(/[^0-9]/g, "");
|
const numeric = input.replace(/[^0-9]/g, "");
|
||||||
// If it starts with a 0, remove the 0
|
// If it starts with a 0, remove the 0
|
||||||
const noLeadingZero = numeric.replace(/^0([^.]|$)/g, "$1");
|
const noLeadingZero = numeric.replace(/^0([^.]|$)/g, "$1");
|
||||||
|
|
||||||
return noLeadingZero;
|
return noLeadingZero;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SingleDigitButton(props: {
|
function SingleDigitButton(props: {
|
||||||
character: string;
|
character: string;
|
||||||
onClick: (c: string) => void;
|
onClick: (c: string) => void;
|
||||||
fiat: boolean;
|
fiat: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
// Skip the "." if it's fiat
|
// Skip the "." if it's fiat
|
||||||
<Show when={props.fiat || !(props.character === ".")} fallback={<div />}>
|
<Show
|
||||||
<button
|
when={props.fiat || !(props.character === ".")}
|
||||||
class="disabled:opacity-50 p-2 rounded-lg md:hover:bg-white/10 active:bg-m-blue text-white text-4xl font-semi font-mono"
|
fallback={<div />}
|
||||||
onClick={() => props.onClick(props.character)}
|
>
|
||||||
>
|
<button
|
||||||
{props.character}
|
class="disabled:opacity-50 p-2 rounded-lg md:hover:bg-white/10 active:bg-m-blue text-white text-4xl font-semi font-mono"
|
||||||
</button>
|
onClick={() => props.onClick(props.character)}
|
||||||
</Show>
|
>
|
||||||
);
|
{props.character}
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BigScalingText(props: { text: string; fiat: boolean }) {
|
function BigScalingText(props: { text: string; fiat: boolean }) {
|
||||||
const chars = () => props.text.length;
|
const chars = () => props.text.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h1
|
<h1
|
||||||
class="font-light text-center transition-transform ease-out duration-300 text-4xl"
|
class="font-light text-center transition-transform ease-out duration-300 text-4xl"
|
||||||
classList={{
|
classList={{
|
||||||
"scale-90": chars() > 9,
|
"scale-90": chars() > 9,
|
||||||
"scale-95": chars() > 8,
|
"scale-95": chars() > 8,
|
||||||
"scale-100": chars() > 7,
|
"scale-100": chars() > 7,
|
||||||
"scale-105": chars() > 6,
|
"scale-105": chars() > 6,
|
||||||
"scale-110": chars() > 5,
|
"scale-110": chars() > 5,
|
||||||
"scale-125": chars() > 4,
|
"scale-125": chars() > 4,
|
||||||
"scale-150": chars() <= 4
|
"scale-150": chars() <= 4
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.text} <span class="text-xl">{props.fiat ? "USD" : "SATS"}</span>
|
{props.text}
|
||||||
</h1>
|
<span class="text-xl">{props.fiat ? "USD" : "SATS"}</span>
|
||||||
);
|
</h1>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
||||||
return (
|
return (
|
||||||
<h2 class="text-xl font-light text-neutral-400">
|
<h2 class="text-xl font-light text-neutral-400">
|
||||||
≈ {props.text} <span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
≈ {props.text}
|
||||||
</h2>
|
<span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||||
);
|
</h2>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDisplayHandleNaN(input: string, _fiat: boolean): string {
|
function toDisplayHandleNaN(input: string, _fiat: boolean): string {
|
||||||
const parsed = Number(input);
|
const parsed = Number(input);
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
return "0";
|
return "0";
|
||||||
} else {
|
} else {
|
||||||
return parsed.toLocaleString();
|
return parsed.toLocaleString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AmountEditable: ParentComponent<{
|
export const AmountEditable: ParentComponent<{
|
||||||
initialAmountSats: string;
|
initialAmountSats: string;
|
||||||
initialOpen: boolean;
|
initialOpen: boolean;
|
||||||
setAmountSats: (s: bigint) => void;
|
setAmountSats: (s: bigint) => void;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [isOpen, setIsOpen] = createSignal(props.initialOpen);
|
const [isOpen, setIsOpen] = createSignal(props.initialOpen);
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
const [mode, setMode] = createSignal<"fiat" | "sats">("sats");
|
const [mode, setMode] = createSignal<"fiat" | "sats">("sats");
|
||||||
const [localSats, setLocalSats] = createSignal(props.initialAmountSats || "0");
|
const [localSats, setLocalSats] = createSignal(
|
||||||
const [localFiat, setLocalFiat] = createSignal(
|
props.initialAmountSats || "0"
|
||||||
satsToUsd(state.price, parseInt(props.initialAmountSats || "0") || 0, false)
|
);
|
||||||
);
|
const [localFiat, setLocalFiat] = createSignal(
|
||||||
|
satsToUsd(
|
||||||
|
state.price,
|
||||||
|
parseInt(props.initialAmountSats || "0") || 0,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
||||||
const displayFiat = () => `$${toDisplayHandleNaN(localFiat(), true)}`;
|
const displayFiat = () => `$${toDisplayHandleNaN(localFiat(), true)}`;
|
||||||
|
|
||||||
let satsInputRef!: HTMLInputElement;
|
let satsInputRef!: HTMLInputElement;
|
||||||
let fiatInputRef!: HTMLInputElement;
|
let fiatInputRef!: HTMLInputElement;
|
||||||
|
|
||||||
const [inboundCapacity] = createResource(async () => {
|
const [inboundCapacity] = createResource(async () => {
|
||||||
const channels = await state.mutiny_wallet?.list_channels();
|
const channels = await state.mutiny_wallet?.list_channels();
|
||||||
let inbound = 0;
|
let inbound = 0;
|
||||||
|
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
inbound += channel.size - (channel.balance + channel.reserve);
|
inbound += channel.size - (channel.balance + channel.reserve);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inbound;
|
||||||
|
});
|
||||||
|
|
||||||
|
const warningText = () => {
|
||||||
|
if ((state.balance?.lightning || 0n) === 0n) {
|
||||||
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
if (network === "bitcoin") {
|
||||||
|
return "Your first lightning receive needs to be 50,000 sats or greater.";
|
||||||
|
} else {
|
||||||
|
return "Your first lightning receive needs to be 10,000 sats or greater.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(localSats());
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed > (inboundCapacity() || 0)) {
|
||||||
|
return "A lightning setup fee will be charged if paid over lightning.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleCharacterInput(character: string) {
|
||||||
|
const isFiatMode = mode() === "fiat";
|
||||||
|
const inputSanitizer = isFiatMode
|
||||||
|
? fiatInputSanitizer
|
||||||
|
: satsInputSanitizer;
|
||||||
|
const localValue = isFiatMode ? localFiat : localSats;
|
||||||
|
|
||||||
|
let sane;
|
||||||
|
|
||||||
|
if (character === "DEL") {
|
||||||
|
sane = inputSanitizer(localValue().slice(0, -1));
|
||||||
|
} else {
|
||||||
|
if (localValue() === "0") {
|
||||||
|
sane = inputSanitizer(character);
|
||||||
|
} else {
|
||||||
|
sane = inputSanitizer(localValue() + character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFiatMode) {
|
||||||
|
setLocalFiat(sane);
|
||||||
|
setLocalSats(
|
||||||
|
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setLocalSats(sane);
|
||||||
|
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a button press make sure we re-focus the input
|
||||||
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
return inbound;
|
function setFixedAmount(amount: string) {
|
||||||
});
|
if (mode() === "fiat") {
|
||||||
|
setLocalFiat(amount);
|
||||||
const warningText = () => {
|
setLocalSats(
|
||||||
if ((state.balance?.lightning || 0n) === 0n) {
|
usdToSats(state.price, parseFloat(amount || "0") || 0, false)
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
);
|
||||||
if (network === "bitcoin") {
|
} else {
|
||||||
return "Your first lightning receive needs to be 50,000 sats or greater.";
|
setLocalSats(amount);
|
||||||
} else {
|
setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false));
|
||||||
return "Your first lightning receive needs to be 10,000 sats or greater.";
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = Number(localSats());
|
// What we're all here for in the first place: returning a value
|
||||||
if (isNaN(parsed)) {
|
function handleSubmit(e: SubmitEvent | MouseEvent) {
|
||||||
return undefined;
|
e.preventDefault();
|
||||||
|
props.setAmountSats(BigInt(localSats()));
|
||||||
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed > (inboundCapacity() || 0)) {
|
function handleSatsInput(e: InputEvent) {
|
||||||
return "A lightning setup fee will be charged if paid over lightning.";
|
const { value } = e.target as HTMLInputElement;
|
||||||
|
const sane = satsInputSanitizer(value);
|
||||||
|
setLocalSats(sane);
|
||||||
|
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
function handleFiatInput(e: InputEvent) {
|
||||||
};
|
const { value } = e.target as HTMLInputElement;
|
||||||
|
const sane = fiatInputSanitizer(value);
|
||||||
function handleCharacterInput(character: string) {
|
setLocalFiat(sane);
|
||||||
const isFiatMode = mode() === "fiat";
|
setLocalSats(
|
||||||
const inputSanitizer = isFiatMode ? fiatInputSanitizer : satsInputSanitizer;
|
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||||
const localValue = isFiatMode ? localFiat : localSats;
|
);
|
||||||
|
|
||||||
let sane;
|
|
||||||
|
|
||||||
if (character === "DEL") {
|
|
||||||
sane = inputSanitizer(localValue().slice(0, -1));
|
|
||||||
} else {
|
|
||||||
if (localValue() === "0") {
|
|
||||||
sane = inputSanitizer(character);
|
|
||||||
} else {
|
|
||||||
sane = inputSanitizer(localValue() + character);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFiatMode) {
|
function toggle() {
|
||||||
setLocalFiat(sane);
|
setMode((m) => (m === "sats" ? "fiat" : "sats"));
|
||||||
setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false));
|
focus();
|
||||||
} else {
|
|
||||||
setLocalSats(sane);
|
|
||||||
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a button press make sure we re-focus the input
|
onMount(() => {
|
||||||
focus();
|
focus();
|
||||||
}
|
});
|
||||||
|
|
||||||
function setFixedAmount(amount: string) {
|
function focus() {
|
||||||
if (mode() === "fiat") {
|
// Make sure we actually have the inputs mounted before we try to focus them
|
||||||
setLocalFiat(amount);
|
if (isOpen() && satsInputRef && fiatInputRef) {
|
||||||
setLocalSats(usdToSats(state.price, parseFloat(amount || "0") || 0, false));
|
if (mode() === "sats") {
|
||||||
} else {
|
satsInputRef.focus();
|
||||||
setLocalSats(amount);
|
} else {
|
||||||
setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false));
|
fiatInputRef.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// What we're all here for in the first place: returning a value
|
return (
|
||||||
function handleSubmit(e: SubmitEvent | MouseEvent) {
|
<Dialog.Root open={isOpen()}>
|
||||||
e.preventDefault();
|
<button
|
||||||
props.setAmountSats(BigInt(localSats()));
|
onClick={() => setIsOpen(true)}
|
||||||
setIsOpen(false);
|
class="px-4 py-2 rounded-xl border-2 border-m-blue flex gap-2 items-center"
|
||||||
}
|
>
|
||||||
|
<Show
|
||||||
function handleSatsInput(e: InputEvent) {
|
when={localSats() !== "0"}
|
||||||
const { value } = e.target as HTMLInputElement;
|
fallback={
|
||||||
const sane = satsInputSanitizer(value);
|
<div class="inline-block font-semibold">Set amount</div>
|
||||||
setLocalSats(sane);
|
}
|
||||||
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
>
|
||||||
}
|
<InlineAmount amount={localSats()} />
|
||||||
|
</Show>
|
||||||
function handleFiatInput(e: InputEvent) {
|
<img src={pencil} alt="Edit" />
|
||||||
const { value } = e.target as HTMLInputElement;
|
{/* {props.children} */}
|
||||||
const sane = fiatInputSanitizer(value);
|
</button>
|
||||||
setLocalFiat(sane);
|
<Dialog.Portal>
|
||||||
setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false));
|
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
||||||
}
|
<div class={DIALOG_POSITIONER}>
|
||||||
|
<Dialog.Content
|
||||||
function toggle() {
|
class={DIALOG_CONTENT}
|
||||||
setMode((m) => (m === "sats" ? "fiat" : "sats"));
|
onEscapeKeyDown={() => setIsOpen(false)}
|
||||||
focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
// Make sure we actually have the inputs mounted before we try to focus them
|
|
||||||
if (isOpen() && satsInputRef && fiatInputRef) {
|
|
||||||
if (mode() === "sats") {
|
|
||||||
satsInputRef.focus();
|
|
||||||
} else {
|
|
||||||
fiatInputRef.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root open={isOpen()}>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(true)}
|
|
||||||
class="px-4 py-2 rounded-xl border-2 border-m-blue flex gap-2 items-center"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={localSats() !== "0"}
|
|
||||||
fallback={<div class="inline-block font-semibold">Set amount</div>}
|
|
||||||
>
|
|
||||||
<InlineAmount amount={localSats()} />
|
|
||||||
</Show>
|
|
||||||
<img src={pencil} alt="Edit" />
|
|
||||||
{/* {props.children} */}
|
|
||||||
</button>
|
|
||||||
<Dialog.Portal>
|
|
||||||
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
|
||||||
<div class={DIALOG_POSITIONER}>
|
|
||||||
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
|
|
||||||
{/* TODO: figure out how to submit on enter */}
|
|
||||||
<div class="w-full flex justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
class="hover:bg-white/10 rounded-lg active:bg-m-blue w-8 h-8"
|
|
||||||
>
|
|
||||||
<img src={close} alt="Close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* <form onSubmit={handleSubmit} class="text-black"> */}
|
|
||||||
<form onSubmit={handleSubmit} class="opacity-0 absolute -z-10">
|
|
||||||
<input
|
|
||||||
ref={(el) => (satsInputRef = el)}
|
|
||||||
disabled={mode() === "fiat"}
|
|
||||||
type="text"
|
|
||||||
value={localSats()}
|
|
||||||
onInput={handleSatsInput}
|
|
||||||
inputMode="none"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref={(el) => (fiatInputRef = el)}
|
|
||||||
disabled={mode() === "sats"}
|
|
||||||
type="text"
|
|
||||||
value={localFiat()}
|
|
||||||
onInput={handleFiatInput}
|
|
||||||
inputMode="none"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="flex flex-col flex-1 justify-around gap-2 max-w-[400px] mx-auto w-full">
|
|
||||||
<div class="p-4 flex flex-col gap-4 items-center justify-center" onClick={toggle}>
|
|
||||||
<BigScalingText
|
|
||||||
text={mode() === "fiat" ? displayFiat() : displaySats()}
|
|
||||||
fiat={mode() === "fiat"}
|
|
||||||
/>
|
|
||||||
<SmallSubtleAmount
|
|
||||||
text={mode() === "fiat" ? displaySats() : displayFiat()}
|
|
||||||
fiat={mode() !== "fiat"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Show when={warningText()}>
|
|
||||||
<InfoBox accent="green">{warningText()}</InfoBox>
|
|
||||||
</Show>
|
|
||||||
<div class="flex justify-center gap-4 my-2">
|
|
||||||
<For each={mode() === "fiat" ? FIXED_AMOUNTS_USD : FIXED_AMOUNTS_SATS}>
|
|
||||||
{(amount) => (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setFixedAmount(amount.amount);
|
|
||||||
focus();
|
|
||||||
}}
|
|
||||||
class="py-2 px-4 rounded-lg bg-white/10"
|
|
||||||
>
|
>
|
||||||
{amount.label}
|
{/* TODO: figure out how to submit on enter */}
|
||||||
</button>
|
<div class="w-full flex justify-end">
|
||||||
)}
|
<button
|
||||||
</For>
|
onClick={() => setIsOpen(false)}
|
||||||
</div>
|
class="hover:bg-white/10 rounded-lg active:bg-m-blue w-8 h-8"
|
||||||
<div class="grid grid-cols-3 w-full flex-none">
|
>
|
||||||
<For each={CHARACTERS}>
|
<img src={close} alt="Close" />
|
||||||
{(character) => (
|
</button>
|
||||||
<SingleDigitButton
|
</div>
|
||||||
fiat={mode() === "fiat"}
|
{/* <form onSubmit={handleSubmit} class="text-black"> */}
|
||||||
character={character}
|
<form
|
||||||
onClick={handleCharacterInput}
|
onSubmit={handleSubmit}
|
||||||
/>
|
class="opacity-0 absolute -z-10"
|
||||||
)}
|
>
|
||||||
</For>
|
<input
|
||||||
</div>
|
ref={(el) => (satsInputRef = el)}
|
||||||
<Button intent="blue" class="w-full flex-none" onClick={handleSubmit}>
|
disabled={mode() === "fiat"}
|
||||||
Set Amount
|
type="text"
|
||||||
</Button>
|
value={localSats()}
|
||||||
</div>
|
onInput={handleSatsInput}
|
||||||
</Dialog.Content>
|
inputMode="none"
|
||||||
</div>
|
/>
|
||||||
</Dialog.Portal>
|
<input
|
||||||
</Dialog.Root>
|
ref={(el) => (fiatInputRef = el)}
|
||||||
);
|
disabled={mode() === "sats"}
|
||||||
|
type="text"
|
||||||
|
value={localFiat()}
|
||||||
|
onInput={handleFiatInput}
|
||||||
|
inputMode="none"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-1 justify-around gap-2 max-w-[400px] mx-auto w-full">
|
||||||
|
<div
|
||||||
|
class="p-4 flex flex-col gap-4 items-center justify-center"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<BigScalingText
|
||||||
|
text={
|
||||||
|
mode() === "fiat"
|
||||||
|
? displayFiat()
|
||||||
|
: displaySats()
|
||||||
|
}
|
||||||
|
fiat={mode() === "fiat"}
|
||||||
|
/>
|
||||||
|
<SmallSubtleAmount
|
||||||
|
text={
|
||||||
|
mode() === "fiat"
|
||||||
|
? displaySats()
|
||||||
|
: displayFiat()
|
||||||
|
}
|
||||||
|
fiat={mode() !== "fiat"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Show when={warningText()}>
|
||||||
|
<InfoBox accent="green">
|
||||||
|
{warningText()}
|
||||||
|
</InfoBox>
|
||||||
|
</Show>
|
||||||
|
<div class="flex justify-center gap-4 my-2">
|
||||||
|
<For
|
||||||
|
each={
|
||||||
|
mode() === "fiat"
|
||||||
|
? FIXED_AMOUNTS_USD
|
||||||
|
: FIXED_AMOUNTS_SATS
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(amount) => (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setFixedAmount(amount.amount);
|
||||||
|
focus();
|
||||||
|
}}
|
||||||
|
class="py-2 px-4 rounded-lg bg-white/10"
|
||||||
|
>
|
||||||
|
{amount.label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 w-full flex-none">
|
||||||
|
<For each={CHARACTERS}>
|
||||||
|
{(character) => (
|
||||||
|
<SingleDigitButton
|
||||||
|
fiat={mode() === "fiat"}
|
||||||
|
character={character}
|
||||||
|
onClick={handleCharacterInput}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
intent="blue"
|
||||||
|
class="w-full flex-none"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Set Amount
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,60 +1,66 @@
|
|||||||
import logo from '~/assets/icons/mutiny-logo.svg';
|
import logo from "~/assets/icons/mutiny-logo.svg";
|
||||||
import { DefaultMain, SafeArea, VStack, Card } from "~/components/layout";
|
import { DefaultMain, SafeArea, VStack, Card } from "~/components/layout";
|
||||||
import BalanceBox, { LoadingShimmer } from "~/components/BalanceBox";
|
import BalanceBox, { LoadingShimmer } from "~/components/BalanceBox";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import ReloadPrompt from "~/components/Reload";
|
import ReloadPrompt from "~/components/Reload";
|
||||||
import { A } from 'solid-start';
|
import { A } from "solid-start";
|
||||||
import { OnboardWarning } from '~/components/OnboardWarning';
|
import { OnboardWarning } from "~/components/OnboardWarning";
|
||||||
import { CombinedActivity } from './Activity';
|
import { CombinedActivity } from "./Activity";
|
||||||
import userClock from '~/assets/icons/user-clock.svg';
|
import userClock from "~/assets/icons/user-clock.svg";
|
||||||
import { useMegaStore } from '~/state/megaStore';
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { Show } from 'solid-js';
|
import { Show } from "solid-js";
|
||||||
import { ExternalLink } from "./layout/ExternalLink";
|
import { ExternalLink } from "./layout/ExternalLink";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
||||||
<img src={logo} class="h-10" alt="logo" />
|
<img src={logo} class="h-10" alt="logo" />
|
||||||
<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/activity">
|
<A
|
||||||
<img src={userClock} alt="Activity" class="h-8 w-8" />
|
class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue"
|
||||||
</A>
|
href="/activity"
|
||||||
</header>
|
>
|
||||||
<Show when={!state.wallet_loading}>
|
<img src={userClock} alt="Activity" class="h-8 w-8" />
|
||||||
<OnboardWarning />
|
</A>
|
||||||
<ReloadPrompt />
|
</header>
|
||||||
</Show>
|
<Show when={!state.wallet_loading}>
|
||||||
<BalanceBox loading={state.wallet_loading} />
|
<OnboardWarning />
|
||||||
<Card title="Activity">
|
<ReloadPrompt />
|
||||||
<div class="p-1" />
|
</Show>
|
||||||
<VStack>
|
<BalanceBox loading={state.wallet_loading} />
|
||||||
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
|
<Card title="Activity">
|
||||||
<CombinedActivity limit={3} />
|
<div class="p-1" />
|
||||||
</Show>
|
<VStack>
|
||||||
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
<Show
|
||||||
</VStack>
|
when={!state.wallet_loading}
|
||||||
<Show when={state.activity && state.activity.length > 0}>
|
fallback={<LoadingShimmer />}
|
||||||
<A
|
>
|
||||||
href="/activity"
|
<CombinedActivity limit={3} />
|
||||||
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center"
|
</Show>
|
||||||
>
|
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
||||||
View All
|
</VStack>
|
||||||
</A>
|
<Show when={state.activity && state.activity.length > 0}>
|
||||||
</Show>
|
<A
|
||||||
</Card>
|
href="/activity"
|
||||||
<p class="self-center text-neutral-500 mt-4">
|
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center"
|
||||||
Bugs? Feedback?{" "}
|
>
|
||||||
<span class="text-neutral-400">
|
View All
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
</A>
|
||||||
Create an issue
|
</Show>
|
||||||
</ExternalLink>
|
</Card>
|
||||||
</span>
|
<p class="self-center text-neutral-500 mt-4">
|
||||||
</p>
|
Bugs? Feedback?{" "}
|
||||||
</DefaultMain>
|
<span class="text-neutral-400">
|
||||||
<NavBar activeTab="home" />
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
||||||
</SafeArea>
|
Create an issue
|
||||||
|
</ExternalLink>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</DefaultMain>
|
||||||
|
<NavBar activeTab="home" />
|
||||||
|
</SafeArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,71 +6,90 @@ import { A, useNavigate } from "solid-start";
|
|||||||
import shuffle from "~/assets/icons/shuffle.svg";
|
import shuffle from "~/assets/icons/shuffle.svg";
|
||||||
|
|
||||||
export function LoadingShimmer() {
|
export function LoadingShimmer() {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2 animate-pulse">
|
<div class="flex flex-col gap-2 animate-pulse">
|
||||||
<h1 class="text-4xl font-light">
|
<h1 class="text-4xl font-light">
|
||||||
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]" />
|
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]" />
|
||||||
</h1>
|
</h1>
|
||||||
<h2 class="text-xl font-light text-white/70">
|
<h2 class="text-xl font-light text-white/70">
|
||||||
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]" />
|
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]" />
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const STYLE =
|
const STYLE =
|
||||||
"px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold";
|
"px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold";
|
||||||
|
|
||||||
export default function BalanceBox(props: { loading?: boolean }) {
|
export default function BalanceBox(props: { loading?: boolean }) {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const emptyBalance = () =>
|
const emptyBalance = () =>
|
||||||
(state.balance?.confirmed || 0n) === 0n &&
|
(state.balance?.confirmed || 0n) === 0n &&
|
||||||
(state.balance?.lightning || 0n) === 0n &&
|
(state.balance?.lightning || 0n) === 0n &&
|
||||||
(state.balance?.force_close || 0n) === 0n &&
|
(state.balance?.force_close || 0n) === 0n &&
|
||||||
(state.balance?.unconfirmed || 0n) === 0n;
|
(state.balance?.unconfirmed || 0n) === 0n;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const totalOnchain = () => (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n) + (state.balance?.force_close || 0n);
|
const totalOnchain = () =>
|
||||||
|
(state.balance?.confirmed || 0n) +
|
||||||
|
(state.balance?.unconfirmed || 0n) +
|
||||||
|
(state.balance?.force_close || 0n);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FancyCard title="Lightning">
|
<FancyCard title="Lightning">
|
||||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
<Amount
|
||||||
</Show>
|
amountSats={state.balance?.lightning || 0}
|
||||||
</FancyCard>
|
showFiat
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</FancyCard>
|
||||||
|
|
||||||
<FancyCard
|
<FancyCard
|
||||||
title="On-Chain"
|
title="On-Chain"
|
||||||
subtitle={((Number(state.balance?.unconfirmed) || 0) + (Number(state.balance?.force_close) || 0)) ? "Unconfirmed" : undefined}
|
subtitle={
|
||||||
>
|
(Number(state.balance?.unconfirmed) || 0) +
|
||||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
(Number(state.balance?.force_close) || 0)
|
||||||
<div class="flex justify-between">
|
? "Unconfirmed"
|
||||||
<Amount amountSats={totalOnchain()} showFiat />
|
: undefined
|
||||||
<Show when={!emptyBalance()}>
|
}
|
||||||
<div class="self-end justify-self-end">
|
>
|
||||||
<A href="/swap" class={STYLE}>
|
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||||
<img src={shuffle} alt="swap" class="h-8 w-8" />
|
<div class="flex justify-between">
|
||||||
</A>
|
<Amount amountSats={totalOnchain()} showFiat />
|
||||||
</div>
|
<Show when={!emptyBalance()}>
|
||||||
</Show>
|
<div class="self-end justify-self-end">
|
||||||
</div>
|
<A href="/swap" class={STYLE}>
|
||||||
</Show>
|
<img
|
||||||
</FancyCard>
|
src={shuffle}
|
||||||
<div class="flex gap-2 py-4">
|
alt="swap"
|
||||||
<Button
|
class="h-8 w-8"
|
||||||
onClick={() => navigate("/send")}
|
/>
|
||||||
disabled={emptyBalance() || props.loading}
|
</A>
|
||||||
intent="green"
|
</div>
|
||||||
>
|
</Show>
|
||||||
Send
|
</div>
|
||||||
</Button>
|
</Show>
|
||||||
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">
|
</FancyCard>
|
||||||
Receive
|
<div class="flex gap-2 py-4">
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
onClick={() => navigate("/send")}
|
||||||
</>
|
disabled={emptyBalance() || props.loading}
|
||||||
);
|
intent="green"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate("/receive")}
|
||||||
|
disabled={props.loading}
|
||||||
|
intent="blue"
|
||||||
|
>
|
||||||
|
Receive
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,69 @@
|
|||||||
import { Match, Switch, createSignal } from 'solid-js';
|
import { Match, Switch, createSignal } from "solid-js";
|
||||||
import { SmallHeader, TinyButton } from '~/components/layout';
|
import { SmallHeader, TinyButton } from "~/components/layout";
|
||||||
import { Dialog } from '@kobalte/core';
|
import { Dialog } from "@kobalte/core";
|
||||||
import close from "~/assets/icons/close.svg";
|
import close from "~/assets/icons/close.svg";
|
||||||
import { SubmitHandler } from '@modular-forms/solid';
|
import { SubmitHandler } from "@modular-forms/solid";
|
||||||
import { ContactForm } from './ContactForm';
|
import { ContactForm } from "./ContactForm";
|
||||||
import { ContactFormValues } from './ContactViewer';
|
import { ContactFormValues } from "./ContactViewer";
|
||||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from '~/styles/dialogs';
|
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||||
|
|
||||||
export function ContactEditor(props: { createContact: (contact: ContactFormValues) => void, list?: boolean }) {
|
export function ContactEditor(props: {
|
||||||
|
createContact: (contact: ContactFormValues) => void;
|
||||||
|
list?: boolean;
|
||||||
|
}) {
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
|
||||||
// What we're all here for in the first place: returning a value
|
// What we're all here for in the first place: returning a value
|
||||||
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
|
const handleSubmit: SubmitHandler<ContactFormValues> = (
|
||||||
props.createContact(c)
|
c: ContactFormValues
|
||||||
|
) => {
|
||||||
|
props.createContact(c);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={isOpen()}>
|
<Dialog.Root open={isOpen()}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.list}>
|
<Match when={props.list}>
|
||||||
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2">
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
class="flex flex-col items-center gap-2"
|
||||||
|
>
|
||||||
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
|
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
|
||||||
<span class="leading-[4rem]">+</span>
|
<span class="leading-[4rem]">+</span>
|
||||||
</div>
|
</div>
|
||||||
<SmallHeader class="overflow-ellipsis">
|
<SmallHeader class="overflow-ellipsis">new</SmallHeader>
|
||||||
new
|
|
||||||
</SmallHeader>
|
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!props.list}>
|
<Match when={!props.list}>
|
||||||
<TinyButton onClick={() => setIsOpen(true)}>+ Add Contact</TinyButton>
|
<TinyButton onClick={() => setIsOpen(true)}>
|
||||||
|
+ Add Contact
|
||||||
|
</TinyButton>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<div class={DIALOG_POSITIONER}>
|
<div class={DIALOG_POSITIONER}>
|
||||||
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
|
<Dialog.Content
|
||||||
|
class={DIALOG_CONTENT}
|
||||||
|
onEscapeKeyDown={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
<div class="w-full flex justify-end">
|
<div class="w-full flex justify-end">
|
||||||
<button tabindex="-1" onClick={() => setIsOpen(false)} class="hover:bg-white/10 rounded-lg active:bg-m-blue">
|
<button
|
||||||
|
tabindex="-1"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
class="hover:bg-white/10 rounded-lg active:bg-m-blue"
|
||||||
|
>
|
||||||
<img src={close} alt="Close" />
|
<img src={close} alt="Close" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} />
|
<ContactForm
|
||||||
|
title="New contact"
|
||||||
|
cta="Create contact"
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root >
|
</Dialog.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,36 @@ import { Button, LargeHeader, VStack } from "~/components/layout";
|
|||||||
import { TextField } from "~/components/layout/TextField";
|
import { TextField } from "~/components/layout/TextField";
|
||||||
import { ContactFormValues } from "./ContactViewer";
|
import { ContactFormValues } from "./ContactViewer";
|
||||||
|
|
||||||
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactFormValues>, initialValues?: ContactFormValues, title: string, cta: string }) {
|
export function ContactForm(props: {
|
||||||
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({ initialValues: props.initialValues });
|
handleSubmit: SubmitHandler<ContactFormValues>;
|
||||||
|
initialValues?: ContactFormValues;
|
||||||
|
title: string;
|
||||||
|
cta: string;
|
||||||
|
}) {
|
||||||
|
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({
|
||||||
|
initialValues: props.initialValues
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={props.handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full">
|
<Form
|
||||||
|
onSubmit={props.handleSubmit}
|
||||||
|
class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<LargeHeader>{props.title}</LargeHeader>
|
<LargeHeader>{props.title}</LargeHeader>
|
||||||
<VStack>
|
<VStack>
|
||||||
<Field name="name" validate={[required("We at least need a name")]}>
|
<Field
|
||||||
|
name="name"
|
||||||
|
validate={[required("We at least need a name")]}
|
||||||
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField {...props} placeholder='Satoshi' value={field.value} error={field.error} label="Name" />
|
<TextField
|
||||||
|
{...props}
|
||||||
|
placeholder="Satoshi"
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Name"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
{/* <Field name="npub" validate={[]}>
|
{/* <Field name="npub" validate={[]}>
|
||||||
@@ -27,5 +46,5 @@ export function ContactForm(props: { handleSubmit: SubmitHandler<ContactFormValu
|
|||||||
{props.cta}
|
{props.cta}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,44 @@
|
|||||||
import { Match, Switch, createSignal } from 'solid-js';
|
import { Match, Switch, createSignal } from "solid-js";
|
||||||
import { Button, Card, NiceP, SmallHeader } from '~/components/layout';
|
import { Button, Card, NiceP, SmallHeader } from "~/components/layout";
|
||||||
import { Dialog } from '@kobalte/core';
|
import { Dialog } from "@kobalte/core";
|
||||||
import close from "~/assets/icons/close.svg";
|
import close from "~/assets/icons/close.svg";
|
||||||
import { SubmitHandler } from '@modular-forms/solid';
|
import { SubmitHandler } from "@modular-forms/solid";
|
||||||
import { ContactForm } from './ContactForm';
|
import { ContactForm } from "./ContactForm";
|
||||||
import { showToast } from './Toaster';
|
import { showToast } from "./Toaster";
|
||||||
import { Contact } from '@mutinywallet/mutiny-wasm';
|
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from '~/styles/dialogs';
|
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||||
|
|
||||||
export type ContactFormValues = {
|
export type ContactFormValues = {
|
||||||
name: string,
|
name: string;
|
||||||
npub?: string,
|
npub?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function ContactViewer(props: { contact: Contact, gradient: string, saveContact: (contact: Contact) => void }) {
|
export function ContactViewer(props: {
|
||||||
|
contact: Contact;
|
||||||
|
gradient: string;
|
||||||
|
saveContact: (contact: Contact) => void;
|
||||||
|
}) {
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
const [isEditing, setIsEditing] = createSignal(false);
|
const [isEditing, setIsEditing] = createSignal(false);
|
||||||
|
|
||||||
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
|
const handleSubmit: SubmitHandler<ContactFormValues> = (
|
||||||
|
c: ContactFormValues
|
||||||
|
) => {
|
||||||
// FIXME: merge with existing contact if saving (need edit contact method)
|
// FIXME: merge with existing contact if saving (need edit contact method)
|
||||||
// FIXME: npub not valid? other undefineds
|
// FIXME: npub not valid? other undefineds
|
||||||
const contact = new Contact(c.name, undefined, undefined, undefined)
|
const contact = new Contact(c.name, undefined, undefined, undefined);
|
||||||
props.saveContact(contact)
|
props.saveContact(contact);
|
||||||
setIsEditing(false)
|
setIsEditing(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={isOpen()}>
|
<Dialog.Root open={isOpen()}>
|
||||||
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden">
|
<button
|
||||||
<div class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
onClick={() => setIsOpen(true)}
|
||||||
|
class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||||
style={{ background: props.gradient }}
|
style={{ background: props.gradient }}
|
||||||
>
|
>
|
||||||
{props.contact.name[0]}
|
{props.contact.name[0]}
|
||||||
@@ -39,33 +49,72 @@ export function ContactViewer(props: { contact: Contact, gradient: string, saveC
|
|||||||
</button>
|
</button>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<div class={DIALOG_POSITIONER}>
|
<div class={DIALOG_POSITIONER}>
|
||||||
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
|
<Dialog.Content
|
||||||
|
class={DIALOG_CONTENT}
|
||||||
|
onEscapeKeyDown={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
<div class="w-full flex justify-end">
|
<div class="w-full flex justify-end">
|
||||||
<button tabindex="-1" onClick={() => setIsOpen(false)} class="hover:bg-white/10 rounded-lg active:bg-m-blue">
|
<button
|
||||||
|
tabindex="-1"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
class="hover:bg-white/10 rounded-lg active:bg-m-blue"
|
||||||
|
>
|
||||||
<img src={close} alt="Close" />
|
<img src={close} alt="Close" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isEditing()}>
|
<Match when={isEditing()}>
|
||||||
<ContactForm title="Edit contact" cta="Save contact" handleSubmit={handleSubmit} initialValues={props.contact} />
|
<ContactForm
|
||||||
|
title="Edit contact"
|
||||||
|
cta="Save contact"
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
initialValues={props.contact}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!isEditing()}>
|
<Match when={!isEditing()}>
|
||||||
<div class="flex flex-col flex-1 justify-around items-center gap-4 max-w-[400px] mx-auto w-full">
|
<div class="flex flex-col flex-1 justify-around items-center gap-4 max-w-[400px] mx-auto w-full">
|
||||||
<div class="flex flex-col items-center w-full">
|
<div class="flex flex-col items-center w-full">
|
||||||
<div class="flex-none h-32 w-32 rounded-full flex items-center justify-center text-8xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
<div
|
||||||
style={{ background: props.gradient }}
|
class="flex-none h-32 w-32 rounded-full flex items-center justify-center text-8xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||||
|
style={{
|
||||||
|
background: props.gradient
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{props.contact.name[0]}
|
{props.contact.name[0]}
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">{props.contact.name}</h1>
|
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">
|
||||||
|
{props.contact.name}
|
||||||
|
</h1>
|
||||||
<Card title="Payment history">
|
<Card title="Payment history">
|
||||||
<NiceP>No payments yet with <span class="font-semibold">{props.contact.name}</span></NiceP>
|
<NiceP>
|
||||||
|
No payments yet with{" "}
|
||||||
|
<span class="font-semibold">
|
||||||
|
{props.contact.name}
|
||||||
|
</span>
|
||||||
|
</NiceP>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full gap-2">
|
<div class="flex w-full gap-2">
|
||||||
<Button layout="flex" intent="green" onClick={() => setIsEditing(true)}>Edit</Button>
|
<Button
|
||||||
<Button intent="blue" onClick={() => { showToast({ title: "Unimplemented", description: "We don't do that yet" }) }}>Pay</Button>
|
layout="flex"
|
||||||
|
intent="green"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
intent="blue"
|
||||||
|
onClick={() => {
|
||||||
|
showToast({
|
||||||
|
title: "Unimplemented",
|
||||||
|
description:
|
||||||
|
"We don't do that yet"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pay
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -73,6 +122,6 @@ export function ContactViewer(props: { contact: Contact, gradient: string, saveC
|
|||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root >
|
</Dialog.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,22 @@ import { QRCodeSVG } from "solid-qr-code";
|
|||||||
import { useCopy } from "~/utils/useCopy";
|
import { useCopy } from "~/utils/useCopy";
|
||||||
|
|
||||||
export function CopyableQR(props: { value: string }) {
|
export function CopyableQR(props: { value: string }) {
|
||||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
return (
|
return (
|
||||||
<div id="qr" class="w-full bg-white rounded-xl relative" onClick={() => copy(props.value)}>
|
<div
|
||||||
<Show when={copied()}>
|
id="qr"
|
||||||
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
class="w-full bg-white rounded-xl relative"
|
||||||
<p class="text-xl font-bold">Copied</p>
|
onClick={() => copy(props.value)}
|
||||||
|
>
|
||||||
|
<Show when={copied()}>
|
||||||
|
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
||||||
|
<p class="text-xl font-bold">Copied</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<QRCodeSVG
|
||||||
|
value={props.value}
|
||||||
|
class="w-full h-full p-8 max-h-[400px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
);
|
||||||
<QRCodeSVG value={props.value} class="w-full h-full p-8 max-h-[400px]" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,18 @@ export function DeleteEverything() {
|
|||||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||||
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
||||||
|
|
||||||
|
|
||||||
async function resetNode() {
|
async function resetNode() {
|
||||||
try {
|
try {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
await actions.deleteMutinyWallet();
|
await actions.deleteMutinyWallet();
|
||||||
showToast({ title: "Deleted", description: `Deleted all data` })
|
showToast({ title: "Deleted", description: `Deleted all data` });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
showToast(eify(e))
|
showToast(eify(e));
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmOpen(false);
|
setConfirmOpen(false);
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
@@ -37,9 +36,14 @@ export function DeleteEverything() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={confirmReset}>Delete Everything</Button>
|
<Button onClick={confirmReset}>Delete Everything</Button>
|
||||||
<ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={resetNode} onCancel={() => setConfirmOpen(false)}>
|
<ConfirmDialog
|
||||||
|
loading={confirmLoading()}
|
||||||
|
open={confirmOpen()}
|
||||||
|
onConfirm={resetNode}
|
||||||
|
onCancel={() => setConfirmOpen(false)}
|
||||||
|
>
|
||||||
This will delete your node's state. This can't be undone!
|
This will delete your node's state. This can't be undone!
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Dialog } from "@kobalte/core"
|
import { Dialog } from "@kobalte/core";
|
||||||
import {
|
import {
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
ParentComponent,
|
ParentComponent,
|
||||||
Show,
|
Show,
|
||||||
Suspense,
|
Suspense,
|
||||||
Switch,
|
Switch,
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource
|
createResource
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { Hr, ModalCloseButton, TinyButton, VStack } from "~/components/layout";
|
import { Hr, ModalCloseButton, TinyButton, VStack } from "~/components/layout";
|
||||||
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||||
@@ -29,286 +29,320 @@ import { Network } from "~/logic/mutinyWalletSetup";
|
|||||||
import { AmountSmall } from "./Amount";
|
import { AmountSmall } from "./Amount";
|
||||||
|
|
||||||
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||||
export const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
export const DIALOG_POSITIONER =
|
||||||
|
"fixed inset-0 z-50 flex items-center justify-center";
|
||||||
export const DIALOG_CONTENT =
|
export const DIALOG_CONTENT =
|
||||||
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||||
|
|
||||||
function LightningHeader(props: { info: MutinyInvoice }) {
|
function LightningHeader(props: { info: MutinyInvoice }) {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const tags = createMemo(() => {
|
const tags = createMemo(() => {
|
||||||
if (props.info.labels.length) {
|
if (props.info.labels.length) {
|
||||||
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
|
const contact = state.mutiny_wallet?.get_contact(
|
||||||
if (contact) {
|
props.info.labels[0]
|
||||||
return [tagToMutinyTag(contact)];
|
);
|
||||||
} else {
|
if (contact) {
|
||||||
return [];
|
return [tagToMutinyTag(contact)];
|
||||||
}
|
} else {
|
||||||
} else {
|
return [];
|
||||||
return [];
|
}
|
||||||
}
|
} else {
|
||||||
});
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<div class="p-4 bg-neutral-100 rounded-full">
|
<div class="p-4 bg-neutral-100 rounded-full">
|
||||||
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="uppercase font-semibold">
|
<h1 class="uppercase font-semibold">
|
||||||
{props.info.inbound ? "Lightning receive" : "Lightning send"}
|
{props.info.inbound ? "Lightning receive" : "Lightning send"}
|
||||||
</h1>
|
</h1>
|
||||||
<ActivityAmount
|
<ActivityAmount
|
||||||
center
|
center
|
||||||
amount={props.info.amount_sats?.toString() ?? "0"}
|
amount={props.info.amount_sats?.toString() ?? "0"}
|
||||||
price={state.price}
|
price={state.price}
|
||||||
positive={props.info.inbound}
|
positive={props.info.inbound}
|
||||||
/>
|
/>
|
||||||
<For each={tags()}>
|
<For each={tags()}>
|
||||||
{(tag) => (
|
{(tag) => (
|
||||||
<TinyButton
|
<TinyButton
|
||||||
tag={tag}
|
tag={tag}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// noop
|
// noop
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</TinyButton>
|
</TinyButton>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OnchainHeader(props: { info: OnChainTx }) {
|
function OnchainHeader(props: { info: OnChainTx }) {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const tags = createMemo(() => {
|
const tags = createMemo(() => {
|
||||||
if (props.info.labels.length) {
|
if (props.info.labels.length) {
|
||||||
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
|
const contact = state.mutiny_wallet?.get_contact(
|
||||||
if (contact) {
|
props.info.labels[0]
|
||||||
return [tagToMutinyTag(contact)];
|
);
|
||||||
} else {
|
if (contact) {
|
||||||
return [];
|
return [tagToMutinyTag(contact)];
|
||||||
}
|
} else {
|
||||||
} else {
|
return [];
|
||||||
return [];
|
}
|
||||||
}
|
} else {
|
||||||
});
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const isSend = () => {
|
const isSend = () => {
|
||||||
return props.info.sent > props.info.received;
|
return props.info.sent > props.info.received;
|
||||||
};
|
};
|
||||||
|
|
||||||
const amount = () => {
|
const amount = () => {
|
||||||
if (isSend()) {
|
if (isSend()) {
|
||||||
return (props.info.sent - props.info.received).toString();
|
return (props.info.sent - props.info.received).toString();
|
||||||
} else {
|
} else {
|
||||||
return (props.info.received - props.info.sent).toString();
|
return (props.info.received - props.info.sent).toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<div class="p-4 bg-neutral-100 rounded-full">
|
<div class="p-4 bg-neutral-100 rounded-full">
|
||||||
<img src={chain} alt="blockchain" class="w-8 h-8" />
|
<img src={chain} alt="blockchain" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="uppercase font-semibold">{isSend() ? "On-chain send" : "On-chain receive"}</h1>
|
<h1 class="uppercase font-semibold">
|
||||||
<ActivityAmount center amount={amount() ?? "0"} price={state.price} positive={!isSend()} />
|
{isSend() ? "On-chain send" : "On-chain receive"}
|
||||||
<For each={tags()}>
|
</h1>
|
||||||
{(tag) => (
|
<ActivityAmount
|
||||||
<TinyButton
|
center
|
||||||
tag={tag}
|
amount={amount() ?? "0"}
|
||||||
onClick={() => {
|
price={state.price}
|
||||||
// noop
|
positive={!isSend()}
|
||||||
}}
|
/>
|
||||||
>
|
<For each={tags()}>
|
||||||
{tag.name}
|
{(tag) => (
|
||||||
</TinyButton>
|
<TinyButton
|
||||||
)}
|
tag={tag}
|
||||||
</For>
|
onClick={() => {
|
||||||
</div>
|
// noop
|
||||||
);
|
}}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</TinyButton>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<li class="flex justify-between items-center gap-4">
|
<li class="flex justify-between items-center gap-4">
|
||||||
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
|
<span class="uppercase font-semibold whitespace-nowrap">
|
||||||
<span class="font-light">{props.children}</span>
|
{props.key}
|
||||||
</li>
|
</span>
|
||||||
);
|
<span class="font-light">{props.children}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function MiniStringShower(props: { text: string }) {
|
function MiniStringShower(props: { text: string }) {
|
||||||
const [copy, _copied] = useCopy({ copiedTimeout: 1000 });
|
const [copy, _copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
||||||
<pre class="truncate text-neutral-300 font-light">{props.text}</pre>
|
<pre class="truncate text-neutral-300 font-light">{props.text}</pre>
|
||||||
<button class="w-[1rem]" onClick={() => copy(props.text)}>
|
<button class="w-[1rem]" onClick={() => copy(props.text)}>
|
||||||
<img src={copyIcon} alt="copy" class="w-4 h-4" />
|
<img src={copyIcon} alt="copy" class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LightningDetails(props: { info: MutinyInvoice }) {
|
function LightningDetails(props: { info: MutinyInvoice }) {
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<VStack>
|
||||||
<ul class="flex flex-col gap-4">
|
<ul class="flex flex-col gap-4">
|
||||||
<KeyValue key="Status">
|
<KeyValue key="Status">
|
||||||
<span class="text-neutral-300">{props.info.paid ? "Paid" : "Unpaid"}</span>
|
<span class="text-neutral-300">
|
||||||
</KeyValue>
|
{props.info.paid ? "Paid" : "Unpaid"}
|
||||||
<KeyValue key="When">
|
</span>
|
||||||
<span class="text-neutral-300">{prettyPrintTime(Number(props.info.last_updated))}</span>
|
</KeyValue>
|
||||||
</KeyValue>
|
<KeyValue key="When">
|
||||||
<Show when={props.info.description}>
|
<span class="text-neutral-300">
|
||||||
<KeyValue key="Description">
|
{prettyPrintTime(Number(props.info.last_updated))}
|
||||||
<span class="text-neutral-300 truncate">{props.info.description}</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
<Show when={props.info.description}>
|
||||||
<KeyValue key="Fees">
|
<KeyValue key="Description">
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300 truncate">
|
||||||
<AmountSmall amountSats={props.info.fees_paid} />
|
{props.info.description}
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="Bolt11">
|
</Show>
|
||||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
<KeyValue key="Fees">
|
||||||
</KeyValue>
|
<span class="text-neutral-300">
|
||||||
<KeyValue key="Payment Hash">
|
<AmountSmall amountSats={props.info.fees_paid} />
|
||||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="Preimage">
|
<KeyValue key="Bolt11">
|
||||||
<MiniStringShower text={props.info.preimage ?? ""} />
|
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</ul>
|
<KeyValue key="Payment Hash">
|
||||||
</VStack>
|
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||||
);
|
</KeyValue>
|
||||||
|
<KeyValue key="Preimage">
|
||||||
|
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||||
|
</KeyValue>
|
||||||
|
</ul>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OnchainDetails(props: { info: OnChainTx }) {
|
function OnchainDetails(props: { info: OnChainTx }) {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const confirmationTime = () => {
|
const confirmationTime = () => {
|
||||||
return props.info.confirmation_time?.Confirmed?.time;
|
return props.info.confirmation_time?.Confirmed?.time;
|
||||||
};
|
};
|
||||||
|
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<VStack>
|
||||||
{/* <pre>{JSON.stringify(props.info, null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(props.info, null, 2)}</pre> */}
|
||||||
<ul class="flex flex-col gap-4">
|
<ul class="flex flex-col gap-4">
|
||||||
<KeyValue key="Status">
|
<KeyValue key="Status">
|
||||||
<span class="text-neutral-300">{confirmationTime() ? "Confirmed" : "Unconfirmed"}</span>
|
<span class="text-neutral-300">
|
||||||
</KeyValue>
|
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
|
||||||
<Show when={confirmationTime()}>
|
</span>
|
||||||
<KeyValue key="When">
|
</KeyValue>
|
||||||
<span class="text-neutral-300">
|
<Show when={confirmationTime()}>
|
||||||
{confirmationTime() ? prettyPrintTime(Number(confirmationTime())) : "Pending"}
|
<KeyValue key="When">
|
||||||
</span>
|
<span class="text-neutral-300">
|
||||||
</KeyValue>
|
{confirmationTime()
|
||||||
</Show>
|
? prettyPrintTime(Number(confirmationTime()))
|
||||||
<Show when={props.info.fee && props.info.fee > 0}>
|
: "Pending"}
|
||||||
<KeyValue key="Fee">
|
</span>
|
||||||
<span class="text-neutral-300">
|
</KeyValue>
|
||||||
<AmountSmall amountSats={props.info.fee} />
|
</Show>
|
||||||
</span>
|
<Show when={props.info.fee && props.info.fee > 0}>
|
||||||
</KeyValue>
|
<KeyValue key="Fee">
|
||||||
</Show>
|
<span class="text-neutral-300">
|
||||||
<KeyValue key="Txid">
|
<AmountSmall amountSats={props.info.fee} />
|
||||||
<MiniStringShower text={props.info.txid ?? ""} />
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</ul>
|
</Show>
|
||||||
<a
|
<KeyValue key="Txid">
|
||||||
class="uppercase font-light text-center"
|
<MiniStringShower text={props.info.txid ?? ""} />
|
||||||
href={mempoolTxUrl(props.info.txid, network)}
|
</KeyValue>
|
||||||
target="_blank"
|
</ul>
|
||||||
rel="noreferrer"
|
<a
|
||||||
>
|
class="uppercase font-light text-center"
|
||||||
Mempool.space
|
href={mempoolTxUrl(props.info.txid, network)}
|
||||||
</a>
|
target="_blank"
|
||||||
</VStack>
|
rel="noreferrer"
|
||||||
);
|
>
|
||||||
|
Mempool.space
|
||||||
|
</a>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailsIdModal(props: {
|
export function DetailsIdModal(props: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
kind?: HackActivityType;
|
kind?: HackActivityType;
|
||||||
id: string;
|
id: string;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const id = () => props.id;
|
const id = () => props.id;
|
||||||
const kind = () => props.kind;
|
const kind = () => props.kind;
|
||||||
|
|
||||||
// TODO: is there a cleaner way to do refetch when id changes?
|
// TODO: is there a cleaner way to do refetch when id changes?
|
||||||
const [data, { refetch }] = createResource(async () => {
|
const [data, { refetch }] = createResource(async () => {
|
||||||
if (kind() === "Lightning") {
|
if (kind() === "Lightning") {
|
||||||
console.log("reading invoice: ", id());
|
console.log("reading invoice: ", id());
|
||||||
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(id());
|
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
|
||||||
return invoice;
|
id()
|
||||||
} else {
|
);
|
||||||
console.log("reading tx: ", id());
|
return invoice;
|
||||||
const tx = await state.mutiny_wallet?.get_transaction(id());
|
} else {
|
||||||
return tx;
|
console.log("reading tx: ", id());
|
||||||
}
|
const tx = await state.mutiny_wallet?.get_transaction(id());
|
||||||
});
|
return tx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.id && props.kind && props.open) {
|
if (props.id && props.kind && props.open) {
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = createMemo(() => JSON.stringify(data() || "", null, 2));
|
const json = createMemo(() => JSON.stringify(data() || "", null, 2));
|
||||||
|
|
||||||
const isInvoice = () => {
|
const isInvoice = () => {
|
||||||
return props.kind === "Lightning";
|
return props.kind === "Lightning";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay class={OVERLAY} />
|
<Dialog.Overlay class={OVERLAY} />
|
||||||
<div class={DIALOG_POSITIONER}>
|
<div class={DIALOG_POSITIONER}>
|
||||||
<Dialog.Content class={DIALOG_CONTENT}>
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<div class="flex justify-between mb-2">
|
<div class="flex justify-between mb-2">
|
||||||
<div />
|
<div />
|
||||||
<Dialog.CloseButton>
|
<Dialog.CloseButton>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
</Dialog.CloseButton>
|
</Dialog.CloseButton>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isInvoice()}>
|
<Match when={isInvoice()}>
|
||||||
<LightningHeader info={data() as MutinyInvoice} />
|
<LightningHeader
|
||||||
</Match>
|
info={data() as MutinyInvoice}
|
||||||
<Match when={true}>
|
/>
|
||||||
<OnchainHeader info={data() as OnChainTx} />
|
</Match>
|
||||||
</Match>
|
<Match when={true}>
|
||||||
</Switch>
|
<OnchainHeader
|
||||||
</Dialog.Title>
|
info={data() as OnChainTx}
|
||||||
<Hr />
|
/>
|
||||||
<Dialog.Description class="flex flex-col gap-4">
|
</Match>
|
||||||
<Switch>
|
</Switch>
|
||||||
<Match when={isInvoice()}>
|
</Dialog.Title>
|
||||||
<LightningDetails info={data() as MutinyInvoice} />
|
<Hr />
|
||||||
</Match>
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
<Match when={true}>
|
<Switch>
|
||||||
<OnchainDetails info={data() as OnChainTx} />
|
<Match when={isInvoice()}>
|
||||||
</Match>
|
<LightningDetails
|
||||||
</Switch>
|
info={data() as MutinyInvoice}
|
||||||
<div class="flex justify-center">
|
/>
|
||||||
<CopyButton title="Copy" text={json()} />
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<OnchainDetails
|
||||||
|
info={data() as OnChainTx}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<CopyButton title="Copy" text={json()} />
|
||||||
|
</div>
|
||||||
|
</Dialog.Description>
|
||||||
|
</Suspense>
|
||||||
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Description>
|
</Dialog.Portal>
|
||||||
</Suspense>
|
</Dialog.Root>
|
||||||
</Dialog.Content>
|
);
|
||||||
</div>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,18 @@ import { Dialog } from "@kobalte/core";
|
|||||||
import { ParentComponent } from "solid-js";
|
import { ParentComponent } from "solid-js";
|
||||||
import { Button, SmallHeader } from "./layout";
|
import { Button, SmallHeader } from "./layout";
|
||||||
|
|
||||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||||
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
const DIALOG_CONTENT =
|
||||||
|
"w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||||
|
|
||||||
// TODO: implement this like toast so it's just one global confirm and I can call it with `confirm({ title: "Are you sure?", description: "This will delete your node" })`
|
// TODO: implement this like toast so it's just one global confirm and I can call it with `confirm({ title: "Are you sure?", description: "This will delete your node" })`
|
||||||
export const ConfirmDialog: ParentComponent<{ open: boolean; loading: boolean; onCancel: () => void, onConfirm: () => void }> = (props) => {
|
export const ConfirmDialog: ParentComponent<{
|
||||||
|
open: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={props.open} onOpenChange={props.onCancel}>
|
<Dialog.Root open={props.open} onOpenChange={props.onCancel}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
@@ -15,18 +21,27 @@ export const ConfirmDialog: ParentComponent<{ open: boolean; loading: boolean; o
|
|||||||
<div class={DIALOG_POSITIONER}>
|
<div class={DIALOG_POSITIONER}>
|
||||||
<Dialog.Content class={DIALOG_CONTENT}>
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
<div class="flex justify-between mb-2">
|
<div class="flex justify-between mb-2">
|
||||||
<Dialog.Title><SmallHeader>Are you sure?</SmallHeader></Dialog.Title>
|
<Dialog.Title>
|
||||||
|
<SmallHeader>Are you sure?</SmallHeader>
|
||||||
|
</Dialog.Title>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description class="flex flex-col gap-4">
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
{props.children}
|
{props.children}
|
||||||
<div class="flex gap-4 w-full justify-end">
|
<div class="flex gap-4 w-full justify-end">
|
||||||
<Button onClick={props.onCancel}>Cancel</Button>
|
<Button onClick={props.onCancel}>Cancel</Button>
|
||||||
<Button intent="red" onClick={props.onConfirm} loading={props.loading} disabled={props.loading}>Confirm</Button>
|
<Button
|
||||||
|
intent="red"
|
||||||
|
onClick={props.onConfirm}
|
||||||
|
loading={props.loading}
|
||||||
|
disabled={props.loading}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root >
|
</Dialog.Root>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Title } from "solid-start";
|
import { Title } from "solid-start";
|
||||||
import { Button, DefaultMain, LargeHeader, SafeArea, SmallHeader } from "~/components/layout";
|
import {
|
||||||
|
Button,
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
SafeArea,
|
||||||
|
SmallHeader
|
||||||
|
} from "~/components/layout";
|
||||||
|
|
||||||
export default function ErrorDisplay(props: { error: Error }) {
|
export default function ErrorDisplay(props: { error: Error }) {
|
||||||
return (
|
return (
|
||||||
@@ -9,11 +15,16 @@ export default function ErrorDisplay(props: { error: Error }) {
|
|||||||
<LargeHeader>Error</LargeHeader>
|
<LargeHeader>Error</LargeHeader>
|
||||||
<SmallHeader>This never should've happened</SmallHeader>
|
<SmallHeader>This never should've happened</SmallHeader>
|
||||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||||
<span class="font-bold">
|
<span class="font-bold">{props.error.name}</span>:{" "}
|
||||||
{props.error.name}</span>: {props.error.message}
|
{props.error.message}
|
||||||
</p>
|
</p>
|
||||||
<div class="h-full" />
|
<div class="h-full" />
|
||||||
<Button onClick={() => window.location.href = "/"} intent="red">Dangit</Button>
|
<Button
|
||||||
|
onClick={() => (window.location.href = "/")}
|
||||||
|
intent="red"
|
||||||
|
>
|
||||||
|
Dangit
|
||||||
|
</Button>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import { createSignal } from "solid-js";
|
|||||||
import eify from "~/utils/eify";
|
import eify from "~/utils/eify";
|
||||||
import { showToast } from "./Toaster";
|
import { showToast } from "./Toaster";
|
||||||
import { downloadTextFile } from "~/utils/download";
|
import { downloadTextFile } from "~/utils/download";
|
||||||
import { createFileUploader } from "@solid-primitives/upload"
|
import { createFileUploader } from "@solid-primitives/upload";
|
||||||
import { ConfirmDialog } from "./Dialog";
|
import { ConfirmDialog } from "./Dialog";
|
||||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
|
|
||||||
export function ImportExport() {
|
export function ImportExport() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const json = await state.mutiny_wallet?.export_json()
|
const json = await state.mutiny_wallet?.export_json();
|
||||||
downloadTextFile(json || "", "mutiny-state.json")
|
downloadTextFile(json || "", "mutiny-state.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { files, selectFiles } = createFileUploader();
|
const { files, selectFiles } = createFileUploader();
|
||||||
@@ -26,7 +26,7 @@ export function ImportExport() {
|
|||||||
const file: File = files()[0].file;
|
const file: File = files()[0].file;
|
||||||
|
|
||||||
const text = await new Promise<string | null>((resolve, reject) => {
|
const text = await new Promise<string | null>((resolve, reject) => {
|
||||||
fileReader.onload = e => {
|
fileReader.onload = (e) => {
|
||||||
const result = e.target?.result?.toString();
|
const result = e.target?.result?.toString();
|
||||||
if (result) {
|
if (result) {
|
||||||
resolve(result);
|
resolve(result);
|
||||||
@@ -34,7 +34,8 @@ export function ImportExport() {
|
|||||||
reject(new Error("No text found in file"));
|
reject(new Error("No text found in file"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fileReader.onerror = _e => reject(new Error("File read error"));
|
fileReader.onerror = (_e) =>
|
||||||
|
reject(new Error("File read error"));
|
||||||
fileReader.readAsText(file, "UTF-8");
|
fileReader.readAsText(file, "UTF-8");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,7 +50,6 @@ export function ImportExport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(eify(e));
|
showToast(eify(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -59,12 +59,12 @@ export function ImportExport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFile() {
|
async function uploadFile() {
|
||||||
selectFiles(async files => {
|
selectFiles(async (files) => {
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
setConfirmOpen(true);
|
setConfirmOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||||
@@ -78,9 +78,14 @@ export function ImportExport() {
|
|||||||
<Button onClick={uploadFile}>Upload Saved State</Button>
|
<Button onClick={uploadFile}>Upload Saved State</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
<ConfirmDialog loading={confirmLoading()} open={confirmOpen()} onConfirm={importJson} onCancel={() => setConfirmOpen(false)}>
|
<ConfirmDialog
|
||||||
|
loading={confirmLoading()}
|
||||||
|
open={confirmOpen()}
|
||||||
|
onConfirm={importJson}
|
||||||
|
onCancel={() => setConfirmOpen(false)}
|
||||||
|
>
|
||||||
Do you want to replace your state with {files()[0].name}?
|
Do you want to replace your state with {files()[0].name}?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { ParentComponent } from "solid-js";
|
import { ParentComponent } from "solid-js";
|
||||||
import info from "~/assets/icons/info.svg"
|
import info from "~/assets/icons/info.svg";
|
||||||
|
|
||||||
export const InfoBox: ParentComponent<{ accent: "red" | "blue" | "green" | "white" }> = (props) => {
|
export const InfoBox: ParentComponent<{
|
||||||
return (
|
accent: "red" | "blue" | "green" | "white";
|
||||||
<div
|
}> = (props) => {
|
||||||
class="grid grid-cols-[auto_minmax(0,_1fr)] rounded-xl px-4 py-2 md:p-4 gap-4 bg-neutral-950/50 border"
|
return (
|
||||||
classList={{
|
<div
|
||||||
"border-m-red": props.accent === "red",
|
class="grid grid-cols-[auto_minmax(0,_1fr)] rounded-xl px-4 py-2 md:p-4 gap-4 bg-neutral-950/50 border"
|
||||||
"border-m-blue": props.accent === "blue",
|
classList={{
|
||||||
"border-m-green": props.accent === "green",
|
"border-m-red": props.accent === "red",
|
||||||
"border-white": props.accent === "white"
|
"border-m-blue": props.accent === "blue",
|
||||||
}}
|
"border-m-green": props.accent === "green",
|
||||||
>
|
"border-white": props.accent === "white"
|
||||||
<div class="self-center">
|
}}
|
||||||
<img src={info} alt="info" class="w-8 h-8" />
|
>
|
||||||
</div>
|
<div class="self-center">
|
||||||
<div class="flex items-center">
|
<img src={info} alt="info" class="w-8 h-8" />
|
||||||
<p class="text-base font-light">{props.children}</p>
|
</div>
|
||||||
</div>
|
<div class="flex items-center">
|
||||||
</div>
|
<p class="text-base font-light">{props.children}</p>
|
||||||
);
|
</div>
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import { Dialog } from "@kobalte/core";
|
import { Dialog } from "@kobalte/core";
|
||||||
import { JSX, createMemo } from "solid-js";
|
import { JSX, createMemo } from "solid-js";
|
||||||
import { ModalCloseButton, SmallHeader } from "~/components/layout";
|
import { ModalCloseButton, SmallHeader } from "~/components/layout";
|
||||||
import { DIALOG_CONTENT, DIALOG_POSITIONER, OVERLAY } from "~/components/DetailsModal";
|
import {
|
||||||
|
DIALOG_CONTENT,
|
||||||
|
DIALOG_POSITIONER,
|
||||||
|
OVERLAY
|
||||||
|
} from "~/components/DetailsModal";
|
||||||
import { CopyButton } from "./ShareCard";
|
import { CopyButton } from "./ShareCard";
|
||||||
|
|
||||||
export function JsonModal(props: { title: string, open: boolean, plaintext?: string, data?: unknown, setOpen: (open: boolean) => void, children?: JSX.Element }) {
|
export function JsonModal(props: {
|
||||||
const json = createMemo(() => props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2));
|
title: string;
|
||||||
|
open: boolean;
|
||||||
|
plaintext?: string;
|
||||||
|
data?: unknown;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
children?: JSX.Element;
|
||||||
|
}) {
|
||||||
|
const json = createMemo(() =>
|
||||||
|
props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||||
@@ -15,9 +28,7 @@ export function JsonModal(props: { title: string, open: boolean, plaintext?: str
|
|||||||
<Dialog.Content class={DIALOG_CONTENT}>
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
<div class="flex justify-between mb-2 items-center">
|
<div class="flex justify-between mb-2 items-center">
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
<SmallHeader>
|
<SmallHeader>{props.title}</SmallHeader>
|
||||||
{props.title}
|
|
||||||
</SmallHeader>
|
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.CloseButton>
|
<Dialog.CloseButton>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
@@ -35,6 +46,6 @@ export function JsonModal(props: { title: string, open: boolean, plaintext?: str
|
|||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root >
|
</Dialog.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { Card, Hr, SmallHeader, Button, InnerCard, VStack } from "~/components/layout";
|
import {
|
||||||
|
Card,
|
||||||
|
Hr,
|
||||||
|
SmallHeader,
|
||||||
|
Button,
|
||||||
|
InnerCard,
|
||||||
|
VStack
|
||||||
|
} from "~/components/layout";
|
||||||
import PeerConnectModal from "~/components/PeerConnectModal";
|
import PeerConnectModal from "~/components/PeerConnectModal";
|
||||||
import NostrWalletConnectModal from "~/components/NostrWalletConnectModal";
|
import NostrWalletConnectModal from "~/components/NostrWalletConnectModal";
|
||||||
import { For, Show, Suspense, createEffect, createResource, createSignal, onCleanup } from "solid-js";
|
import {
|
||||||
|
For,
|
||||||
|
Show,
|
||||||
|
Suspense,
|
||||||
|
createEffect,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
onCleanup
|
||||||
|
} from "solid-js";
|
||||||
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
||||||
import { Collapsible, TextField } from "@kobalte/core";
|
import { Collapsible, TextField } from "@kobalte/core";
|
||||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||||
@@ -15,190 +30,221 @@ import { ExternalLink } from "./layout/ExternalLink";
|
|||||||
|
|
||||||
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
||||||
type RefetchPeersType = (
|
type RefetchPeersType = (
|
||||||
info?: unknown
|
info?: unknown
|
||||||
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;
|
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;
|
||||||
|
|
||||||
function PeerItem(props: { peer: MutinyPeer }) {
|
function PeerItem(props: { peer: MutinyPeer }) {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const handleDisconnectPeer = async () => {
|
const handleDisconnectPeer = async () => {
|
||||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
const firstNode = (nodes[0] as string) || "";
|
const firstNode = (nodes[0] as string) || "";
|
||||||
|
|
||||||
if (props.peer.is_connected) {
|
if (props.peer.is_connected) {
|
||||||
await state.mutiny_wallet?.disconnect_peer(firstNode, props.peer.pubkey);
|
await state.mutiny_wallet?.disconnect_peer(
|
||||||
} else {
|
firstNode,
|
||||||
await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey);
|
props.peer.pubkey
|
||||||
}
|
);
|
||||||
};
|
} else {
|
||||||
|
await state.mutiny_wallet?.delete_peer(
|
||||||
|
firstNode,
|
||||||
|
props.peer.pubkey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root>
|
<Collapsible.Root>
|
||||||
<Collapsible.Trigger class="w-full">
|
<Collapsible.Trigger class="w-full">
|
||||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||||
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
{">"}{" "}
|
||||||
</h2>
|
{props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
||||||
</Collapsible.Trigger>
|
</h2>
|
||||||
<Collapsible.Content>
|
</Collapsible.Trigger>
|
||||||
<VStack>
|
<Collapsible.Content>
|
||||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
<VStack>
|
||||||
{JSON.stringify(props.peer, null, 2)}
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
</pre>
|
{JSON.stringify(props.peer, null, 2)}
|
||||||
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>
|
</pre>
|
||||||
Disconnect
|
<Button
|
||||||
</Button>
|
intent="glowy"
|
||||||
</VStack>
|
layout="xs"
|
||||||
</Collapsible.Content>
|
onClick={handleDisconnectPeer}
|
||||||
</Collapsible.Root>
|
>
|
||||||
);
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeersList() {
|
function PeersList() {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const getPeers = async () => {
|
const getPeers = async () => {
|
||||||
return (await state.mutiny_wallet?.list_peers()) as Promise<MutinyPeer[]>;
|
return (await state.mutiny_wallet?.list_peers()) as Promise<
|
||||||
};
|
MutinyPeer[]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
const [peers, { refetch }] = createResource(getPeers);
|
const [peers, { refetch }] = createResource(getPeers);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// refetch peers every 5 seconds
|
// refetch peers every 5 seconds
|
||||||
const interval = setTimeout(() => {
|
const interval = setTimeout(() => {
|
||||||
refetch();
|
refetch();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SmallHeader>Peers</SmallHeader>
|
<SmallHeader>Peers</SmallHeader>
|
||||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<VStack>
|
<VStack>
|
||||||
<For each={peers()} fallback={<code>No peers</code>}>
|
<For each={peers()} fallback={<code>No peers</code>}>
|
||||||
{(peer) => <PeerItem peer={peer} />}
|
{(peer) => <PeerItem peer={peer} />}
|
||||||
</For>
|
</For>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Button layout="small" onClick={refetch}>
|
<Button layout="small" onClick={refetch}>
|
||||||
Refresh Peers
|
Refresh Peers
|
||||||
</Button>
|
</Button>
|
||||||
<ConnectPeer refetchPeers={refetch} />
|
<ConnectPeer refetchPeers={refetch} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [value, setValue] = createSignal("");
|
const [value, setValue] = createSignal("");
|
||||||
|
|
||||||
const onSubmit = async (e: SubmitEvent) => {
|
const onSubmit = async (e: SubmitEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const peerConnectString = value().trim();
|
const peerConnectString = value().trim();
|
||||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
const firstNode = (nodes[0] as string) || "";
|
const firstNode = (nodes[0] as string) || "";
|
||||||
|
|
||||||
await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString);
|
await state.mutiny_wallet?.connect_to_peer(
|
||||||
|
firstNode,
|
||||||
|
peerConnectString
|
||||||
|
);
|
||||||
|
|
||||||
await props.refetchPeers();
|
await props.refetchPeers();
|
||||||
|
|
||||||
setValue("");
|
setValue("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InnerCard>
|
<InnerCard>
|
||||||
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
value={value()}
|
value={value()}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
validationState={value() == "" ? "valid" : "invalid"}
|
validationState={value() == "" ? "valid" : "invalid"}
|
||||||
class="flex flex-col gap-4"
|
class="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">Connect Peer</TextField.Label>
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
<TextField.Input
|
Connect Peer
|
||||||
class="w-full p-2 rounded-lg text-black"
|
</TextField.Label>
|
||||||
placeholder="mutiny:028241..."
|
<TextField.Input
|
||||||
/>
|
class="w-full p-2 rounded-lg text-black"
|
||||||
<TextField.ErrorMessage class="text-red-500">
|
placeholder="mutiny:028241..."
|
||||||
Expecting something like mutiny:abc123...
|
/>
|
||||||
</TextField.ErrorMessage>
|
<TextField.ErrorMessage class="text-red-500">
|
||||||
</TextField.Root>
|
Expecting something like mutiny:abc123...
|
||||||
<Button layout="small" type="submit">
|
</TextField.ErrorMessage>
|
||||||
Connect
|
</TextField.Root>
|
||||||
</Button>
|
<Button layout="small" type="submit">
|
||||||
</form>
|
Connect
|
||||||
</InnerCard>
|
</Button>
|
||||||
);
|
</form>
|
||||||
|
</InnerCard>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefetchChannelsListType = (
|
type RefetchChannelsListType = (
|
||||||
info?: unknown
|
info?: unknown
|
||||||
) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined;
|
) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined;
|
||||||
|
|
||||||
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||||
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
||||||
|
|
||||||
function handleCloseChannel() {
|
function handleCloseChannel() {
|
||||||
setConfirmOpen(true);
|
setConfirmOpen(true);
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmCloseChannel() {
|
|
||||||
setConfirmLoading(true);
|
|
||||||
try {
|
|
||||||
await state.mutiny_wallet?.close_channel(props.channel.outpoint as string);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showToast(eify(e));
|
|
||||||
}
|
}
|
||||||
setConfirmLoading(false);
|
|
||||||
setConfirmOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
async function confirmCloseChannel() {
|
||||||
<Collapsible.Root>
|
setConfirmLoading(true);
|
||||||
<Collapsible.Trigger class="w-full">
|
try {
|
||||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
await state.mutiny_wallet?.close_channel(
|
||||||
{">"} {props.channel.peer}
|
props.channel.outpoint as string
|
||||||
</h2>
|
);
|
||||||
</Collapsible.Trigger>
|
} catch (e) {
|
||||||
<Collapsible.Content>
|
console.error(e);
|
||||||
<VStack>
|
showToast(eify(e));
|
||||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
}
|
||||||
{JSON.stringify(props.channel, null, 2)}
|
setConfirmLoading(false);
|
||||||
</pre>
|
setConfirmOpen(false);
|
||||||
<ExternalLink href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)}>
|
}
|
||||||
View Transaction
|
|
||||||
</ExternalLink>
|
return (
|
||||||
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>
|
<Collapsible.Root>
|
||||||
Close Channel
|
<Collapsible.Trigger class="w-full">
|
||||||
</Button>
|
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||||
</VStack>
|
{">"} {props.channel.peer}
|
||||||
<ConfirmDialog
|
</h2>
|
||||||
open={confirmOpen()}
|
</Collapsible.Trigger>
|
||||||
onConfirm={confirmCloseChannel}
|
<Collapsible.Content>
|
||||||
onCancel={() => setConfirmOpen(false)}
|
<VStack>
|
||||||
loading={confirmLoading()}
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
>
|
{JSON.stringify(props.channel, null, 2)}
|
||||||
<p>Are you sure you want to close this channel?</p>
|
</pre>
|
||||||
</ConfirmDialog>
|
<ExternalLink
|
||||||
</Collapsible.Content>
|
href={mempoolTxUrl(
|
||||||
</Collapsible.Root>
|
props.channel.outpoint?.split(":")[0],
|
||||||
);
|
props.network
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
View Transaction
|
||||||
|
</ExternalLink>
|
||||||
|
<Button
|
||||||
|
intent="glowy"
|
||||||
|
layout="xs"
|
||||||
|
onClick={handleCloseChannel}
|
||||||
|
>
|
||||||
|
Close Channel
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmOpen()}
|
||||||
|
onConfirm={confirmCloseChannel}
|
||||||
|
onCancel={() => setConfirmOpen(false)}
|
||||||
|
loading={confirmLoading()}
|
||||||
|
>
|
||||||
|
<p>Are you sure you want to close this channel?</p>
|
||||||
|
</ConfirmDialog>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChannelsList() {
|
function ChannelsList() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const getChannels = async () => {
|
const getChannels = async () => {
|
||||||
return await state.mutiny_wallet?.list_channels() as Promise<MutinyChannel[]>
|
return (await state.mutiny_wallet?.list_channels()) as Promise<
|
||||||
|
MutinyChannel[]
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [channels, { refetch }] = createResource(getChannels);
|
const [channels, { refetch }] = createResource(getChannels);
|
||||||
@@ -211,32 +257,38 @@ function ChannelsList() {
|
|||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SmallHeader>
|
<SmallHeader>Channels</SmallHeader>
|
||||||
Channels
|
|
||||||
</SmallHeader>
|
|
||||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<For each={channels()} fallback={<code>No channels</code>}>
|
<For each={channels()} fallback={<code>No channels</code>}>
|
||||||
{(channel) => (
|
{(channel) => (
|
||||||
<ChannelItem channel={channel} network={network} />
|
<ChannelItem channel={channel} network={network} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</For>
|
</For>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Button type="button" layout="small" onClick={(e) => { e.preventDefault(); refetch() }}>Refresh Channels</Button>
|
<Button
|
||||||
|
type="button"
|
||||||
|
layout="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh Channels
|
||||||
|
</Button>
|
||||||
<OpenChannel refetchChannels={refetch} />
|
<OpenChannel refetchChannels={refetch} />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [creationError, setCreationError] = createSignal<Error>();
|
const [creationError, setCreationError] = createSignal<Error>();
|
||||||
|
|
||||||
@@ -256,63 +308,82 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
const bigAmount = BigInt(amount());
|
const bigAmount = BigInt(amount());
|
||||||
|
|
||||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
const firstNode = nodes[0] as string || ""
|
const firstNode = (nodes[0] as string) || "";
|
||||||
|
|
||||||
const new_channel = await state.mutiny_wallet?.open_channel(firstNode, pubkey, bigAmount)
|
const new_channel = await state.mutiny_wallet?.open_channel(
|
||||||
|
firstNode,
|
||||||
|
pubkey,
|
||||||
|
bigAmount
|
||||||
|
);
|
||||||
|
|
||||||
setNewChannel(new_channel)
|
setNewChannel(new_channel);
|
||||||
|
|
||||||
await props.refetchChannels()
|
await props.refetchChannels();
|
||||||
|
|
||||||
setAmount("");
|
setAmount("");
|
||||||
setPeerPubkey("");
|
setPeerPubkey("");
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setCreationError(eify(e))
|
setCreationError(eify(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InnerCard>
|
<InnerCard>
|
||||||
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
value={peerPubkey()}
|
value={peerPubkey()}
|
||||||
onChange={setPeerPubkey}
|
onChange={setPeerPubkey}
|
||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">Pubkey</TextField.Label>
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
Pubkey
|
||||||
</TextField.Root>
|
</TextField.Label>
|
||||||
<TextField.Root value={amount()} onChange={setAmount} class="flex flex-col gap-2">
|
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">Amount</TextField.Label>
|
</TextField.Root>
|
||||||
<TextField.Input type="number" class="w-full p-2 rounded-lg text-black" />
|
<TextField.Root
|
||||||
</TextField.Root>
|
value={amount()}
|
||||||
<Button layout="small" type="submit">
|
onChange={setAmount}
|
||||||
Open Channel
|
class="flex flex-col gap-2"
|
||||||
</Button>
|
>
|
||||||
</form>
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
</InnerCard>
|
Amount
|
||||||
<Show when={newChannel()}>
|
</TextField.Label>
|
||||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
<TextField.Input
|
||||||
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
type="number"
|
||||||
</pre>
|
class="w-full p-2 rounded-lg text-black"
|
||||||
<pre>{newChannel()?.outpoint}</pre>
|
/>
|
||||||
<ExternalLink href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], network)}>
|
</TextField.Root>
|
||||||
View Transaction
|
<Button layout="small" type="submit">
|
||||||
</ExternalLink>
|
Open Channel
|
||||||
</Show>
|
</Button>
|
||||||
<Show when={creationError()}>
|
</form>
|
||||||
<pre>{creationError()?.message}</pre>
|
</InnerCard>
|
||||||
</Show>
|
<Show when={newChannel()}>
|
||||||
</>
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
|
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
||||||
|
</pre>
|
||||||
|
<pre>{newChannel()?.outpoint}</pre>
|
||||||
|
<ExternalLink
|
||||||
|
href={mempoolTxUrl(
|
||||||
|
newChannel()?.outpoint?.split(":")[0],
|
||||||
|
network
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
View Transaction
|
||||||
|
</ExternalLink>
|
||||||
|
</Show>
|
||||||
|
<Show when={creationError()}>
|
||||||
|
<pre>{creationError()?.message}</pre>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LnUrlAuth() {
|
function LnUrlAuth() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [value, setValue] = createSignal("");
|
const [value, setValue] = createSignal("");
|
||||||
|
|
||||||
@@ -320,37 +391,50 @@ function LnUrlAuth() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const lnurl = value().trim();
|
const lnurl = value().trim();
|
||||||
await state.mutiny_wallet?.lnurl_auth(0, lnurl)
|
await state.mutiny_wallet?.lnurl_auth(0, lnurl);
|
||||||
|
|
||||||
setValue("");
|
setValue("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InnerCard>
|
<InnerCard>
|
||||||
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
|
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
value={value()}
|
value={value()}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
validationState={(value() == "" || value().toLowerCase().startsWith("lnurl")) ? "valid" : "invalid"}
|
validationState={
|
||||||
|
value() == "" ||
|
||||||
|
value().toLowerCase().startsWith("lnurl")
|
||||||
|
? "valid"
|
||||||
|
: "invalid"
|
||||||
|
}
|
||||||
class="flex flex-col gap-4"
|
class="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<TextField.Label class="text-sm font-semibold uppercase" >LNURL Auth</TextField.Label>
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
<TextField.Input class="w-full p-2 rounded-lg text-black" placeholder="LNURL..." />
|
LNURL Auth
|
||||||
<TextField.ErrorMessage class="text-red-500">Expecting something like LNURL...</TextField.ErrorMessage>
|
</TextField.Label>
|
||||||
|
<TextField.Input
|
||||||
|
class="w-full p-2 rounded-lg text-black"
|
||||||
|
placeholder="LNURL..."
|
||||||
|
/>
|
||||||
|
<TextField.ErrorMessage class="text-red-500">
|
||||||
|
Expecting something like LNURL...
|
||||||
|
</TextField.ErrorMessage>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
<Button layout="small" type="submit">Auth</Button>
|
<Button layout="small" type="submit">
|
||||||
</form >
|
Auth
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListTags() {
|
function ListTags() {
|
||||||
const [_state, actions] = useMegaStore()
|
const [_state, actions] = useMegaStore();
|
||||||
|
|
||||||
const [tags] = createResource(actions.listTags)
|
const [tags] = createResource(actions.listTags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Collapsible.Root>
|
<Collapsible.Root>
|
||||||
<Collapsible.Trigger class="w-full">
|
<Collapsible.Trigger class="w-full">
|
||||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||||
@@ -365,13 +449,9 @@ function ListTags() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function KitchenSink() {
|
export default function KitchenSink() {
|
||||||
return (
|
return (
|
||||||
<Card title="Kitchen Sink">
|
<Card title="Kitchen Sink">
|
||||||
@@ -390,5 +470,5 @@ export default function KitchenSink() {
|
|||||||
<Hr />
|
<Hr />
|
||||||
<ImportExport />
|
<ImportExport />
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,11 @@ import { useMegaStore } from "~/state/megaStore";
|
|||||||
import { downloadTextFile } from "~/utils/download";
|
import { downloadTextFile } from "~/utils/download";
|
||||||
|
|
||||||
export function Logs() {
|
export function Logs() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const logs = await state.mutiny_wallet?.get_logs()
|
const logs = await state.mutiny_wallet?.get_logs();
|
||||||
downloadTextFile(logs.join("") || "", "mutiny-logs.txt", "text/plain")
|
downloadTextFile(logs.join("") || "", "mutiny-logs.txt", "text/plain");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,6 +17,5 @@ export function Logs() {
|
|||||||
<Button onClick={handleSave}>Download Logs</Button>
|
<Button onClick={handleSave}>Download Logs</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,51 +1,91 @@
|
|||||||
import mutiny_m from '~/assets/icons/m.svg';
|
import mutiny_m from "~/assets/icons/m.svg";
|
||||||
import airplane from '~/assets/icons/airplane.svg';
|
import airplane from "~/assets/icons/airplane.svg";
|
||||||
import settings from '~/assets/icons/settings.svg';
|
import settings from "~/assets/icons/settings.svg";
|
||||||
import receive from '~/assets/icons/big-receive.svg';
|
import receive from "~/assets/icons/big-receive.svg";
|
||||||
import redshift from '~/assets/icons/rs.svg';
|
import redshift from "~/assets/icons/rs.svg";
|
||||||
import userClock from '~/assets/icons/user-clock.svg';
|
import userClock from "~/assets/icons/user-clock.svg";
|
||||||
|
|
||||||
import { A } from "solid-start";
|
import { A } from "solid-start";
|
||||||
|
|
||||||
type ActiveTab = 'home' | 'scan' | 'send' | 'receive' | 'settings' | 'redshift' | 'activity' | 'none';
|
type ActiveTab =
|
||||||
|
| "home"
|
||||||
|
| "scan"
|
||||||
|
| "send"
|
||||||
|
| "receive"
|
||||||
|
| "settings"
|
||||||
|
| "redshift"
|
||||||
|
| "activity"
|
||||||
|
| "none";
|
||||||
|
|
||||||
export default function NavBar(props: { activeTab: ActiveTab }) {
|
export default function NavBar(props: { activeTab: ActiveTab }) {
|
||||||
const activeStyle = 'border-t-0 border-b-0 p-2 bg-black rounded-lg'
|
const activeStyle = "border-t-0 border-b-0 p-2 bg-black rounded-lg";
|
||||||
const inactiveStyle = "p-2 hover:bg-white/5 rounded-lg active:bg-m-blue"
|
const inactiveStyle = "p-2 hover:bg-white/5 rounded-lg active:bg-m-blue";
|
||||||
return (
|
return (
|
||||||
<nav class='hidden md:block fixed shadow-none z-40 safe-bottom top-0 bottom-auto left-0 h-full'>
|
<nav class="hidden md:block fixed shadow-none z-40 safe-bottom top-0 bottom-auto left-0 h-full">
|
||||||
<ul class='h-16 flex flex-col justify-start gap-4 px-4 mt-4'>
|
<ul class="h-16 flex flex-col justify-start gap-4 px-4 mt-4">
|
||||||
<li class={props.activeTab === "home" ? activeStyle : inactiveStyle}>
|
<li
|
||||||
|
class={
|
||||||
|
props.activeTab === "home" ? activeStyle : inactiveStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
<A href="/">
|
<A href="/">
|
||||||
<img src={mutiny_m} alt="home" />
|
<img src={mutiny_m} alt="home" />
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
<li class={props.activeTab === "send" ? activeStyle : inactiveStyle}>
|
<li
|
||||||
|
class={
|
||||||
|
props.activeTab === "send" ? activeStyle : inactiveStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
<A href="/send">
|
<A href="/send">
|
||||||
<img src={airplane} alt="send" />
|
<img src={airplane} alt="send" />
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
<li class={props.activeTab === "receive" ? activeStyle : inactiveStyle}>
|
<li
|
||||||
|
class={
|
||||||
|
props.activeTab === "receive"
|
||||||
|
? activeStyle
|
||||||
|
: inactiveStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
<A href="/receive">
|
<A href="/receive">
|
||||||
<img src={receive} alt="receive" />
|
<img src={receive} alt="receive" />
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
<li class={props.activeTab === "activity" ? activeStyle : inactiveStyle}>
|
<li
|
||||||
|
class={
|
||||||
|
props.activeTab === "activity"
|
||||||
|
? activeStyle
|
||||||
|
: inactiveStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
<A href="/activity">
|
<A href="/activity">
|
||||||
<img src={userClock} alt="activity" />
|
<img src={userClock} alt="activity" />
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
<li class={props.activeTab === "redshift" ? activeStyle : inactiveStyle}>
|
<li
|
||||||
|
class={
|
||||||
|
props.activeTab === "redshift"
|
||||||
|
? activeStyle
|
||||||
|
: inactiveStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
<A href="/redshift">
|
<A href="/redshift">
|
||||||
<img src={redshift} alt="redshift" width={36} />
|
<img src={redshift} alt="redshift" width={36} />
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
<li class={props.activeTab === "settings" ? activeStyle : inactiveStyle}>
|
<li
|
||||||
|
class={
|
||||||
|
props.activeTab === "settings"
|
||||||
|
? activeStyle
|
||||||
|
: inactiveStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
<A href="/settings">
|
<A href="/settings">
|
||||||
<img src={settings} alt="settings" />
|
<img src={settings} alt="settings" />
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav >
|
</nav>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import {QRCodeSVG} from "solid-qr-code";
|
import { QRCodeSVG } from "solid-qr-code";
|
||||||
import {As, Dialog} from "@kobalte/core";
|
import { As, Dialog } from "@kobalte/core";
|
||||||
import {Button, Card} from "~/components/layout";
|
import { Button, Card } from "~/components/layout";
|
||||||
import {useMegaStore} from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import {createResource, Show} from "solid-js";
|
import { createResource, Show } from "solid-js";
|
||||||
|
|
||||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||||
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] max-h-[100dvh] overflow-y-auto disable-scrollbars p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
const DIALOG_CONTENT =
|
||||||
const SMALL_HEADER = "text-sm font-semibold uppercase"
|
"w-[80vw] max-w-[400px] max-h-[100dvh] overflow-y-auto disable-scrollbars p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||||
|
const SMALL_HEADER = "text-sm font-semibold uppercase";
|
||||||
|
|
||||||
export default function NostrWalletConnectModal() {
|
export default function NostrWalletConnectModal() {
|
||||||
const [state, actions] = useMegaStore()
|
const [state, actions] = useMegaStore();
|
||||||
|
|
||||||
const getConnectionURI = () => {
|
const getConnectionURI = () => {
|
||||||
if (state.mutiny_wallet) {
|
if (state.mutiny_wallet) {
|
||||||
return state.mutiny_wallet.get_nwc_uri()
|
return state.mutiny_wallet.get_nwc_uri();
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,15 +25,15 @@ export default function NostrWalletConnectModal() {
|
|||||||
|
|
||||||
const toggleNwc = async () => {
|
const toggleNwc = async () => {
|
||||||
if (state.nwc_enabled) {
|
if (state.nwc_enabled) {
|
||||||
actions.setNwc(false)
|
actions.setNwc(false);
|
||||||
window.location.reload()
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
actions.setNwc(true)
|
actions.setNwc(true);
|
||||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
const firstNode = nodes[0] as string || "";
|
const firstNode = (nodes[0] as string) || "";
|
||||||
await state.mutiny_wallet?.start_nostr_wallet_connect(firstNode);
|
await state.mutiny_wallet?.start_nostr_wallet_connect(firstNode);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
||||||
return (
|
return (
|
||||||
@@ -45,7 +46,9 @@ export default function NostrWalletConnectModal() {
|
|||||||
<div class={DIALOG_POSITIONER}>
|
<div class={DIALOG_POSITIONER}>
|
||||||
<Dialog.Content class={DIALOG_CONTENT}>
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
<div class="flex justify-between mb-2">
|
<div class="flex justify-between mb-2">
|
||||||
<Dialog.Title class={SMALL_HEADER}>Nostr Wallet Connect</Dialog.Title>
|
<Dialog.Title class={SMALL_HEADER}>
|
||||||
|
Nostr Wallet Connect
|
||||||
|
</Dialog.Title>
|
||||||
<Dialog.CloseButton class="dialog__close-button">
|
<Dialog.CloseButton class="dialog__close-button">
|
||||||
<code>X</code>
|
<code>X</code>
|
||||||
</Dialog.CloseButton>
|
</Dialog.CloseButton>
|
||||||
@@ -53,17 +56,24 @@ export default function NostrWalletConnectModal() {
|
|||||||
<Dialog.Description class="flex flex-col gap-4">
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
<Show when={connectionURI()}>
|
<Show when={connectionURI()}>
|
||||||
<div class="w-full bg-white rounded-xl">
|
<div class="w-full bg-white rounded-xl">
|
||||||
<QRCodeSVG value={connectionURI() || ""} class="w-full h-full p-8 max-h-[400px]" />
|
<QRCodeSVG
|
||||||
|
value={connectionURI() || ""}
|
||||||
|
class="w-full h-full p-8 max-h-[400px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
<code class="break-all">{connectionURI() || ""}</code>
|
<code class="break-all">
|
||||||
|
{connectionURI() || ""}
|
||||||
|
</code>
|
||||||
</Card>
|
</Card>
|
||||||
</Show>
|
</Show>
|
||||||
<Button onClick={toggleNwc}>{state.nwc_enabled ? "Disable" : "Enable"}</Button>
|
<Button onClick={toggleNwc}>
|
||||||
|
{state.nwc_enabled ? "Disable" : "Enable"}
|
||||||
|
</Button>
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root >
|
</Dialog.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -7,80 +7,97 @@ import close from "~/assets/icons/close.svg";
|
|||||||
import restore from "~/assets/icons/upload.svg";
|
import restore from "~/assets/icons/upload.svg";
|
||||||
|
|
||||||
export function OnboardWarning() {
|
export function OnboardWarning() {
|
||||||
const [state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
const [dismissedBackup, setDismissedBackup] = createSignal(false);
|
const [dismissedBackup, setDismissedBackup] = createSignal(false);
|
||||||
|
|
||||||
function hasMoney() {
|
function hasMoney() {
|
||||||
return state.balance?.confirmed || state.balance?.lightning || state.balance?.unconfirmed || state.balance?.force_close;
|
return (
|
||||||
}
|
state.balance?.confirmed ||
|
||||||
|
state.balance?.lightning ||
|
||||||
|
state.balance?.unconfirmed ||
|
||||||
|
state.balance?.force_close
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* TODO: show this once we have a restore flow */}
|
{/* TODO: show this once we have a restore flow */}
|
||||||
<Show when={false}>
|
<Show when={false}>
|
||||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
|
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
|
||||||
<div class="self-center">
|
<div class="self-center">
|
||||||
<img src={restore} alt="backup" class="w-8 h-8" />
|
<img src={restore} alt="backup" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex md:flex-row flex-col items-center gap-4">
|
<div class="flex md:flex-row flex-col items-center gap-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<SmallHeader>Welcome!</SmallHeader>
|
<SmallHeader>Welcome!</SmallHeader>
|
||||||
<p class="text-base font-light">
|
<p class="text-base font-light">
|
||||||
If you've used Mutiny before you can restore from a backup. Otherwise you can skip
|
If you've used Mutiny before you can restore
|
||||||
this and enjoy your new wallet!
|
from a backup. Otherwise you can skip this and
|
||||||
</p>
|
enjoy your new wallet!
|
||||||
</div>
|
</p>
|
||||||
<Button
|
</div>
|
||||||
intent="green"
|
<Button
|
||||||
layout="xs"
|
intent="green"
|
||||||
class="self-start md:self-auto"
|
layout="xs"
|
||||||
onClick={() => {
|
class="self-start md:self-auto"
|
||||||
showToast({ title: "Unimplemented", description: "We don't do that yet" });
|
onClick={() => {
|
||||||
}}
|
showToast({
|
||||||
|
title: "Unimplemented",
|
||||||
|
description: "We don't do that yet"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
tabindex="-1"
|
||||||
|
onClick={() => {
|
||||||
|
actions.dismissRestorePrompt();
|
||||||
|
}}
|
||||||
|
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8"
|
||||||
|
>
|
||||||
|
<img src={close} alt="Close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show
|
||||||
|
when={!state.has_backed_up && hasMoney() && !dismissedBackup()}
|
||||||
>
|
>
|
||||||
Restore
|
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
|
||||||
</Button>
|
<div class="self-center">
|
||||||
</div>
|
<img src={save} alt="backup" class="w-8 h-8" />
|
||||||
<button
|
</div>
|
||||||
tabindex="-1"
|
<div class="flex flex-row max-md:items-center justify-between gap-4">
|
||||||
onClick={() => {
|
<div class="flex flex-col">
|
||||||
actions.dismissRestorePrompt();
|
<SmallHeader>Secure your funds</SmallHeader>
|
||||||
}}
|
<p class="text-base font-light max-md:hidden">
|
||||||
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8"
|
You have money stored in this browser. Let's
|
||||||
>
|
make sure you have a backup.
|
||||||
<img src={close} alt="Close" />
|
</p>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="flex items-center">
|
||||||
</Show>
|
<ButtonLink
|
||||||
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
|
intent="blue"
|
||||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
|
layout="xs"
|
||||||
<div class="self-center">
|
class="self-auto"
|
||||||
<img src={save} alt="backup" class="w-8 h-8" />
|
href="/backup"
|
||||||
</div>
|
>
|
||||||
<div class="flex flex-row max-md:items-center justify-between gap-4">
|
Backup
|
||||||
<div class="flex flex-col">
|
</ButtonLink>
|
||||||
<SmallHeader>Secure your funds</SmallHeader>
|
</div>
|
||||||
<p class="text-base font-light max-md:hidden">
|
</div>
|
||||||
You have money stored in this browser. Let's make sure you have a backup.
|
<button
|
||||||
</p>
|
tabindex="-1"
|
||||||
</div>
|
onClick={() => {
|
||||||
<div class="flex items-center">
|
setDismissedBackup(true);
|
||||||
<ButtonLink intent="blue" layout="xs" class="self-auto" href="/backup">
|
}}
|
||||||
Backup
|
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8"
|
||||||
</ButtonLink>
|
>
|
||||||
</div>
|
<img src={close} alt="Close" />
|
||||||
</div>
|
</button>
|
||||||
<button
|
</div>
|
||||||
tabindex="-1"
|
</Show>
|
||||||
onClick={() => {
|
</>
|
||||||
setDismissedBackup(true);
|
);
|
||||||
}}
|
|
||||||
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8"
|
|
||||||
>
|
|
||||||
<img src={close} alt="Close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,30 +6,30 @@ import { Show, createResource } from "solid-js";
|
|||||||
import { getExistingSettings } from "~/logic/mutinyWalletSetup";
|
import { getExistingSettings } from "~/logic/mutinyWalletSetup";
|
||||||
import getHostname from "~/utils/getHostname";
|
import getHostname from "~/utils/getHostname";
|
||||||
|
|
||||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||||
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
const DIALOG_CONTENT =
|
||||||
const SMALL_HEADER = "text-sm font-semibold uppercase"
|
"w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||||
|
const SMALL_HEADER = "text-sm font-semibold uppercase";
|
||||||
|
|
||||||
export default function PeerConnectModal() {
|
export default function PeerConnectModal() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const getPeerConnectString = async () => {
|
const getPeerConnectString = async () => {
|
||||||
if (state.mutiny_wallet) {
|
if (state.mutiny_wallet) {
|
||||||
const { proxy } = getExistingSettings();
|
const { proxy } = getExistingSettings();
|
||||||
const nodes = await state.mutiny_wallet.list_nodes();
|
const nodes = await state.mutiny_wallet.list_nodes();
|
||||||
const firstNode = nodes[0] as string || ""
|
const firstNode = (nodes[0] as string) || "";
|
||||||
const hostName = getHostname(proxy || "")
|
const hostName = getHostname(proxy || "");
|
||||||
const connectString = `mutiny:${firstNode}@${hostName}`
|
const connectString = `mutiny:${firstNode}@${hostName}`;
|
||||||
return connectString
|
return connectString;
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [peerConnectString] = createResource(getPeerConnectString);
|
const [peerConnectString] = createResource(getPeerConnectString);
|
||||||
|
|
||||||
|
|
||||||
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
||||||
return (
|
return (
|
||||||
<Dialog.Root>
|
<Dialog.Root>
|
||||||
@@ -41,7 +41,9 @@ export default function PeerConnectModal() {
|
|||||||
<div class={DIALOG_POSITIONER}>
|
<div class={DIALOG_POSITIONER}>
|
||||||
<Dialog.Content class={DIALOG_CONTENT}>
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
<div class="flex justify-between mb-2">
|
<div class="flex justify-between mb-2">
|
||||||
<Dialog.Title class={SMALL_HEADER}>Peer connect info</Dialog.Title>
|
<Dialog.Title class={SMALL_HEADER}>
|
||||||
|
Peer connect info
|
||||||
|
</Dialog.Title>
|
||||||
<Dialog.CloseButton class="dialog__close-button">
|
<Dialog.CloseButton class="dialog__close-button">
|
||||||
<code>X</code>
|
<code>X</code>
|
||||||
</Dialog.CloseButton>
|
</Dialog.CloseButton>
|
||||||
@@ -49,16 +51,21 @@ export default function PeerConnectModal() {
|
|||||||
<Dialog.Description class="flex flex-col gap-4">
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
<Show when={peerConnectString()}>
|
<Show when={peerConnectString()}>
|
||||||
<div class="w-full bg-white rounded-xl">
|
<div class="w-full bg-white rounded-xl">
|
||||||
<QRCodeSVG value={peerConnectString() || ""} class="w-full h-full p-8 max-h-[400px]" />
|
<QRCodeSVG
|
||||||
|
value={peerConnectString() || ""}
|
||||||
|
class="w-full h-full p-8 max-h-[400px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
<code class="break-all">{peerConnectString() || ""}</code>
|
<code class="break-all">
|
||||||
|
{peerConnectString() || ""}
|
||||||
|
</code>
|
||||||
</Card>
|
</Card>
|
||||||
</Show>
|
</Show>
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root >
|
</Dialog.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import QrScanner from 'qr-scanner';
|
import QrScanner from "qr-scanner";
|
||||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
import { createSignal, onCleanup, onMount } from "solid-js";
|
||||||
|
|
||||||
export default function Scanner(props: { onResult: (result: string) => void }) {
|
export default function Scanner(props: { onResult: (result: string) => void }) {
|
||||||
let container: HTMLVideoElement | null;
|
let container: HTMLVideoElement | null;
|
||||||
@@ -9,19 +9,15 @@ export default function Scanner(props: { onResult: (result: string) => void }) {
|
|||||||
|
|
||||||
const handleResult = (result: { data: string }) => {
|
const handleResult = (result: { data: string }) => {
|
||||||
props.onResult(result.data);
|
props.onResult(result.data);
|
||||||
}
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (container) {
|
if (container) {
|
||||||
const newScanner = new QrScanner(
|
const newScanner = new QrScanner(container, handleResult, {
|
||||||
container,
|
returnDetailedScanResult: true
|
||||||
handleResult,
|
});
|
||||||
{
|
|
||||||
returnDetailedScanResult: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
newScanner.start();
|
newScanner.start();
|
||||||
setScanner(newScanner)
|
setScanner(newScanner);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,7 +30,10 @@ export default function Scanner(props: { onResult: (result: string) => void }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="video-container">
|
<div id="video-container">
|
||||||
<video ref={el => container = el} class="w-full h-full fixed object-cover bg-gray" />
|
<video
|
||||||
|
ref={(el) => (container = el)}
|
||||||
|
class="w-full h-full fixed object-cover bg-gray"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import type { Component } from 'solid-js'
|
import type { Component } from "solid-js";
|
||||||
import { Show } from 'solid-js'
|
import { Show } from "solid-js";
|
||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { useRegisterSW } from 'virtual:pwa-register/solid'
|
import { useRegisterSW } from "virtual:pwa-register/solid";
|
||||||
|
|
||||||
const ReloadPrompt: Component = () => {
|
const ReloadPrompt: Component = () => {
|
||||||
const {
|
const {
|
||||||
offlineReady: [offlineReady, _setOfflineReady],
|
offlineReady: [offlineReady, _setOfflineReady],
|
||||||
needRefresh: [needRefresh, _setNeedRefresh],
|
needRefresh: [needRefresh, _setNeedRefresh],
|
||||||
updateServiceWorker: _update,
|
updateServiceWorker: _update
|
||||||
} = useRegisterSW({
|
} = useRegisterSW({
|
||||||
immediate: true,
|
immediate: true,
|
||||||
onRegisteredSW(swUrl, r) {
|
onRegisteredSW(swUrl, r) {
|
||||||
console.log('SW Registered: ' + r?.scope)
|
console.log("SW Registered: " + r?.scope);
|
||||||
},
|
},
|
||||||
onRegisterError(error: Error) {
|
onRegisterError(error: Error) {
|
||||||
console.log('SW registration error', error)
|
console.log("SW registration error", error);
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// const close = () => {
|
// const close = () => {
|
||||||
// setOfflineReady(false)
|
// setOfflineReady(false)
|
||||||
@@ -40,7 +40,7 @@ const ReloadPrompt: Component = () => {
|
|||||||
<Button onClick={() => close()}>Close</Button>
|
<Button onClick={() => close()}>Close</Button>
|
||||||
</Card> */}
|
</Card> */}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ReloadPrompt
|
export default ReloadPrompt;
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ import { Button, Card, NiceP, VStack } from "~/components/layout";
|
|||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
export function Restart() {
|
export function Restart() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
async function handleStop() {
|
async function handleStop() {
|
||||||
await state.mutiny_wallet?.stop()
|
await state.mutiny_wallet?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>Something *extra* screwy going on? Stop the nodes!</NiceP>
|
<NiceP>
|
||||||
|
Something *extra* screwy going on? Stop the nodes!
|
||||||
|
</NiceP>
|
||||||
<Button onClick={handleStop}>Stop</Button>
|
<Button onClick={handleStop}>Stop</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,42 @@
|
|||||||
import { For, Match, Switch, createMemo, createSignal } from "solid-js"
|
import { For, Match, Switch, createMemo, createSignal } from "solid-js";
|
||||||
|
|
||||||
export function SeedWords(props: { words: string, setHasSeen?: (hasSeen: boolean) => void }) {
|
export function SeedWords(props: {
|
||||||
const [shouldShow, setShouldShow] = createSignal(false)
|
words: string;
|
||||||
|
setHasSeen?: (hasSeen: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const [shouldShow, setShouldShow] = createSignal(false);
|
||||||
|
|
||||||
function toggleShow() {
|
function toggleShow() {
|
||||||
setShouldShow(!shouldShow())
|
setShouldShow(!shouldShow());
|
||||||
if (shouldShow()) {
|
if (shouldShow()) {
|
||||||
props.setHasSeen?.(true)
|
props.setHasSeen?.(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitWords = createMemo(() => props.words.split(" "))
|
const splitWords = createMemo(() => props.words.split(" "));
|
||||||
|
|
||||||
return (<button class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden" onClick={toggleShow}>
|
return (
|
||||||
<Switch>
|
<button
|
||||||
<Match when={!shouldShow()}>
|
class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden"
|
||||||
<div class="cursor-pointer">
|
onClick={toggleShow}
|
||||||
<code class="text-red">TAP TO REVEAL SEED WORDS</code>
|
>
|
||||||
</div>
|
<Switch>
|
||||||
</Match>
|
<Match when={!shouldShow()}>
|
||||||
|
<div class="cursor-pointer">
|
||||||
|
<code class="text-red">TAP TO REVEAL SEED WORDS</code>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
|
||||||
<Match when={shouldShow()}>
|
<Match when={shouldShow()}>
|
||||||
<ol class="cursor-pointer overflow-hidden grid grid-cols-2 w-full list-decimal list-inside">
|
<ol class="cursor-pointer overflow-hidden grid grid-cols-2 w-full list-decimal list-inside">
|
||||||
<For each={splitWords()}>
|
<For each={splitWords()}>
|
||||||
{(word) => (
|
{(word) => (
|
||||||
<li class="font-mono text-left">
|
<li class="font-mono text-left">{word}</li>
|
||||||
{word}
|
)}
|
||||||
</li>
|
</For>
|
||||||
)}
|
</ol>
|
||||||
</For>
|
</Match>
|
||||||
</ol>
|
</Switch>
|
||||||
</Match>
|
</button>
|
||||||
</Switch>
|
);
|
||||||
</button >)
|
|
||||||
}
|
}
|
||||||
@@ -1,61 +1,100 @@
|
|||||||
import { createForm, url } from '@modular-forms/solid';
|
import { createForm, url } from "@modular-forms/solid";
|
||||||
import { TextField } from '~/components/layout/TextField';
|
import { TextField } from "~/components/layout/TextField";
|
||||||
import { MutinyWalletSettingStrings, getExistingSettings } from '~/logic/mutinyWalletSetup';
|
import {
|
||||||
import { Button, Card, SmallHeader } from '~/components/layout';
|
MutinyWalletSettingStrings,
|
||||||
import { showToast } from './Toaster';
|
getExistingSettings
|
||||||
import eify from '~/utils/eify';
|
} from "~/logic/mutinyWalletSetup";
|
||||||
import { useMegaStore } from '~/state/megaStore';
|
import { Button, Card, SmallHeader } from "~/components/layout";
|
||||||
|
import { showToast } from "./Toaster";
|
||||||
|
import eify from "~/utils/eify";
|
||||||
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
export function SettingsStringsEditor() {
|
export function SettingsStringsEditor() {
|
||||||
const existingSettings = getExistingSettings();
|
const existingSettings = getExistingSettings();
|
||||||
const [_settingsForm, { Form, Field }] = createForm<MutinyWalletSettingStrings>({ initialValues: existingSettings });
|
const [_settingsForm, { Form, Field }] =
|
||||||
|
createForm<MutinyWalletSettingStrings>({
|
||||||
|
initialValues: existingSettings
|
||||||
|
});
|
||||||
const [_store, actions] = useMegaStore();
|
const [_store, actions] = useMegaStore();
|
||||||
|
|
||||||
async function handleSubmit(values: MutinyWalletSettingStrings) {
|
async function handleSubmit(values: MutinyWalletSettingStrings) {
|
||||||
try {
|
try {
|
||||||
const existing = getExistingSettings();
|
const existing = getExistingSettings();
|
||||||
const newSettings = { ...existing, ...values }
|
const newSettings = { ...existing, ...values };
|
||||||
await actions.setupMutinyWallet(newSettings);
|
await actions.setupMutinyWallet(newSettings);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
showToast(eify(e))
|
showToast(eify(e));
|
||||||
}
|
}
|
||||||
console.log(values)
|
console.log(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Card>
|
return (
|
||||||
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
<Card>
|
||||||
<h2 class="text-2xl font-light">Don't trust us! Use your own servers to back Mutiny.</h2>
|
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-2">
|
<h2 class="text-2xl font-light">
|
||||||
<SmallHeader>Network</SmallHeader>
|
Don't trust us! Use your own servers to back Mutiny.
|
||||||
<pre>
|
</h2>
|
||||||
{existingSettings.network}
|
<div class="flex flex-col gap-2">
|
||||||
</pre>
|
<SmallHeader>Network</SmallHeader>
|
||||||
</div>
|
<pre>{existingSettings.network}</pre>
|
||||||
|
</div>
|
||||||
<Field name="proxy" validate={[url("Should be a url starting with wss://")]}>
|
|
||||||
{(field, props) => (
|
|
||||||
<TextField {...props} value={field.value} error={field.error} label="Websockets Proxy" />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name="esplora" validate={[url("That doesn't look like a URL")]}>
|
|
||||||
{(field, props) => (
|
|
||||||
<TextField {...props} value={field.value} error={field.error} label="Esplora" />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name="rgs" validate={[url("That doesn't look like a URL")]}>
|
|
||||||
{(field, props) => (
|
|
||||||
<TextField {...props} value={field.value} error={field.error} label="RGS" />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name="lsp" validate={[url("That doesn't look like a URL")]}>
|
|
||||||
{(field, props) => (
|
|
||||||
<TextField {...props} value={field.value} error={field.error} label="LSP" />
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Button type="submit">Save</Button>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
|
<Field
|
||||||
|
name="proxy"
|
||||||
|
validate={[url("Should be a url starting with wss://")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Websockets Proxy"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
name="esplora"
|
||||||
|
validate={[url("That doesn't look like a URL")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Esplora"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
name="rgs"
|
||||||
|
validate={[url("That doesn't look like a URL")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="RGS"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
name="lsp"
|
||||||
|
validate={[url("That doesn't look like a URL")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="LSP"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,69 +1,83 @@
|
|||||||
import { Card, VStack } from "~/components/layout";
|
import { Card, VStack } from "~/components/layout";
|
||||||
import { useCopy } from "~/utils/useCopy";
|
import { useCopy } from "~/utils/useCopy";
|
||||||
import copyIcon from "~/assets/icons/copy.svg"
|
import copyIcon from "~/assets/icons/copy.svg";
|
||||||
import shareIcon from "~/assets/icons/share.svg"
|
import shareIcon from "~/assets/icons/share.svg";
|
||||||
import eyeIcon from "~/assets/icons/eye.svg"
|
import eyeIcon from "~/assets/icons/eye.svg";
|
||||||
import { Show, createSignal } from "solid-js";
|
import { Show, createSignal } from "solid-js";
|
||||||
import { JsonModal } from "./JsonModal";
|
import { JsonModal } from "./JsonModal";
|
||||||
|
|
||||||
const STYLE = "px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors"
|
const STYLE =
|
||||||
|
"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors";
|
||||||
|
|
||||||
export function ShareButton(props: { receiveString: string }) {
|
export function ShareButton(props: { receiveString: string }) {
|
||||||
async function share(receiveString: string) {
|
async function share(receiveString: string) {
|
||||||
// If the browser doesn't support share we can just copy the address
|
// If the browser doesn't support share we can just copy the address
|
||||||
if (!navigator.share) {
|
if (!navigator.share) {
|
||||||
console.error("Share not supported")
|
console.error("Share not supported");
|
||||||
}
|
}
|
||||||
const shareData: ShareData = {
|
const shareData: ShareData = {
|
||||||
title: "Mutiny Wallet",
|
title: "Mutiny Wallet",
|
||||||
text: receiveString,
|
text: receiveString
|
||||||
}
|
};
|
||||||
try {
|
try {
|
||||||
await navigator.share(shareData)
|
await navigator.share(shareData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button class={STYLE} onClick={(_) => share(props.receiveString)}><span>Share</span><img src={shareIcon} alt="share" /></button>
|
<button class={STYLE} onClick={(_) => share(props.receiveString)}>
|
||||||
)
|
<span>Share</span>
|
||||||
|
<img src={shareIcon} alt="share" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TruncateMiddle(props: { text: string }) {
|
function TruncateMiddle(props: { text: string }) {
|
||||||
return (
|
return (
|
||||||
<div class="flex text-neutral-400 font-mono">
|
<div class="flex text-neutral-400 font-mono">
|
||||||
<span class="truncate">{props.text}</span>
|
<span class="truncate">{props.text}</span>
|
||||||
<span class="pr-2">{props.text.length > 8 ? props.text.slice(-8) : ""}</span>
|
<span class="pr-2">
|
||||||
</div>
|
{props.text.length > 8 ? props.text.slice(-8) : ""}
|
||||||
);
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StringShower(props: { text: string }) {
|
export function StringShower(props: { text: string }) {
|
||||||
const [open, setOpen] = createSignal(false);
|
const [open, setOpen] = createSignal(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<JsonModal open={open()} plaintext={props.text} title="Details" setOpen={setOpen} />
|
<JsonModal
|
||||||
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
open={open()}
|
||||||
<TruncateMiddle text={props.text} />
|
plaintext={props.text}
|
||||||
<button class="w-[2rem]" onClick={() => setOpen(true)}>
|
title="Details"
|
||||||
<img src={eyeIcon} alt="eye" />
|
setOpen={setOpen}
|
||||||
</button>
|
/>
|
||||||
</div>
|
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
||||||
</>
|
<TruncateMiddle text={props.text} />
|
||||||
);
|
<button class="w-[2rem]" onClick={() => setOpen(true)}>
|
||||||
|
<img src={eyeIcon} alt="eye" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CopyButton(props: { text?: string, title?: string }) {
|
export function CopyButton(props: { text?: string; title?: string }) {
|
||||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
|
|
||||||
function handleCopy() {
|
function handleCopy() {
|
||||||
copy(props.text ?? "")
|
copy(props.text ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button class={STYLE} onClick={handleCopy}>{copied() ? "Copied" : props.title ?? "Copy"}<img src={copyIcon} alt="copy" /></button>
|
<button class={STYLE} onClick={handleCopy}>
|
||||||
)
|
{copied() ? "Copied" : props.title ?? "Copy"}
|
||||||
|
<img src={copyIcon} alt="copy" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShareCard(props: { text?: string }) {
|
export function ShareCard(props: { text?: string }) {
|
||||||
@@ -78,7 +92,6 @@ export function ShareCard(props: { text?: string }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card >
|
</Card>
|
||||||
)
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Select, createOptions } from "@thisbeyond/solid-select";
|
import { Select, createOptions } from "@thisbeyond/solid-select";
|
||||||
import "~/styles/solid-select.css"
|
import "~/styles/solid-select.css";
|
||||||
import { For, Show, createMemo, createSignal, onMount } from "solid-js";
|
import { For, Show, createMemo, createSignal, onMount } from "solid-js";
|
||||||
import { TinyButton } from "./layout";
|
import { TinyButton } from "./layout";
|
||||||
import { MutinyTagItem, sortByLastUsed } from "~/utils/tags";
|
import { MutinyTagItem, sortByLastUsed } from "~/utils/tags";
|
||||||
@@ -10,36 +10,43 @@ const createLabelValue = (label: string): Partial<MutinyTagItem> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function TagEditor(props: {
|
export function TagEditor(props: {
|
||||||
selectedValues: Partial<MutinyTagItem>[],
|
selectedValues: Partial<MutinyTagItem>[];
|
||||||
setSelectedValues: (value: Partial<MutinyTagItem>[]) => void,
|
setSelectedValues: (value: Partial<MutinyTagItem>[]) => void;
|
||||||
placeholder: string
|
placeholder: string;
|
||||||
}) {
|
}) {
|
||||||
const [_state, actions] = useMegaStore();
|
const [_state, actions] = useMegaStore();
|
||||||
const [availableTags, setAvailableTags] = createSignal<MutinyTagItem[]>([]);
|
const [availableTags, setAvailableTags] = createSignal<MutinyTagItem[]>([]);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const tags = await actions.listTags()
|
const tags = await actions.listTags();
|
||||||
if (tags) {
|
if (tags) {
|
||||||
setAvailableTags(tags.filter((tag) => tag.kind === "Contact").sort(sortByLastUsed))
|
setAvailableTags(
|
||||||
|
tags
|
||||||
|
.filter((tag) => tag.kind === "Contact")
|
||||||
|
.sort(sortByLastUsed)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const selectProps = createMemo(() => {
|
const selectProps = createMemo(() => {
|
||||||
return createOptions(availableTags() || [], {
|
return createOptions(availableTags() || [], {
|
||||||
key: "name",
|
key: "name",
|
||||||
filterable: true, // Default
|
filterable: true, // Default
|
||||||
createable: createLabelValue,
|
createable: createLabelValue
|
||||||
});
|
});
|
||||||
|
});
|
||||||
})
|
|
||||||
|
|
||||||
const onChange = (selected: MutinyTagItem[]) => {
|
const onChange = (selected: MutinyTagItem[]) => {
|
||||||
props.setSelectedValues(selected);
|
props.setSelectedValues(selected);
|
||||||
|
|
||||||
console.log(selected)
|
console.log(selected);
|
||||||
|
|
||||||
const lastValue = selected[selected.length - 1];
|
const lastValue = selected[selected.length - 1];
|
||||||
if (lastValue && availableTags() && !availableTags()!.includes(lastValue)) {
|
if (
|
||||||
|
lastValue &&
|
||||||
|
availableTags() &&
|
||||||
|
!availableTags()!.includes(lastValue)
|
||||||
|
) {
|
||||||
setAvailableTags([...availableTags(), lastValue]);
|
setAvailableTags([...availableTags(), lastValue]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -50,7 +57,7 @@ export function TagEditor(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2 flex-shrink flex-1" >
|
<div class="flex flex-col gap-2 flex-shrink flex-1">
|
||||||
<Select
|
<Select
|
||||||
multiple
|
multiple
|
||||||
initialValue={props.selectedValues}
|
initialValue={props.selectedValues}
|
||||||
@@ -70,6 +77,6 @@ export function TagEditor(props: {
|
|||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -10,37 +10,52 @@ export function Toaster() {
|
|||||||
<Toast.List class="z-[9999] max-w-[100vw] w-[400px] mt-8 flex flex-col gap-4" />
|
<Toast.List class="z-[9999] max-w-[100vw] w-[400px] mt-8 flex flex-col gap-4" />
|
||||||
</Toast.Region>
|
</Toast.Region>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToastArg = { title: string, description: string } | Error
|
type ToastArg = { title: string; description: string } | Error;
|
||||||
|
|
||||||
export function showToast(arg: ToastArg) {
|
export function showToast(arg: ToastArg) {
|
||||||
if (arg instanceof Error) {
|
if (arg instanceof Error) {
|
||||||
return toaster.show(props => (
|
return toaster.show((props) => (
|
||||||
<ToastItem title="Error" description={arg.message} isError {...props} />
|
<ToastItem
|
||||||
))
|
title="Error"
|
||||||
|
description={arg.message}
|
||||||
|
isError
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
return toaster.show(props => (
|
return toaster.show((props) => (
|
||||||
<ToastItem title={arg.title} description={arg.description} {...props} />
|
<ToastItem
|
||||||
))
|
title={arg.title}
|
||||||
|
description={arg.description}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToastItem(props: { toastId: number, title: string, description: string, isError?: boolean }) {
|
export function ToastItem(props: {
|
||||||
|
toastId: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
isError?: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Toast.Root toastId={props.toastId} class={`w-[80vw] max-w-[400px] mx-auto p-4 bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border ${props.isError ? "border-m-red/50" : "border-white/10"} `}>
|
<Toast.Root
|
||||||
|
toastId={props.toastId}
|
||||||
|
class={`w-[80vw] max-w-[400px] mx-auto p-4 bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border ${
|
||||||
|
props.isError ? "border-m-red/50" : "border-white/10"
|
||||||
|
} `}
|
||||||
|
>
|
||||||
<div class="flex gap-4 w-full justify-between items-start">
|
<div class="flex gap-4 w-full justify-between items-start">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Toast.Title>
|
<Toast.Title>
|
||||||
<SmallHeader>
|
<SmallHeader>{props.title}</SmallHeader>
|
||||||
{props.title}
|
|
||||||
</SmallHeader>
|
|
||||||
</Toast.Title>
|
</Toast.Title>
|
||||||
<Toast.Description>
|
<Toast.Description>
|
||||||
<p>
|
<p>{props.description}</p>
|
||||||
{props.description}
|
|
||||||
</p>
|
|
||||||
</Toast.Description>
|
</Toast.Description>
|
||||||
</div>
|
</div>
|
||||||
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue flex-0">
|
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue flex-0">
|
||||||
@@ -48,5 +63,5 @@ export function ToastItem(props: { toastId: number, title: string, description:
|
|||||||
</Toast.CloseButton>
|
</Toast.CloseButton>
|
||||||
</div>
|
</div>
|
||||||
</Toast.Root>
|
</Toast.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Back } from "~/assets/svg/Back";
|
import { Back } from "~/assets/svg/Back";
|
||||||
|
|
||||||
export function BackButton(props: {
|
export function BackButton(props: {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
showOnDesktop?: boolean;
|
showOnDesktop?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => props.onClick()}
|
onClick={() => props.onClick()}
|
||||||
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center"
|
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center"
|
||||||
classList={{ "md:!flex": props.showOnDesktop }}
|
classList={{ "md:!flex": props.showOnDesktop }}
|
||||||
>
|
>
|
||||||
<Back />
|
<Back />
|
||||||
{props.title ? props.title : "Home"}
|
{props.title ? props.title : "Home"}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import { A } from "solid-start";
|
import { A } from "solid-start";
|
||||||
import { Back } from "~/assets/svg/Back";
|
import { Back } from "~/assets/svg/Back";
|
||||||
|
|
||||||
export function BackLink(props: { href?: string, title?: string }) {
|
export function BackLink(props: { href?: string; title?: string }) {
|
||||||
return (<A href={props.href ? props.href : "/"} class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center"><Back />{props.title ? props.title : "Home"}</A>)
|
return (
|
||||||
|
<A
|
||||||
|
href={props.href ? props.href : "/"}
|
||||||
|
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center"
|
||||||
|
>
|
||||||
|
<Back />
|
||||||
|
{props.title ? props.title : "Home"}
|
||||||
|
</A>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -5,78 +5,95 @@ import { A } from "solid-start";
|
|||||||
import { LoadingSpinner } from ".";
|
import { LoadingSpinner } from ".";
|
||||||
|
|
||||||
const button = cva(
|
const button = cva(
|
||||||
"p-3 rounded-xl font-semibold disabled:opacity-20 disabled:grayscale transition",
|
"p-3 rounded-xl font-semibold disabled:opacity-20 disabled:grayscale transition",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
// TODO: button hover has to work different than buttonlinks (like disabled state)
|
// TODO: button hover has to work different than buttonlinks (like disabled state)
|
||||||
intent: {
|
intent: {
|
||||||
active: "bg-white text-black border border-white hover:text-[#3B6CCC]",
|
active: "bg-white text-black border border-white hover:text-[#3B6CCC]",
|
||||||
inactive: "bg-black text-white border border-white hover:text-[#3B6CCC]",
|
inactive:
|
||||||
glowy:
|
"bg-black text-white border border-white hover:text-[#3B6CCC]",
|
||||||
"bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue",
|
glowy: "bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue",
|
||||||
blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button",
|
blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button",
|
||||||
red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button",
|
red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button",
|
||||||
green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button"
|
green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button"
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
flex: "flex-1 text-xl",
|
flex: "flex-1 text-xl",
|
||||||
pad: "px-8 text-xl",
|
pad: "px-8 text-xl",
|
||||||
small: "px-4 py-2 w-auto text-lg",
|
small: "px-4 py-2 w-auto text-lg",
|
||||||
xs: "px-4 py-2 w-auto rounded-lg text-base"
|
xs: "px-4 py-2 w-auto rounded-lg text-base"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
intent: "inactive",
|
intent: "inactive",
|
||||||
layout: "flex"
|
layout: "flex"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Help from https://github.com/arpadgabor/credee/blob/main/packages/www/src/components/ui/button.tsx
|
// Help from https://github.com/arpadgabor/credee/blob/main/packages/www/src/components/ui/button.tsx
|
||||||
|
|
||||||
type StyleProps = VariantProps<typeof button>
|
type StyleProps = VariantProps<typeof button>;
|
||||||
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, StyleProps {
|
interface ButtonProps
|
||||||
loading?: boolean,
|
extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
disabled?: boolean,
|
StyleProps {
|
||||||
|
loading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button: ParentComponent<ButtonProps> = props => {
|
export const Button: ParentComponent<ButtonProps> = (props) => {
|
||||||
const slot = children(() => props.children)
|
const slot = children(() => props.children);
|
||||||
const [local, attrs] = splitProps(props, ['children', 'intent', 'layout', 'class'])
|
const [local, attrs] = splitProps(props, [
|
||||||
|
"children",
|
||||||
|
"intent",
|
||||||
|
"layout",
|
||||||
|
"class"
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...attrs}
|
{...attrs}
|
||||||
disabled={props.disabled || props.loading}
|
disabled={props.disabled || props.loading}
|
||||||
class={button({
|
class={button({
|
||||||
class: local.class || "",
|
class: local.class || "",
|
||||||
intent: local.intent,
|
intent: local.intent,
|
||||||
layout: local.layout
|
layout: local.layout
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Show when={props.loading} fallback={slot()}>
|
<Show when={props.loading} fallback={slot()}>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
{/* TODO: constrain this to the exact height of the button */}
|
{/* TODO: constrain this to the exact height of the button */}
|
||||||
<LoadingSpinner wide />
|
<LoadingSpinner wide />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ButtonLinkProps
|
||||||
|
extends JSX.ButtonHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
StyleProps {
|
||||||
|
href: string;
|
||||||
|
target?: string;
|
||||||
|
rel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonLinkProps extends JSX.ButtonHTMLAttributes<HTMLAnchorElement>, StyleProps {
|
export const ButtonLink: ParentComponent<ButtonLinkProps> = (props) => {
|
||||||
href: string
|
const slot = children(() => props.children);
|
||||||
target?: string
|
const [local, attrs] = splitProps(props, [
|
||||||
rel?: string
|
"children",
|
||||||
}
|
"intent",
|
||||||
|
"layout",
|
||||||
export const ButtonLink: ParentComponent<ButtonLinkProps> = props => {
|
"class",
|
||||||
const slot = children(() => props.children)
|
"href",
|
||||||
const [local, attrs] = splitProps(props, ['children', 'intent', 'layout', 'class', 'href', 'target', 'rel'])
|
"target",
|
||||||
|
"rel"
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={local.href?.includes('://') ? 'a' : A}
|
component={local.href?.includes("://") ? "a" : A}
|
||||||
href={local.href}
|
href={local.href}
|
||||||
target={local.target}
|
target={local.target}
|
||||||
rel={local.rel}
|
rel={local.rel}
|
||||||
@@ -84,10 +101,10 @@ export const ButtonLink: ParentComponent<ButtonLinkProps> = props => {
|
|||||||
class={button({
|
class={button({
|
||||||
class: `flex justify-center no-underline ${local.class || ""}`,
|
class: `flex justify-center no-underline ${local.class || ""}`,
|
||||||
intent: local.intent,
|
intent: local.intent,
|
||||||
layout: local.layout,
|
layout: local.layout
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{slot()}
|
{slot()}
|
||||||
</Dynamic>
|
</Dynamic>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { ParentComponent } from "solid-js";
|
import { ParentComponent } from "solid-js";
|
||||||
|
|
||||||
export const ExternalLink: ParentComponent<{ href: string }> = (props) => {
|
export const ExternalLink: ParentComponent<{ href: string }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<a target="_blank" rel="noopener noreferrer" href={props.href}>
|
<a target="_blank" rel="noopener noreferrer" href={props.href}>
|
||||||
{props.children}{" "}
|
{props.children}{" "}
|
||||||
<svg
|
<svg
|
||||||
class="inline-block"
|
class="inline-block"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z"
|
d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { JSX } from 'solid-js';
|
import { JSX } from "solid-js";
|
||||||
|
|
||||||
interface LinkifyProps {
|
interface LinkifyProps {
|
||||||
initialText: string;
|
initialText: string;
|
||||||
@@ -15,7 +15,7 @@ export default function Linkify(props: LinkifyProps): JSX.Element {
|
|||||||
|
|
||||||
while ((match = pattern.exec(text)) !== null) {
|
while ((match = pattern.exec(text)) !== null) {
|
||||||
const link = match[1];
|
const link = match[1];
|
||||||
const href = link.startsWith('http') ? link : `https://${link}`;
|
const href = link.startsWith("http") ? link : `https://${link}`;
|
||||||
const beforeLink = text.slice(lastIndex, match.index);
|
const beforeLink = text.slice(lastIndex, match.index);
|
||||||
lastIndex = pattern.lastIndex;
|
lastIndex = pattern.lastIndex;
|
||||||
|
|
||||||
@@ -23,7 +23,16 @@ export default function Linkify(props: LinkifyProps): JSX.Element {
|
|||||||
links.push(beforeLink);
|
links.push(beforeLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
links.push(<a href={href} class="break-all" target="_blank" rel="noopener noreferrer">{link}</a>);
|
links.push(
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
class="break-all"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingText = text.slice(lastIndex);
|
const remainingText = text.slice(lastIndex);
|
||||||
|
|||||||
@@ -3,36 +3,43 @@ import { SmallHeader } from ".";
|
|||||||
|
|
||||||
export default function formatNumber(num: number) {
|
export default function formatNumber(num: number) {
|
||||||
const map = [
|
const map = [
|
||||||
{ suffix: 'T', threshold: 1e12 },
|
{ suffix: "T", threshold: 1e12 },
|
||||||
{ suffix: 'B', threshold: 1e9 },
|
{ suffix: "B", threshold: 1e9 },
|
||||||
{ suffix: 'M', threshold: 1e6 },
|
{ suffix: "M", threshold: 1e6 },
|
||||||
{ suffix: 'K', threshold: 1e3 },
|
{ suffix: "K", threshold: 1e3 },
|
||||||
{ suffix: '', threshold: 1 },
|
{ suffix: "", threshold: 1 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const found = map.find((x) => Math.abs(num) >= x.threshold);
|
const found = map.find((x) => Math.abs(num) >= x.threshold);
|
||||||
if (found) {
|
if (found) {
|
||||||
const formatted = (num / found.threshold).toLocaleString() + found.suffix;
|
const formatted =
|
||||||
|
(num / found.threshold).toLocaleString() + found.suffix;
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return num.toLocaleString();
|
return num.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProgressBar(props: { value: number, max: number }) {
|
export function ProgressBar(props: { value: number; max: number }) {
|
||||||
return (<Progress.Root
|
return (
|
||||||
value={props.value}
|
<Progress.Root
|
||||||
minValue={0}
|
value={props.value}
|
||||||
maxValue={props.max}
|
minValue={0}
|
||||||
getValueLabel={({ value, max }) => `${formatNumber(value)} of ${formatNumber(max)} sats sent`}
|
maxValue={props.max}
|
||||||
class="w-full flex flex-col gap-2"
|
getValueLabel={({ value, max }) =>
|
||||||
>
|
`${formatNumber(value)} of ${formatNumber(max)} sats sent`
|
||||||
<div class="flex justify-between">
|
}
|
||||||
<Progress.Label><SmallHeader>Sending...</SmallHeader></Progress.Label>
|
class="w-full flex flex-col gap-2"
|
||||||
<Progress.ValueLabel class="text-sm font-semibold uppercase" />
|
>
|
||||||
</div>
|
<div class="flex justify-between">
|
||||||
<Progress.Track class="h-6 bg-white/10 rounded">
|
<Progress.Label>
|
||||||
<Progress.Fill class="bg-m-red rounded h-full w-[var(--kb-progress-fill-width)] transition-[width]" />
|
<SmallHeader>Sending...</SmallHeader>
|
||||||
</Progress.Track>
|
</Progress.Label>
|
||||||
</Progress.Root>)
|
<Progress.ValueLabel class="text-sm font-semibold uppercase" />
|
||||||
|
</div>
|
||||||
|
<Progress.Track class="h-6 bg-white/10 rounded">
|
||||||
|
<Progress.Fill class="bg-m-red rounded h-full w-[var(--kb-progress-fill-width)] transition-[width]" />
|
||||||
|
</Progress.Track>
|
||||||
|
</Progress.Root>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,73 @@
|
|||||||
import { RadioGroup } from "@kobalte/core";
|
import { RadioGroup } from "@kobalte/core";
|
||||||
import { For, Show } from "solid-js";
|
import { For, Show } from "solid-js";
|
||||||
|
|
||||||
type Choices = { value: string; label: string; caption: string; disabled?: boolean }[];
|
type Choices = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
caption: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}[];
|
||||||
|
|
||||||
// TODO: how could would it be if we could just pass the estimated fees in here?
|
// TODO: how could would it be if we could just pass the estimated fees in here?
|
||||||
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) {
|
export function StyledRadioGroup(props: {
|
||||||
|
value: string;
|
||||||
|
choices: Choices;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
small?: boolean;
|
||||||
|
accent?: "red" | "white";
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
// TODO: rewrite this with CVA, props are bad for tailwind
|
// TODO: rewrite this with CVA, props are bad for tailwind
|
||||||
<RadioGroup.Root
|
<RadioGroup.Root
|
||||||
value={props.value}
|
value={props.value}
|
||||||
onChange={props.onValueChange}
|
onChange={props.onValueChange}
|
||||||
class={"grid w-full gap-4"}
|
class={"grid w-full gap-4"}
|
||||||
classList={{
|
classList={{
|
||||||
"grid-cols-2": props.choices.length === 2,
|
"grid-cols-2": props.choices.length === 2,
|
||||||
"grid-cols-3": props.choices.length === 3,
|
"grid-cols-3": props.choices.length === 3,
|
||||||
"gap-2": props.small
|
"gap-2": props.small
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<For each={props.choices}>
|
<For each={props.choices}>
|
||||||
{(choice) => (
|
{(choice) => (
|
||||||
<RadioGroup.Item
|
<RadioGroup.Item
|
||||||
value={choice.value}
|
value={choice.value}
|
||||||
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
|
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
|
||||||
classList={{
|
classList={{
|
||||||
"ui-checked:outline-m-red": props.accent === "red",
|
"ui-checked:outline-m-red": props.accent === "red",
|
||||||
"ui-checked:outline-white": props.accent === "white",
|
"ui-checked:outline-white":
|
||||||
"ui-disabled:opacity-50": choice.disabled
|
props.accent === "white",
|
||||||
}}
|
"ui-disabled:opacity-50": choice.disabled
|
||||||
disabled={choice.disabled}
|
}}
|
||||||
>
|
disabled={choice.disabled}
|
||||||
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
|
||||||
<RadioGroup.ItemInput />
|
|
||||||
<RadioGroup.ItemControl>
|
|
||||||
<RadioGroup.ItemIndicator />
|
|
||||||
</RadioGroup.ItemControl>
|
|
||||||
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
|
||||||
<div class="block">
|
|
||||||
<div
|
|
||||||
classList={{ "text-base": props.small, "text-lg": !props.small }}
|
|
||||||
class={`font-semibold max-sm:text-sm`}
|
|
||||||
>
|
>
|
||||||
{choice.label}
|
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
||||||
</div>
|
<RadioGroup.ItemInput />
|
||||||
<Show when={!props.small}>
|
<RadioGroup.ItemControl>
|
||||||
<div class="text-sm font-light">{choice.caption}</div>
|
<RadioGroup.ItemIndicator />
|
||||||
</Show>
|
</RadioGroup.ItemControl>
|
||||||
</div>
|
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
||||||
</RadioGroup.ItemLabel>
|
<div class="block">
|
||||||
</div>
|
<div
|
||||||
</RadioGroup.Item>
|
classList={{
|
||||||
)}
|
"text-base": props.small,
|
||||||
</For>
|
"text-lg": !props.small
|
||||||
</RadioGroup.Root>
|
}}
|
||||||
|
class={`font-semibold max-sm:text-sm`}
|
||||||
|
>
|
||||||
|
{choice.label}
|
||||||
|
</div>
|
||||||
|
<Show when={!props.small}>
|
||||||
|
<div class="text-sm font-light">
|
||||||
|
{choice.caption}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.ItemLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</RadioGroup.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { TextField as KTextField } from '@kobalte/core';
|
import { TextField as KTextField } from "@kobalte/core";
|
||||||
import { type JSX, Show, splitProps } from 'solid-js';
|
import { type JSX, Show, splitProps } from "solid-js";
|
||||||
|
|
||||||
type TextFieldProps = {
|
type TextFieldProps = {
|
||||||
name: string;
|
name: string;
|
||||||
type?: 'text' | 'email' | 'tel' | 'password' | 'url' | 'date';
|
type?: "text" | "email" | "tel" | "password" | "url" | "date";
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
@@ -11,25 +11,31 @@ type TextFieldProps = {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
|
ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
|
||||||
onInput: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
|
onInput: JSX.EventHandler<
|
||||||
|
HTMLInputElement | HTMLTextAreaElement,
|
||||||
|
InputEvent
|
||||||
|
>;
|
||||||
onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
|
onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
|
||||||
onBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent>;
|
onBlur: JSX.EventHandler<
|
||||||
|
HTMLInputElement | HTMLTextAreaElement,
|
||||||
|
FocusEvent
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TextField(props: TextFieldProps) {
|
export function TextField(props: TextFieldProps) {
|
||||||
const [fieldProps] = splitProps(props, [
|
const [fieldProps] = splitProps(props, [
|
||||||
'placeholder',
|
"placeholder",
|
||||||
'ref',
|
"ref",
|
||||||
'onInput',
|
"onInput",
|
||||||
'onChange',
|
"onChange",
|
||||||
'onBlur',
|
"onBlur"
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<KTextField.Root
|
<KTextField.Root
|
||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
name={props.name}
|
name={props.name}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
validationState={props.error ? 'invalid' : 'valid'}
|
validationState={props.error ? "invalid" : "valid"}
|
||||||
isRequired={props.required}
|
isRequired={props.required}
|
||||||
>
|
>
|
||||||
<Show when={props.label}>
|
<Show when={props.label}>
|
||||||
@@ -39,9 +45,19 @@ export function TextField(props: TextFieldProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={props.multiline}
|
when={props.multiline}
|
||||||
fallback={<KTextField.Input {...fieldProps} type={props.type} class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400" />}
|
fallback={
|
||||||
|
<KTextField.Input
|
||||||
|
{...fieldProps}
|
||||||
|
type={props.type}
|
||||||
|
class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<KTextField.TextArea {...fieldProps} autoResize class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400" />
|
<KTextField.TextArea
|
||||||
|
{...fieldProps}
|
||||||
|
autoResize
|
||||||
|
class="w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<KTextField.ErrorMessage>{props.error}</KTextField.ErrorMessage>
|
<KTextField.ErrorMessage>{props.error}</KTextField.ErrorMessage>
|
||||||
</KTextField.Root>
|
</KTextField.Root>
|
||||||
|
|||||||
@@ -1,59 +1,66 @@
|
|||||||
import { JSX, ParentComponent, Show, Suspense, createResource } from "solid-js"
|
import { JSX, ParentComponent, Show, Suspense, createResource } from "solid-js";
|
||||||
import Linkify from "./Linkify"
|
import Linkify from "./Linkify";
|
||||||
import { Button, ButtonLink } from "./Button"
|
import { Button, ButtonLink } from "./Button";
|
||||||
import { Checkbox as KCheckbox, Separator } from "@kobalte/core"
|
import { Checkbox as KCheckbox, Separator } from "@kobalte/core";
|
||||||
import { useMegaStore } from "~/state/megaStore"
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import check from "~/assets/icons/check.svg"
|
import check from "~/assets/icons/check.svg";
|
||||||
import { MutinyTagItem } from "~/utils/tags"
|
import { MutinyTagItem } from "~/utils/tags";
|
||||||
import { generateGradient } from "~/utils/gradientHash"
|
import { generateGradient } from "~/utils/gradientHash";
|
||||||
import close from "~/assets/icons/close.svg"
|
import close from "~/assets/icons/close.svg";
|
||||||
|
|
||||||
export {
|
export { Button, ButtonLink, Linkify };
|
||||||
Button,
|
|
||||||
ButtonLink,
|
|
||||||
Linkify,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
|
export const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
|
||||||
return <header class={`text-sm font-semibold uppercase ${props.class}`}>{props.children}</header>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Card: ParentComponent<{ title?: string, titleElement?: JSX.Element }> = (props) => {
|
|
||||||
return (
|
return (
|
||||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950/50 w-full'>
|
<header class={`text-sm font-semibold uppercase ${props.class}`}>
|
||||||
|
{props.children}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Card: ParentComponent<{
|
||||||
|
title?: string;
|
||||||
|
titleElement?: JSX.Element;
|
||||||
|
}> = (props) => {
|
||||||
|
return (
|
||||||
|
<div class="rounded-xl p-4 flex flex-col gap-2 bg-neutral-950/50 w-full">
|
||||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||||
{props.titleElement && props.titleElement}
|
{props.titleElement && props.titleElement}
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const InnerCard: ParentComponent<{ title?: string }> = (props) => {
|
export const InnerCard: ParentComponent<{ title?: string }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class='rounded-xl p-4 flex flex-col gap-2 border border-white/10 bg-[rgba(255,255,255,0.05)]'>
|
<div class="rounded-xl p-4 flex flex-col gap-2 border border-white/10 bg-[rgba(255,255,255,0.05)]">
|
||||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const FancyCard: ParentComponent<{
|
export const FancyCard: ParentComponent<{
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
tag?: JSX.Element;
|
tag?: JSX.Element;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="border border-black/50 rounded-xl border-b-4 p-4 flex flex-col gap-2 bg-neutral-800/50 shadow-fancy-card">
|
<div class="border border-black/50 rounded-xl border-b-4 p-4 flex flex-col gap-2 bg-neutral-800/50 shadow-fancy-card">
|
||||||
<div class="w-full flex justify-between items-center">
|
<div class="w-full flex justify-between items-center">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||||
{props.subtitle && <SmallHeader class="text-neutral-500">{props.subtitle}</SmallHeader>}
|
{props.subtitle && (
|
||||||
|
<SmallHeader class="text-neutral-500">
|
||||||
|
{props.subtitle}
|
||||||
|
</SmallHeader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{props.tag && props.tag}
|
||||||
|
</div>
|
||||||
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
{props.tag && props.tag}
|
);
|
||||||
</div>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SafeArea: ParentComponent = (props) => {
|
export const SafeArea: ParentComponent = (props) => {
|
||||||
@@ -62,9 +69,9 @@ export const SafeArea: ParentComponent = (props) => {
|
|||||||
{/* <div class="flex-1 disable-scrollbars overflow-y-scroll md:pl-[8rem] md:pr-[6rem]"> */}
|
{/* <div class="flex-1 disable-scrollbars overflow-y-scroll md:pl-[8rem] md:pr-[6rem]"> */}
|
||||||
{props.children}
|
{props.children}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
</div >
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DefaultMain: ParentComponent = (props) => {
|
export const DefaultMain: ParentComponent = (props) => {
|
||||||
return (
|
return (
|
||||||
@@ -73,124 +80,165 @@ export const DefaultMain: ParentComponent = (props) => {
|
|||||||
{/* CSS is hard sometimes */}
|
{/* CSS is hard sometimes */}
|
||||||
<div class="py-4" />
|
<div class="py-4" />
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const FullscreenLoader = () => {
|
export const FullscreenLoader = () => {
|
||||||
return (
|
return (
|
||||||
<div class="w-full h-[100dvh] flex justify-center items-center">
|
<div class="w-full h-[100dvh] flex justify-center items-center">
|
||||||
<LoadingSpinner wide />
|
<LoadingSpinner wide />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const MutinyWalletGuard: ParentComponent = (props) => {
|
export const MutinyWalletGuard: ParentComponent = (props) => {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<FullscreenLoader />}>
|
<Suspense fallback={<FullscreenLoader />}>
|
||||||
<Show when={state.mutiny_wallet && !state.wallet_loading}>{props.children}</Show>
|
<Show when={state.mutiny_wallet && !state.wallet_loading}>
|
||||||
</Suspense>
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const LoadingSpinner = (props: { big?: boolean, wide?: boolean }) => {
|
export const LoadingSpinner = (props: { big?: boolean; wide?: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
classList={{ "flex justify-center": props.wide, "h-full grid": props.big }}
|
classList={{
|
||||||
>
|
"flex justify-center": props.wide,
|
||||||
<svg
|
"h-full grid": props.big
|
||||||
aria-hidden="true"
|
}}
|
||||||
class="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-m-red place-self-center"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
aria-hidden="true"
|
||||||
fill="currentColor"
|
class="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-m-red place-self-center"
|
||||||
/>
|
viewBox="0 0 100 101"
|
||||||
<path
|
fill="none"
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="currentFill"
|
>
|
||||||
/>
|
<path
|
||||||
</svg>
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
<span class="sr-only">Loading...</span>
|
fill="currentColor"
|
||||||
</div>
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Hr = () => <Separator.Root class="my-4 border-white/20" />
|
export const Hr = () => <Separator.Root class="my-4 border-white/20" />;
|
||||||
|
|
||||||
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (props) => {
|
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (
|
||||||
|
props
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
||||||
<h1 class="text-3xl font-semibold">{props.children}</h1>
|
<h1 class="text-3xl font-semibold">{props.children}</h1>
|
||||||
<Show when={props.action}>
|
<Show when={props.action}>{props.action}</Show>
|
||||||
{props.action}
|
|
||||||
</Show>
|
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const VStack: ParentComponent<{ biggap?: boolean }> = (props) => {
|
export const VStack: ParentComponent<{ biggap?: boolean }> = (props) => {
|
||||||
return (<div class={`flex flex-col gap-${props.biggap ? "8" : "4"}`}>{props.children}</div>)
|
return (
|
||||||
}
|
<div class={`flex flex-col gap-${props.biggap ? "8" : "4"}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const HStack: ParentComponent<{ biggap?: boolean }> = (props) => {
|
export const HStack: ParentComponent<{ biggap?: boolean }> = (props) => {
|
||||||
return (<div class={`flex gap-${props.biggap ? "8" : "4"}`}>{props.children}</div>)
|
return (
|
||||||
}
|
<div class={`flex gap-${props.biggap ? "8" : "4"}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const SmallAmount: ParentComponent<{ amount: number | bigint, sign?: string }> = (props) => {
|
export const SmallAmount: ParentComponent<{
|
||||||
return (<h2 class="font-light text-lg">{props.sign ? `${props.sign} ` : ""}{props.amount.toLocaleString()} <span class="text-sm">SATS</span></h2>)
|
amount: number | bigint;
|
||||||
}
|
sign?: string;
|
||||||
|
}> = (props) => {
|
||||||
|
return (
|
||||||
|
<h2 class="font-light text-lg">
|
||||||
|
{props.sign ? `${props.sign} ` : ""}
|
||||||
|
{props.amount.toLocaleString()} <span class="text-sm">SATS</span>
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NiceP: ParentComponent = (props) => {
|
export const NiceP: ParentComponent = (props) => {
|
||||||
return (<p class="text-xl font-light">{props.children}</p>)
|
return <p class="text-xl font-light">{props.children}</p>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const TinyButton: ParentComponent<{ onClick: () => void, tag?: MutinyTagItem }> = (props) => {
|
export const TinyButton: ParentComponent<{
|
||||||
|
onClick: () => void;
|
||||||
|
tag?: MutinyTagItem;
|
||||||
|
}> = (props) => {
|
||||||
// TODO: don't need to run this if it's not a contact
|
// TODO: don't need to run this if it's not a contact
|
||||||
const [gradient] = createResource(async () => {
|
const [gradient] = createResource(async () => {
|
||||||
return generateGradient(props.tag?.name || "?")
|
return generateGradient(props.tag?.name || "?");
|
||||||
})
|
});
|
||||||
|
|
||||||
const bg = () => (props.tag?.name && props.tag?.kind === "Contact") ? gradient() : "rgb(255 255 255 / 0.1)"
|
const bg = () =>
|
||||||
|
props.tag?.name && props.tag?.kind === "Contact"
|
||||||
|
? gradient()
|
||||||
|
: "rgb(255 255 255 / 0.1)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}
|
<button
|
||||||
|
class="py-1 px-2 rounded-lg bg-white/10"
|
||||||
|
onClick={() => props.onClick()}
|
||||||
style={{ background: bg() }}
|
style={{ background: bg() }}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Indicator: ParentComponent = (props) => {
|
export const Indicator: ParentComponent = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">{props.children}</div>
|
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">
|
||||||
)
|
{props.children}
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function Checkbox(props: { label: string, checked: boolean, onChange: (checked: boolean) => void }) {
|
export function Checkbox(props: {
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<KCheckbox.Root class="inline-flex items-center gap-2" checked={props.checked} onChange={props.onChange}>
|
<KCheckbox.Root
|
||||||
|
class="inline-flex items-center gap-2"
|
||||||
|
checked={props.checked}
|
||||||
|
onChange={props.onChange}
|
||||||
|
>
|
||||||
<KCheckbox.Input class="" />
|
<KCheckbox.Input class="" />
|
||||||
<KCheckbox.Control class="flex-0 w-8 h-8 rounded-lg border-2 border-white bg-neutral-800 ui-checked:bg-m-red">
|
<KCheckbox.Control class="flex-0 w-8 h-8 rounded-lg border-2 border-white bg-neutral-800 ui-checked:bg-m-red">
|
||||||
<KCheckbox.Indicator>
|
<KCheckbox.Indicator>
|
||||||
<img src={check} class="w-8 h-8" alt="check" />
|
<img src={check} class="w-8 h-8" alt="check" />
|
||||||
</KCheckbox.Indicator>
|
</KCheckbox.Indicator>
|
||||||
</KCheckbox.Control>
|
</KCheckbox.Control>
|
||||||
<KCheckbox.Label class="flex-1 text-xl font-light">{props.label}</KCheckbox.Label>
|
<KCheckbox.Label class="flex-1 text-xl font-light">
|
||||||
|
{props.label}
|
||||||
|
</KCheckbox.Label>
|
||||||
</KCheckbox.Root>
|
</KCheckbox.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModalCloseButton() {
|
export function ModalCloseButton() {
|
||||||
return (<button
|
return (
|
||||||
class="self-center justify-self-center hover:bg-white/10 rounded-lg active:bg-m-blue"
|
<button class="self-center justify-self-center hover:bg-white/10 rounded-lg active:bg-m-blue">
|
||||||
>
|
<img src={close} alt="Close" class="w-8 h-8" />
|
||||||
<img src={close} alt="Close" class="w-8 h-8" />
|
</button>
|
||||||
</button>)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import megacheck from "~/assets/icons/megacheck.png";
|
import megacheck from "~/assets/icons/megacheck.png";
|
||||||
|
|
||||||
export function MegaCheck() {
|
export function MegaCheck() {
|
||||||
return <img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] flex-shrink" />;
|
return (
|
||||||
|
<img
|
||||||
|
src={megacheck}
|
||||||
|
alt="success"
|
||||||
|
class="w-1/2 mx-auto max-w-[50vh] flex-shrink"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import megaex from "~/assets/icons/megaex.png";
|
import megaex from "~/assets/icons/megaex.png";
|
||||||
|
|
||||||
export function MegaEx() {
|
export function MegaEx() {
|
||||||
return <img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />;
|
return (
|
||||||
|
<img
|
||||||
|
src={megaex}
|
||||||
|
alt="fail"
|
||||||
|
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,40 +4,42 @@ import { Button, LargeHeader } from "~/components/layout";
|
|||||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||||
|
|
||||||
type SuccessModalProps = {
|
type SuccessModalProps = {
|
||||||
title: string;
|
title: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
onConfirm?: () => void;
|
onConfirm?: () => void;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SuccessModal(props: SuccessModalProps) {
|
export function SuccessModal(props: SuccessModalProps) {
|
||||||
const onNice = () => {
|
const onNice = () => {
|
||||||
props.onConfirm ? props.onConfirm() : props.setOpen(false);
|
props.onConfirm ? props.onConfirm() : props.setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// <div class="flex flex-col items-center gap-8 h-full max-w-[400px]">
|
// <div class="flex flex-col items-center gap-8 h-full max-w-[400px]">
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<div class={DIALOG_POSITIONER}>
|
<div class={DIALOG_POSITIONER}>
|
||||||
<Dialog.Content class={DIALOG_CONTENT}>
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
<LargeHeader>{props.title}</LargeHeader>
|
<LargeHeader>{props.title}</LargeHeader>
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description class="flex flex-col items-center justify-center gap-8 pb-4 h-full w-full max-w-[400px] mx-auto">
|
<Dialog.Description class="flex flex-col items-center justify-center gap-8 pb-4 h-full w-full max-w-[400px] mx-auto">
|
||||||
{props.children}
|
{props.children}
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
<div class="w-full flex max-w-[300px] mx-auto">
|
<div class="w-full flex max-w-[300px] mx-auto">
|
||||||
<Button onClick={onNice}>{props.confirmText ?? "Nice"}</Button>
|
<Button onClick={onNice}>
|
||||||
</div>
|
{props.confirmText ?? "Nice"}
|
||||||
</Dialog.Content>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Portal>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</div>
|
||||||
);
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Component, For, createEffect, createSignal } from "solid-js";
|
import { Component, For, createEffect, createSignal } from "solid-js";
|
||||||
|
|
||||||
import { Event, nip19 } from "nostr-tools"
|
import { Event, nip19 } from "nostr-tools";
|
||||||
import { Linkify } from "~/components/layout";
|
import { Linkify } from "~/components/layout";
|
||||||
|
|
||||||
type NostrEvent = {
|
type NostrEvent = {
|
||||||
"content": string, "created_at": number, id?: string
|
content: string;
|
||||||
}
|
created_at: number;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const Note: Component<{ e: NostrEvent }> = (props) => {
|
const Note: Component<{ e: NostrEvent }> = (props) => {
|
||||||
const linkRoot = "https://snort.social/e/";
|
const linkRoot = "https://snort.social/e/";
|
||||||
@@ -14,46 +16,62 @@ const Note: Component<{ e: NostrEvent }> = (props) => {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.e.id) {
|
if (props.e.id) {
|
||||||
setNoteId(nip19.noteEncode(props.e.id))
|
setNoteId(nip19.noteEncode(props.e.id));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex gap-4 border-b border-faint-white py-6 items-start w-full">
|
<div class="flex gap-4 border-b border-faint-white py-6 items-start w-full">
|
||||||
<img class="bg-black rounded-xl flex-0" src="../180.png" width={45} height={45} />
|
<img
|
||||||
|
class="bg-black rounded-xl flex-0"
|
||||||
|
src="../180.png"
|
||||||
|
width={45}
|
||||||
|
height={45}
|
||||||
|
/>
|
||||||
<div class="flex flex-col gap-2 flex-1">
|
<div class="flex flex-col gap-2 flex-1">
|
||||||
<p class="break-words">
|
<p class="break-words">
|
||||||
{/* {props.e.content} */}
|
{/* {props.e.content} */}
|
||||||
<Linkify initialText={props.e.content} />
|
<Linkify initialText={props.e.content} />
|
||||||
</p>
|
</p>
|
||||||
<a class="no-underline hover:underline hover:decoration-light-text" href={`${linkRoot}${noteId()}`}>
|
<a
|
||||||
<small class="text-light-text">{(new Date(props.e.created_at * 1000)).toLocaleString()}</small>
|
class="no-underline hover:underline hover:decoration-light-text"
|
||||||
|
href={`${linkRoot}${noteId()}`}
|
||||||
|
>
|
||||||
|
<small class="text-light-text">
|
||||||
|
{new Date(props.e.created_at * 1000).toLocaleString()}
|
||||||
|
</small>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function filterReplies(event: Event) {
|
function filterReplies(event: Event) {
|
||||||
// If there's a "p" tag or an "e" tag we want to return false, otherwise true
|
// If there's a "p" tag or an "e" tag we want to return false, otherwise true
|
||||||
for (const tag of event.tags) {
|
for (const tag of event.tags) {
|
||||||
if (tag[0] === "p" || tag[0] === "e") {
|
if (tag[0] === "p" || tag[0] === "e") {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Notes: Component<{ notes: Event[] }> = (props) => {
|
const Notes: Component<{ notes: Event[] }> = (props) => {
|
||||||
return (<ul class="flex flex-col">
|
return (
|
||||||
<For each={props.notes.filter(filterReplies).sort((a, b) => b.created_at - a.created_at)}>
|
<ul class="flex flex-col">
|
||||||
{(item) =>
|
<For
|
||||||
<li class="w-full"><Note e={item as NostrEvent} /></li>
|
each={props.notes
|
||||||
}
|
.filter(filterReplies)
|
||||||
</For>
|
.sort((a, b) => b.created_at - a.created_at)}
|
||||||
</ul>)
|
>
|
||||||
|
{(item) => (
|
||||||
|
<li class="w-full">
|
||||||
|
<Note e={item as NostrEvent} />
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
export default Notes;
|
||||||
|
|
||||||
export default Notes
|
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ const relayUrls = [
|
|||||||
"wss://nostr.fmt.wiz.biz",
|
"wss://nostr.fmt.wiz.biz",
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://eden.nostr.land"
|
"wss://eden.nostr.land"
|
||||||
]
|
];
|
||||||
|
|
||||||
import { SimplePool } from 'nostr-tools'
|
import { SimplePool } from "nostr-tools";
|
||||||
import { LoadingSpinner } from "~/components/layout";
|
import { LoadingSpinner } from "~/components/layout";
|
||||||
import Notes from "~/components/waitlist/Notes";
|
import Notes from "~/components/waitlist/Notes";
|
||||||
import logo from '~/assets/icons/mutiny-logo.svg';
|
import logo from "~/assets/icons/mutiny-logo.svg";
|
||||||
|
|
||||||
const pool = new SimplePool()
|
const pool = new SimplePool();
|
||||||
|
|
||||||
const postsFetcher = async () => {
|
const postsFetcher = async () => {
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
authors: [
|
authors: [
|
||||||
"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708"
|
"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708"
|
||||||
@@ -26,34 +25,36 @@ const postsFetcher = async () => {
|
|||||||
kinds: [1]
|
kinds: [1]
|
||||||
};
|
};
|
||||||
|
|
||||||
const events = await pool.list(relayUrls, [filter])
|
const events = await pool.list(relayUrls, [filter]);
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function WaitlistAlreadyIn() {
|
export function WaitlistAlreadyIn() {
|
||||||
const [posts] = createResource("", postsFetcher);
|
const [posts] = createResource("", postsFetcher);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="flex flex-col gap-4 sm:gap-4 py-8 px-4 max-w-xl mx-auto items-start drop-shadow-blue-glow">
|
<main class="flex flex-col gap-4 sm:gap-4 py-8 px-4 max-w-xl mx-auto items-start drop-shadow-blue-glow">
|
||||||
<a href="https://mutinywallet.com">
|
<a href="https://mutinywallet.com">
|
||||||
<img src={logo} class="h-10" alt="logo" />
|
<img src={logo} class="h-10" alt="logo" />
|
||||||
</a>
|
</a>
|
||||||
<h1 class="text-4xl font-bold">You're on a list!</h1>
|
<h1 class="text-4xl font-bold">You're on a list!</h1>
|
||||||
<h2 class="text-xl pr-4">We'll message you when Mutiny Wallet is ready.</h2>
|
<h2 class="text-xl pr-4">
|
||||||
<div class="px-4 sm:px-8 py-8 rounded-xl bg-half-black w-full">
|
We'll message you when Mutiny Wallet is ready.
|
||||||
<h2 class="text-sm font-semibold uppercase">Recent Updates</h2>
|
</h2>
|
||||||
<Show
|
<div class="px-4 sm:px-8 py-8 rounded-xl bg-half-black w-full">
|
||||||
when={!posts.loading}
|
<h2 class="text-sm font-semibold uppercase">Recent Updates</h2>
|
||||||
fallback={
|
<Show
|
||||||
<div class="h-[10rem]">
|
when={!posts.loading}
|
||||||
<LoadingSpinner big wide />
|
fallback={
|
||||||
</div>
|
<div class="h-[10rem]">
|
||||||
}
|
<LoadingSpinner big wide />
|
||||||
>
|
</div>
|
||||||
<Notes notes={(posts() && posts()) || []} />
|
}
|
||||||
</Show>
|
>
|
||||||
</div>
|
<Notes notes={(posts() && posts()) || []} />
|
||||||
</main>
|
</Show>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,109 +2,190 @@ import { Match, Switch, createSignal } from "solid-js";
|
|||||||
import { Button } from "~/components/layout";
|
import { Button } from "~/components/layout";
|
||||||
import { StyledRadioGroup } from "../layout/Radio";
|
import { StyledRadioGroup } from "../layout/Radio";
|
||||||
import { TextField } from "../layout/TextField";
|
import { TextField } from "../layout/TextField";
|
||||||
import { SubmitHandler, createForm, email, getValue, required, setValue } from "@modular-forms/solid";
|
import {
|
||||||
|
SubmitHandler,
|
||||||
|
createForm,
|
||||||
|
email,
|
||||||
|
getValue,
|
||||||
|
required,
|
||||||
|
setValue
|
||||||
|
} from "@modular-forms/solid";
|
||||||
import { showToast } from "../Toaster";
|
import { showToast } from "../Toaster";
|
||||||
import eify from "~/utils/eify";
|
import eify from "~/utils/eify";
|
||||||
import logo from '~/assets/icons/mutiny-logo.svg';
|
import logo from "~/assets/icons/mutiny-logo.svg";
|
||||||
|
|
||||||
const WAITLIST_ENDPOINT = "https://waitlist.mutiny-waitlist.workers.dev/waitlist";
|
const WAITLIST_ENDPOINT =
|
||||||
|
"https://waitlist.mutiny-waitlist.workers.dev/waitlist";
|
||||||
|
|
||||||
const COMMUNICATION_METHODS = [{ value: "nostr", label: "Nostr", caption: "Your freshest npub" }, { value: "email", label: "Email", caption: "Burners welcome" }]
|
const COMMUNICATION_METHODS = [
|
||||||
|
{ value: "nostr", label: "Nostr", caption: "Your freshest npub" },
|
||||||
|
{ value: "email", label: "Email", caption: "Burners welcome" }
|
||||||
|
];
|
||||||
|
|
||||||
type WaitlistForm = {
|
type WaitlistForm = {
|
||||||
user_type: "nostr" | "email",
|
user_type: "nostr" | "email";
|
||||||
id: string
|
id: string;
|
||||||
comment?: string
|
comment?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const initialValues: WaitlistForm = { user_type: "nostr", id: "", comment: "" };
|
const initialValues: WaitlistForm = { user_type: "nostr", id: "", comment: "" };
|
||||||
|
|
||||||
export default function WaitlistForm() {
|
export default function WaitlistForm() {
|
||||||
const [waitlistForm, { Form, Field }] = createForm<WaitlistForm>({ initialValues });
|
const [waitlistForm, { Form, Field }] = createForm<WaitlistForm>({
|
||||||
|
initialValues
|
||||||
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
const newHandleSubmit: SubmitHandler<WaitlistForm> = async (f: WaitlistForm) => {
|
const newHandleSubmit: SubmitHandler<WaitlistForm> = async (
|
||||||
|
f: WaitlistForm
|
||||||
|
) => {
|
||||||
console.log(f);
|
console.log(f);
|
||||||
|
|
||||||
// TODO: not sure why waitlistForm.submitting doesn't work for me
|
// TODO: not sure why waitlistForm.submitting doesn't work for me
|
||||||
// https://modularforms.dev/solid/guides/handle-submission
|
// https://modularforms.dev/solid/guides/handle-submission
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(WAITLIST_ENDPOINT, {
|
const res = await fetch(WAITLIST_ENDPOINT, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(f)
|
body: JSON.stringify(f)
|
||||||
})
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error("nope");
|
throw new Error("nope");
|
||||||
} else {
|
} else {
|
||||||
// On success set the id in local storage and reload the page
|
// On success set the id in local storage and reload the page
|
||||||
localStorage.setItem('waitlist_id', f.id);
|
localStorage.setItem("waitlist_id", f.id);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (f.user_type === "nostr") {
|
if (f.user_type === "nostr") {
|
||||||
const error = new Error("Something went wrong. Are you sure that's a valid npub?")
|
const error = new Error(
|
||||||
showToast(eify(error))
|
"Something went wrong. Are you sure that's a valid npub?"
|
||||||
|
);
|
||||||
|
showToast(eify(error));
|
||||||
} else {
|
} else {
|
||||||
const error = new Error("Something went wrong. Not sure what.")
|
const error = new Error("Something went wrong. Not sure what.");
|
||||||
showToast(eify(error))
|
showToast(eify(error));
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class='flex flex-col gap-8 py-8 px-4 max-w-xl mx-auto'>
|
<main class="flex flex-col gap-8 py-8 px-4 max-w-xl mx-auto">
|
||||||
<a href="https://mutinywallet.com">
|
<a href="https://mutinywallet.com">
|
||||||
<img src={logo} class="h-10" alt="logo" />
|
<img src={logo} class="h-10" alt="logo" />
|
||||||
</a>
|
</a>
|
||||||
<h1 class='text-4xl font-bold'>Join Waitlist</h1>
|
<h1 class="text-4xl font-bold">Join Waitlist</h1>
|
||||||
<h2 class="text-xl">
|
<h2 class="text-xl">
|
||||||
Sign up for our waitlist and we'll send a message when Mutiny Wallet is ready for you.
|
Sign up for our waitlist and we'll send a message when Mutiny
|
||||||
|
Wallet is ready for you.
|
||||||
</h2>
|
</h2>
|
||||||
<Form onSubmit={newHandleSubmit} class="flex flex-col gap-8">
|
<Form onSubmit={newHandleSubmit} class="flex flex-col gap-8">
|
||||||
<Field name="user_type">
|
<Field name="user_type">
|
||||||
{(field, _props) => (
|
{(field, _props) => (
|
||||||
// TODO: there's probably a "real" way to do this with modular-forms
|
// TODO: there's probably a "real" way to do this with modular-forms
|
||||||
<StyledRadioGroup value={field.value || "nostr"} onValueChange={(newValue) => setValue(waitlistForm, "user_type", newValue as "nostr" | "email")} choices={COMMUNICATION_METHODS} />
|
<StyledRadioGroup
|
||||||
|
value={field.value || "nostr"}
|
||||||
|
onValueChange={(newValue) =>
|
||||||
|
setValue(
|
||||||
|
waitlistForm,
|
||||||
|
"user_type",
|
||||||
|
newValue as "nostr" | "email"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
choices={COMMUNICATION_METHODS}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={getValue(waitlistForm, 'user_type', { shouldActive: false }) === 'nostr'}>
|
<Match
|
||||||
<Field name="id"
|
when={
|
||||||
validate={[required("We need some way to contact you")]}
|
getValue(waitlistForm, "user_type", {
|
||||||
|
shouldActive: false
|
||||||
|
}) === "nostr"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
name="id"
|
||||||
|
validate={[
|
||||||
|
required("We need some way to contact you")
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField {...props} value={field.value} error={field.error} label="Nostr npub or NIP-05" placeholder="npub..." />
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Nostr npub or NIP-05"
|
||||||
|
placeholder="npub..."
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={getValue(waitlistForm, 'user_type', { shouldActive: false }) === 'email'}>
|
<Match
|
||||||
<Field name="id"
|
when={
|
||||||
validate={[required("We need some way to contact you"), email("That doesn't look like an email address to me")]}
|
getValue(waitlistForm, "user_type", {
|
||||||
|
shouldActive: false
|
||||||
|
}) === "email"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
name="id"
|
||||||
|
validate={[
|
||||||
|
required("We need some way to contact you"),
|
||||||
|
email(
|
||||||
|
"That doesn't look like an email address to me"
|
||||||
|
)
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField {...props} value={field.value} error={field.error} type="email" label="Email" placeholder="email@nokycemail.com" />
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
type="email"
|
||||||
|
label="Email"
|
||||||
|
placeholder="email@nokycemail.com"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Field name="comment">
|
<Field name="comment">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField multiline {...props} value={field.value} error={field.error} label="Comments" placeholder="I want a lightning wallet that does..." />
|
<TextField
|
||||||
|
multiline
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Comments"
|
||||||
|
placeholder="I want a lightning wallet that does..."
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Button loading={loading()} disabled={loading() || !waitlistForm.dirty || waitlistForm.submitting || waitlistForm.invalid} class="self-start" intent="red" type="submit" layout="pad">Submit</Button>
|
<Button
|
||||||
|
loading={loading()}
|
||||||
|
disabled={
|
||||||
|
loading() ||
|
||||||
|
!waitlistForm.dirty ||
|
||||||
|
waitlistForm.submitting ||
|
||||||
|
waitlistForm.invalid
|
||||||
|
}
|
||||||
|
class="self-start"
|
||||||
|
intent="red"
|
||||||
|
type="submit"
|
||||||
|
layout="pad"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
createHandler,
|
createHandler,
|
||||||
renderAsync,
|
renderAsync,
|
||||||
StartServer,
|
StartServer
|
||||||
} from "solid-start/entry-server";
|
} from "solid-start/entry-server";
|
||||||
|
|
||||||
export default createHandler(
|
export default createHandler(
|
||||||
renderAsync((event) => <StartServer event={event} />)
|
renderAsync((event) => <StartServer event={event} />)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
import initMutinyWallet, { MutinyWallet } from '@mutinywallet/mutiny-wasm';
|
import initWaila from "@mutinywallet/waila-wasm";
|
||||||
import initWaila from '@mutinywallet/waila-wasm'
|
|
||||||
|
|
||||||
// export type MutinyWalletSettingStrings = {
|
// export type MutinyWalletSettingStrings = {
|
||||||
// network?: string, proxy?: string, esplora?: string, rgs?: string, lsp?: string,
|
// network?: string, proxy?: string, esplora?: string, rgs?: string, lsp?: string,
|
||||||
@@ -8,20 +7,34 @@ import initWaila from '@mutinywallet/waila-wasm'
|
|||||||
|
|
||||||
export type Network = "bitcoin" | "testnet" | "regtest" | "signet";
|
export type Network = "bitcoin" | "testnet" | "regtest" | "signet";
|
||||||
export type MutinyWalletSettingStrings = {
|
export type MutinyWalletSettingStrings = {
|
||||||
network?: Network, proxy?: string, esplora?: string, rgs?: string, lsp?: string,
|
network?: Network;
|
||||||
}
|
proxy?: string;
|
||||||
|
esplora?: string;
|
||||||
|
rgs?: string;
|
||||||
|
lsp?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function getExistingSettings(): MutinyWalletSettingStrings {
|
export function getExistingSettings(): MutinyWalletSettingStrings {
|
||||||
const network = localStorage.getItem('MUTINY_SETTINGS_network') || import.meta.env.VITE_NETWORK;
|
const network =
|
||||||
const proxy = localStorage.getItem('MUTINY_SETTINGS_proxy') || import.meta.env.VITE_PROXY;
|
localStorage.getItem("MUTINY_SETTINGS_network") ||
|
||||||
const esplora = localStorage.getItem('MUTINY_SETTINGS_esplora') || import.meta.env.VITE_ESPLORA;
|
import.meta.env.VITE_NETWORK;
|
||||||
const rgs = localStorage.getItem('MUTINY_SETTINGS_rgs') || import.meta.env.VITE_RGS;
|
const proxy =
|
||||||
const lsp = localStorage.getItem('MUTINY_SETTINGS_lsp') || import.meta.env.VITE_LSP;
|
localStorage.getItem("MUTINY_SETTINGS_proxy") ||
|
||||||
|
import.meta.env.VITE_PROXY;
|
||||||
|
const esplora =
|
||||||
|
localStorage.getItem("MUTINY_SETTINGS_esplora") ||
|
||||||
|
import.meta.env.VITE_ESPLORA;
|
||||||
|
const rgs =
|
||||||
|
localStorage.getItem("MUTINY_SETTINGS_rgs") || import.meta.env.VITE_RGS;
|
||||||
|
const lsp =
|
||||||
|
localStorage.getItem("MUTINY_SETTINGS_lsp") || import.meta.env.VITE_LSP;
|
||||||
|
|
||||||
return { network, proxy, esplora, rgs, lsp }
|
return { network, proxy, esplora, rgs, lsp };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setAndGetMutinySettings(settings?: MutinyWalletSettingStrings): Promise<MutinyWalletSettingStrings> {
|
export async function setAndGetMutinySettings(
|
||||||
|
settings?: MutinyWalletSettingStrings
|
||||||
|
): Promise<MutinyWalletSettingStrings> {
|
||||||
let { network, proxy, esplora, rgs, lsp } = settings || {};
|
let { network, proxy, esplora, rgs, lsp } = settings || {};
|
||||||
|
|
||||||
const existingSettings = getExistingSettings();
|
const existingSettings = getExistingSettings();
|
||||||
@@ -33,66 +46,84 @@ export async function setAndGetMutinySettings(settings?: MutinyWalletSettingStri
|
|||||||
lsp = lsp || existingSettings.lsp;
|
lsp = lsp || existingSettings.lsp;
|
||||||
|
|
||||||
if (!network || !proxy || !esplora) {
|
if (!network || !proxy || !esplora) {
|
||||||
throw new Error("Missing a default setting for network, proxy, or esplora. Check your .env file to make sure it looks like .env.sample")
|
throw new Error(
|
||||||
|
"Missing a default setting for network, proxy, or esplora. Check your .env file to make sure it looks like .env.sample"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('MUTINY_SETTINGS_network', network);
|
localStorage.setItem("MUTINY_SETTINGS_network", network);
|
||||||
localStorage.setItem('MUTINY_SETTINGS_proxy', proxy);
|
localStorage.setItem("MUTINY_SETTINGS_proxy", proxy);
|
||||||
localStorage.setItem('MUTINY_SETTINGS_esplora', esplora);
|
localStorage.setItem("MUTINY_SETTINGS_esplora", esplora);
|
||||||
|
|
||||||
if (!rgs || !lsp) {
|
if (!rgs || !lsp) {
|
||||||
console.warn("RGS or LSP not set")
|
console.warn("RGS or LSP not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
rgs && localStorage.setItem('MUTINY_SETTINGS_rgs', rgs);
|
rgs && localStorage.setItem("MUTINY_SETTINGS_rgs", rgs);
|
||||||
lsp && localStorage.setItem('MUTINY_SETTINGS_lsp', lsp);
|
lsp && localStorage.setItem("MUTINY_SETTINGS_lsp", lsp);
|
||||||
|
|
||||||
return { network, proxy, esplora, rgs, lsp }
|
return { network, proxy, esplora, rgs, lsp };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkForWasm() {
|
export async function checkForWasm() {
|
||||||
try {
|
try {
|
||||||
if (typeof WebAssembly === "object"
|
if (
|
||||||
&& typeof WebAssembly.instantiate === "function") {
|
typeof WebAssembly === "object" &&
|
||||||
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
|
typeof WebAssembly.instantiate === "function"
|
||||||
|
) {
|
||||||
|
const module = new WebAssembly.Module(
|
||||||
|
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
|
||||||
|
);
|
||||||
if (!(module instanceof WebAssembly.Module)) {
|
if (!(module instanceof WebAssembly.Module)) {
|
||||||
throw new Error("Couldn't instantiate WASM Module")
|
throw new Error("Couldn't instantiate WASM Module");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("No WebAssembly global object found")
|
throw new Error("No WebAssembly global object found");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<MutinyWallet> {
|
export async function setupMutinyWallet(
|
||||||
|
settings?: MutinyWalletSettingStrings
|
||||||
|
): Promise<MutinyWallet> {
|
||||||
await initMutinyWallet();
|
await initMutinyWallet();
|
||||||
// Might as well init waila while we're at it
|
// Might as well init waila while we're at it
|
||||||
await initWaila();
|
await initWaila();
|
||||||
|
|
||||||
console.time("Setup");
|
console.time("Setup");
|
||||||
console.log("Starting setup...")
|
console.log("Starting setup...");
|
||||||
const { network, proxy, esplora, rgs, lsp } = await setAndGetMutinySettings(settings)
|
const { network, proxy, esplora, rgs, lsp } = await setAndGetMutinySettings(
|
||||||
console.log("Initializing Mutiny Manager")
|
settings
|
||||||
|
);
|
||||||
|
console.log("Initializing Mutiny Manager");
|
||||||
console.log("Using network", network);
|
console.log("Using network", network);
|
||||||
console.log("Using proxy", proxy);
|
console.log("Using proxy", proxy);
|
||||||
console.log("Using esplora address", esplora);
|
console.log("Using esplora address", esplora);
|
||||||
console.log("Using rgs address", rgs);
|
console.log("Using rgs address", rgs);
|
||||||
console.log("Using lsp address", lsp);
|
console.log("Using lsp address", lsp);
|
||||||
|
|
||||||
const mutinyWallet = await new MutinyWallet("", undefined, proxy, network, esplora, rgs, lsp)
|
const mutinyWallet = await new MutinyWallet(
|
||||||
|
"",
|
||||||
|
undefined,
|
||||||
|
proxy,
|
||||||
|
network,
|
||||||
|
esplora,
|
||||||
|
rgs,
|
||||||
|
lsp
|
||||||
|
);
|
||||||
|
|
||||||
const nodes = await mutinyWallet.list_nodes();
|
const nodes = await mutinyWallet.list_nodes();
|
||||||
|
|
||||||
// If we don't have any nodes yet, create one
|
// If we don't have any nodes yet, create one
|
||||||
if (!nodes.length) {
|
if (!nodes.length) {
|
||||||
await mutinyWallet?.new_node()
|
await mutinyWallet?.new_node();
|
||||||
}
|
}
|
||||||
|
|
||||||
return mutinyWallet
|
return mutinyWallet;
|
||||||
}
|
}
|
||||||
46
src/root.css
46
src/root.css
@@ -3,51 +3,51 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply text-white bg-neutral-900;
|
@apply text-white bg-neutral-900;
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
min-height: 100.3%;
|
min-height: 100.3%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-gradient {
|
.bg-gradient {
|
||||||
@apply bg-fixed bg-no-repeat bg-gradient-to-b from-black to-[#0b215b];
|
@apply bg-fixed bg-no-repeat bg-gradient-to-b from-black to-[#0b215b];
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-gray {
|
.bg-gray {
|
||||||
@apply bg-fixed bg-no-repeat bg-gradient-to-b from-[hsl(224,5%,5%)] to-[hsl(224,5%,20%)];
|
@apply bg-fixed bg-no-repeat bg-gradient-to-b from-[hsl(224,5%,5%)] to-[hsl(224,5%,20%)];
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-modal-sheet-container {
|
.react-modal-sheet-container {
|
||||||
@apply !bg-[#262626];
|
@apply !bg-[#262626];
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply underline decoration-light-text hover:decoration-white;
|
@apply underline decoration-light-text hover:decoration-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#video-container {
|
#video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#video-container .scan-region-highlight {
|
#video-container .scan-region-highlight {
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
outline: rgba(0, 0, 0, 0.25) solid 50vmax;
|
outline: rgba(0, 0, 0, 0.25) solid 50vmax;
|
||||||
}
|
}
|
||||||
|
|
||||||
#video-container .scan-region-highlight-svg {
|
#video-container .scan-region-highlight-svg {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@apply appearance-none;
|
@apply appearance-none;
|
||||||
@apply block;
|
@apply block;
|
||||||
@apply border-[2px] focus:outline-none focus:ring-2 focus:ring-offset-2 ring-offset-black;
|
@apply border-[2px] focus:outline-none focus:ring-2 focus:ring-offset-2 ring-offset-black;
|
||||||
@apply font-light text-lg;
|
@apply font-light text-lg;
|
||||||
@apply py-4 pl-4 pr-8;
|
@apply py-4 pl-4 pr-8;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg aria-hidden='true' class='w-4 h-4 ml-1' fill='white' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 0 1 1.414 0L10 10.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg aria-hidden='true' class='w-4 h-4 ml-1' fill='white' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 0 1 1.414 0L10 10.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E");
|
||||||
background-position: right 0.75rem center;
|
background-position: right 0.75rem center;
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
93
src/root.tsx
93
src/root.tsx
@@ -1,16 +1,16 @@
|
|||||||
// @refresh reload
|
// @refresh reload
|
||||||
import { Suspense } from "solid-js";
|
import { Suspense } from "solid-js";
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
FileRoutes,
|
FileRoutes,
|
||||||
Head,
|
Head,
|
||||||
Html,
|
Html,
|
||||||
Link,
|
Link,
|
||||||
Meta,
|
Meta,
|
||||||
Routes,
|
Routes,
|
||||||
Scripts,
|
Scripts,
|
||||||
Title,
|
Title
|
||||||
} from "solid-start";
|
} from "solid-start";
|
||||||
import "./root.css";
|
import "./root.css";
|
||||||
import { Provider as MegaStoreProvider } from "~/state/megaStore";
|
import { Provider as MegaStoreProvider } from "~/state/megaStore";
|
||||||
@@ -18,35 +18,46 @@ import { Toaster } from "~/components/Toaster";
|
|||||||
import ErrorDisplay from "./components/ErrorDisplay";
|
import ErrorDisplay from "./components/ErrorDisplay";
|
||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
return (
|
return (
|
||||||
<Html lang="en">
|
<Html lang="en">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Mutiny Wallet</Title>
|
<Title>Mutiny Wallet</Title>
|
||||||
<Meta charset="utf-8" />
|
<Meta charset="utf-8" />
|
||||||
<Meta
|
<Meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0 height=device-height viewport-fit=cover user-scalable=no"
|
content="width=device-width, initial-scale=1.0 height=device-height viewport-fit=cover user-scalable=no"
|
||||||
/>
|
/>
|
||||||
<Link rel="manifest" href="/manifest.webmanifest" />
|
<Link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<Meta name="theme-color" content="rgb(23,23,23)" />
|
<Meta name="theme-color" content="rgb(23,23,23)" />
|
||||||
<Meta name="description" content="Lightning wallet for the web" />
|
<Meta
|
||||||
<Link rel="icon" href="/favicon.ico" />
|
name="description"
|
||||||
<Link rel="apple-touch-icon" href="/images/icon.png" sizes="512x512" />
|
content="Lightning wallet for the web"
|
||||||
<Link rel="mask-icon" href="/mutiny_logo_mask.svg" color="#000" />
|
/>
|
||||||
</Head>
|
<Link rel="icon" href="/favicon.ico" />
|
||||||
<Body>
|
<Link
|
||||||
<Suspense>
|
rel="apple-touch-icon"
|
||||||
<ErrorBoundary fallback={(e) => <ErrorDisplay error={e} />}>
|
href="/images/icon.png"
|
||||||
<MegaStoreProvider>
|
sizes="512x512"
|
||||||
<Routes>
|
/>
|
||||||
<FileRoutes />
|
<Link
|
||||||
</Routes>
|
rel="mask-icon"
|
||||||
<Toaster />
|
href="/mutiny_logo_mask.svg"
|
||||||
</MegaStoreProvider>
|
color="#000"
|
||||||
</ErrorBoundary>
|
/>
|
||||||
</Suspense>
|
</Head>
|
||||||
<Scripts />
|
<Body>
|
||||||
</Body>
|
<Suspense>
|
||||||
</Html>
|
<ErrorBoundary fallback={(e) => <ErrorDisplay error={e} />}>
|
||||||
);
|
<MegaStoreProvider>
|
||||||
|
<Routes>
|
||||||
|
<FileRoutes />
|
||||||
|
</Routes>
|
||||||
|
<Toaster />
|
||||||
|
</MegaStoreProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Suspense>
|
||||||
|
<Scripts />
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { For, Show, createResource } from "solid-js";
|
import { For, Show, createResource } from "solid-js";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
LargeHeader,
|
LargeHeader,
|
||||||
NiceP,
|
NiceP,
|
||||||
MutinyWalletGuard,
|
MutinyWalletGuard,
|
||||||
SafeArea,
|
SafeArea,
|
||||||
VStack
|
VStack
|
||||||
} from "~/components/layout";
|
} from "~/components/layout";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
import { CombinedActivity } from "~/components/Activity";
|
import { CombinedActivity } from "~/components/Activity";
|
||||||
@@ -24,114 +24,120 @@ import { showToast } from "~/components/Toaster";
|
|||||||
import { LoadingShimmer } from "~/components/BalanceBox";
|
import { LoadingShimmer } from "~/components/BalanceBox";
|
||||||
|
|
||||||
function ContactRow() {
|
function ContactRow() {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
const [contacts, { refetch }] = createResource(async () => {
|
const [contacts, { refetch }] = createResource(async () => {
|
||||||
const contacts = state.mutiny_wallet?.get_contacts();
|
const contacts = state.mutiny_wallet?.get_contacts();
|
||||||
console.log(contacts);
|
console.log(contacts);
|
||||||
|
|
||||||
// FIXME: this is just types shenanigans I believe
|
// FIXME: this is just types shenanigans I believe
|
||||||
const c: Contact[] = [];
|
const c: Contact[] = [];
|
||||||
if (contacts) {
|
if (contacts) {
|
||||||
for (const contact in contacts) {
|
for (const contact in contacts) {
|
||||||
c.push(contacts[contact]);
|
c.push(contacts[contact]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return c || [];
|
||||||
|
});
|
||||||
|
const [gradients] = createResource(contacts, gradientsPerContact);
|
||||||
|
|
||||||
|
async function createContact(contact: ContactFormValues) {
|
||||||
|
// FIXME: npub not valid? other undefineds
|
||||||
|
const c = new Contact(contact.name, undefined, undefined, undefined);
|
||||||
|
await state.mutiny_wallet?.create_new_contact(c);
|
||||||
|
refetch();
|
||||||
}
|
}
|
||||||
return c || [];
|
|
||||||
});
|
|
||||||
const [gradients] = createResource(contacts, gradientsPerContact);
|
|
||||||
|
|
||||||
async function createContact(contact: ContactFormValues) {
|
//
|
||||||
// FIXME: npub not valid? other undefineds
|
async function saveContact(_contact: ContactFormValues) {
|
||||||
const c = new Contact(contact.name, undefined, undefined, undefined);
|
showToast(new Error("Unimplemented"));
|
||||||
await state.mutiny_wallet?.create_new_contact(c);
|
// await editContact(contact)
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
return (
|
||||||
async function saveContact(_contact: ContactFormValues) {
|
<div class="flex gap-4">
|
||||||
showToast(new Error("Unimplemented"));
|
<ContactEditor list createContact={createContact} />
|
||||||
// await editContact(contact)
|
<Show when={contacts()}>
|
||||||
refetch();
|
<div class="flex gap-4 flex-1 overflow-x-scroll disable-scrollbars">
|
||||||
}
|
<For each={contacts()}>
|
||||||
|
{(contact) => (
|
||||||
return (
|
<ContactViewer
|
||||||
<div class="flex gap-4">
|
contact={contact}
|
||||||
<ContactEditor list createContact={createContact} />
|
gradient={gradients()?.get(contact.name)}
|
||||||
<Show when={contacts()}>
|
saveContact={saveContact}
|
||||||
<div class="flex gap-4 flex-1 overflow-x-scroll disable-scrollbars">
|
/>
|
||||||
<For each={contacts()}>
|
)}
|
||||||
{(contact) => (
|
</For>
|
||||||
<ContactViewer
|
</div>
|
||||||
contact={contact}
|
</Show>
|
||||||
gradient={gradients()?.get(contact.name)}
|
|
||||||
saveContact={saveContact}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAB =
|
const TAB =
|
||||||
"flex-1 inline-block px-8 py-4 text-lg font-semibold rounded-lg ui-selected:bg-white/10 bg-neutral-950 hover:bg-white/10";
|
"flex-1 inline-block px-8 py-4 text-lg font-semibold rounded-lg ui-selected:bg-white/10 bg-neutral-950 hover:bg-white/10";
|
||||||
|
|
||||||
export default function Activity() {
|
export default function Activity() {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
return (
|
return (
|
||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink />
|
<BackLink />
|
||||||
<LargeHeader
|
<LargeHeader
|
||||||
action={
|
action={
|
||||||
<A
|
<A
|
||||||
class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue"
|
class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue"
|
||||||
href="/settings"
|
href="/settings"
|
||||||
>
|
>
|
||||||
<img src={settings} alt="Settings" />
|
<img src={settings} alt="Settings" />
|
||||||
</A>
|
</A>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Activity
|
Activity
|
||||||
</LargeHeader>
|
</LargeHeader>
|
||||||
<ContactRow />
|
<ContactRow />
|
||||||
<Tabs.Root defaultValue="mutiny">
|
<Tabs.Root defaultValue="mutiny">
|
||||||
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
|
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
|
||||||
<Tabs.Trigger value="mutiny" class={TAB}>
|
<Tabs.Trigger value="mutiny" class={TAB}>
|
||||||
Mutiny
|
Mutiny
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="nostr" class={TAB}>
|
<Tabs.Trigger value="nostr" class={TAB}>
|
||||||
Nostr
|
Nostr
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
{/* <Tabs.Indicator class="absolute bg-m-blue transition-all bottom-[-1px] h-[2px]" /> */}
|
{/* <Tabs.Indicator class="absolute bg-m-blue transition-all bottom-[-1px] h-[2px]" /> */}
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Content value="mutiny">
|
<Tabs.Content value="mutiny">
|
||||||
{/* <MutinyActivity /> */}
|
{/* <MutinyActivity /> */}
|
||||||
<Card title="Activity">
|
<Card title="Activity">
|
||||||
<div class="p-1" />
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
|
<Show
|
||||||
<CombinedActivity />
|
when={!state.wallet_loading}
|
||||||
</Show>
|
fallback={<LoadingShimmer />}
|
||||||
</VStack>
|
>
|
||||||
</Card>
|
<CombinedActivity />
|
||||||
</Tabs.Content>
|
</Show>
|
||||||
<Tabs.Content value="nostr">
|
</VStack>
|
||||||
<VStack>
|
</Card>
|
||||||
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
|
</Tabs.Content>
|
||||||
<NiceP>Import your contacts from nostr to see who they're zapping.</NiceP>
|
<Tabs.Content value="nostr">
|
||||||
<Button disabled intent="blue">
|
<VStack>
|
||||||
Coming soon
|
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
|
||||||
</Button>
|
<NiceP>
|
||||||
</div>
|
Import your contacts from nostr to see
|
||||||
</VStack>
|
who they're zapping.
|
||||||
</Tabs.Content>
|
</NiceP>
|
||||||
</Tabs.Root>
|
<Button disabled intent="blue">
|
||||||
</DefaultMain>
|
Coming soon
|
||||||
<NavBar activeTab="activity" />
|
</Button>
|
||||||
</SafeArea>
|
</div>
|
||||||
</MutinyWalletGuard>
|
</VStack>
|
||||||
);
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
</DefaultMain>
|
||||||
|
<NavBar activeTab="activity" />
|
||||||
|
</SafeArea>
|
||||||
|
</MutinyWalletGuard>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { DeleteEverything } from "~/components/DeleteEverything";
|
import { DeleteEverything } from "~/components/DeleteEverything";
|
||||||
import KitchenSink from "~/components/KitchenSink";
|
import KitchenSink from "~/components/KitchenSink";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { Card, DefaultMain, LargeHeader, MutinyWalletGuard, SafeArea, SmallHeader, VStack } from "~/components/layout";
|
import {
|
||||||
|
Card,
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
MutinyWalletGuard,
|
||||||
|
SafeArea,
|
||||||
|
SmallHeader,
|
||||||
|
VStack
|
||||||
|
} from "~/components/layout";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
|
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
@@ -12,9 +20,14 @@ export default function Admin() {
|
|||||||
<BackLink href="/settings" title="Settings" />
|
<BackLink href="/settings" title="Settings" />
|
||||||
<LargeHeader>Admin</LargeHeader>
|
<LargeHeader>Admin</LargeHeader>
|
||||||
<VStack>
|
<VStack>
|
||||||
<Card><p>If you know what you're doing you're in the right place!</p></Card>
|
<Card>
|
||||||
|
<p>
|
||||||
|
If you know what you're doing you're in the
|
||||||
|
right place!
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
<KitchenSink />
|
<KitchenSink />
|
||||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden'>
|
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden">
|
||||||
<SmallHeader>Danger zone</SmallHeader>
|
<SmallHeader>Danger zone</SmallHeader>
|
||||||
<DeleteEverything />
|
<DeleteEverything />
|
||||||
</div>
|
</div>
|
||||||
@@ -23,5 +36,5 @@ export default function Admin() {
|
|||||||
<NavBar activeTab="none" />
|
<NavBar activeTab="none" />
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
</MutinyWalletGuard>
|
</MutinyWalletGuard>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
import { Button, DefaultMain, LargeHeader, NiceP, MutinyWalletGuard, SafeArea, VStack, Checkbox } from "~/components/layout";
|
import {
|
||||||
|
Button,
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
NiceP,
|
||||||
|
MutinyWalletGuard,
|
||||||
|
SafeArea,
|
||||||
|
VStack,
|
||||||
|
Checkbox
|
||||||
|
} from "~/components/layout";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { useNavigate } from 'solid-start';
|
import { useNavigate } from "solid-start";
|
||||||
import { SeedWords } from '~/components/SeedWords';
|
import { SeedWords } from "~/components/SeedWords";
|
||||||
import { useMegaStore } from '~/state/megaStore';
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { Show, createEffect, createSignal } from 'solid-js';
|
import { Show, createEffect, createSignal } from "solid-js";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
|
|
||||||
function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
|
function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
|
||||||
@@ -13,19 +22,31 @@ function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (one() && two() && three()) {
|
if (one() && two() && three()) {
|
||||||
props.setHasCheckedAll(true)
|
props.setHasCheckedAll(true);
|
||||||
} else {
|
} else {
|
||||||
props.setHasCheckedAll(false)
|
props.setHasCheckedAll(false);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<VStack>
|
||||||
<Checkbox checked={one()} onChange={setOne} label="I wrote down the words" />
|
<Checkbox
|
||||||
<Checkbox checked={two()} onChange={setTwo} label="I understand that my funds are my responsibility" />
|
checked={one()}
|
||||||
<Checkbox checked={three()} onChange={setThree} label="I'm not lying just to get this over with" />
|
onChange={setOne}
|
||||||
|
label="I wrote down the words"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={two()}
|
||||||
|
onChange={setTwo}
|
||||||
|
label="I understand that my funds are my responsibility"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={three()}
|
||||||
|
onChange={setThree}
|
||||||
|
label="I'm not lying just to get this over with"
|
||||||
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -36,8 +57,8 @@ export default function App() {
|
|||||||
const [hasCheckedAll, setHasCheckedAll] = createSignal(false);
|
const [hasCheckedAll, setHasCheckedAll] = createSignal(false);
|
||||||
|
|
||||||
function wroteDownTheWords() {
|
function wroteDownTheWords() {
|
||||||
actions.setHasBackedUp()
|
actions.setHasBackedUp();
|
||||||
navigate("/")
|
navigate("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,16 +70,32 @@ export default function App() {
|
|||||||
|
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>Let's get these funds secured.</NiceP>
|
<NiceP>Let's get these funds secured.</NiceP>
|
||||||
<NiceP>We'll show you 12 words. You write down the 12 words.</NiceP>
|
|
||||||
<NiceP>
|
<NiceP>
|
||||||
If you clear your browser history, or lose your device, these 12 words are the only way you can restore your wallet.
|
We'll show you 12 words. You write down the 12
|
||||||
|
words.
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>Mutiny is self-custodial. It's all up to you...</NiceP>
|
<NiceP>
|
||||||
<SeedWords words={store.mutiny_wallet?.show_seed() || ""} setHasSeen={setHasSeenBackup} />
|
If you clear your browser history, or lose your
|
||||||
|
device, these 12 words are the only way you can
|
||||||
|
restore your wallet.
|
||||||
|
</NiceP>
|
||||||
|
<NiceP>
|
||||||
|
Mutiny is self-custodial. It's all up to you...
|
||||||
|
</NiceP>
|
||||||
|
<SeedWords
|
||||||
|
words={store.mutiny_wallet?.show_seed() || ""}
|
||||||
|
setHasSeen={setHasSeenBackup}
|
||||||
|
/>
|
||||||
<Show when={hasSeenBackup()}>
|
<Show when={hasSeenBackup()}>
|
||||||
<Quiz setHasCheckedAll={setHasCheckedAll} />
|
<Quiz setHasCheckedAll={setHasCheckedAll} />
|
||||||
</Show>
|
</Show>
|
||||||
<Button disabled={!hasSeenBackup() || !hasCheckedAll()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button>
|
<Button
|
||||||
|
disabled={!hasSeenBackup() || !hasCheckedAll()}
|
||||||
|
intent="blue"
|
||||||
|
onClick={wroteDownTheWords}
|
||||||
|
>
|
||||||
|
I wrote down the words
|
||||||
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="none" />
|
<NavBar activeTab="none" />
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import { Contact, MutinyBip21RawMaterials, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
|
||||||
import {
|
import {
|
||||||
createEffect,
|
Contact,
|
||||||
createMemo,
|
MutinyBip21RawMaterials,
|
||||||
createResource,
|
MutinyInvoice
|
||||||
createSignal,
|
} from "@mutinywallet/mutiny-wasm";
|
||||||
Match,
|
import {
|
||||||
onCleanup,
|
createEffect,
|
||||||
Show,
|
createMemo,
|
||||||
Switch
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
Match,
|
||||||
|
onCleanup,
|
||||||
|
Show,
|
||||||
|
Switch
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
Indicator,
|
Indicator,
|
||||||
LargeHeader,
|
LargeHeader,
|
||||||
MutinyWalletGuard,
|
MutinyWalletGuard,
|
||||||
SafeArea
|
SafeArea
|
||||||
} from "~/components/layout";
|
} from "~/components/layout";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
@@ -40,33 +44,33 @@ import { CopyableQR } from "~/components/CopyableQR";
|
|||||||
import { InfoBox } from "~/components/InfoBox";
|
import { InfoBox } from "~/components/InfoBox";
|
||||||
|
|
||||||
type OnChainTx = {
|
type OnChainTx = {
|
||||||
transaction: {
|
transaction: {
|
||||||
version: number;
|
version: number;
|
||||||
lock_time: number;
|
lock_time: number;
|
||||||
input: Array<{
|
input: Array<{
|
||||||
previous_output: string;
|
previous_output: string;
|
||||||
script_sig: string;
|
script_sig: string;
|
||||||
sequence: number;
|
sequence: number;
|
||||||
witness: Array<string>;
|
witness: Array<string>;
|
||||||
}>;
|
}>;
|
||||||
output: Array<{
|
output: Array<{
|
||||||
value: number;
|
value: number;
|
||||||
script_pubkey: string;
|
script_pubkey: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
txid: string;
|
txid: string;
|
||||||
received: number;
|
received: number;
|
||||||
sent: number;
|
sent: number;
|
||||||
confirmation_time: {
|
confirmation_time: {
|
||||||
height: number;
|
height: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const RECEIVE_FLAVORS = [
|
const RECEIVE_FLAVORS = [
|
||||||
{ value: "unified", label: "Unified", caption: "Sender decides" },
|
{ value: "unified", label: "Unified", caption: "Sender decides" },
|
||||||
{ value: "lightning", label: "Lightning", caption: "Fast and cool" },
|
{ value: "lightning", label: "Lightning", caption: "Fast and cool" },
|
||||||
{ value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }
|
{ value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }
|
||||||
];
|
];
|
||||||
|
|
||||||
type ReceiveFlavor = "unified" | "lightning" | "onchain";
|
type ReceiveFlavor = "unified" | "lightning" | "onchain";
|
||||||
@@ -74,297 +78,364 @@ type ReceiveState = "edit" | "show" | "paid";
|
|||||||
type PaidState = "lightning_paid" | "onchain_paid";
|
type PaidState = "lightning_paid" | "onchain_paid";
|
||||||
|
|
||||||
function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {
|
function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {
|
||||||
return (
|
return (
|
||||||
// TODO: probably won't always be fixed 2500?
|
// TODO: probably won't always be fixed 2500?
|
||||||
<Show when={props.fee > 1000n}>
|
<Show when={props.fee > 1000n}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.flavor === "unified"}>
|
<Match when={props.flavor === "unified"}>
|
||||||
<InfoBox accent="green">
|
<InfoBox accent="green">
|
||||||
A lightning setup fee of <AmountSmall amountSats={props.fee} /> will be charged if paid
|
A lightning setup fee of{" "}
|
||||||
over lightning.
|
<AmountSmall amountSats={props.fee} /> will be charged
|
||||||
</InfoBox>
|
if paid over lightning.
|
||||||
</Match>
|
</InfoBox>
|
||||||
<Match when={props.flavor === "lightning"}>
|
</Match>
|
||||||
<InfoBox accent="green">
|
<Match when={props.flavor === "lightning"}>
|
||||||
A lightning setup fee of <AmountSmall amountSats={props.fee} /> will be charged for this
|
<InfoBox accent="green">
|
||||||
receive.
|
A lightning setup fee of{" "}
|
||||||
</InfoBox>
|
<AmountSmall amountSats={props.fee} /> will be charged
|
||||||
</Match>
|
for this receive.
|
||||||
</Switch>
|
</InfoBox>
|
||||||
</Show>
|
</Match>
|
||||||
);
|
</Switch>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeeExplanation(props: { fee: bigint }) {
|
function FeeExplanation(props: { fee: bigint }) {
|
||||||
return (
|
return (
|
||||||
// TODO: probably won't always be a fixed 2500?
|
// TODO: probably won't always be a fixed 2500?
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.fee > 1000n}>
|
<Match when={props.fee > 1000n}>
|
||||||
<InfoBox accent="green">
|
<InfoBox accent="green">
|
||||||
A lightning setup fee of <AmountSmall amountSats={props.fee} /> was charged for this
|
A lightning setup fee of{" "}
|
||||||
receive.
|
<AmountSmall amountSats={props.fee} /> was charged for this
|
||||||
</InfoBox>
|
receive.
|
||||||
</Match>
|
</InfoBox>
|
||||||
<Match when={props.fee > 0n}>
|
</Match>
|
||||||
<InfoBox accent="green">
|
<Match when={props.fee > 0n}>
|
||||||
A lightning service fee of <AmountSmall amountSats={props.fee} /> was charged for this
|
<InfoBox accent="green">
|
||||||
receive.
|
A lightning service fee of{" "}
|
||||||
</InfoBox>
|
<AmountSmall amountSats={props.fee} /> was charged for this
|
||||||
</Match>
|
receive.
|
||||||
</Switch>
|
</InfoBox>
|
||||||
);
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Receive() {
|
export default function Receive() {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [amount, setAmount] = createSignal("");
|
const [amount, setAmount] = createSignal("");
|
||||||
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit");
|
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit");
|
||||||
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
||||||
const [unified, setUnified] = createSignal("");
|
const [unified, setUnified] = createSignal("");
|
||||||
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true);
|
const [shouldShowAmountEditor, setShouldShowAmountEditor] =
|
||||||
|
createSignal(true);
|
||||||
|
|
||||||
const [lspFee, setLspFee] = createSignal(0n);
|
const [lspFee, setLspFee] = createSignal(0n);
|
||||||
|
|
||||||
// Tagging stuff
|
// Tagging stuff
|
||||||
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
|
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// The data we get after a payment
|
// The data we get after a payment
|
||||||
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
||||||
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
||||||
|
|
||||||
// The flavor of the receive
|
// The flavor of the receive
|
||||||
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
|
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
|
||||||
|
|
||||||
const receiveString = createMemo(() => {
|
const receiveString = createMemo(() => {
|
||||||
if (unified() && receiveState() === "show") {
|
if (unified() && receiveState() === "show") {
|
||||||
if (flavor() === "unified") {
|
if (flavor() === "unified") {
|
||||||
return unified();
|
return unified();
|
||||||
} else if (flavor() === "lightning") {
|
} else if (flavor() === "lightning") {
|
||||||
return bip21Raw()?.invoice ?? "";
|
return bip21Raw()?.invoice ?? "";
|
||||||
} else if (flavor() === "onchain") {
|
} else if (flavor() === "onchain") {
|
||||||
return bip21Raw()?.address ?? "";
|
return bip21Raw()?.address ?? "";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function clearAll() {
|
|
||||||
setAmount("");
|
|
||||||
setReceiveState("edit");
|
|
||||||
setBip21Raw(undefined);
|
|
||||||
setUnified("");
|
|
||||||
setPaymentTx(undefined);
|
|
||||||
setPaymentInvoice(undefined);
|
|
||||||
setSelectedValues([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processContacts(contacts: Partial<MutinyTagItem>[]): Promise<string[]> {
|
|
||||||
console.log("Processing contacts", contacts);
|
|
||||||
|
|
||||||
if (contacts.length) {
|
|
||||||
const first = contacts![0];
|
|
||||||
|
|
||||||
if (!first.name) {
|
|
||||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!first.id && first.name) {
|
|
||||||
console.error("Creating new contact", first.name);
|
|
||||||
const c = new Contact(first.name, undefined, undefined, undefined);
|
|
||||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
|
||||||
if (newContactId) {
|
|
||||||
return [newContactId];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (first.id) {
|
|
||||||
console.error("Using existing contact", first.name, first.id);
|
|
||||||
return [first.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUnifiedQr(amount: string) {
|
|
||||||
const bigAmount = BigInt(amount);
|
|
||||||
try {
|
|
||||||
const tags = await processContacts(selectedValues());
|
|
||||||
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tags);
|
|
||||||
// Save the raw info so we can watch the address and invoice
|
|
||||||
setBip21Raw(raw);
|
|
||||||
|
|
||||||
const params = objectToSearchParams({
|
|
||||||
amount: raw?.btc_amount,
|
|
||||||
lightning: raw?.invoice
|
|
||||||
});
|
|
||||||
|
|
||||||
return `bitcoin:${raw?.address}?${params}`;
|
|
||||||
} catch (e) {
|
|
||||||
showToast(new Error("Couldn't create invoice. Are you asking for enough?"));
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const unifiedQr = await getUnifiedQr(amount());
|
|
||||||
|
|
||||||
setUnified(unifiedQr || "");
|
|
||||||
setReceiveState("show");
|
|
||||||
setShouldShowAmountEditor(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
|
||||||
if (bip21) {
|
|
||||||
console.debug("checking if paid...");
|
|
||||||
const lightning = bip21.invoice;
|
|
||||||
const address = bip21.address;
|
|
||||||
|
|
||||||
const invoice = await state.mutiny_wallet?.get_invoice(lightning);
|
|
||||||
|
|
||||||
// If the invoice has a fees amount that's probably the LSP fee
|
|
||||||
if (invoice?.fees_paid) {
|
|
||||||
setLspFee(invoice.fees_paid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invoice && invoice.paid) {
|
|
||||||
setReceiveState("paid");
|
|
||||||
setPaymentInvoice(invoice);
|
|
||||||
return "lightning_paid";
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = (await state.mutiny_wallet?.check_address(address)) as OnChainTx | undefined;
|
|
||||||
|
|
||||||
if (tx) {
|
|
||||||
setReceiveState("paid");
|
|
||||||
setPaymentTx(tx);
|
|
||||||
return "onchain_paid";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid);
|
|
||||||
|
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (receiveState() === "show") refetch();
|
|
||||||
}, 1000); // Poll every second
|
|
||||||
onCleanup(() => {
|
|
||||||
clearInterval(interval);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
function clearAll() {
|
||||||
<MutinyWalletGuard>
|
setAmount("");
|
||||||
<SafeArea>
|
setReceiveState("edit");
|
||||||
<DefaultMain>
|
setBip21Raw(undefined);
|
||||||
<Show when={receiveState() === "show"} fallback={<BackLink />}>
|
setUnified("");
|
||||||
<BackButton onClick={() => setReceiveState("edit")} title="Edit" showOnDesktop />
|
setPaymentTx(undefined);
|
||||||
</Show>
|
setPaymentInvoice(undefined);
|
||||||
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>
|
setSelectedValues([]);
|
||||||
Receive Bitcoin
|
}
|
||||||
</LargeHeader>
|
|
||||||
<Switch>
|
|
||||||
<Match when={!unified() || receiveState() === "edit"}>
|
|
||||||
<div class="flex flex-col flex-1 gap-8">
|
|
||||||
<AmountCard
|
|
||||||
initialOpen={shouldShowAmountEditor()}
|
|
||||||
amountSats={amount() || "0"}
|
|
||||||
setAmountSats={setAmount}
|
|
||||||
isAmountEditable
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card title="Private tags">
|
async function processContacts(
|
||||||
<TagEditor
|
contacts: Partial<MutinyTagItem>[]
|
||||||
selectedValues={selectedValues()}
|
): Promise<string[]> {
|
||||||
setSelectedValues={setSelectedValues}
|
console.log("Processing contacts", contacts);
|
||||||
placeholder="Add the sender for your records"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div class="flex-1" />
|
if (contacts.length) {
|
||||||
<Button
|
const first = contacts![0];
|
||||||
class="w-full flex-grow-0"
|
|
||||||
disabled={!amount()}
|
if (!first.name) {
|
||||||
intent="green"
|
console.error(
|
||||||
onClick={onSubmit}
|
"Something went wrong with contact creation, proceeding anyway"
|
||||||
>
|
);
|
||||||
Continue
|
return [];
|
||||||
</Button>
|
}
|
||||||
</div>
|
|
||||||
</Match>
|
if (!first.id && first.name) {
|
||||||
<Match when={unified() && receiveState() === "show"}>
|
console.error("Creating new contact", first.name);
|
||||||
<FeeWarning fee={lspFee()} flavor={flavor()} />
|
const c = new Contact(
|
||||||
<CopyableQR value={receiveString() ?? ""} />
|
first.name,
|
||||||
<p class="text-neutral-400 text-center">
|
undefined,
|
||||||
<Switch>
|
undefined,
|
||||||
<Match when={flavor() === "lightning"}>
|
undefined
|
||||||
Show or share this invoice with the sender.
|
);
|
||||||
</Match>
|
const newContactId =
|
||||||
<Match when={flavor() === "onchain"}>
|
await state.mutiny_wallet?.create_new_contact(c);
|
||||||
Show or share this address with the sender.
|
if (newContactId) {
|
||||||
</Match>
|
return [newContactId];
|
||||||
<Match when={flavor() === "unified"}>
|
}
|
||||||
Show or share this code with the sender. Sender decides method of payment.
|
}
|
||||||
</Match>
|
|
||||||
</Switch>
|
if (first.id) {
|
||||||
</p>
|
console.error("Using existing contact", first.name, first.id);
|
||||||
<StyledRadioGroup
|
return [first.id];
|
||||||
small
|
}
|
||||||
value={flavor()}
|
}
|
||||||
onValueChange={setFlavor}
|
|
||||||
choices={RECEIVE_FLAVORS}
|
console.error(
|
||||||
accent="white"
|
"Something went wrong with contact creation, proceeding anyway"
|
||||||
/>{" "}
|
);
|
||||||
<ShareCard text={receiveString() ?? ""} />
|
return [];
|
||||||
</Match>
|
}
|
||||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
|
||||||
<SuccessModal
|
async function getUnifiedQr(amount: string) {
|
||||||
title="Payment Received"
|
const bigAmount = BigInt(amount);
|
||||||
open={!!paidState()}
|
try {
|
||||||
setOpen={(open: boolean) => {
|
const tags = await processContacts(selectedValues());
|
||||||
if (!open) clearAll();
|
const raw = await state.mutiny_wallet?.create_bip21(
|
||||||
}}
|
bigAmount,
|
||||||
onConfirm={() => {
|
tags
|
||||||
clearAll();
|
);
|
||||||
navigate("/");
|
// Save the raw info so we can watch the address and invoice
|
||||||
}}
|
setBip21Raw(raw);
|
||||||
>
|
|
||||||
<MegaCheck />
|
const params = objectToSearchParams({
|
||||||
<FeeExplanation fee={lspFee()} />
|
amount: raw?.btc_amount,
|
||||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat centered />
|
lightning: raw?.invoice
|
||||||
</SuccessModal>
|
});
|
||||||
</Match>
|
|
||||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
return `bitcoin:${raw?.address}?${params}`;
|
||||||
<SuccessModal
|
} catch (e) {
|
||||||
title="Payment Received"
|
showToast(
|
||||||
open={!!paidState()}
|
new Error("Couldn't create invoice. Are you asking for enough?")
|
||||||
setOpen={(open: boolean) => {
|
);
|
||||||
if (!open) clearAll();
|
console.error(e);
|
||||||
}}
|
}
|
||||||
onConfirm={() => {
|
}
|
||||||
clearAll();
|
|
||||||
navigate("/");
|
async function onSubmit(e: Event) {
|
||||||
}}
|
e.preventDefault();
|
||||||
>
|
|
||||||
<MegaCheck />
|
const unifiedQr = await getUnifiedQr(amount());
|
||||||
<Amount amountSats={paymentTx()?.received} showFiat centered />
|
|
||||||
<ExternalLink href={mempoolTxUrl(paymentTx()?.txid, network)}>
|
setUnified(unifiedQr || "");
|
||||||
View Transaction
|
setReceiveState("show");
|
||||||
</ExternalLink>
|
setShouldShowAmountEditor(false);
|
||||||
</SuccessModal>
|
}
|
||||||
</Match>
|
|
||||||
</Switch>
|
async function checkIfPaid(
|
||||||
</DefaultMain>
|
bip21?: MutinyBip21RawMaterials
|
||||||
<NavBar activeTab="receive" />
|
): Promise<PaidState | undefined> {
|
||||||
</SafeArea>
|
if (bip21) {
|
||||||
</MutinyWalletGuard>
|
console.debug("checking if paid...");
|
||||||
);
|
const lightning = bip21.invoice;
|
||||||
|
const address = bip21.address;
|
||||||
|
|
||||||
|
const invoice = await state.mutiny_wallet?.get_invoice(lightning);
|
||||||
|
|
||||||
|
// If the invoice has a fees amount that's probably the LSP fee
|
||||||
|
if (invoice?.fees_paid) {
|
||||||
|
setLspFee(invoice.fees_paid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoice && invoice.paid) {
|
||||||
|
setReceiveState("paid");
|
||||||
|
setPaymentInvoice(invoice);
|
||||||
|
return "lightning_paid";
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = (await state.mutiny_wallet?.check_address(address)) as
|
||||||
|
| OnChainTx
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
setReceiveState("paid");
|
||||||
|
setPaymentTx(tx);
|
||||||
|
return "onchain_paid";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid);
|
||||||
|
|
||||||
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (receiveState() === "show") refetch();
|
||||||
|
}, 1000); // Poll every second
|
||||||
|
onCleanup(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MutinyWalletGuard>
|
||||||
|
<SafeArea>
|
||||||
|
<DefaultMain>
|
||||||
|
<Show
|
||||||
|
when={receiveState() === "show"}
|
||||||
|
fallback={<BackLink />}
|
||||||
|
>
|
||||||
|
<BackButton
|
||||||
|
onClick={() => setReceiveState("edit")}
|
||||||
|
title="Edit"
|
||||||
|
showOnDesktop
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<LargeHeader
|
||||||
|
action={
|
||||||
|
receiveState() === "show" && (
|
||||||
|
<Indicator>Checking</Indicator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Receive Bitcoin
|
||||||
|
</LargeHeader>
|
||||||
|
<Switch>
|
||||||
|
<Match when={!unified() || receiveState() === "edit"}>
|
||||||
|
<div class="flex flex-col flex-1 gap-8">
|
||||||
|
<AmountCard
|
||||||
|
initialOpen={shouldShowAmountEditor()}
|
||||||
|
amountSats={amount() || "0"}
|
||||||
|
setAmountSats={setAmount}
|
||||||
|
isAmountEditable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card title="Private tags">
|
||||||
|
<TagEditor
|
||||||
|
selectedValues={selectedValues()}
|
||||||
|
setSelectedValues={setSelectedValues}
|
||||||
|
placeholder="Add the sender for your records"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div class="flex-1" />
|
||||||
|
<Button
|
||||||
|
class="w-full flex-grow-0"
|
||||||
|
disabled={!amount()}
|
||||||
|
intent="green"
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={unified() && receiveState() === "show"}>
|
||||||
|
<FeeWarning fee={lspFee()} flavor={flavor()} />
|
||||||
|
<CopyableQR value={receiveString() ?? ""} />
|
||||||
|
<p class="text-neutral-400 text-center">
|
||||||
|
<Switch>
|
||||||
|
<Match when={flavor() === "lightning"}>
|
||||||
|
Show or share this invoice with the
|
||||||
|
sender.
|
||||||
|
</Match>
|
||||||
|
<Match when={flavor() === "onchain"}>
|
||||||
|
Show or share this address with the
|
||||||
|
sender.
|
||||||
|
</Match>
|
||||||
|
<Match when={flavor() === "unified"}>
|
||||||
|
Show or share this code with the sender.
|
||||||
|
Sender decides method of payment.
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</p>
|
||||||
|
<StyledRadioGroup
|
||||||
|
small
|
||||||
|
value={flavor()}
|
||||||
|
onValueChange={setFlavor}
|
||||||
|
choices={RECEIVE_FLAVORS}
|
||||||
|
accent="white"
|
||||||
|
/>{" "}
|
||||||
|
<ShareCard text={receiveString() ?? ""} />
|
||||||
|
</Match>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
receiveState() === "paid" &&
|
||||||
|
paidState() === "lightning_paid"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SuccessModal
|
||||||
|
title="Payment Received"
|
||||||
|
open={!!paidState()}
|
||||||
|
setOpen={(open: boolean) => {
|
||||||
|
if (!open) clearAll();
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
clearAll();
|
||||||
|
navigate("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MegaCheck />
|
||||||
|
<FeeExplanation fee={lspFee()} />
|
||||||
|
<Amount
|
||||||
|
amountSats={paymentInvoice()?.amount_sats}
|
||||||
|
showFiat
|
||||||
|
centered
|
||||||
|
/>
|
||||||
|
</SuccessModal>
|
||||||
|
</Match>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
receiveState() === "paid" &&
|
||||||
|
paidState() === "onchain_paid"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SuccessModal
|
||||||
|
title="Payment Received"
|
||||||
|
open={!!paidState()}
|
||||||
|
setOpen={(open: boolean) => {
|
||||||
|
if (!open) clearAll();
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
clearAll();
|
||||||
|
navigate("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MegaCheck />
|
||||||
|
<Amount
|
||||||
|
amountSats={paymentTx()?.received}
|
||||||
|
showFiat
|
||||||
|
centered
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href={mempoolTxUrl(
|
||||||
|
paymentTx()?.txid,
|
||||||
|
network
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
View Transaction
|
||||||
|
</ExternalLink>
|
||||||
|
</SuccessModal>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</DefaultMain>
|
||||||
|
<NavBar activeTab="receive" />
|
||||||
|
</SafeArea>
|
||||||
|
</MutinyWalletGuard>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -15,27 +15,40 @@ export type ParsedParams = {
|
|||||||
memo?: string;
|
memo?: string;
|
||||||
node_pubkey?: string;
|
node_pubkey?: string;
|
||||||
lnurl?: string;
|
lnurl?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function toParsedParams(str: string, ourNetwork: string): Result<ParsedParams> {
|
export function toParsedParams(
|
||||||
|
str: string,
|
||||||
|
ourNetwork: string
|
||||||
|
): Result<ParsedParams> {
|
||||||
let params;
|
let params;
|
||||||
try {
|
try {
|
||||||
params = new PaymentParams(str || "")
|
params = new PaymentParams(str || "");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
return { ok: false, error: new Error("Invalid payment request") }
|
return { ok: false, error: new Error("Invalid payment request") };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If WAILA doesn't return a network we should default to our own
|
// If WAILA doesn't return a network we should default to our own
|
||||||
// If the networks is testnet and we're on signet we should use signet
|
// If the networks is testnet and we're on signet we should use signet
|
||||||
const network = !params.network ? ourNetwork : params.network === "testnet" && ourNetwork === "signet" ? "signet" : params.network;
|
const network = !params.network
|
||||||
|
? ourNetwork
|
||||||
|
: params.network === "testnet" && ourNetwork === "signet"
|
||||||
|
? "signet"
|
||||||
|
: params.network;
|
||||||
|
|
||||||
if (network !== ourNetwork) {
|
if (network !== ourNetwork) {
|
||||||
return { ok: false, error: new Error(`Destination is for ${params.network} but you're on ${ourNetwork}`) }
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: new Error(
|
||||||
|
`Destination is for ${params.network} but you're on ${ourNetwork}`
|
||||||
|
)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true, value: {
|
ok: true,
|
||||||
|
value: {
|
||||||
address: params.address,
|
address: params.address,
|
||||||
invoice: params.invoice,
|
invoice: params.invoice,
|
||||||
amount_sats: params.amount_sats,
|
amount_sats: params.amount_sats,
|
||||||
@@ -44,7 +57,7 @@ export function toParsedParams(str: string, ourNetwork: string): Result<ParsedPa
|
|||||||
node_pubkey: params.node_pubkey,
|
node_pubkey: params.node_pubkey,
|
||||||
lnurl: params.lnurl
|
lnurl: params.lnurl
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Scanner() {
|
export default function Scanner() {
|
||||||
@@ -62,14 +75,14 @@ export default function Scanner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePaste() {
|
function handlePaste() {
|
||||||
navigator.clipboard.readText().then(text => {
|
navigator.clipboard.readText().then((text) => {
|
||||||
setScanResult(text);
|
setScanResult(text);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await init()
|
await init();
|
||||||
})
|
});
|
||||||
|
|
||||||
// When we have a nice result we can head over to the send screen
|
// When we have a nice result we can head over to the send screen
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -80,20 +93,27 @@ export default function Scanner() {
|
|||||||
showToast(result.error);
|
showToast(result.error);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey || result.value?.lnurl) {
|
if (
|
||||||
|
result.value?.address ||
|
||||||
|
result.value?.invoice ||
|
||||||
|
result.value?.node_pubkey ||
|
||||||
|
result.value?.lnurl
|
||||||
|
) {
|
||||||
actions.setScanResult(result.value);
|
actions.setScanResult(result.value);
|
||||||
navigate("/send")
|
navigate("/send");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="safe-top safe-left safe-right safe-bottom h-full">
|
<div class="safe-top safe-left safe-right safe-bottom h-full">
|
||||||
<Reader onResult={onResult} />
|
<Reader onResult={onResult} />
|
||||||
<div class="w-full flex flex-col items-center fixed bottom-[2rem] gap-8 px-8">
|
<div class="w-full flex flex-col items-center fixed bottom-[2rem] gap-8 px-8">
|
||||||
<div class="w-full max-w-[800px] flex flex-col gap-2">
|
<div class="w-full max-w-[800px] flex flex-col gap-2">
|
||||||
<Button intent="blue" onClick={handlePaste}>Paste Something</Button>
|
<Button intent="blue" onClick={handlePaste}>
|
||||||
|
Paste Something
|
||||||
|
</Button>
|
||||||
<Button onClick={exit}>Cancel</Button>
|
<Button onClick={exit}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,11 @@
|
|||||||
import { ButtonLink, DefaultMain, LargeHeader, MutinyWalletGuard, SafeArea, VStack } from "~/components/layout";
|
import {
|
||||||
|
ButtonLink,
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
MutinyWalletGuard,
|
||||||
|
SafeArea,
|
||||||
|
VStack
|
||||||
|
} from "~/components/layout";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
import { Logs } from "~/components/Logs";
|
import { Logs } from "~/components/Logs";
|
||||||
import { Restart } from "~/components/Restart";
|
import { Restart } from "~/components/Restart";
|
||||||
@@ -18,17 +25,23 @@ export default function Settings() {
|
|||||||
<LargeHeader>Settings</LargeHeader>
|
<LargeHeader>Settings</LargeHeader>
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
<VStack>
|
<VStack>
|
||||||
<p class="text-2xl font-light">Write down these words or you'll die!</p>
|
<p class="text-2xl font-light">
|
||||||
<SeedWords words={store.mutiny_wallet?.show_seed() || ""} />
|
Write down these words or you'll die!
|
||||||
|
</p>
|
||||||
|
<SeedWords
|
||||||
|
words={store.mutiny_wallet?.show_seed() || ""}
|
||||||
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
<SettingsStringsEditor />
|
<SettingsStringsEditor />
|
||||||
<Logs />
|
<Logs />
|
||||||
<Restart />
|
<Restart />
|
||||||
<ButtonLink href="/admin">"I know what I'm doing"</ButtonLink>
|
<ButtonLink href="/admin">
|
||||||
|
"I know what I'm doing"
|
||||||
|
</ButtonLink>
|
||||||
</VStack>
|
</VStack>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="settings" />
|
<NavBar activeTab="settings" />
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
</MutinyWalletGuard>
|
</MutinyWalletGuard>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,27 @@ import { AmountCard } from "~/components/AmountCard";
|
|||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { OnboardWarning } from "~/components/OnboardWarning";
|
import { OnboardWarning } from "~/components/OnboardWarning";
|
||||||
import { ShareCard } from "~/components/ShareCard";
|
import { ShareCard } from "~/components/ShareCard";
|
||||||
import { DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
|
import {
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
SafeArea,
|
||||||
|
VStack
|
||||||
|
} from "~/components/layout";
|
||||||
|
|
||||||
const SAMPLE =
|
const SAMPLE =
|
||||||
"bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6";
|
"bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6";
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
return (
|
return (
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<LargeHeader>Storybook</LargeHeader>
|
<LargeHeader>Storybook</LargeHeader>
|
||||||
<OnboardWarning />
|
<OnboardWarning />
|
||||||
<VStack>
|
<VStack>
|
||||||
<AmountCard amountSats={"100000"} fee={"69"} />
|
<AmountCard amountSats={"100000"} fee={"69"} />
|
||||||
<ShareCard text={SAMPLE} />
|
<ShareCard text={SAMPLE} />
|
||||||
</VStack>
|
</VStack>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="none" />
|
<NavBar activeTab="none" />
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
import { createForm, required } from "@modular-forms/solid";
|
import { createForm, required } from "@modular-forms/solid";
|
||||||
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
||||||
import { For, Match, Show, Switch, createResource, createSignal } from "solid-js";
|
import {
|
||||||
|
For,
|
||||||
|
Match,
|
||||||
|
Show,
|
||||||
|
Switch,
|
||||||
|
createResource,
|
||||||
|
createSignal
|
||||||
|
} from "solid-js";
|
||||||
import { AmountCard } from "~/components/AmountCard";
|
import { AmountCard } from "~/components/AmountCard";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { showToast } from "~/components/Toaster";
|
import { showToast } from "~/components/Toaster";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
LargeHeader,
|
LargeHeader,
|
||||||
MutinyWalletGuard,
|
MutinyWalletGuard,
|
||||||
SafeArea,
|
SafeArea,
|
||||||
VStack
|
VStack
|
||||||
} from "~/components/layout";
|
} from "~/components/layout";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
import { TextField } from "~/components/layout/TextField";
|
import { TextField } from "~/components/layout/TextField";
|
||||||
@@ -28,274 +35,354 @@ import { ExternalLink } from "~/components/layout/ExternalLink";
|
|||||||
import { Network } from "~/logic/mutinyWalletSetup";
|
import { Network } from "~/logic/mutinyWalletSetup";
|
||||||
|
|
||||||
const CHANNEL_FEE_ESTIMATE_ADDRESS =
|
const CHANNEL_FEE_ESTIMATE_ADDRESS =
|
||||||
"bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf";
|
"bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf";
|
||||||
|
|
||||||
type PeerConnectForm = {
|
type PeerConnectForm = {
|
||||||
peer: string;
|
peer: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChannelOpenDetails = {
|
type ChannelOpenDetails = {
|
||||||
channel?: MutinyChannel;
|
channel?: MutinyChannel;
|
||||||
failure_reason?: Error;
|
failure_reason?: Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Swap() {
|
export default function Swap() {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [source, setSource] = createSignal<SendSource>("onchain");
|
const [source, setSource] = createSignal<SendSource>("onchain");
|
||||||
const [amountSats, setAmountSats] = createSignal(0n);
|
const [amountSats, setAmountSats] = createSignal(0n);
|
||||||
const [isConnecting, setIsConnecting] = createSignal(false);
|
const [isConnecting, setIsConnecting] = createSignal(false);
|
||||||
|
|
||||||
const [selectedPeer, setSelectedPeer] = createSignal<string>("");
|
const [selectedPeer, setSelectedPeer] = createSignal<string>("");
|
||||||
|
|
||||||
const [channelOpenResult, setChannelOpenResult] = createSignal<ChannelOpenDetails>();
|
const [channelOpenResult, setChannelOpenResult] =
|
||||||
|
createSignal<ChannelOpenDetails>();
|
||||||
|
|
||||||
const feeEstimate = () => {
|
const feeEstimate = () => {
|
||||||
if (amountSats()) {
|
if (amountSats()) {
|
||||||
try {
|
try {
|
||||||
return state.mutiny_wallet?.estimate_tx_fee(
|
return state.mutiny_wallet?.estimate_tx_fee(
|
||||||
CHANNEL_FEE_ESTIMATE_ADDRESS,
|
CHANNEL_FEE_ESTIMATE_ADDRESS,
|
||||||
amountSats(),
|
amountSats(),
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
// showToast(eify(new Error("Unsufficient funds")))
|
// showToast(eify(new Error("Unsufficient funds")))
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasLsp = () => {
|
|
||||||
return !!localStorage.getItem("MUTINY_SETTINGS_lsp") || !!import.meta.env.VITE_LSP;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPeers = async () => {
|
|
||||||
return (await state.mutiny_wallet?.list_peers()) as Promise<MutinyPeer[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [peers, { refetch }] = createResource(getPeers);
|
|
||||||
|
|
||||||
const [_peerForm, { Form, Field }] = createForm<PeerConnectForm>();
|
|
||||||
|
|
||||||
const onSubmit = async (values: PeerConnectForm) => {
|
|
||||||
setIsConnecting(true);
|
|
||||||
try {
|
|
||||||
const peerConnectString = values.peer.trim();
|
|
||||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
|
||||||
const firstNode = (nodes[0] as string) || "";
|
|
||||||
|
|
||||||
await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString);
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
|
|
||||||
// If peers list contains the peer we just connected to, select it
|
|
||||||
const peer = peers()?.find((p) => p.pubkey === peerConnectString.split("@")[0]);
|
|
||||||
|
|
||||||
if (peer) {
|
|
||||||
setSelectedPeer(peer.pubkey);
|
|
||||||
} else {
|
|
||||||
showToast(new Error("Peer not found"));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showToast(eify(e));
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePeerSelect = (
|
|
||||||
e: Event & {
|
|
||||||
currentTarget: HTMLSelectElement;
|
|
||||||
target: HTMLSelectElement;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
setSelectedPeer(e.currentTarget.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSwap = async () => {
|
|
||||||
if (canSwap()) {
|
|
||||||
try {
|
|
||||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
|
||||||
const firstNode = (nodes[0] as string) || "";
|
|
||||||
|
|
||||||
if (hasLsp()) {
|
|
||||||
const new_channel = await state.mutiny_wallet?.open_channel(
|
|
||||||
firstNode,
|
|
||||||
undefined,
|
|
||||||
amountSats()
|
|
||||||
);
|
|
||||||
|
|
||||||
setChannelOpenResult({ channel: new_channel });
|
|
||||||
} else {
|
|
||||||
const new_channel = await state.mutiny_wallet?.open_channel(
|
|
||||||
firstNode,
|
|
||||||
selectedPeer(),
|
|
||||||
amountSats()
|
|
||||||
);
|
|
||||||
|
|
||||||
setChannelOpenResult({ channel: new_channel });
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return undefined;
|
||||||
setChannelOpenResult({ failure_reason: eify(e) });
|
};
|
||||||
// showToast(eify(e))
|
|
||||||
}
|
const hasLsp = () => {
|
||||||
}
|
return (
|
||||||
};
|
!!localStorage.getItem("MUTINY_SETTINGS_lsp") ||
|
||||||
|
!!import.meta.env.VITE_LSP
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPeers = async () => {
|
||||||
|
return (await state.mutiny_wallet?.list_peers()) as Promise<
|
||||||
|
MutinyPeer[]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [peers, { refetch }] = createResource(getPeers);
|
||||||
|
|
||||||
|
const [_peerForm, { Form, Field }] = createForm<PeerConnectForm>();
|
||||||
|
|
||||||
|
const onSubmit = async (values: PeerConnectForm) => {
|
||||||
|
setIsConnecting(true);
|
||||||
|
try {
|
||||||
|
const peerConnectString = values.peer.trim();
|
||||||
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
|
const firstNode = (nodes[0] as string) || "";
|
||||||
|
|
||||||
|
await state.mutiny_wallet?.connect_to_peer(
|
||||||
|
firstNode,
|
||||||
|
peerConnectString
|
||||||
|
);
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
|
||||||
|
// If peers list contains the peer we just connected to, select it
|
||||||
|
const peer = peers()?.find(
|
||||||
|
(p) => p.pubkey === peerConnectString.split("@")[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (peer) {
|
||||||
|
setSelectedPeer(peer.pubkey);
|
||||||
|
} else {
|
||||||
|
showToast(new Error("Peer not found"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast(eify(e));
|
||||||
|
} finally {
|
||||||
|
setIsConnecting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePeerSelect = (
|
||||||
|
e: Event & {
|
||||||
|
currentTarget: HTMLSelectElement;
|
||||||
|
target: HTMLSelectElement;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
setSelectedPeer(e.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwap = async () => {
|
||||||
|
if (canSwap()) {
|
||||||
|
try {
|
||||||
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
|
const firstNode = (nodes[0] as string) || "";
|
||||||
|
|
||||||
|
if (hasLsp()) {
|
||||||
|
const new_channel = await state.mutiny_wallet?.open_channel(
|
||||||
|
firstNode,
|
||||||
|
undefined,
|
||||||
|
amountSats()
|
||||||
|
);
|
||||||
|
|
||||||
|
setChannelOpenResult({ channel: new_channel });
|
||||||
|
} else {
|
||||||
|
const new_channel = await state.mutiny_wallet?.open_channel(
|
||||||
|
firstNode,
|
||||||
|
selectedPeer(),
|
||||||
|
amountSats()
|
||||||
|
);
|
||||||
|
|
||||||
|
setChannelOpenResult({ channel: new_channel });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setChannelOpenResult({ failure_reason: eify(e) });
|
||||||
|
// showToast(eify(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSwap = () => {
|
||||||
|
const balance =
|
||||||
|
(state.balance?.confirmed || 0n) +
|
||||||
|
(state.balance?.unconfirmed || 0n);
|
||||||
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
|
if (network === "bitcoin") {
|
||||||
|
return (
|
||||||
|
(!!selectedPeer() || !!hasLsp()) &&
|
||||||
|
amountSats() >= 50000n &&
|
||||||
|
amountSats() <= balance
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
(!!selectedPeer() || !!hasLsp()) &&
|
||||||
|
amountSats() >= 10000n &&
|
||||||
|
amountSats() <= balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const amountWarning = () => {
|
||||||
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
|
if (network === "bitcoin" && amountSats() < 50000n) {
|
||||||
|
return "It's just silly to make a channel smaller than 50,000 sats";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountSats() < 10000n) {
|
||||||
|
return "It's just silly to make a channel smaller than 10,000 sats";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
amountSats() >
|
||||||
|
(state.balance?.confirmed || 0n) +
|
||||||
|
(state.balance?.unconfirmed || 0n) ||
|
||||||
|
!feeEstimate()
|
||||||
|
) {
|
||||||
|
return "You don't have enough funds to make this channel";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const canSwap = () => {
|
|
||||||
const balance = (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
|
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
if (network === "bitcoin") {
|
return (
|
||||||
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 50000n && amountSats() <= balance;
|
<MutinyWalletGuard>
|
||||||
} else {
|
<SafeArea>
|
||||||
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 10000n && amountSats() <= balance;
|
<DefaultMain>
|
||||||
}
|
<BackLink />
|
||||||
};
|
<LargeHeader>Swap to Lightning</LargeHeader>
|
||||||
|
<SuccessModal
|
||||||
|
title={
|
||||||
|
channelOpenResult()?.channel
|
||||||
|
? "Swap Success"
|
||||||
|
: "Swap Failed"
|
||||||
|
}
|
||||||
|
confirmText={
|
||||||
|
channelOpenResult()?.channel ? "Nice" : "Too Bad"
|
||||||
|
}
|
||||||
|
open={!!channelOpenResult()}
|
||||||
|
setOpen={(open: boolean) => {
|
||||||
|
if (!open) setChannelOpenResult(undefined);
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
setChannelOpenResult(undefined);
|
||||||
|
navigate("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch>
|
||||||
|
<Match when={channelOpenResult()?.failure_reason}>
|
||||||
|
<img
|
||||||
|
src={megaex}
|
||||||
|
alt="fail"
|
||||||
|
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||||
|
/>
|
||||||
|
|
||||||
const amountWarning = () => {
|
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
{
|
||||||
|
channelOpenResult()?.failure_reason
|
||||||
if (network === "bitcoin" && amountSats() < 50000n) {
|
?.message
|
||||||
return "It's just silly to make a channel smaller than 50,000 sats";
|
}
|
||||||
}
|
</p>
|
||||||
|
</Match>
|
||||||
if (amountSats() < 10000n) {
|
<Match when={true}>
|
||||||
return "It's just silly to make a channel smaller than 10,000 sats";
|
<img
|
||||||
}
|
src={megacheck}
|
||||||
|
alt="success"
|
||||||
if (
|
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||||
amountSats() > (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n) ||
|
/>
|
||||||
!feeEstimate()
|
<AmountCard
|
||||||
) {
|
amountSats={
|
||||||
return "You don't have enough funds to make this channel";
|
channelOpenResult()?.channel?.balance?.toString() ||
|
||||||
}
|
""
|
||||||
|
}
|
||||||
return undefined;
|
reserve={
|
||||||
};
|
channelOpenResult()?.channel?.reserve?.toString() ||
|
||||||
|
""
|
||||||
const network = state.mutiny_wallet?.get_network() as Network;
|
}
|
||||||
|
/>
|
||||||
return (
|
<Show
|
||||||
<MutinyWalletGuard>
|
when={
|
||||||
<SafeArea>
|
channelOpenResult()?.channel?.outpoint
|
||||||
<DefaultMain>
|
}
|
||||||
<BackLink />
|
>
|
||||||
<LargeHeader>Swap to Lightning</LargeHeader>
|
<ExternalLink
|
||||||
<SuccessModal
|
href={mempoolTxUrl(
|
||||||
title={channelOpenResult()?.channel ? "Swap Success" : "Swap Failed"}
|
channelOpenResult()?.channel?.outpoint?.split(
|
||||||
confirmText={channelOpenResult()?.channel ? "Nice" : "Too Bad"}
|
":"
|
||||||
open={!!channelOpenResult()}
|
)[0],
|
||||||
setOpen={(open: boolean) => {
|
network
|
||||||
if (!open) setChannelOpenResult(undefined);
|
)}
|
||||||
}}
|
>
|
||||||
onConfirm={() => {
|
View Transaction
|
||||||
setChannelOpenResult(undefined);
|
</ExternalLink>
|
||||||
navigate("/");
|
</Show>
|
||||||
}}
|
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
|
||||||
>
|
</Match>
|
||||||
<Switch>
|
</Switch>
|
||||||
<Match when={channelOpenResult()?.failure_reason}>
|
</SuccessModal>
|
||||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
<VStack biggap>
|
||||||
|
<MethodChooser
|
||||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
source={source()}
|
||||||
{channelOpenResult()?.failure_reason?.message}
|
setSource={setSource}
|
||||||
</p>
|
both={false}
|
||||||
</Match>
|
/>
|
||||||
<Match when={true}>
|
<VStack>
|
||||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
<Show when={!hasLsp()}>
|
||||||
<AmountCard
|
<Card>
|
||||||
amountSats={channelOpenResult()?.channel?.balance?.toString() || ""}
|
<VStack>
|
||||||
reserve={channelOpenResult()?.channel?.reserve?.toString() || ""}
|
<div class="w-full flex flex-col gap-2">
|
||||||
/>
|
<label
|
||||||
<Show when={channelOpenResult()?.channel?.outpoint}>
|
for="peerselect"
|
||||||
<ExternalLink
|
class="uppercase font-semibold text-sm"
|
||||||
href={mempoolTxUrl(
|
>
|
||||||
channelOpenResult()?.channel?.outpoint?.split(":")[0],
|
Use existing peer
|
||||||
network
|
</label>
|
||||||
)}
|
<select
|
||||||
>
|
name="peerselect"
|
||||||
View Transaction
|
class="bg-black px-4 py-2 rounded truncate w-full"
|
||||||
</ExternalLink>
|
onChange={handlePeerSelect}
|
||||||
</Show>
|
value={selectedPeer()}
|
||||||
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
|
>
|
||||||
</Match>
|
<option
|
||||||
</Switch>
|
value=""
|
||||||
</SuccessModal>
|
class=""
|
||||||
<VStack biggap>
|
selected
|
||||||
<MethodChooser source={source()} setSource={setSource} both={false} />
|
>
|
||||||
<VStack>
|
Choose a peer
|
||||||
<Show when={!hasLsp()}>
|
</option>
|
||||||
<Card>
|
<For each={peers()}>
|
||||||
<VStack>
|
{(peer) => (
|
||||||
<div class="w-full flex flex-col gap-2">
|
<option
|
||||||
<label for="peerselect" class="uppercase font-semibold text-sm">
|
value={peer.pubkey}
|
||||||
Use existing peer
|
>
|
||||||
</label>
|
{peer.alias ??
|
||||||
<select
|
peer.pubkey}
|
||||||
name="peerselect"
|
</option>
|
||||||
class="bg-black px-4 py-2 rounded truncate w-full"
|
)}
|
||||||
onChange={handlePeerSelect}
|
</For>
|
||||||
value={selectedPeer()}
|
</select>
|
||||||
>
|
</div>
|
||||||
<option value="" class="" selected>
|
<Show when={!selectedPeer()}>
|
||||||
Choose a peer
|
<Form
|
||||||
</option>
|
onSubmit={onSubmit}
|
||||||
<For each={peers()}>
|
class="flex flex-col gap-4"
|
||||||
{(peer) => (
|
>
|
||||||
<option value={peer.pubkey}>{peer.alias ?? peer.pubkey}</option>
|
<Field
|
||||||
)}
|
name="peer"
|
||||||
</For>
|
validate={[required("")]}
|
||||||
</select>
|
>
|
||||||
</div>
|
{(field, props) => (
|
||||||
<Show when={!selectedPeer()}>
|
<TextField
|
||||||
<Form onSubmit={onSubmit} class="flex flex-col gap-4">
|
{...props}
|
||||||
<Field name="peer" validate={[required("")]}>
|
value={field.value}
|
||||||
{(field, props) => (
|
error={field.error}
|
||||||
<TextField
|
label="Connect to new peer"
|
||||||
{...props}
|
placeholder="Peer connect string"
|
||||||
value={field.value}
|
/>
|
||||||
error={field.error}
|
)}
|
||||||
label="Connect to new peer"
|
</Field>
|
||||||
placeholder="Peer connect string"
|
<Button
|
||||||
/>
|
layout="small"
|
||||||
)}
|
type="submit"
|
||||||
</Field>
|
disabled={isConnecting()}
|
||||||
<Button layout="small" type="submit" disabled={isConnecting()}>
|
>
|
||||||
{isConnecting() ? "Connecting..." : "Connect"}
|
{isConnecting()
|
||||||
</Button>
|
? "Connecting..."
|
||||||
</Form>
|
: "Connect"}
|
||||||
</Show>
|
</Button>
|
||||||
</VStack>
|
</Form>
|
||||||
</Card>
|
</Show>
|
||||||
</Show>
|
</VStack>
|
||||||
</VStack>
|
</Card>
|
||||||
<AmountCard
|
</Show>
|
||||||
amountSats={amountSats().toString()}
|
</VStack>
|
||||||
setAmountSats={setAmountSats}
|
<AmountCard
|
||||||
fee={feeEstimate()?.toString()}
|
amountSats={amountSats().toString()}
|
||||||
isAmountEditable={true}
|
setAmountSats={setAmountSats}
|
||||||
/>
|
fee={feeEstimate()?.toString()}
|
||||||
<Show when={amountWarning() && amountSats() > 0n}>
|
isAmountEditable={true}
|
||||||
<InfoBox accent={"red"}>{amountWarning()}</InfoBox>
|
/>
|
||||||
</Show>
|
<Show when={amountWarning() && amountSats() > 0n}>
|
||||||
</VStack>
|
<InfoBox accent={"red"}>{amountWarning()}</InfoBox>
|
||||||
<div class="flex-1" />
|
</Show>
|
||||||
<Button
|
</VStack>
|
||||||
class="w-full flex-grow-0"
|
<div class="flex-1" />
|
||||||
disabled={!canSwap()}
|
<Button
|
||||||
intent="blue"
|
class="w-full flex-grow-0"
|
||||||
onClick={handleSwap}
|
disabled={!canSwap()}
|
||||||
loading={false}
|
intent="blue"
|
||||||
>
|
onClick={handleSwap}
|
||||||
{"Confirm Swap"}
|
loading={false}
|
||||||
</Button>
|
>
|
||||||
</DefaultMain>
|
{"Confirm Swap"}
|
||||||
<NavBar activeTab="none" />
|
</Button>
|
||||||
</SafeArea>
|
</DefaultMain>
|
||||||
</MutinyWalletGuard>
|
<NavBar activeTab="none" />
|
||||||
);
|
</SafeArea>
|
||||||
|
</MutinyWalletGuard>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
import { Title } from "solid-start";
|
import { Title } from "solid-start";
|
||||||
import { HttpStatusCode } from "solid-start/server";
|
import { HttpStatusCode } from "solid-start/server";
|
||||||
import { ButtonLink, DefaultMain, LargeHeader, SafeArea } from "~/components/layout";
|
import {
|
||||||
|
ButtonLink,
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
SafeArea
|
||||||
|
} from "~/components/layout";
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<Title>Not Found</Title>
|
<Title>Not Found</Title>
|
||||||
<HttpStatusCode code={404} />
|
<HttpStatusCode code={404} />
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<LargeHeader>Not Found</LargeHeader>
|
<LargeHeader>Not Found</LargeHeader>
|
||||||
<p>
|
<p>This is probably Paul's fault.</p>
|
||||||
This is probably Paul's fault.
|
<div class="h-full" />
|
||||||
</p>
|
<ButtonLink href="/" intent="red">
|
||||||
<div class="h-full" />
|
Dangit
|
||||||
<ButtonLink href="/" intent="red">Dangit</ButtonLink>
|
</ButtonLink>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import App from "~/components/App";
|
import App from "~/components/App";
|
||||||
import { Switch, Match } from "solid-js";
|
import { Switch, Match } from "solid-js";
|
||||||
import { WaitlistAlreadyIn } from "~/components/waitlist/WaitlistAlreadyIn";
|
import { WaitlistAlreadyIn } from "~/components/waitlist/WaitlistAlreadyIn";
|
||||||
@@ -7,22 +6,22 @@ import { useMegaStore } from "~/state/megaStore";
|
|||||||
import { FullscreenLoader } from "~/components/layout";
|
import { FullscreenLoader } from "~/components/layout";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Switch fallback={<FullscreenLoader />} >
|
<Switch fallback={<FullscreenLoader />}>
|
||||||
{/* TODO: can you put a suspense around a match? */}
|
{/* TODO: can you put a suspense around a match? */}
|
||||||
<Match when={state.user_status === "approved"}>
|
<Match when={state.user_status === "approved"}>
|
||||||
<App />
|
<App />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={state.user_status === "waitlisted"}>
|
<Match when={state.user_status === "waitlisted"}>
|
||||||
<WaitlistAlreadyIn />
|
<WaitlistAlreadyIn />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={state.user_status === "new_here"}>
|
<Match when={state.user_status === "new_here"}>
|
||||||
<WaitlistForm />
|
<WaitlistForm />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
/* @refresh reload */
|
/* @refresh reload */
|
||||||
|
|
||||||
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
|
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
|
||||||
import { ParentComponent, createContext, createEffect, onCleanup, onMount, useContext } from "solid-js";
|
|
||||||
import { createStore, reconcile } from "solid-js/store";
|
|
||||||
import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup";
|
|
||||||
import {
|
import {
|
||||||
MutinyBalance,
|
ParentComponent,
|
||||||
MutinyWallet,
|
createContext,
|
||||||
ActivityItem as MutinyActivity
|
createEffect,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
useContext
|
||||||
|
} from "solid-js";
|
||||||
|
import { createStore, reconcile } from "solid-js/store";
|
||||||
|
import {
|
||||||
|
MutinyWalletSettingStrings,
|
||||||
|
setupMutinyWallet
|
||||||
|
} from "~/logic/mutinyWalletSetup";
|
||||||
|
import {
|
||||||
|
MutinyBalance,
|
||||||
|
MutinyWallet,
|
||||||
|
ActivityItem as MutinyActivity
|
||||||
} from "@mutinywallet/mutiny-wasm";
|
} from "@mutinywallet/mutiny-wasm";
|
||||||
import { ParsedParams } from "~/routes/Scanner";
|
import { ParsedParams } from "~/routes/Scanner";
|
||||||
import { MutinyTagItem } from "~/utils/tags";
|
import { MutinyTagItem } from "~/utils/tags";
|
||||||
@@ -17,211 +27,229 @@ const MegaStoreContext = createContext<MegaStore>();
|
|||||||
type UserStatus = undefined | "new_here" | "waitlisted" | "approved" | "paid";
|
type UserStatus = undefined | "new_here" | "waitlisted" | "approved" | "paid";
|
||||||
|
|
||||||
export type MegaStore = [
|
export type MegaStore = [
|
||||||
{
|
{
|
||||||
already_approved?: boolean;
|
already_approved?: boolean;
|
||||||
waitlist_id?: string;
|
waitlist_id?: string;
|
||||||
mutiny_wallet?: MutinyWallet;
|
mutiny_wallet?: MutinyWallet;
|
||||||
deleting: boolean;
|
deleting: boolean;
|
||||||
user_status: UserStatus;
|
user_status: UserStatus;
|
||||||
scan_result?: ParsedParams;
|
scan_result?: ParsedParams;
|
||||||
balance?: MutinyBalance;
|
balance?: MutinyBalance;
|
||||||
is_syncing?: boolean;
|
is_syncing?: boolean;
|
||||||
last_sync?: number;
|
last_sync?: number;
|
||||||
price: number;
|
price: number;
|
||||||
has_backed_up: boolean;
|
has_backed_up: boolean;
|
||||||
dismissed_restore_prompt: boolean;
|
dismissed_restore_prompt: boolean;
|
||||||
wallet_loading: boolean;
|
wallet_loading: boolean;
|
||||||
nwc_enabled: boolean;
|
nwc_enabled: boolean;
|
||||||
activity: MutinyActivity[];
|
activity: MutinyActivity[];
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fetchUserStatus(): Promise<UserStatus>;
|
fetchUserStatus(): Promise<UserStatus>;
|
||||||
setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void>;
|
setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void>;
|
||||||
deleteMutinyWallet(): Promise<void>;
|
deleteMutinyWallet(): Promise<void>;
|
||||||
setWaitlistId(waitlist_id: string): void;
|
setWaitlistId(waitlist_id: string): void;
|
||||||
setScanResult(scan_result: ParsedParams | undefined): void;
|
setScanResult(scan_result: ParsedParams | undefined): void;
|
||||||
sync(): Promise<void>;
|
sync(): Promise<void>;
|
||||||
dismissRestorePrompt(): void;
|
dismissRestorePrompt(): void;
|
||||||
setHasBackedUp(): void;
|
setHasBackedUp(): void;
|
||||||
listTags(): Promise<MutinyTagItem[]>;
|
listTags(): Promise<MutinyTagItem[]>;
|
||||||
setNwc(enabled: boolean): void;
|
setNwc(enabled: boolean): void;
|
||||||
syncActivity(): Promise<void>;
|
syncActivity(): Promise<void>;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Provider: ParentComponent = (props) => {
|
export const Provider: ParentComponent = (props) => {
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
already_approved:
|
already_approved:
|
||||||
import.meta.env.VITE_SELFHOSTED === "true" ||
|
import.meta.env.VITE_SELFHOSTED === "true" ||
|
||||||
localStorage.getItem("already_approved") === "true",
|
localStorage.getItem("already_approved") === "true",
|
||||||
waitlist_id: localStorage.getItem("waitlist_id"),
|
waitlist_id: localStorage.getItem("waitlist_id"),
|
||||||
mutiny_wallet: undefined as MutinyWallet | undefined,
|
mutiny_wallet: undefined as MutinyWallet | undefined,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
user_status: undefined as UserStatus,
|
user_status: undefined as UserStatus,
|
||||||
scan_result: undefined as ParsedParams | undefined,
|
scan_result: undefined as ParsedParams | undefined,
|
||||||
price: 0,
|
price: 0,
|
||||||
has_backed_up: localStorage.getItem("has_backed_up") === "true",
|
has_backed_up: localStorage.getItem("has_backed_up") === "true",
|
||||||
balance: undefined as MutinyBalance | undefined,
|
balance: undefined as MutinyBalance | undefined,
|
||||||
last_sync: undefined as number | undefined,
|
last_sync: undefined as number | undefined,
|
||||||
is_syncing: false,
|
is_syncing: false,
|
||||||
dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true",
|
dismissed_restore_prompt:
|
||||||
wallet_loading: true,
|
localStorage.getItem("dismissed_restore_prompt") === "true",
|
||||||
nwc_enabled: localStorage.getItem("nwc_enabled") === "true",
|
wallet_loading: true,
|
||||||
activity: [] as MutinyActivity[]
|
nwc_enabled: localStorage.getItem("nwc_enabled") === "true",
|
||||||
});
|
activity: [] as MutinyActivity[]
|
||||||
|
|
||||||
const actions = {
|
|
||||||
async fetchUserStatus(): Promise<UserStatus> {
|
|
||||||
if (state.already_approved) {
|
|
||||||
console.log("welcome back!");
|
|
||||||
return "approved";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.waitlist_id) {
|
|
||||||
return "new_here";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`https://waitlist.mutiny-waitlist.workers.dev/waitlist/${state.waitlist_id}`
|
|
||||||
);
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data.approval_date) {
|
|
||||||
// Remember them so we don't have to check every time
|
|
||||||
localStorage.setItem("already_approved", "true");
|
|
||||||
return "approved";
|
|
||||||
} else {
|
|
||||||
return "waitlisted";
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return "new_here";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void> {
|
|
||||||
try {
|
|
||||||
setState({ wallet_loading: true });
|
|
||||||
const mutinyWallet = await setupMutinyWallet(settings);
|
|
||||||
// Get balance optimistically
|
|
||||||
const balance = await mutinyWallet.get_balance();
|
|
||||||
// start nwc if enabled
|
|
||||||
if (state.nwc_enabled) {
|
|
||||||
const nodes = await mutinyWallet.list_nodes();
|
|
||||||
const firstNode = (nodes[0] as string) || "";
|
|
||||||
await mutinyWallet.start_nostr_wallet_connect(firstNode);
|
|
||||||
}
|
|
||||||
setState({ mutiny_wallet: mutinyWallet, wallet_loading: false, balance });
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteMutinyWallet(): Promise<void> {
|
|
||||||
await state.mutiny_wallet?.stop();
|
|
||||||
setState((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
mutiny_wallet: undefined,
|
|
||||||
deleting: true
|
|
||||||
}));
|
|
||||||
MutinyWallet.import_json("{}");
|
|
||||||
localStorage.clear();
|
|
||||||
},
|
|
||||||
setWaitlistId(waitlist_id: string) {
|
|
||||||
setState({ waitlist_id });
|
|
||||||
},
|
|
||||||
async sync(): Promise<void> {
|
|
||||||
try {
|
|
||||||
if (state.mutiny_wallet && !state.is_syncing) {
|
|
||||||
setState({ is_syncing: true });
|
|
||||||
const newBalance = await state.mutiny_wallet?.get_balance();
|
|
||||||
const price = await state.mutiny_wallet?.get_bitcoin_price();
|
|
||||||
setState({
|
|
||||||
balance: newBalance,
|
|
||||||
last_sync: Date.now(),
|
|
||||||
price: price || 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
setState({ is_syncing: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async syncActivity(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const activity = await state.mutiny_wallet?.get_activity();
|
|
||||||
setState("activity", reconcile(activity, { merge: true }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setScanResult(scan_result: ParsedParams) {
|
|
||||||
setState({ scan_result });
|
|
||||||
},
|
|
||||||
setHasBackedUp() {
|
|
||||||
localStorage.setItem("has_backed_up", "true");
|
|
||||||
setState({ has_backed_up: true });
|
|
||||||
},
|
|
||||||
dismissRestorePrompt() {
|
|
||||||
localStorage.setItem("dismissed_restore_prompt", "true");
|
|
||||||
setState({ dismissed_restore_prompt: true });
|
|
||||||
},
|
|
||||||
async listTags(): Promise<MutinyTagItem[]> {
|
|
||||||
return state.mutiny_wallet?.get_tag_items() as MutinyTagItem[];
|
|
||||||
},
|
|
||||||
setNwc(enabled: boolean) {
|
|
||||||
localStorage.setItem("nwc_enabled", enabled.toString());
|
|
||||||
setState({ nwc_enabled: enabled });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch status from remote on load
|
|
||||||
onMount(() => {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
actions.fetchUserStatus().then((status) => {
|
|
||||||
setState({ user_status: status });
|
|
||||||
|
|
||||||
// Only load node manager when status is approved
|
|
||||||
if (state.user_status === "approved" && !state.mutiny_wallet && !state.deleting) {
|
|
||||||
console.log("running setup node manager...");
|
|
||||||
actions.setupMutinyWallet().then(() => console.log("node manager setup done"));
|
|
||||||
|
|
||||||
// Setup an event listener to stop the mutiny wallet when the page unloads
|
|
||||||
window.onunload = async (_e) => {
|
|
||||||
console.log("stopping mutiny_wallet");
|
|
||||||
await state.mutiny_wallet?.stop();
|
|
||||||
console.log("mutiny_wallet stopped");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Be reactive to changes in waitlist_id
|
const actions = {
|
||||||
createEffect(() => {
|
async fetchUserStatus(): Promise<UserStatus> {
|
||||||
state.waitlist_id
|
if (state.already_approved) {
|
||||||
? localStorage.setItem("waitlist_id", state.waitlist_id)
|
console.log("welcome back!");
|
||||||
: localStorage.removeItem("waitlist_id");
|
return "approved";
|
||||||
});
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
if (!state.waitlist_id) {
|
||||||
const interval = setInterval(async () => {
|
return "new_here";
|
||||||
await actions.sync();
|
}
|
||||||
}, 3 * 1000); // Poll every 3 seconds
|
|
||||||
|
|
||||||
onCleanup(() => {
|
try {
|
||||||
clearInterval(interval);
|
const res = await fetch(
|
||||||
|
`https://waitlist.mutiny-waitlist.workers.dev/waitlist/${state.waitlist_id}`
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.approval_date) {
|
||||||
|
// Remember them so we don't have to check every time
|
||||||
|
localStorage.setItem("already_approved", "true");
|
||||||
|
return "approved";
|
||||||
|
} else {
|
||||||
|
return "waitlisted";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return "new_here";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setupMutinyWallet(
|
||||||
|
settings?: MutinyWalletSettingStrings
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
setState({ wallet_loading: true });
|
||||||
|
const mutinyWallet = await setupMutinyWallet(settings);
|
||||||
|
// Get balance optimistically
|
||||||
|
const balance = await mutinyWallet.get_balance();
|
||||||
|
// start nwc if enabled
|
||||||
|
if (state.nwc_enabled) {
|
||||||
|
const nodes = await mutinyWallet.list_nodes();
|
||||||
|
const firstNode = (nodes[0] as string) || "";
|
||||||
|
await mutinyWallet.start_nostr_wallet_connect(firstNode);
|
||||||
|
}
|
||||||
|
setState({
|
||||||
|
mutiny_wallet: mutinyWallet,
|
||||||
|
wallet_loading: false,
|
||||||
|
balance
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteMutinyWallet(): Promise<void> {
|
||||||
|
await state.mutiny_wallet?.stop();
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
mutiny_wallet: undefined,
|
||||||
|
deleting: true
|
||||||
|
}));
|
||||||
|
MutinyWallet.import_json("{}");
|
||||||
|
localStorage.clear();
|
||||||
|
},
|
||||||
|
setWaitlistId(waitlist_id: string) {
|
||||||
|
setState({ waitlist_id });
|
||||||
|
},
|
||||||
|
async sync(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (state.mutiny_wallet && !state.is_syncing) {
|
||||||
|
setState({ is_syncing: true });
|
||||||
|
const newBalance = await state.mutiny_wallet?.get_balance();
|
||||||
|
const price =
|
||||||
|
await state.mutiny_wallet?.get_bitcoin_price();
|
||||||
|
setState({
|
||||||
|
balance: newBalance,
|
||||||
|
last_sync: Date.now(),
|
||||||
|
price: price || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
setState({ is_syncing: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async syncActivity(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const activity = await state.mutiny_wallet?.get_activity();
|
||||||
|
setState("activity", reconcile(activity, { merge: true }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setScanResult(scan_result: ParsedParams) {
|
||||||
|
setState({ scan_result });
|
||||||
|
},
|
||||||
|
setHasBackedUp() {
|
||||||
|
localStorage.setItem("has_backed_up", "true");
|
||||||
|
setState({ has_backed_up: true });
|
||||||
|
},
|
||||||
|
dismissRestorePrompt() {
|
||||||
|
localStorage.setItem("dismissed_restore_prompt", "true");
|
||||||
|
setState({ dismissed_restore_prompt: true });
|
||||||
|
},
|
||||||
|
async listTags(): Promise<MutinyTagItem[]> {
|
||||||
|
return state.mutiny_wallet?.get_tag_items() as MutinyTagItem[];
|
||||||
|
},
|
||||||
|
setNwc(enabled: boolean) {
|
||||||
|
localStorage.setItem("nwc_enabled", enabled.toString());
|
||||||
|
setState({ nwc_enabled: enabled });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch status from remote on load
|
||||||
|
onMount(() => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
actions.fetchUserStatus().then((status) => {
|
||||||
|
setState({ user_status: status });
|
||||||
|
|
||||||
|
// Only load node manager when status is approved
|
||||||
|
if (
|
||||||
|
state.user_status === "approved" &&
|
||||||
|
!state.mutiny_wallet &&
|
||||||
|
!state.deleting
|
||||||
|
) {
|
||||||
|
console.log("running setup node manager...");
|
||||||
|
actions
|
||||||
|
.setupMutinyWallet()
|
||||||
|
.then(() => console.log("node manager setup done"));
|
||||||
|
|
||||||
|
// Setup an event listener to stop the mutiny wallet when the page unloads
|
||||||
|
window.onunload = async (_e) => {
|
||||||
|
console.log("stopping mutiny_wallet");
|
||||||
|
await state.mutiny_wallet?.stop();
|
||||||
|
console.log("mutiny_wallet stopped");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const store = [state, actions] as MegaStore;
|
// Be reactive to changes in waitlist_id
|
||||||
|
createEffect(() => {
|
||||||
|
state.waitlist_id
|
||||||
|
? localStorage.setItem("waitlist_id", state.waitlist_id)
|
||||||
|
: localStorage.removeItem("waitlist_id");
|
||||||
|
});
|
||||||
|
|
||||||
return <MegaStoreContext.Provider value={store}>{props.children}</MegaStoreContext.Provider>;
|
createEffect(() => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await actions.sync();
|
||||||
|
}, 3 * 1000); // Poll every 3 seconds
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = [state, actions] as MegaStore;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MegaStoreContext.Provider value={store}>
|
||||||
|
{props.children}
|
||||||
|
</MegaStoreContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useMegaStore() {
|
export function useMegaStore() {
|
||||||
// This is a trick to narrow the typescript types: https://docs.solidjs.com/references/api-reference/component-apis/createContext
|
// This is a trick to narrow the typescript types: https://docs.solidjs.com/references/api-reference/component-apis/createContext
|
||||||
const context = useContext(MegaStoreContext);
|
const context = useContext(MegaStoreContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useMegaStore: cannot find a MegaStoreContext")
|
throw new Error("useMegaStore: cannot find a MegaStoreContext");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export const DIALOG_POSITIONER = "fixed inset-0 h-[100dvh] z-50"
|
export const DIALOG_POSITIONER = "fixed inset-0 h-[100dvh] z-50";
|
||||||
export const DIALOG_CONTENT = "h-[100dvh] flex flex-col justify-between px-4 pt-4 pb-8 bg-neutral-800/80 backdrop-blur-xl touch-manipulation select-none"
|
export const DIALOG_CONTENT =
|
||||||
|
"h-[100dvh] flex flex-col justify-between px-4 pt-4 pb-8 bg-neutral-800/80 backdrop-blur-xl touch-manipulation select-none";
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
.solid-select-container[data-disabled="true"] {
|
.solid-select-container[data-disabled="true"] {
|
||||||
@apply pointer-events-none;
|
@apply pointer-events-none;
|
||||||
}
|
}
|
||||||
.solid-select-container {
|
.solid-select-container {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
}
|
}
|
||||||
.solid-select-control[data-disabled="true"] {
|
.solid-select-control[data-disabled="true"] {
|
||||||
}
|
}
|
||||||
.solid-select-control {
|
.solid-select-control {
|
||||||
@apply w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400;
|
@apply w-full p-2 rounded-lg bg-white/10 placeholder-neutral-400;
|
||||||
@apply grid leading-6;
|
@apply grid leading-6;
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
.solid-select-control[data-multiple="true"][data-has-value="true"] {
|
.solid-select-control[data-multiple="true"][data-has-value="true"] {
|
||||||
@apply flex items-stretch gap-1 flex-wrap;
|
@apply flex items-stretch gap-1 flex-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solid-select-placeholder {
|
.solid-select-placeholder {
|
||||||
@apply text-neutral-400;
|
@apply text-neutral-400;
|
||||||
@apply col-start-1 row-start-1;
|
@apply col-start-1 row-start-1;
|
||||||
}
|
}
|
||||||
.solid-select-single-value {
|
.solid-select-single-value {
|
||||||
@apply col-start-1 row-start-1;
|
@apply col-start-1 row-start-1;
|
||||||
}
|
}
|
||||||
.solid-select-multi-value {
|
.solid-select-multi-value {
|
||||||
@apply flex bg-white/20 rounded items-center px-1;
|
@apply flex bg-white/20 rounded items-center px-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solid-select-multi-value-remove {
|
.solid-select-multi-value-remove {
|
||||||
/* TODO: there's gotta be a better way to vertically center this */
|
/* TODO: there's gotta be a better way to vertically center this */
|
||||||
@apply pl-2 pr-1 leading-3 -mt-2 text-2xl;
|
@apply pl-2 pr-1 leading-3 -mt-2 text-2xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solid-select-input {
|
.solid-select-input {
|
||||||
@apply bg-transparent caret-transparent flex-grow flex-shrink;
|
@apply bg-transparent caret-transparent flex-grow flex-shrink;
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
@apply col-start-1 row-start-1;
|
@apply col-start-1 row-start-1;
|
||||||
}
|
}
|
||||||
.solid-select-input:read-only {
|
.solid-select-input:read-only {
|
||||||
@apply cursor-default;
|
@apply cursor-default;
|
||||||
}
|
}
|
||||||
.solid-select-input[data-multiple="true"] {
|
.solid-select-input[data-multiple="true"] {
|
||||||
@apply caret-current;
|
@apply caret-current;
|
||||||
}
|
}
|
||||||
.solid-select-input[data-is-active="true"] {
|
.solid-select-input[data-is-active="true"] {
|
||||||
@apply caret-current;
|
@apply caret-current;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solid-select-list {
|
.solid-select-list {
|
||||||
@apply max-h-[50vh] min-w-full overflow-y-auto absolute whitespace-nowrap z-10 bg-neutral-950 p-2 rounded-lg;
|
@apply max-h-[50vh] min-w-full overflow-y-auto absolute whitespace-nowrap z-10 bg-neutral-950 p-2 rounded-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solid-select-option[data-focused="true"] {
|
.solid-select-option[data-focused="true"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.solid-select-option > mark {
|
.solid-select-option > mark {
|
||||||
@apply underline bg-white/10 text-white;
|
@apply underline bg-white/10 text-white;
|
||||||
}
|
}
|
||||||
.solid-select-option {
|
.solid-select-option {
|
||||||
@apply cursor-default select-none p-1 hover:bg-neutral-800 rounded;
|
@apply cursor-default select-none p-1 hover:bg-neutral-800 rounded;
|
||||||
}
|
}
|
||||||
.solid-select-option[data-disabled="true"] {
|
.solid-select-option[data-disabled="true"] {
|
||||||
@apply pointer-events-none opacity-50;
|
@apply pointer-events-none opacity-50;
|
||||||
}
|
}
|
||||||
.solid-select-list-placeholder {
|
.solid-select-list-placeholder {
|
||||||
@apply cursor-default select-none;
|
@apply cursor-default select-none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
// Take in a string that looks like this:
|
|
||||||
// bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl
|
|
||||||
// and return an object with this shape: { address: string, amount: number, label: string, lightning: string }
|
|
||||||
// using typescript type annotations
|
|
||||||
export function bip21decode(bip21: string): { address?: string, amount?: number, label?: string, lightning?: string } {
|
|
||||||
const [scheme, data] = bip21.split(':')
|
|
||||||
if (scheme !== 'bitcoin') {
|
|
||||||
// TODO: this is a WAILA job I just want to debug more of the send flow
|
|
||||||
if (bip21.startsWith('lnt')) {
|
|
||||||
return { lightning: bip21 }
|
|
||||||
} else if (bip21.startsWith('tb1')) {
|
|
||||||
return { address: bip21 }
|
|
||||||
} else {
|
|
||||||
throw new Error('Not a bitcoin URI')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const [address, query] = data.split('?')
|
|
||||||
const params = new URLSearchParams(query)
|
|
||||||
return {
|
|
||||||
address,
|
|
||||||
amount: Number(params.get('amount')) || undefined,
|
|
||||||
label: params.get('label') || undefined,
|
|
||||||
lightning: params.get('lightning') || undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +1,57 @@
|
|||||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
|
|
||||||
export function satsToUsd(amount: number | undefined, price: number, formatted: boolean): string {
|
export function satsToUsd(
|
||||||
if (typeof amount !== "number" || isNaN(amount)) {
|
amount: number | undefined,
|
||||||
return "";
|
price: number,
|
||||||
}
|
formatted: boolean
|
||||||
try {
|
): string {
|
||||||
const btc = MutinyWallet.convert_sats_to_btc(BigInt(Math.floor(amount)));
|
if (typeof amount !== "number" || isNaN(amount)) {
|
||||||
const usd = btc * price;
|
return "";
|
||||||
|
}
|
||||||
if (formatted) {
|
try {
|
||||||
return usd.toLocaleString("en-US", { style: "currency", currency: "USD" });
|
const btc = MutinyWallet.convert_sats_to_btc(
|
||||||
} else {
|
BigInt(Math.floor(amount))
|
||||||
// Some float fighting shenaningans
|
);
|
||||||
const roundedUsd = Math.round(usd);
|
const usd = btc * price;
|
||||||
if (roundedUsd * 100 === Math.round(usd * 100)) {
|
|
||||||
return usd.toFixed(0);
|
if (formatted) {
|
||||||
} else {
|
return usd.toLocaleString("en-US", {
|
||||||
return usd.toFixed(2);
|
style: "currency",
|
||||||
}
|
currency: "USD"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Some float fighting shenaningans
|
||||||
|
const roundedUsd = Math.round(usd);
|
||||||
|
if (roundedUsd * 100 === Math.round(usd * 100)) {
|
||||||
|
return usd.toFixed(0);
|
||||||
|
} else {
|
||||||
|
return usd.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usdToSats(amount: number | undefined, price: number, formatted: boolean): string {
|
export function usdToSats(
|
||||||
if (typeof amount !== "number" || isNaN(amount)) {
|
amount: number | undefined,
|
||||||
return "";
|
price: number,
|
||||||
}
|
formatted: boolean
|
||||||
try {
|
): string {
|
||||||
const btc = price / amount;
|
if (typeof amount !== "number" || isNaN(amount)) {
|
||||||
const sats = MutinyWallet.convert_btc_to_sats(btc);
|
return "";
|
||||||
if (formatted) {
|
}
|
||||||
return parseInt(sats.toString()).toLocaleString();
|
try {
|
||||||
} else {
|
const btc = price / amount;
|
||||||
return sats.toString();
|
const sats = MutinyWallet.convert_btc_to_sats(btc);
|
||||||
|
if (formatted) {
|
||||||
|
return parseInt(sats.toString()).toLocaleString();
|
||||||
|
} else {
|
||||||
|
return sats.toString();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// https://stackoverflow.com/questions/34156282/how-do-i-save-json-to-local-text-file
|
// https://stackoverflow.com/questions/34156282/how-do-i-save-json-to-local-text-file
|
||||||
|
|
||||||
export function downloadTextFile(content: string, fileName: string, type?: string) {
|
export function downloadTextFile(
|
||||||
|
content: string,
|
||||||
|
fileName: string,
|
||||||
|
type?: string
|
||||||
|
) {
|
||||||
const contentType = type ? type : "application/json";
|
const contentType = type ? type : "application/json";
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
const file = new Blob([content], { type: contentType });
|
const file = new Blob([content], { type: contentType });
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
export default function eify(e: unknown): Error {
|
export default function eify(e: unknown): Error {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
return e;
|
return e;
|
||||||
} else if (typeof e === 'string') {
|
} else if (typeof e === "string") {
|
||||||
return new Error(e);
|
return new Error(e);
|
||||||
} else {
|
} else {
|
||||||
return new Error('Unknown error');
|
return new Error("Unknown error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
// Simple storage for fake labels
|
// Simple storage for fake labels
|
||||||
// For each outpoint string, we can store a boolean whether it's redshifted or not
|
// For each outpoint string, we can store a boolean whether it's redshifted or not
|
||||||
|
|
||||||
function setRedshifted(redshifted: boolean, outpoint?: string,) {
|
function setRedshifted(redshifted: boolean, outpoint?: string) {
|
||||||
if (outpoint === undefined) return;
|
if (outpoint === undefined) return;
|
||||||
localStorage.setItem(outpoint, redshifted.toString())
|
localStorage.setItem(outpoint, redshifted.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRedshifted(outpoint: string): boolean {
|
function getRedshifted(outpoint: string): boolean {
|
||||||
const redshifted = localStorage.getItem(outpoint)
|
const redshifted = localStorage.getItem(outpoint);
|
||||||
if (redshifted === null) {
|
if (redshifted === null) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
return redshifted === 'true'
|
return redshifted === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEST_UTXO = "47651763fbd74488a478aad80e4205c3e34bbadcfc42b5cd9557ef12a15ab00c:1"
|
const TEST_UTXO =
|
||||||
|
"47651763fbd74488a478aad80e4205c3e34bbadcfc42b5cd9557ef12a15ab00c:1";
|
||||||
|
|
||||||
export { setRedshifted, getRedshifted, TEST_UTXO }
|
export { setRedshifted, getRedshifted, TEST_UTXO };
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Contact } from "@mutinywallet/mutiny-wasm";
|
|||||||
export async function generateGradient(str: string) {
|
export async function generateGradient(str: string) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const data = encoder.encode(str);
|
const data = encoder.encode(str);
|
||||||
const digestBuffer = await crypto.subtle.digest('SHA-256', data);
|
const digestBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||||
const digestArray = new Uint8Array(digestBuffer);
|
const digestArray = new Uint8Array(digestBuffer);
|
||||||
const h1 = digestArray[0] % 360;
|
const h1 = digestArray[0] % 360;
|
||||||
const h2 = (h1 + 180) % 360;
|
const h2 = (h1 + 180) % 360;
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { Network } from "~/logic/mutinyWalletSetup"
|
import { Network } from "~/logic/mutinyWalletSetup";
|
||||||
|
|
||||||
export default function mempoolTxUrl(txid?: string, network?: Network) {
|
export default function mempoolTxUrl(txid?: string, network?: Network) {
|
||||||
if (!txid || !network) {
|
if (!txid || !network) {
|
||||||
console.error("Problem creating the mempool url")
|
console.error("Problem creating the mempool url");
|
||||||
return "#"
|
return "#";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (network) {
|
if (network) {
|
||||||
switch (network) {
|
switch (network) {
|
||||||
case "bitcoin":
|
case "bitcoin":
|
||||||
return `https://mempool.space/tx/${txid}`
|
return `https://mempool.space/tx/${txid}`;
|
||||||
case "testnet":
|
case "testnet":
|
||||||
return `https://mempool.space/testnet/tx/${txid}`
|
return `https://mempool.space/testnet/tx/${txid}`;
|
||||||
case "signet":
|
case "signet":
|
||||||
return `https://mutinynet.com/tx/${txid}`
|
return `https://mutinynet.com/tx/${txid}`;
|
||||||
default:
|
default:
|
||||||
return `https://mempool.space/tx/${txid}`
|
return `https://mempool.space/tx/${txid}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://mempool.space/tx/${txid}`
|
return `https://mempool.space/tx/${txid}`;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
export function objectToSearchParams<T extends Record<string, string | undefined>>(obj: T): string {
|
export function objectToSearchParams<
|
||||||
return Object.entries(obj)
|
T extends Record<string, string | undefined>
|
||||||
.filter(([_, value]) => value !== undefined)
|
>(obj: T): string {
|
||||||
// Value shouldn't be null we just filtered it out but typescript is dumb
|
return (
|
||||||
.map(([key, value]) => value ? `${encodeURIComponent(key)}=${encodeURIComponent(value)}` : "")
|
Object.entries(obj)
|
||||||
.join("&");
|
.filter(([_, value]) => value !== undefined)
|
||||||
|
// Value shouldn't be null we just filtered it out but typescript is dumb
|
||||||
|
.map(([key, value]) =>
|
||||||
|
value
|
||||||
|
? `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
||||||
|
: ""
|
||||||
|
)
|
||||||
|
.join("&")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
export function prettyPrintTime(ts: number) {
|
export function prettyPrintTime(ts: number) {
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: 'short',
|
month: "short",
|
||||||
day: 'numeric',
|
day: "numeric",
|
||||||
hour: 'numeric',
|
hour: "numeric",
|
||||||
minute: 'numeric'
|
minute: "numeric"
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Date(ts * 1000).toLocaleString('en-US', options);
|
return new Date(ts * 1000).toLocaleString("en-US", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeAgo(ts?: number | bigint): string {
|
export function timeAgo(ts?: number | bigint): string {
|
||||||
@@ -23,15 +23,15 @@ export function timeAgo(ts?: number | bigint): string {
|
|||||||
if (elapsedSeconds < 60) {
|
if (elapsedSeconds < 60) {
|
||||||
return "Just now";
|
return "Just now";
|
||||||
} else if (elapsedMinutes < 60) {
|
} else if (elapsedMinutes < 60) {
|
||||||
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? 's' : ''} ago`;
|
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? "s" : ""} ago`;
|
||||||
} else if (elapsedHours < 24) {
|
} else if (elapsedHours < 24) {
|
||||||
return `${elapsedHours} hour${elapsedHours > 1 ? 's' : ''} ago`;
|
return `${elapsedHours} hour${elapsedHours > 1 ? "s" : ""} ago`;
|
||||||
} else if (elapsedDays < 7) {
|
} else if (elapsedDays < 7) {
|
||||||
return `${elapsedDays} day${elapsedDays > 1 ? 's' : ''} ago`;
|
return `${elapsedDays} day${elapsedDays > 1 ? "s" : ""} ago`;
|
||||||
} else {
|
} else {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
return `${month}/${day}/${year}`;
|
return `${month}/${day}/${year}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
import { TagItem } from "@mutinywallet/mutiny-wasm"
|
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||||
|
|
||||||
export type MutinyTagItem = {
|
export type MutinyTagItem = {
|
||||||
id: string,
|
id: string;
|
||||||
kind: "Label" | "Contact"
|
kind: "Label" | "Contact";
|
||||||
name: string,
|
name: string;
|
||||||
last_used_time: bigint,
|
last_used_time: bigint;
|
||||||
npub?: string,
|
npub?: string;
|
||||||
ln_address?: string,
|
ln_address?: string;
|
||||||
lnurl?: string,
|
lnurl?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const UNKNOWN_TAG: MutinyTagItem = { id: "Unknown", kind: "Label", name: "Unknown", last_used_time: 0n }
|
export const UNKNOWN_TAG: MutinyTagItem = {
|
||||||
|
id: "Unknown",
|
||||||
|
kind: "Label",
|
||||||
|
name: "Unknown",
|
||||||
|
last_used_time: 0n
|
||||||
|
};
|
||||||
|
|
||||||
export function tagsToIds(tags?: MutinyTagItem[]): string[] {
|
export function tagsToIds(tags?: MutinyTagItem[]): string[] {
|
||||||
if (!tags) {
|
if (!tags) {
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id)
|
return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tagToMutinyTag(tag: TagItem): MutinyTagItem {
|
export function tagToMutinyTag(tag: TagItem): MutinyTagItem {
|
||||||
// @ts-expect-error: FIXME: make typescript less mad about this
|
// @ts-expect-error: FIXME: make typescript less mad about this
|
||||||
return tag as MutinyTagItem
|
return tag as MutinyTagItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortByLastUsed(a: MutinyTagItem, b: MutinyTagItem) {
|
export function sortByLastUsed(a: MutinyTagItem, b: MutinyTagItem) {
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
// Thanks you https://soorria.com/snippets/use-copy-solidjs
|
// Thanks you https://soorria.com/snippets/use-copy-solidjs
|
||||||
import type { Accessor } from 'solid-js'
|
import type { Accessor } from "solid-js";
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from "solid-js";
|
||||||
export type UseCopyProps = {
|
export type UseCopyProps = {
|
||||||
copiedTimeout?: number
|
copiedTimeout?: number;
|
||||||
}
|
};
|
||||||
type CopyFn = (text: string) => Promise<void>
|
type CopyFn = (text: string) => Promise<void>;
|
||||||
export const useCopy = ({ copiedTimeout = 2000 }: UseCopyProps = {}): [
|
export const useCopy = ({ copiedTimeout = 2000 }: UseCopyProps = {}): [
|
||||||
copy: CopyFn,
|
copy: CopyFn,
|
||||||
copied: Accessor<boolean>
|
copied: Accessor<boolean>
|
||||||
] => {
|
] => {
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false);
|
||||||
let timeout: NodeJS.Timeout
|
let timeout: NodeJS.Timeout;
|
||||||
const copy: CopyFn = async text => {
|
const copy: CopyFn = async (text) => {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true)
|
setCopied(true);
|
||||||
if (timeout) clearTimeout(timeout)
|
if (timeout) clearTimeout(timeout);
|
||||||
timeout = setTimeout(() => setCopied(false), copiedTimeout)
|
timeout = setTimeout(() => setCopied(false), copiedTimeout);
|
||||||
}
|
};
|
||||||
return [copy, copied]
|
return [copy, copied];
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -56,50 +56,41 @@ module.exports = {
|
|||||||
require("@kobalte/tailwindcss"),
|
require("@kobalte/tailwindcss"),
|
||||||
plugin(function ({ addUtilities }) {
|
plugin(function ({ addUtilities }) {
|
||||||
const newUtilities = {
|
const newUtilities = {
|
||||||
'.safe-top': {
|
".safe-top": {
|
||||||
paddingTop: 'constant(safe-area-inset-top)',
|
paddingTop: "constant(safe-area-inset-top)",
|
||||||
paddingTop: 'env(safe-area-inset-top)'
|
paddingTop: "env(safe-area-inset-top)"
|
||||||
},
|
|
||||||
'.safe-left': {
|
|
||||||
paddingLeft: 'constant(safe-area-inset-left)',
|
|
||||||
paddingLeft: 'env(safe-area-inset-left)'
|
|
||||||
},
|
|
||||||
'.safe-right': {
|
|
||||||
paddingRight: 'constant(safe-area-inset-right)',
|
|
||||||
paddingRight: 'env(safe-area-inset-right)'
|
|
||||||
},
|
|
||||||
'.safe-bottom': {
|
|
||||||
paddingBottom: 'constant(safe-area-inset-bottom)',
|
|
||||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
|
||||||
},
|
|
||||||
'.h-screen-safe': {
|
|
||||||
height: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
|
||||||
},
|
|
||||||
'.min-h-screen-safe': {
|
|
||||||
minHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
|
||||||
},
|
|
||||||
'.max-h-screen-safe': {
|
|
||||||
maxHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
|
||||||
},
|
|
||||||
'.disable-scrollbars': {
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
'-ms-overflow-style': 'none',
|
|
||||||
'&::-webkit-scrollbar': {
|
|
||||||
width: '0px',
|
|
||||||
background: 'transparent',
|
|
||||||
display: 'none'
|
|
||||||
},
|
},
|
||||||
'& *::-webkit-scrollbar': {
|
".safe-left": {
|
||||||
width: '0px',
|
paddingLeft: "constant(safe-area-inset-left)",
|
||||||
background: 'transparent',
|
paddingLeft: "env(safe-area-inset-left)"
|
||||||
display: 'none'
|
|
||||||
},
|
},
|
||||||
'& *': {
|
".safe-right": {
|
||||||
scrollbarWidth: 'none',
|
paddingRight: "constant(safe-area-inset-right)",
|
||||||
'-ms-overflow-style': 'none'
|
paddingRight: "env(safe-area-inset-right)"
|
||||||
|
},
|
||||||
|
".safe-bottom": {
|
||||||
|
paddingBottom: "constant(safe-area-inset-bottom)",
|
||||||
|
paddingBottom: "env(safe-area-inset-bottom)"
|
||||||
|
},
|
||||||
|
".disable-scrollbars": {
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
"-ms-overflow-style": "none",
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
width: "0px",
|
||||||
|
background: "transparent",
|
||||||
|
display: "none"
|
||||||
|
},
|
||||||
|
"& *::-webkit-scrollbar": {
|
||||||
|
width: "0px",
|
||||||
|
background: "transparent",
|
||||||
|
display: "none"
|
||||||
|
},
|
||||||
|
"& *": {
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
"-ms-overflow-style": "none"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
addUtilities(newUtilities);
|
addUtilities(newUtilities);
|
||||||
}),
|
}),
|
||||||
// Text shadow!
|
// Text shadow!
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
{
|
{
|
||||||
"include": ["global.d.ts", "src/**/*"],
|
"include": [
|
||||||
"compilerOptions": {
|
"global.d.ts",
|
||||||
"allowSyntheticDefaultImports": true,
|
"src/**/*",
|
||||||
"esModuleInterop": true,
|
"tailwind.config.cjs",
|
||||||
"target": "ESNext",
|
".eslintrc.cjs"
|
||||||
"module": "ESNext",
|
],
|
||||||
"moduleResolution": "node",
|
"compilerOptions": {
|
||||||
"jsxImportSource": "solid-js",
|
"allowSyntheticDefaultImports": true,
|
||||||
"jsx": "preserve",
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"target": "ESNext",
|
||||||
"types": ["solid-start/env"],
|
"module": "ESNext",
|
||||||
"baseUrl": "./",
|
"moduleResolution": "node",
|
||||||
"paths": {
|
"jsxImportSource": "solid-js",
|
||||||
"~/*": ["./src/*"]
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
"types": ["solid-start/env"],
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user