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",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
destructuredArrayIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", 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
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"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",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-solid": "0.11.0",
|
"eslint-plugin-solid": "0.11.0",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
"solid-start-node": "^0.2.26",
|
"solid-start-node": "^0.2.26",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
|
|||||||
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
|
||||||
|
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>
|
</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
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
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 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" />
|
<path
|
||||||
</svg>)
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ 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 = {
|
||||||
@@ -42,7 +44,10 @@ function UnifiedActivityItem(props: {
|
|||||||
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 (
|
||||||
@@ -97,17 +102,25 @@ export function CombinedActivity(props: { limit?: number }) {
|
|||||||
<NiceP>Receive some sats to get started</NiceP>
|
<NiceP>Receive some sats to get started</NiceP>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.limit && state.activity.length > props.limit}>
|
<Match
|
||||||
|
when={props.limit && state.activity.length > props.limit}
|
||||||
|
>
|
||||||
<For each={state.activity.slice(0, props.limit)}>
|
<For each={state.activity.slice(0, props.limit)}>
|
||||||
{(activityItem) => (
|
{(activityItem) => (
|
||||||
<UnifiedActivityItem item={activityItem} onClick={openDetailsModal} />
|
<UnifiedActivityItem
|
||||||
|
item={activityItem}
|
||||||
|
onClick={openDetailsModal}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={state.activity.length >= 0}>
|
<Match when={state.activity.length >= 0}>
|
||||||
<For each={state.activity}>
|
<For each={state.activity}>
|
||||||
{(activityItem) => (
|
{(activityItem) => (
|
||||||
<UnifiedActivityItem item={activityItem} onClick={openDetailsModal} />
|
<UnifiedActivityItem
|
||||||
|
item={activityItem}
|
||||||
|
onClick={openDetailsModal}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -35,27 +41,42 @@ export const ActivityAmount: ParentComponent<{
|
|||||||
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,
|
||||||
|
"items-center": props.center
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="text-base"
|
||||||
|
classList={{ "text-m-green": props.positive }}
|
||||||
>
|
>
|
||||||
<div class="text-base" classList={{ "text-m-green": props.positive }}>
|
|
||||||
{props.positive && "+ "}
|
{props.positive && "+ "}
|
||||||
{prettyPrint()} <span class="text-sm">SATS</span>
|
{prettyPrint()} <span class="text-sm">SATS</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-neutral-500">
|
<div class="text-sm text-neutral-500">
|
||||||
≈ {amountInUsd()} <span class="text-sm">USD</span>
|
≈ {amountInUsd()}
|
||||||
|
<span class="text-sm">USD</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function LabelCircle(props: { name?: string; contact: boolean; label: boolean }) {
|
function LabelCircle(props: {
|
||||||
|
name?: string;
|
||||||
|
contact: boolean;
|
||||||
|
label: boolean;
|
||||||
|
}) {
|
||||||
// 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.name || "?");
|
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
|
||||||
|
? props.name[0]
|
||||||
|
: props.label
|
||||||
|
? "≡"
|
||||||
|
: "?";
|
||||||
const bg = () => (props.name && props.contact ? gradient() : "gray");
|
const bg = () => (props.name && props.contact ? gradient() : "gray");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,7 +103,8 @@ export function ActivityItem(props: {
|
|||||||
}) {
|
}) {
|
||||||
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
|
||||||
@@ -115,13 +137,19 @@ export function ActivityItem(props: {
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={firstContact()?.name}>
|
<Match when={firstContact()?.name}>
|
||||||
<span class="text-base font-semibold truncate">{firstContact()?.name}</span>
|
<span class="text-base font-semibold truncate">
|
||||||
|
{firstContact()?.name}
|
||||||
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.labels.length > 0}>
|
<Match when={props.labels.length > 0}>
|
||||||
<span class="text-base font-semibold truncate">{props.labels[0]}</span>
|
<span class="text-base font-semibold truncate">
|
||||||
|
{props.labels[0]}
|
||||||
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<span class="text-base font-semibold text-neutral-500">Unknown</span>
|
<span class="text-base font-semibold text-neutral-500">
|
||||||
|
Unknown
|
||||||
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -129,7 +157,9 @@ export function ActivityItem(props: {
|
|||||||
<time class="text-sm text-neutral-500">Pending</time>
|
<time class="text-sm text-neutral-500">Pending</time>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
|
<time class="text-sm text-neutral-500">
|
||||||
|
{timeAgo(props.date)}
|
||||||
|
</time>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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: {
|
||||||
@@ -17,12 +17,17 @@ export function Amount(props: {
|
|||||||
}) {
|
}) {
|
||||||
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
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
classList={{ "items-center": props.centered }}
|
||||||
|
>
|
||||||
<h1 class="text-4xl font-light">
|
<h1 class="text-4xl font-light">
|
||||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
||||||
|
|
||||||
<span class="text-xl">SATS</span>
|
<span class="text-xl">SATS</span>
|
||||||
</h1>
|
</h1>
|
||||||
<Show when={props.showFiat}>
|
<Show when={props.showFiat}>
|
||||||
@@ -36,13 +41,15 @@ export function Amount(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
? "SAT"
|
||||||
|
: "SATS"}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,16 +10,21 @@ const noop = () => {
|
|||||||
|
|
||||||
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
|
||||||
|
class="flex justify-between items-center"
|
||||||
|
classList={{ "text-neutral-400": props.gray }}
|
||||||
|
>
|
||||||
<div class="font-semibold uppercase">{props.key}</div>
|
<div class="font-semibold uppercase">{props.key}</div>
|
||||||
<div class="font-light">{props.children}</div>
|
<div class="font-light">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InlineAmount: ParentComponent<{ amount: string; sign?: string; fiat?: boolean }> = (
|
export const InlineAmount: ParentComponent<{
|
||||||
props
|
amount: string;
|
||||||
) => {
|
sign?: string;
|
||||||
|
fiat?: boolean;
|
||||||
|
}> = (props) => {
|
||||||
const prettyPrint = createMemo(() => {
|
const prettyPrint = createMemo(() => {
|
||||||
const parsed = Number(props.amount);
|
const parsed = Number(props.amount);
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
@@ -33,20 +38,23 @@ export const InlineAmount: ParentComponent<{ amount: string; sign?: string; fiat
|
|||||||
<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()}{" "}
|
||||||
|
<span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||||
</div>
|
</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()}
|
||||||
|
<span class="text-sm">USD</span>
|
||||||
</div>
|
</div>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -74,12 +82,20 @@ export function AmountCard(props: {
|
|||||||
<KeyValue key="Amount">
|
<KeyValue key="Amount">
|
||||||
<Show
|
<Show
|
||||||
when={props.isAmountEditable}
|
when={props.isAmountEditable}
|
||||||
fallback={<InlineAmount amount={props.amountSats} />}
|
fallback={
|
||||||
|
<InlineAmount
|
||||||
|
amount={props.amountSats}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AmountEditable
|
<AmountEditable
|
||||||
initialOpen={props.initialOpen ?? false}
|
initialOpen={props.initialOpen ?? false}
|
||||||
initialAmountSats={props.amountSats.toString()}
|
initialAmountSats={props.amountSats.toString()}
|
||||||
setAmountSats={props.setAmountSats ? props.setAmountSats : noop}
|
setAmountSats={
|
||||||
|
props.setAmountSats
|
||||||
|
? props.setAmountSats
|
||||||
|
: noop
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
@@ -90,15 +106,28 @@ export function AmountCard(props: {
|
|||||||
<hr class="border-white/20" />
|
<hr class="border-white/20" />
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Total">
|
<KeyValue key="Total">
|
||||||
<InlineAmount amount={add(props.amountSats, props.fee).toString()} />
|
<InlineAmount
|
||||||
|
amount={add(
|
||||||
|
props.amountSats,
|
||||||
|
props.fee
|
||||||
|
).toString()}
|
||||||
|
/>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<USDShower amountSats={props.amountSats} fee={props.fee} />
|
<USDShower
|
||||||
|
amountSats={props.amountSats}
|
||||||
|
fee={props.fee}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.reserve}>
|
<Match when={props.reserve}>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Channel size">
|
<KeyValue key="Channel size">
|
||||||
<InlineAmount amount={add(props.amountSats, props.reserve).toString()} />
|
<InlineAmount
|
||||||
|
amount={add(
|
||||||
|
props.amountSats,
|
||||||
|
props.reserve
|
||||||
|
).toString()}
|
||||||
|
/>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue gray key="- Channel Reserve">
|
<KeyValue gray key="- Channel Reserve">
|
||||||
<InlineAmount amount={props.reserve || "0"} />
|
<InlineAmount amount={props.reserve || "0"} />
|
||||||
@@ -109,7 +138,10 @@ export function AmountCard(props: {
|
|||||||
<KeyValue key="Spendable">
|
<KeyValue key="Spendable">
|
||||||
<InlineAmount amount={props.amountSats} />
|
<InlineAmount amount={props.amountSats} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<USDShower amountSats={props.amountSats} fee={props.reserve} />
|
<USDShower
|
||||||
|
amountSats={props.amountSats}
|
||||||
|
fee={props.reserve}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!props.fee && !props.reserve}>
|
<Match when={!props.fee && !props.reserve}>
|
||||||
@@ -117,12 +149,20 @@ export function AmountCard(props: {
|
|||||||
<KeyValue key="Amount">
|
<KeyValue key="Amount">
|
||||||
<Show
|
<Show
|
||||||
when={props.isAmountEditable}
|
when={props.isAmountEditable}
|
||||||
fallback={<InlineAmount amount={props.amountSats} />}
|
fallback={
|
||||||
|
<InlineAmount
|
||||||
|
amount={props.amountSats}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AmountEditable
|
<AmountEditable
|
||||||
initialOpen={props.initialOpen ?? false}
|
initialOpen={props.initialOpen ?? false}
|
||||||
initialAmountSats={props.amountSats.toString()}
|
initialAmountSats={props.amountSats.toString()}
|
||||||
setAmountSats={props.setAmountSats ? props.setAmountSats : noop}
|
setAmountSats={
|
||||||
|
props.setAmountSats
|
||||||
|
? props.setAmountSats
|
||||||
|
: noop
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
|
|||||||
@@ -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,7 +17,20 @@ 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" },
|
||||||
@@ -32,7 +52,9 @@ function fiatInputSanitizer(input: string): string {
|
|||||||
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");
|
||||||
@@ -56,7 +78,10 @@ function SingleDigitButton(props: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
// Skip the "." if it's fiat
|
// Skip the "." if it's fiat
|
||||||
<Show when={props.fiat || !(props.character === ".")} fallback={<div />}>
|
<Show
|
||||||
|
when={props.fiat || !(props.character === ".")}
|
||||||
|
fallback={<div />}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
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"
|
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"
|
||||||
onClick={() => props.onClick(props.character)}
|
onClick={() => props.onClick(props.character)}
|
||||||
@@ -83,7 +108,8 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
|
|||||||
"scale-150": chars() <= 4
|
"scale-150": chars() <= 4
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.text} <span class="text-xl">{props.fiat ? "USD" : "SATS"}</span>
|
{props.text}
|
||||||
|
<span class="text-xl">{props.fiat ? "USD" : "SATS"}</span>
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -91,7 +117,8 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
|
|||||||
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}
|
||||||
|
<span class="text-sm">{props.fiat ? "USD" : "SATS"}</span>
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,9 +140,15 @@ export const AmountEditable: ParentComponent<{
|
|||||||
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(
|
||||||
|
props.initialAmountSats || "0"
|
||||||
|
);
|
||||||
const [localFiat, setLocalFiat] = createSignal(
|
const [localFiat, setLocalFiat] = createSignal(
|
||||||
satsToUsd(state.price, parseInt(props.initialAmountSats || "0") || 0, false)
|
satsToUsd(
|
||||||
|
state.price,
|
||||||
|
parseInt(props.initialAmountSats || "0") || 0,
|
||||||
|
false
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
||||||
@@ -159,7 +192,9 @@ export const AmountEditable: ParentComponent<{
|
|||||||
|
|
||||||
function handleCharacterInput(character: string) {
|
function handleCharacterInput(character: string) {
|
||||||
const isFiatMode = mode() === "fiat";
|
const isFiatMode = mode() === "fiat";
|
||||||
const inputSanitizer = isFiatMode ? fiatInputSanitizer : satsInputSanitizer;
|
const inputSanitizer = isFiatMode
|
||||||
|
? fiatInputSanitizer
|
||||||
|
: satsInputSanitizer;
|
||||||
const localValue = isFiatMode ? localFiat : localSats;
|
const localValue = isFiatMode ? localFiat : localSats;
|
||||||
|
|
||||||
let sane;
|
let sane;
|
||||||
@@ -176,7 +211,9 @@ export const AmountEditable: ParentComponent<{
|
|||||||
|
|
||||||
if (isFiatMode) {
|
if (isFiatMode) {
|
||||||
setLocalFiat(sane);
|
setLocalFiat(sane);
|
||||||
setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false));
|
setLocalSats(
|
||||||
|
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setLocalSats(sane);
|
setLocalSats(sane);
|
||||||
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
setLocalFiat(satsToUsd(state.price, Number(sane) || 0, false));
|
||||||
@@ -189,7 +226,9 @@ export const AmountEditable: ParentComponent<{
|
|||||||
function setFixedAmount(amount: string) {
|
function setFixedAmount(amount: string) {
|
||||||
if (mode() === "fiat") {
|
if (mode() === "fiat") {
|
||||||
setLocalFiat(amount);
|
setLocalFiat(amount);
|
||||||
setLocalSats(usdToSats(state.price, parseFloat(amount || "0") || 0, false));
|
setLocalSats(
|
||||||
|
usdToSats(state.price, parseFloat(amount || "0") || 0, false)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setLocalSats(amount);
|
setLocalSats(amount);
|
||||||
setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false));
|
setLocalFiat(satsToUsd(state.price, Number(amount) || 0, false));
|
||||||
@@ -214,7 +253,9 @@ export const AmountEditable: ParentComponent<{
|
|||||||
const { value } = e.target as HTMLInputElement;
|
const { value } = e.target as HTMLInputElement;
|
||||||
const sane = fiatInputSanitizer(value);
|
const sane = fiatInputSanitizer(value);
|
||||||
setLocalFiat(sane);
|
setLocalFiat(sane);
|
||||||
setLocalSats(usdToSats(state.price, parseFloat(sane || "0") || 0, false));
|
setLocalSats(
|
||||||
|
usdToSats(state.price, parseFloat(sane || "0") || 0, false)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
@@ -245,7 +286,9 @@ export const AmountEditable: ParentComponent<{
|
|||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={localSats() !== "0"}
|
when={localSats() !== "0"}
|
||||||
fallback={<div class="inline-block font-semibold">Set amount</div>}
|
fallback={
|
||||||
|
<div class="inline-block font-semibold">Set amount</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<InlineAmount amount={localSats()} />
|
<InlineAmount amount={localSats()} />
|
||||||
</Show>
|
</Show>
|
||||||
@@ -255,7 +298,10 @@ export const AmountEditable: ParentComponent<{
|
|||||||
<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} onEscapeKeyDown={() => setIsOpen(false)}>
|
<Dialog.Content
|
||||||
|
class={DIALOG_CONTENT}
|
||||||
|
onEscapeKeyDown={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
{/* TODO: figure out how to submit on enter */}
|
{/* TODO: figure out how to submit on enter */}
|
||||||
<div class="w-full flex justify-end">
|
<div class="w-full flex justify-end">
|
||||||
<button
|
<button
|
||||||
@@ -266,7 +312,10 @@ export const AmountEditable: ParentComponent<{
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* <form onSubmit={handleSubmit} class="text-black"> */}
|
{/* <form onSubmit={handleSubmit} class="text-black"> */}
|
||||||
<form onSubmit={handleSubmit} class="opacity-0 absolute -z-10">
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
class="opacity-0 absolute -z-10"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
ref={(el) => (satsInputRef = el)}
|
ref={(el) => (satsInputRef = el)}
|
||||||
disabled={mode() === "fiat"}
|
disabled={mode() === "fiat"}
|
||||||
@@ -286,21 +335,40 @@ export const AmountEditable: ParentComponent<{
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="flex flex-col flex-1 justify-around gap-2 max-w-[400px] mx-auto w-full">
|
<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}>
|
<div
|
||||||
|
class="p-4 flex flex-col gap-4 items-center justify-center"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
<BigScalingText
|
<BigScalingText
|
||||||
text={mode() === "fiat" ? displayFiat() : displaySats()}
|
text={
|
||||||
|
mode() === "fiat"
|
||||||
|
? displayFiat()
|
||||||
|
: displaySats()
|
||||||
|
}
|
||||||
fiat={mode() === "fiat"}
|
fiat={mode() === "fiat"}
|
||||||
/>
|
/>
|
||||||
<SmallSubtleAmount
|
<SmallSubtleAmount
|
||||||
text={mode() === "fiat" ? displaySats() : displayFiat()}
|
text={
|
||||||
|
mode() === "fiat"
|
||||||
|
? displaySats()
|
||||||
|
: displayFiat()
|
||||||
|
}
|
||||||
fiat={mode() !== "fiat"}
|
fiat={mode() !== "fiat"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show when={warningText()}>
|
<Show when={warningText()}>
|
||||||
<InfoBox accent="green">{warningText()}</InfoBox>
|
<InfoBox accent="green">
|
||||||
|
{warningText()}
|
||||||
|
</InfoBox>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex justify-center gap-4 my-2">
|
<div class="flex justify-center gap-4 my-2">
|
||||||
<For each={mode() === "fiat" ? FIXED_AMOUNTS_USD : FIXED_AMOUNTS_SATS}>
|
<For
|
||||||
|
each={
|
||||||
|
mode() === "fiat"
|
||||||
|
? FIXED_AMOUNTS_USD
|
||||||
|
: FIXED_AMOUNTS_SATS
|
||||||
|
}
|
||||||
|
>
|
||||||
{(amount) => (
|
{(amount) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -325,7 +393,11 @@ export const AmountEditable: ParentComponent<{
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<Button intent="blue" class="w-full flex-none" onClick={handleSubmit}>
|
<Button
|
||||||
|
intent="blue"
|
||||||
|
class="w-full flex-none"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
Set Amount
|
Set Amount
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
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() {
|
||||||
@@ -19,7 +19,10 @@ export default function App() {
|
|||||||
<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
|
||||||
|
class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue"
|
||||||
|
href="/activity"
|
||||||
|
>
|
||||||
<img src={userClock} alt="Activity" class="h-8 w-8" />
|
<img src={userClock} alt="Activity" class="h-8 w-8" />
|
||||||
</A>
|
</A>
|
||||||
</header>
|
</header>
|
||||||
@@ -31,7 +34,10 @@ export default function App() {
|
|||||||
<Card title="Activity">
|
<Card title="Activity">
|
||||||
<div class="p-1" />
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
|
<Show
|
||||||
|
when={!state.wallet_loading}
|
||||||
|
fallback={<LoadingShimmer />}
|
||||||
|
>
|
||||||
<CombinedActivity limit={3} />
|
<CombinedActivity limit={3} />
|
||||||
</Show>
|
</Show>
|
||||||
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
||||||
|
|||||||
@@ -32,19 +32,30 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
|||||||
|
|
||||||
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
|
||||||
|
amountSats={state.balance?.lightning || 0}
|
||||||
|
showFiat
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</FancyCard>
|
</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) +
|
||||||
|
(Number(state.balance?.force_close) || 0)
|
||||||
|
? "Unconfirmed"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
@@ -52,7 +63,11 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
|||||||
<Show when={!emptyBalance()}>
|
<Show when={!emptyBalance()}>
|
||||||
<div class="self-end justify-self-end">
|
<div class="self-end justify-self-end">
|
||||||
<A href="/swap" class={STYLE}>
|
<A href="/swap" class={STYLE}>
|
||||||
<img src={shuffle} alt="swap" class="h-8 w-8" />
|
<img
|
||||||
|
src={shuffle}
|
||||||
|
alt="swap"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
</A>
|
</A>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -67,7 +82,11 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
|||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">
|
<Button
|
||||||
|
onClick={() => navigate("/receive")}
|
||||||
|
disabled={props.loading}
|
||||||
|
intent="blue"
|
||||||
|
>
|
||||||
Receive
|
Receive
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,47 +1,66 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -5,13 +5,20 @@ 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
|
||||||
|
id="qr"
|
||||||
|
class="w-full bg-white rounded-xl relative"
|
||||||
|
onClick={() => copy(props.value)}
|
||||||
|
>
|
||||||
<Show when={copied()}>
|
<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">
|
<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>
|
<p class="text-xl font-bold">Copied</p>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<QRCodeSVG value={props.value} class="w-full h-full p-8 max-h-[400px]" />
|
<QRCodeSVG
|
||||||
|
value={props.value}
|
||||||
|
class="w-full h-full p-8 max-h-[400px]"
|
||||||
|
/>
|
||||||
</div>
|
</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,4 +1,4 @@
|
|||||||
import { Dialog } from "@kobalte/core"
|
import { Dialog } from "@kobalte/core";
|
||||||
import {
|
import {
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
@@ -29,7 +29,8 @@ 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";
|
||||||
|
|
||||||
@@ -38,7 +39,9 @@ function LightningHeader(props: { info: MutinyInvoice }) {
|
|||||||
|
|
||||||
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(
|
||||||
|
props.info.labels[0]
|
||||||
|
);
|
||||||
if (contact) {
|
if (contact) {
|
||||||
return [tagToMutinyTag(contact)];
|
return [tagToMutinyTag(contact)];
|
||||||
} else {
|
} else {
|
||||||
@@ -84,7 +87,9 @@ function OnchainHeader(props: { info: OnChainTx }) {
|
|||||||
|
|
||||||
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(
|
||||||
|
props.info.labels[0]
|
||||||
|
);
|
||||||
if (contact) {
|
if (contact) {
|
||||||
return [tagToMutinyTag(contact)];
|
return [tagToMutinyTag(contact)];
|
||||||
} else {
|
} else {
|
||||||
@@ -112,8 +117,15 @@ function OnchainHeader(props: { info: OnChainTx }) {
|
|||||||
<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"}
|
||||||
|
</h1>
|
||||||
|
<ActivityAmount
|
||||||
|
center
|
||||||
|
amount={amount() ?? "0"}
|
||||||
|
price={state.price}
|
||||||
|
positive={!isSend()}
|
||||||
|
/>
|
||||||
<For each={tags()}>
|
<For each={tags()}>
|
||||||
{(tag) => (
|
{(tag) => (
|
||||||
<TinyButton
|
<TinyButton
|
||||||
@@ -133,7 +145,9 @@ function OnchainHeader(props: { info: OnChainTx }) {
|
|||||||
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">
|
||||||
|
{props.key}
|
||||||
|
</span>
|
||||||
<span class="font-light">{props.children}</span>
|
<span class="font-light">{props.children}</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -157,14 +171,20 @@ function LightningDetails(props: { info: MutinyInvoice }) {
|
|||||||
<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">
|
||||||
|
{props.info.paid ? "Paid" : "Unpaid"}
|
||||||
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="When">
|
<KeyValue key="When">
|
||||||
<span class="text-neutral-300">{prettyPrintTime(Number(props.info.last_updated))}</span>
|
<span class="text-neutral-300">
|
||||||
|
{prettyPrintTime(Number(props.info.last_updated))}
|
||||||
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<Show when={props.info.description}>
|
<Show when={props.info.description}>
|
||||||
<KeyValue key="Description">
|
<KeyValue key="Description">
|
||||||
<span class="text-neutral-300 truncate">{props.info.description}</span>
|
<span class="text-neutral-300 truncate">
|
||||||
|
{props.info.description}
|
||||||
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
<KeyValue key="Fees">
|
<KeyValue key="Fees">
|
||||||
@@ -200,12 +220,16 @@ function OnchainDetails(props: { info: OnChainTx }) {
|
|||||||
{/* <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">
|
||||||
|
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
|
||||||
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<Show when={confirmationTime()}>
|
<Show when={confirmationTime()}>
|
||||||
<KeyValue key="When">
|
<KeyValue key="When">
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
{confirmationTime() ? prettyPrintTime(Number(confirmationTime())) : "Pending"}
|
{confirmationTime()
|
||||||
|
? prettyPrintTime(Number(confirmationTime()))
|
||||||
|
: "Pending"}
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -247,7 +271,9 @@ export function DetailsIdModal(props: {
|
|||||||
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(
|
||||||
|
id()
|
||||||
|
);
|
||||||
return invoice;
|
return invoice;
|
||||||
} else {
|
} else {
|
||||||
console.log("reading tx: ", id());
|
console.log("reading tx: ", id());
|
||||||
@@ -284,10 +310,14 @@ export function DetailsIdModal(props: {
|
|||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isInvoice()}>
|
<Match when={isInvoice()}>
|
||||||
<LightningHeader info={data() as MutinyInvoice} />
|
<LightningHeader
|
||||||
|
info={data() as MutinyInvoice}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<OnchainHeader info={data() as OnChainTx} />
|
<OnchainHeader
|
||||||
|
info={data() as OnChainTx}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
@@ -295,10 +325,14 @@ export function DetailsIdModal(props: {
|
|||||||
<Dialog.Description class="flex flex-col gap-4">
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isInvoice()}>
|
<Match when={isInvoice()}>
|
||||||
<LightningDetails info={data() as MutinyInvoice} />
|
<LightningDetails
|
||||||
|
info={data() as MutinyInvoice}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<OnchainDetails info={data() as OnChainTx} />
|
<OnchainDetails
|
||||||
|
info={data() as OnChainTx}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
|
|||||||
@@ -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,7 +1,9 @@
|
|||||||
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<{
|
||||||
|
accent: "red" | "blue" | "green" | "white";
|
||||||
|
}> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[auto_minmax(0,_1fr)] rounded-xl px-4 py-2 md:p-4 gap-4 bg-neutral-950/50 border"
|
class="grid grid-cols-[auto_minmax(0,_1fr)] rounded-xl px-4 py-2 md:p-4 gap-4 bg-neutral-950/50 border"
|
||||||
@@ -20,4 +22,4 @@ export const InfoBox: ParentComponent<{ accent: "red" | "blue" | "green" | "whit
|
|||||||
</div>
|
</div>
|
||||||
</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 />
|
||||||
@@ -36,5 +47,5 @@ export function JsonModal(props: { title: string, open: boolean, plaintext?: str
|
|||||||
</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";
|
||||||
@@ -26,9 +41,15 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
|||||||
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(
|
||||||
|
firstNode,
|
||||||
|
props.peer.pubkey
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey);
|
await state.mutiny_wallet?.delete_peer(
|
||||||
|
firstNode,
|
||||||
|
props.peer.pubkey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +57,8 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
|||||||
<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}
|
{">"}{" "}
|
||||||
|
{props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
||||||
</h2>
|
</h2>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
@@ -44,7 +66,11 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
|||||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
{JSON.stringify(props.peer, null, 2)}
|
{JSON.stringify(props.peer, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>
|
<Button
|
||||||
|
intent="glowy"
|
||||||
|
layout="xs"
|
||||||
|
onClick={handleDisconnectPeer}
|
||||||
|
>
|
||||||
Disconnect
|
Disconnect
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -57,7 +83,9 @@ 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);
|
||||||
@@ -103,7 +131,10 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
|||||||
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();
|
||||||
|
|
||||||
@@ -119,7 +150,9 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
|||||||
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">
|
||||||
|
Connect Peer
|
||||||
|
</TextField.Label>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
class="w-full p-2 rounded-lg text-black"
|
class="w-full p-2 rounded-lg text-black"
|
||||||
placeholder="mutiny:028241..."
|
placeholder="mutiny:028241..."
|
||||||
@@ -153,7 +186,9 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
|||||||
async function confirmCloseChannel() {
|
async function confirmCloseChannel() {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
try {
|
try {
|
||||||
await state.mutiny_wallet?.close_channel(props.channel.outpoint as string);
|
await state.mutiny_wallet?.close_channel(
|
||||||
|
props.channel.outpoint as string
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast(eify(e));
|
showToast(eify(e));
|
||||||
@@ -174,10 +209,19 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
|||||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
{JSON.stringify(props.channel, null, 2)}
|
{JSON.stringify(props.channel, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<ExternalLink href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)}>
|
<ExternalLink
|
||||||
|
href={mempoolTxUrl(
|
||||||
|
props.channel.outpoint?.split(":")[0],
|
||||||
|
props.network
|
||||||
|
)}
|
||||||
|
>
|
||||||
View Transaction
|
View Transaction
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>
|
<Button
|
||||||
|
intent="glowy"
|
||||||
|
layout="xs"
|
||||||
|
onClick={handleCloseChannel}
|
||||||
|
>
|
||||||
Close Channel
|
Close Channel
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -195,10 +239,12 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,19 +308,22 @@ 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));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -283,12 +338,23 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
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">
|
||||||
|
Pubkey
|
||||||
|
</TextField.Label>
|
||||||
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
<TextField.Root value={amount()} onChange={setAmount} class="flex flex-col gap-2">
|
<TextField.Root
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">Amount</TextField.Label>
|
value={amount()}
|
||||||
<TextField.Input type="number" class="w-full p-2 rounded-lg text-black" />
|
onChange={setAmount}
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
|
Amount
|
||||||
|
</TextField.Label>
|
||||||
|
<TextField.Input
|
||||||
|
type="number"
|
||||||
|
class="w-full p-2 rounded-lg text-black"
|
||||||
|
/>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
<Button layout="small" type="submit">
|
<Button layout="small" type="submit">
|
||||||
Open Channel
|
Open Channel
|
||||||
@@ -300,7 +366,12 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<pre>{newChannel()?.outpoint}</pre>
|
<pre>{newChannel()?.outpoint}</pre>
|
||||||
<ExternalLink href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], network)}>
|
<ExternalLink
|
||||||
|
href={mempoolTxUrl(
|
||||||
|
newChannel()?.outpoint?.split(":")[0],
|
||||||
|
network
|
||||||
|
)}
|
||||||
|
>
|
||||||
View Transaction
|
View Transaction
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -312,7 +383,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LnUrlAuth() {
|
function LnUrlAuth() {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [value, setValue] = createSignal("");
|
const [value, setValue] = createSignal("");
|
||||||
|
|
||||||
@@ -320,7 +391,7 @@ 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("");
|
||||||
};
|
};
|
||||||
@@ -331,26 +402,39 @@ function LnUrlAuth() {
|
|||||||
<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">
|
||||||
|
Auth
|
||||||
|
</Button>
|
||||||
</form>
|
</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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,20 @@ 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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,12 @@ export function OnboardWarning() {
|
|||||||
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 (
|
||||||
@@ -26,8 +31,9 @@ export function OnboardWarning() {
|
|||||||
<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
|
||||||
|
enjoy your new wallet!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -35,7 +41,10 @@ export function OnboardWarning() {
|
|||||||
layout="xs"
|
layout="xs"
|
||||||
class="self-start md:self-auto"
|
class="self-start md:self-auto"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showToast({ title: "Unimplemented", description: "We don't do that yet" });
|
showToast({
|
||||||
|
title: "Unimplemented",
|
||||||
|
description: "We don't do that yet"
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Restore
|
Restore
|
||||||
@@ -52,7 +61,9 @@ export function OnboardWarning() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
|
<Show
|
||||||
|
when={!state.has_backed_up && hasMoney() && !dismissedBackup()}
|
||||||
|
>
|
||||||
<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={save} alt="backup" class="w-8 h-8" />
|
<img src={save} alt="backup" class="w-8 h-8" />
|
||||||
@@ -61,11 +72,17 @@ export function OnboardWarning() {
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<SmallHeader>Secure your funds</SmallHeader>
|
<SmallHeader>Secure your funds</SmallHeader>
|
||||||
<p class="text-base font-light max-md:hidden">
|
<p class="text-base font-light max-md:hidden">
|
||||||
You have money stored in this browser. Let's make sure you have a backup.
|
You have money stored in this browser. Let's
|
||||||
|
make sure you have a backup.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ButtonLink intent="blue" layout="xs" class="self-auto" href="/backup">
|
<ButtonLink
|
||||||
|
intent="blue"
|
||||||
|
layout="xs"
|
||||||
|
class="self-auto"
|
||||||
|
href="/backup"
|
||||||
|
>
|
||||||
Backup
|
Backup
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,10 +51,15 @@ 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>
|
||||||
@@ -60,5 +67,5 @@ export default function PeerConnectModal() {
|
|||||||
</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,18 +1,25 @@
|
|||||||
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 (
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden"
|
||||||
|
onClick={toggleShow}
|
||||||
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={!shouldShow()}>
|
<Match when={!shouldShow()}>
|
||||||
<div class="cursor-pointer">
|
<div class="cursor-pointer">
|
||||||
@@ -24,13 +31,12 @@ export function SeedWords(props: { words: string, setHasSeen?: (hasSeen: boolean
|
|||||||
<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>
|
</For>
|
||||||
</ol>
|
</ol>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</button >)
|
</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 (
|
||||||
|
<Card>
|
||||||
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
||||||
<h2 class="text-2xl font-light">Don't trust us! Use your own servers to back Mutiny.</h2>
|
<h2 class="text-2xl font-light">
|
||||||
|
Don't trust us! Use your own servers to back Mutiny.
|
||||||
|
</h2>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<SmallHeader>Network</SmallHeader>
|
<SmallHeader>Network</SmallHeader>
|
||||||
<pre>
|
<pre>{existingSettings.network}</pre>
|
||||||
{existingSettings.network}
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field name="proxy" validate={[url("Should be a url starting with wss://")]}>
|
<Field
|
||||||
|
name="proxy"
|
||||||
|
validate={[url("Should be a url starting with wss://")]}
|
||||||
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField {...props} value={field.value} error={field.error} label="Websockets Proxy" />
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Websockets Proxy"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="esplora" validate={[url("That doesn't look like a URL")]}>
|
<Field
|
||||||
|
name="esplora"
|
||||||
|
validate={[url("That doesn't look like a URL")]}
|
||||||
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField {...props} value={field.value} error={field.error} label="Esplora" />
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Esplora"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="rgs" validate={[url("That doesn't look like a URL")]}>
|
<Field
|
||||||
|
name="rgs"
|
||||||
|
validate={[url("That doesn't look like a URL")]}
|
||||||
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField {...props} value={field.value} error={field.error} label="RGS" />
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="RGS"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="lsp" validate={[url("That doesn't look like a URL")]}>
|
<Field
|
||||||
|
name="lsp"
|
||||||
|
validate={[url("That doesn't look like a URL")]}
|
||||||
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField {...props} value={field.value} error={field.error} label="LSP" />
|
<TextField
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="LSP"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit">Save</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,46 @@
|
|||||||
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">
|
||||||
|
{props.text.length > 8 ? props.text.slice(-8) : ""}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -43,7 +49,12 @@ 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
|
||||||
|
open={open()}
|
||||||
|
plaintext={props.text}
|
||||||
|
title="Details"
|
||||||
|
setOpen={setOpen}
|
||||||
|
/>
|
||||||
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
||||||
<TruncateMiddle text={props.text} />
|
<TruncateMiddle text={props.text} />
|
||||||
<button class="w-[2rem]" onClick={() => setOpen(true)}>
|
<button class="w-[2rem]" onClick={() => setOpen(true)}>
|
||||||
@@ -54,16 +65,19 @@ export function StringShower(props: { text: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }) {
|
||||||
@@ -79,6 +93,5 @@ export function ShareCard(props: { text?: string }) {
|
|||||||
</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]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -71,5 +78,5 @@ export function TagEditor(props: {
|
|||||||
</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,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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,9 @@ const button = cva(
|
|||||||
// 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"
|
||||||
@@ -34,15 +34,22 @@ const button = cva(
|
|||||||
|
|
||||||
// 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
|
||||||
@@ -62,21 +69,31 @@ export const Button: ParentComponent<ButtonProps> = props => {
|
|||||||
</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,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 (
|
||||||
|
<Progress.Root
|
||||||
value={props.value}
|
value={props.value}
|
||||||
minValue={0}
|
minValue={0}
|
||||||
maxValue={props.max}
|
maxValue={props.max}
|
||||||
getValueLabel={({ value, max }) => `${formatNumber(value)} of ${formatNumber(max)} sats sent`}
|
getValueLabel={({ value, max }) =>
|
||||||
|
`${formatNumber(value)} of ${formatNumber(max)} sats sent`
|
||||||
|
}
|
||||||
class="w-full flex flex-col gap-2"
|
class="w-full flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<Progress.Label><SmallHeader>Sending...</SmallHeader></Progress.Label>
|
<Progress.Label>
|
||||||
|
<SmallHeader>Sending...</SmallHeader>
|
||||||
|
</Progress.Label>
|
||||||
<Progress.ValueLabel class="text-sm font-semibold uppercase" />
|
<Progress.ValueLabel class="text-sm font-semibold uppercase" />
|
||||||
</div>
|
</div>
|
||||||
<Progress.Track class="h-6 bg-white/10 rounded">
|
<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.Fill class="bg-m-red rounded h-full w-[var(--kb-progress-fill-width)] transition-[width]" />
|
||||||
</Progress.Track>
|
</Progress.Track>
|
||||||
</Progress.Root>)
|
</Progress.Root>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
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
|
||||||
@@ -24,7 +35,8 @@ export function StyledRadioGroup(props: { value: string, choices: Choices, onVal
|
|||||||
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":
|
||||||
|
props.accent === "white",
|
||||||
"ui-disabled:opacity-50": choice.disabled
|
"ui-disabled:opacity-50": choice.disabled
|
||||||
}}
|
}}
|
||||||
disabled={choice.disabled}
|
disabled={choice.disabled}
|
||||||
@@ -37,13 +49,18 @@ export function StyledRadioGroup(props: { value: string, choices: Choices, onVal
|
|||||||
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div
|
<div
|
||||||
classList={{ "text-base": props.small, "text-lg": !props.small }}
|
classList={{
|
||||||
|
"text-base": props.small,
|
||||||
|
"text-lg": !props.small
|
||||||
|
}}
|
||||||
class={`font-semibold max-sm:text-sm`}
|
class={`font-semibold max-sm:text-sm`}
|
||||||
>
|
>
|
||||||
{choice.label}
|
{choice.label}
|
||||||
</div>
|
</div>
|
||||||
<Show when={!props.small}>
|
<Show when={!props.small}>
|
||||||
<div class="text-sm font-light">{choice.caption}</div>
|
<div class="text-sm font-light">
|
||||||
|
{choice.caption}
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup.ItemLabel>
|
</RadioGroup.ItemLabel>
|
||||||
|
|||||||
@@ -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,41 +1,44 @@
|
|||||||
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;
|
||||||
@@ -47,7 +50,11 @@ export const FancyCard: ParentComponent<{
|
|||||||
<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>
|
</div>
|
||||||
{props.tag && props.tag}
|
{props.tag && props.tag}
|
||||||
</div>
|
</div>
|
||||||
@@ -63,8 +70,8 @@ export const SafeArea: ParentComponent = (props) => {
|
|||||||
{props.children}
|
{props.children}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DefaultMain: ParentComponent = (props) => {
|
export const DefaultMain: ParentComponent = (props) => {
|
||||||
return (
|
return (
|
||||||
@@ -73,8 +80,8 @@ 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 (
|
||||||
@@ -82,23 +89,28 @@ export const FullscreenLoader = () => {
|
|||||||
<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}>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
</Suspense>
|
</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,
|
||||||
|
"h-full grid": props.big
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -119,78 +131,114 @@ export const LoadingSpinner = (props: { big?: boolean, wide?: boolean }) => {
|
|||||||
<span class="sr-only">Loading...</span>
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export function SuccessModal(props: SuccessModalProps) {
|
|||||||
{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}>
|
||||||
|
{props.confirmText ?? "Nice"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
.sort((a, b) => b.created_at - a.created_at)}
|
||||||
|
>
|
||||||
|
{(item) => (
|
||||||
|
<li class="w-full">
|
||||||
|
<Note e={item as NostrEvent} />
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</ul>)
|
</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,10 +25,10 @@ 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);
|
||||||
@@ -40,7 +39,9 @@ export function WaitlistAlreadyIn() {
|
|||||||
<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">
|
||||||
|
We'll message you when Mutiny Wallet is ready.
|
||||||
|
</h2>
|
||||||
<div class="px-4 sm:px-8 py-8 rounded-xl bg-half-black w-full">
|
<div class="px-4 sm:px-8 py-8 rounded-xl bg-half-black w-full">
|
||||||
<h2 class="text-sm font-semibold uppercase">Recent Updates</h2>
|
<h2 class="text-sm font-semibold uppercase">Recent Updates</h2>
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createHandler,
|
createHandler,
|
||||||
renderAsync,
|
renderAsync,
|
||||||
StartServer,
|
StartServer
|
||||||
} from "solid-start/entry-server";
|
} from "solid-start/entry-server";
|
||||||
|
|
||||||
export default createHandler(
|
export default createHandler(
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
19
src/root.tsx
19
src/root.tsx
@@ -10,7 +10,7 @@ import {
|
|||||||
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";
|
||||||
@@ -29,10 +29,21 @@ export default function Root() {
|
|||||||
/>
|
/>
|
||||||
<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
|
||||||
|
name="description"
|
||||||
|
content="Lightning wallet for the web"
|
||||||
|
/>
|
||||||
<Link rel="icon" href="/favicon.ico" />
|
<Link rel="icon" href="/favicon.ico" />
|
||||||
<Link rel="apple-touch-icon" href="/images/icon.png" sizes="512x512" />
|
<Link
|
||||||
<Link rel="mask-icon" href="/mutiny_logo_mask.svg" color="#000" />
|
rel="apple-touch-icon"
|
||||||
|
href="/images/icon.png"
|
||||||
|
sizes="512x512"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
rel="mask-icon"
|
||||||
|
href="/mutiny_logo_mask.svg"
|
||||||
|
color="#000"
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<Body>
|
<Body>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
|
|||||||
@@ -112,7 +112,10 @@ export default function Activity() {
|
|||||||
<Card title="Activity">
|
<Card title="Activity">
|
||||||
<div class="p-1" />
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
|
<Show
|
||||||
|
when={!state.wallet_loading}
|
||||||
|
fallback={<LoadingShimmer />}
|
||||||
|
>
|
||||||
<CombinedActivity />
|
<CombinedActivity />
|
||||||
</Show>
|
</Show>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -121,7 +124,10 @@ export default function Activity() {
|
|||||||
<Tabs.Content value="nostr">
|
<Tabs.Content value="nostr">
|
||||||
<VStack>
|
<VStack>
|
||||||
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
|
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
|
||||||
<NiceP>Import your contacts from nostr to see who they're zapping.</NiceP>
|
<NiceP>
|
||||||
|
Import your contacts from nostr to see
|
||||||
|
who they're zapping.
|
||||||
|
</NiceP>
|
||||||
<Button disabled intent="blue">
|
<Button disabled intent="blue">
|
||||||
Coming soon
|
Coming soon
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -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,4 +1,8 @@
|
|||||||
import { Contact, MutinyBip21RawMaterials, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
import {
|
||||||
|
Contact,
|
||||||
|
MutinyBip21RawMaterials,
|
||||||
|
MutinyInvoice
|
||||||
|
} from "@mutinywallet/mutiny-wasm";
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
@@ -80,14 +84,16 @@ function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {
|
|||||||
<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
|
||||||
|
if paid over lightning.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.flavor === "lightning"}>
|
<Match when={props.flavor === "lightning"}>
|
||||||
<InfoBox accent="green">
|
<InfoBox accent="green">
|
||||||
A lightning setup fee of <AmountSmall amountSats={props.fee} /> will be charged for this
|
A lightning setup fee of{" "}
|
||||||
receive.
|
<AmountSmall amountSats={props.fee} /> will be charged
|
||||||
|
for this receive.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -101,13 +107,15 @@ function FeeExplanation(props: { fee: bigint }) {
|
|||||||
<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{" "}
|
||||||
|
<AmountSmall amountSats={props.fee} /> was charged for this
|
||||||
receive.
|
receive.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.fee > 0n}>
|
<Match when={props.fee > 0n}>
|
||||||
<InfoBox accent="green">
|
<InfoBox accent="green">
|
||||||
A lightning service fee of <AmountSmall amountSats={props.fee} /> was charged for this
|
A lightning service fee of{" "}
|
||||||
|
<AmountSmall amountSats={props.fee} /> was charged for this
|
||||||
receive.
|
receive.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -123,12 +131,15 @@ export default function Receive() {
|
|||||||
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>();
|
||||||
@@ -159,21 +170,31 @@ export default function Receive() {
|
|||||||
setSelectedValues([]);
|
setSelectedValues([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processContacts(contacts: Partial<MutinyTagItem>[]): Promise<string[]> {
|
async function processContacts(
|
||||||
|
contacts: Partial<MutinyTagItem>[]
|
||||||
|
): Promise<string[]> {
|
||||||
console.log("Processing contacts", contacts);
|
console.log("Processing contacts", contacts);
|
||||||
|
|
||||||
if (contacts.length) {
|
if (contacts.length) {
|
||||||
const first = contacts![0];
|
const first = contacts![0];
|
||||||
|
|
||||||
if (!first.name) {
|
if (!first.name) {
|
||||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
console.error(
|
||||||
|
"Something went wrong with contact creation, proceeding anyway"
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!first.id && first.name) {
|
if (!first.id && first.name) {
|
||||||
console.error("Creating new contact", first.name);
|
console.error("Creating new contact", first.name);
|
||||||
const c = new Contact(first.name, undefined, undefined, undefined);
|
const c = new Contact(
|
||||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
first.name,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const newContactId =
|
||||||
|
await state.mutiny_wallet?.create_new_contact(c);
|
||||||
if (newContactId) {
|
if (newContactId) {
|
||||||
return [newContactId];
|
return [newContactId];
|
||||||
}
|
}
|
||||||
@@ -185,7 +206,9 @@ export default function Receive() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
console.error(
|
||||||
|
"Something went wrong with contact creation, proceeding anyway"
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +216,10 @@ export default function Receive() {
|
|||||||
const bigAmount = BigInt(amount);
|
const bigAmount = BigInt(amount);
|
||||||
try {
|
try {
|
||||||
const tags = await processContacts(selectedValues());
|
const tags = await processContacts(selectedValues());
|
||||||
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tags);
|
const raw = await state.mutiny_wallet?.create_bip21(
|
||||||
|
bigAmount,
|
||||||
|
tags
|
||||||
|
);
|
||||||
// Save the raw info so we can watch the address and invoice
|
// Save the raw info so we can watch the address and invoice
|
||||||
setBip21Raw(raw);
|
setBip21Raw(raw);
|
||||||
|
|
||||||
@@ -204,7 +230,9 @@ export default function Receive() {
|
|||||||
|
|
||||||
return `bitcoin:${raw?.address}?${params}`;
|
return `bitcoin:${raw?.address}?${params}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(new Error("Couldn't create invoice. Are you asking for enough?"));
|
showToast(
|
||||||
|
new Error("Couldn't create invoice. Are you asking for enough?")
|
||||||
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +247,9 @@ export default function Receive() {
|
|||||||
setShouldShowAmountEditor(false);
|
setShouldShowAmountEditor(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
async function checkIfPaid(
|
||||||
|
bip21?: MutinyBip21RawMaterials
|
||||||
|
): Promise<PaidState | undefined> {
|
||||||
if (bip21) {
|
if (bip21) {
|
||||||
console.debug("checking if paid...");
|
console.debug("checking if paid...");
|
||||||
const lightning = bip21.invoice;
|
const lightning = bip21.invoice;
|
||||||
@@ -238,7 +268,9 @@ export default function Receive() {
|
|||||||
return "lightning_paid";
|
return "lightning_paid";
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = (await state.mutiny_wallet?.check_address(address)) as OnChainTx | undefined;
|
const tx = (await state.mutiny_wallet?.check_address(address)) as
|
||||||
|
| OnChainTx
|
||||||
|
| undefined;
|
||||||
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
setReceiveState("paid");
|
setReceiveState("paid");
|
||||||
@@ -265,10 +297,23 @@ export default function Receive() {
|
|||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<Show when={receiveState() === "show"} fallback={<BackLink />}>
|
<Show
|
||||||
<BackButton onClick={() => setReceiveState("edit")} title="Edit" showOnDesktop />
|
when={receiveState() === "show"}
|
||||||
|
fallback={<BackLink />}
|
||||||
|
>
|
||||||
|
<BackButton
|
||||||
|
onClick={() => setReceiveState("edit")}
|
||||||
|
title="Edit"
|
||||||
|
showOnDesktop
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>
|
<LargeHeader
|
||||||
|
action={
|
||||||
|
receiveState() === "show" && (
|
||||||
|
<Indicator>Checking</Indicator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
Receive Bitcoin
|
Receive Bitcoin
|
||||||
</LargeHeader>
|
</LargeHeader>
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -306,13 +351,16 @@ export default function Receive() {
|
|||||||
<p class="text-neutral-400 text-center">
|
<p class="text-neutral-400 text-center">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={flavor() === "lightning"}>
|
<Match when={flavor() === "lightning"}>
|
||||||
Show or share this invoice with the sender.
|
Show or share this invoice with the
|
||||||
|
sender.
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={flavor() === "onchain"}>
|
<Match when={flavor() === "onchain"}>
|
||||||
Show or share this address with the sender.
|
Show or share this address with the
|
||||||
|
sender.
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={flavor() === "unified"}>
|
<Match when={flavor() === "unified"}>
|
||||||
Show or share this code with the sender. Sender decides method of payment.
|
Show or share this code with the sender.
|
||||||
|
Sender decides method of payment.
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</p>
|
</p>
|
||||||
@@ -325,7 +373,12 @@ export default function Receive() {
|
|||||||
/>{" "}
|
/>{" "}
|
||||||
<ShareCard text={receiveString() ?? ""} />
|
<ShareCard text={receiveString() ?? ""} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
<Match
|
||||||
|
when={
|
||||||
|
receiveState() === "paid" &&
|
||||||
|
paidState() === "lightning_paid"
|
||||||
|
}
|
||||||
|
>
|
||||||
<SuccessModal
|
<SuccessModal
|
||||||
title="Payment Received"
|
title="Payment Received"
|
||||||
open={!!paidState()}
|
open={!!paidState()}
|
||||||
@@ -339,10 +392,19 @@ export default function Receive() {
|
|||||||
>
|
>
|
||||||
<MegaCheck />
|
<MegaCheck />
|
||||||
<FeeExplanation fee={lspFee()} />
|
<FeeExplanation fee={lspFee()} />
|
||||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat centered />
|
<Amount
|
||||||
|
amountSats={paymentInvoice()?.amount_sats}
|
||||||
|
showFiat
|
||||||
|
centered
|
||||||
|
/>
|
||||||
</SuccessModal>
|
</SuccessModal>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
<Match
|
||||||
|
when={
|
||||||
|
receiveState() === "paid" &&
|
||||||
|
paidState() === "onchain_paid"
|
||||||
|
}
|
||||||
|
>
|
||||||
<SuccessModal
|
<SuccessModal
|
||||||
title="Payment Received"
|
title="Payment Received"
|
||||||
open={!!paidState()}
|
open={!!paidState()}
|
||||||
@@ -355,8 +417,17 @@ export default function Receive() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MegaCheck />
|
<MegaCheck />
|
||||||
<Amount amountSats={paymentTx()?.received} showFiat centered />
|
<Amount
|
||||||
<ExternalLink href={mempoolTxUrl(paymentTx()?.txid, network)}>
|
amountSats={paymentTx()?.received}
|
||||||
|
showFiat
|
||||||
|
centered
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href={mempoolTxUrl(
|
||||||
|
paymentTx()?.txid,
|
||||||
|
network
|
||||||
|
)}
|
||||||
|
>
|
||||||
View Transaction
|
View Transaction
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</SuccessModal>
|
</SuccessModal>
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ import {
|
|||||||
ParentComponent,
|
ParentComponent,
|
||||||
Show,
|
Show,
|
||||||
Suspense,
|
Suspense,
|
||||||
Switch,
|
Switch
|
||||||
} from "solid-js"
|
} from "solid-js";
|
||||||
import {
|
import {
|
||||||
CENTER_COLUMN,
|
CENTER_COLUMN,
|
||||||
MISSING_LABEL,
|
MISSING_LABEL,
|
||||||
REDSHIFT_LABEL,
|
REDSHIFT_LABEL,
|
||||||
RIGHT_COLUMN,
|
RIGHT_COLUMN,
|
||||||
THREE_COLUMNS,
|
THREE_COLUMNS,
|
||||||
UtxoItem,
|
UtxoItem
|
||||||
} from "~/components/Activity"
|
} from "~/components/Activity";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
@@ -29,43 +29,43 @@ import {
|
|||||||
SafeArea,
|
SafeArea,
|
||||||
SmallAmount,
|
SmallAmount,
|
||||||
SmallHeader,
|
SmallHeader,
|
||||||
VStack,
|
VStack
|
||||||
} from "~/components/layout"
|
} from "~/components/layout";
|
||||||
import { BackLink } from "~/components/layout/BackLink"
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
import { StyledRadioGroup } from "~/components/layout/Radio"
|
import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||||
import NavBar from "~/components/NavBar"
|
import NavBar from "~/components/NavBar";
|
||||||
import { useMegaStore } from "~/state/megaStore"
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import wave from "~/assets/wave.gif"
|
import wave from "~/assets/wave.gif";
|
||||||
import utxoIcon from "~/assets/icons/coin.svg"
|
import utxoIcon from "~/assets/icons/coin.svg";
|
||||||
import { Button } from "~/components/layout/Button"
|
import { Button } from "~/components/layout/Button";
|
||||||
import { ProgressBar } from "~/components/layout/ProgressBar"
|
import { ProgressBar } from "~/components/layout/ProgressBar";
|
||||||
import { MutinyChannel } from "@mutinywallet/mutiny-wasm"
|
import { MutinyChannel } from "@mutinywallet/mutiny-wasm";
|
||||||
import mempoolTxUrl from "~/utils/mempoolTxUrl"
|
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||||
import { Amount } from "~/components/Amount"
|
import { Amount } from "~/components/Amount";
|
||||||
import { getRedshifted, setRedshifted } from "~/utils/fakeLabels"
|
import { getRedshifted, setRedshifted } from "~/utils/fakeLabels";
|
||||||
import { Network } from "~/logic/mutinyWalletSetup"
|
import { Network } from "~/logic/mutinyWalletSetup";
|
||||||
|
|
||||||
type ShiftOption = "utxo" | "lightning"
|
type ShiftOption = "utxo" | "lightning";
|
||||||
|
|
||||||
type ShiftStage = "choose" | "observe" | "success" | "failure"
|
type ShiftStage = "choose" | "observe" | "success" | "failure";
|
||||||
|
|
||||||
type OutPoint = string // Replace with the actual TypeScript type for OutPoint
|
type OutPoint = string; // Replace with the actual TypeScript type for OutPoint
|
||||||
type RedshiftStatus = string // Replace with the actual TypeScript type for RedshiftStatus
|
type RedshiftStatus = string; // Replace with the actual TypeScript type for RedshiftStatus
|
||||||
type RedshiftRecipient = unknown // Replace with the actual TypeScript type for RedshiftRecipient
|
type RedshiftRecipient = unknown; // Replace with the actual TypeScript type for RedshiftRecipient
|
||||||
type PublicKey = unknown // Replace with the actual TypeScript type for PublicKey
|
type PublicKey = unknown; // Replace with the actual TypeScript type for PublicKey
|
||||||
|
|
||||||
interface RedshiftResult {
|
interface RedshiftResult {
|
||||||
id: string
|
id: string;
|
||||||
input_utxo: OutPoint
|
input_utxo: OutPoint;
|
||||||
status: RedshiftStatus
|
status: RedshiftStatus;
|
||||||
recipient: RedshiftRecipient
|
recipient: RedshiftRecipient;
|
||||||
output_utxo?: OutPoint
|
output_utxo?: OutPoint;
|
||||||
introduction_channel?: OutPoint
|
introduction_channel?: OutPoint;
|
||||||
output_channel?: OutPoint
|
output_channel?: OutPoint;
|
||||||
introduction_node: PublicKey
|
introduction_node: PublicKey;
|
||||||
amount_sats: bigint
|
amount_sats: bigint;
|
||||||
change_amt?: bigint
|
change_amt?: bigint;
|
||||||
fees_paid: bigint
|
fees_paid: bigint;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dummyRedshift: RedshiftResult = {
|
const dummyRedshift: RedshiftResult = {
|
||||||
@@ -83,16 +83,16 @@ const dummyRedshift: RedshiftResult = {
|
|||||||
introduction_node: {}, // Replace with a dummy value for PublicKey
|
introduction_node: {}, // Replace with a dummy value for PublicKey
|
||||||
amount_sats: BigInt(1000000),
|
amount_sats: BigInt(1000000),
|
||||||
change_amt: BigInt(12345),
|
change_amt: BigInt(12345),
|
||||||
fees_paid: BigInt(2500),
|
fees_paid: BigInt(2500)
|
||||||
}
|
};
|
||||||
|
|
||||||
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||||
const [state, _actions] = useMegaStore()
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const getUtXos = async () => {
|
const getUtXos = async () => {
|
||||||
console.log("Getting utxos")
|
console.log("Getting utxos");
|
||||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[]
|
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
|
||||||
}
|
};
|
||||||
|
|
||||||
// function findUtxoByOutpoint(
|
// function findUtxoByOutpoint(
|
||||||
// outpoint?: string,
|
// outpoint?: string,
|
||||||
@@ -102,7 +102,7 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
// return utxos.find((utxo) => utxo.outpoint === outpoint)
|
// return utxos.find((utxo) => utxo.outpoint === outpoint)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const [_utxos, { refetch: _refetchUtxos }] = createResource(getUtXos)
|
const [_utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
|
||||||
|
|
||||||
// const inputUtxo = createMemo(() => {
|
// const inputUtxo = createMemo(() => {
|
||||||
// console.log(utxos())
|
// console.log(utxos())
|
||||||
@@ -112,14 +112,15 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
// })
|
// })
|
||||||
|
|
||||||
const [redshiftResource, { refetch: _refetchRedshift }] = createResource(
|
const [redshiftResource, { refetch: _refetchRedshift }] = createResource(
|
||||||
|
|
||||||
async () => {
|
async () => {
|
||||||
console.log("Checking redshift", props.redshift.id)
|
console.log("Checking redshift", props.redshift.id);
|
||||||
const redshift = await state.mutiny_wallet?.get_redshift(props.redshift.id)
|
const redshift = await state.mutiny_wallet?.get_redshift(
|
||||||
console.log(redshift)
|
props.redshift.id
|
||||||
return redshift
|
);
|
||||||
|
console.log(redshift);
|
||||||
|
return redshift;
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// const interval = setInterval(() => {
|
// const interval = setInterval(() => {
|
||||||
// if (redshiftResource()) refetch()
|
// if (redshiftResource()) refetch()
|
||||||
@@ -127,23 +128,21 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
// // clearInterval(interval)
|
// // clearInterval(interval)
|
||||||
// // props.setShiftStage("success");
|
// // props.setShiftStage("success");
|
||||||
// // // setSentAmount((0))
|
// // // setSentAmount((0))
|
||||||
|
|
||||||
// // } else {
|
// // } else {
|
||||||
// // setSentAmount((sentAmount() + 50000))
|
// // setSentAmount((sentAmount() + 50000))
|
||||||
// // }
|
// // }
|
||||||
// }, 1000)
|
// }, 1000)
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
// const outputUtxo = createMemo(() => {
|
// const outputUtxo = createMemo(() => {
|
||||||
// return findUtxoByOutpoint(redshiftResource()?.output_utxo, utxos())
|
// return findUtxoByOutpoint(redshiftResource()?.output_utxo, utxos())
|
||||||
// })
|
// })
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setRedshifted(true, redshiftResource()?.output_utxo)
|
setRedshifted(true, redshiftResource()?.output_utxo);
|
||||||
})
|
});
|
||||||
|
|
||||||
const network = state.mutiny_wallet?.get_network() as Network
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
@@ -166,23 +165,34 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</KV> */}
|
</KV> */}
|
||||||
<KV key="Starting amount">
|
<KV key="Starting amount">
|
||||||
<Amount amountSats={redshiftResource()!.amount_sats} />
|
<Amount
|
||||||
|
amountSats={redshiftResource()!.amount_sats}
|
||||||
|
/>
|
||||||
</KV>
|
</KV>
|
||||||
<KV key="Fees paid">
|
<KV key="Fees paid">
|
||||||
<Amount amountSats={redshiftResource()!.fees_paid} />
|
<Amount
|
||||||
|
amountSats={redshiftResource()!.fees_paid}
|
||||||
|
/>
|
||||||
</KV>
|
</KV>
|
||||||
<KV key="Change">
|
<KV key="Change">
|
||||||
<Amount amountSats={redshiftResource()!.change_amt} />
|
<Amount
|
||||||
|
amountSats={redshiftResource()!.change_amt}
|
||||||
|
/>
|
||||||
</KV>
|
</KV>
|
||||||
<KV key="Outbound channel">
|
<KV key="Outbound channel">
|
||||||
<VStack>
|
<VStack>
|
||||||
<pre class="whitespace-pre-wrap break-all">
|
<pre class="whitespace-pre-wrap break-all">
|
||||||
{redshiftResource()!.introduction_channel}
|
{
|
||||||
|
redshiftResource()!
|
||||||
|
.introduction_channel
|
||||||
|
}
|
||||||
</pre>
|
</pre>
|
||||||
<a
|
<a
|
||||||
class=""
|
class=""
|
||||||
href={mempoolTxUrl(
|
href={mempoolTxUrl(
|
||||||
redshiftResource()!.introduction_channel?.split(":")[0],
|
redshiftResource()!.introduction_channel?.split(
|
||||||
|
":"
|
||||||
|
)[0],
|
||||||
network
|
network
|
||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -201,7 +211,9 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
<a
|
<a
|
||||||
class=""
|
class=""
|
||||||
href={mempoolTxUrl(
|
href={mempoolTxUrl(
|
||||||
redshiftResource()!.output_channel?.split(":")[0],
|
redshiftResource()!.output_channel?.split(
|
||||||
|
":"
|
||||||
|
)[0],
|
||||||
network
|
network
|
||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -217,23 +229,30 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHIFT_OPTIONS = [
|
const SHIFT_OPTIONS = [
|
||||||
{ value: "utxo", label: "UTXO", caption: "Trade your UTXO for a fresh UTXO" },
|
{
|
||||||
|
value: "utxo",
|
||||||
|
label: "UTXO",
|
||||||
|
caption: "Trade your UTXO for a fresh UTXO"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "lightning",
|
value: "lightning",
|
||||||
label: "Lightning",
|
label: "Lightning",
|
||||||
caption: "Convert your UTXO into Lightning",
|
caption: "Convert your UTXO into Lightning"
|
||||||
},
|
}
|
||||||
]
|
];
|
||||||
|
|
||||||
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
||||||
const redshifted = createMemo(() => getRedshifted(props.item.outpoint))
|
const redshifted = createMemo(() => getRedshifted(props.item.outpoint));
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class={THREE_COLUMNS} onClick={() => props.onClick && props.onClick()}>
|
<div
|
||||||
|
class={THREE_COLUMNS}
|
||||||
|
onClick={() => props.onClick && props.onClick()}
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<img src={utxoIcon} alt="coin" />
|
<img src={utxoIcon} alt="coin" />
|
||||||
</div>
|
</div>
|
||||||
@@ -250,14 +269,16 @@ export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
<div class={RIGHT_COLUMN}>
|
<div class={RIGHT_COLUMN}>
|
||||||
<SmallHeader
|
<SmallHeader
|
||||||
class={props.item?.is_spent ? "text-m-red" : "text-m-green"}
|
class={
|
||||||
|
props.item?.is_spent ? "text-m-red" : "text-m-green"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* {props.item?.is_spent ? "SPENT" : "UNSPENT"} */}
|
{/* {props.item?.is_spent ? "SPENT" : "UNSPENT"} */}
|
||||||
</SmallHeader>
|
</SmallHeader>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const FAKE_STATES = [
|
const FAKE_STATES = [
|
||||||
@@ -265,30 +286,30 @@ const FAKE_STATES = [
|
|||||||
"Opening a channel",
|
"Opening a channel",
|
||||||
"Sending funds through",
|
"Sending funds through",
|
||||||
"Closing the channel",
|
"Closing the channel",
|
||||||
"Redshift complete",
|
"Redshift complete"
|
||||||
]
|
];
|
||||||
|
|
||||||
function ShiftObserver(props: {
|
function ShiftObserver(props: {
|
||||||
setShiftStage: (stage: ShiftStage) => void
|
setShiftStage: (stage: ShiftStage) => void;
|
||||||
redshiftId: string
|
redshiftId: string;
|
||||||
}) {
|
}) {
|
||||||
const [_state, _actions] = useMegaStore()
|
const [_state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [fakeStage, _setFakeStage] = createSignal(2)
|
const [fakeStage, _setFakeStage] = createSignal(2);
|
||||||
|
|
||||||
const [sentAmount, setSentAmount] = createSignal(0)
|
const [sentAmount, setSentAmount] = createSignal(0);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (sentAmount() === 200000) {
|
if (sentAmount() === 200000) {
|
||||||
clearInterval(interval)
|
clearInterval(interval);
|
||||||
props.setShiftStage("success")
|
props.setShiftStage("success");
|
||||||
// setSentAmount((0))
|
// setSentAmount((0))
|
||||||
} else {
|
} else {
|
||||||
setSentAmount(sentAmount() + 50000)
|
setSentAmount(sentAmount() + 50000);
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000);
|
||||||
})
|
});
|
||||||
|
|
||||||
// async function checkRedshift(id: string) {
|
// async function checkRedshift(id: string) {
|
||||||
// console.log("Checking redshift", id)
|
// console.log("Checking redshift", id)
|
||||||
@@ -336,7 +357,7 @@ function ShiftObserver(props: {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const KV: ParentComponent<{ key: string }> = (props) => {
|
const KV: ParentComponent<{ key: string }> = (props) => {
|
||||||
@@ -345,68 +366,72 @@ const KV: ParentComponent<{ key: string }> = (props) => {
|
|||||||
<p class="text-sm font-semibold uppercase">{props.key}</p>
|
<p class="text-sm font-semibold uppercase">{props.key}</p>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Redshift() {
|
export default function Redshift() {
|
||||||
const [state, _actions] = useMegaStore()
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose")
|
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose");
|
||||||
const [shiftType, setShiftType] = createSignal<ShiftOption>("utxo")
|
const [shiftType, setShiftType] = createSignal<ShiftOption>("utxo");
|
||||||
|
|
||||||
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>()
|
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>();
|
||||||
|
|
||||||
const getUtXos = async () => {
|
const getUtXos = async () => {
|
||||||
console.log("Getting utxos")
|
console.log("Getting utxos");
|
||||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[]
|
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
|
||||||
}
|
};
|
||||||
|
|
||||||
const getChannels = async () => {
|
const getChannels = async () => {
|
||||||
console.log("Getting channels")
|
console.log("Getting channels");
|
||||||
await state.mutiny_wallet?.sync()
|
await state.mutiny_wallet?.sync();
|
||||||
const channels = (await state.mutiny_wallet?.list_channels()) as Promise<
|
const channels =
|
||||||
|
(await state.mutiny_wallet?.list_channels()) as Promise<
|
||||||
MutinyChannel[]
|
MutinyChannel[]
|
||||||
>
|
>;
|
||||||
console.log(channels)
|
console.log(channels);
|
||||||
return channels
|
return channels;
|
||||||
}
|
};
|
||||||
|
|
||||||
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos)
|
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
|
||||||
const [_channels, { refetch: _refetchChannels }] = createResource(getChannels)
|
const [_channels, { refetch: _refetchChannels }] =
|
||||||
|
createResource(getChannels);
|
||||||
|
|
||||||
const redshiftedUtxos = createMemo(() => {
|
const redshiftedUtxos = createMemo(() => {
|
||||||
return utxos()?.filter((utxo) => getRedshifted(utxo.outpoint))
|
return utxos()?.filter((utxo) => getRedshifted(utxo.outpoint));
|
||||||
})
|
});
|
||||||
|
|
||||||
const unredshiftedUtxos = createMemo(() => {
|
const unredshiftedUtxos = createMemo(() => {
|
||||||
return utxos()?.filter((utxo) => !getRedshifted(utxo.outpoint))
|
return utxos()?.filter((utxo) => !getRedshifted(utxo.outpoint));
|
||||||
})
|
});
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
setShiftStage("choose")
|
setShiftStage("choose");
|
||||||
setShiftType("utxo")
|
setShiftType("utxo");
|
||||||
setChosenUtxo(undefined)
|
setChosenUtxo(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function redshiftUtxo(utxo: UtxoItem) {
|
async function redshiftUtxo(utxo: UtxoItem) {
|
||||||
console.log("Redshifting utxo", utxo.outpoint)
|
console.log("Redshifting utxo", utxo.outpoint);
|
||||||
const redshift = await state.mutiny_wallet?.init_redshift(utxo.outpoint)
|
const redshift = await state.mutiny_wallet?.init_redshift(
|
||||||
console.log("Redshift initialized:")
|
utxo.outpoint
|
||||||
console.log(redshift)
|
);
|
||||||
return redshift
|
console.log("Redshift initialized:");
|
||||||
|
console.log(redshift);
|
||||||
|
return redshift;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [initializedRedshift, { refetch: _refetchRedshift }] = createResource(
|
const [initializedRedshift, { refetch: _refetchRedshift }] = createResource(
|
||||||
chosenUtxo,
|
chosenUtxo,
|
||||||
redshiftUtxo
|
redshiftUtxo
|
||||||
)
|
);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (chosenUtxo() && initializedRedshift()) {
|
if (chosenUtxo() && initializedRedshift()) {
|
||||||
// window.location.href = "/"
|
// window.location.href = "/"
|
||||||
setShiftStage("observe")
|
setShiftStage("observe");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
@@ -425,7 +450,9 @@ export default function Redshift() {
|
|||||||
accent="red"
|
accent="red"
|
||||||
value={shiftType()}
|
value={shiftType()}
|
||||||
onValueChange={(newValue) =>
|
onValueChange={(newValue) =>
|
||||||
setShiftType(newValue as ShiftOption)
|
setShiftType(
|
||||||
|
newValue as ShiftOption
|
||||||
|
)
|
||||||
}
|
}
|
||||||
choices={SHIFT_OPTIONS}
|
choices={SHIFT_OPTIONS}
|
||||||
/>
|
/>
|
||||||
@@ -434,7 +461,11 @@ export default function Redshift() {
|
|||||||
<NiceP>
|
<NiceP>
|
||||||
Choose your{" "}
|
Choose your{" "}
|
||||||
<span class="inline-block">
|
<span class="inline-block">
|
||||||
<img class="h-4" src={wave} alt="sine wave" />
|
<img
|
||||||
|
class="h-4"
|
||||||
|
src={wave}
|
||||||
|
alt="sine wave"
|
||||||
|
/>
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
UTXO to begin
|
UTXO to begin
|
||||||
</NiceP>
|
</NiceP>
|
||||||
@@ -446,24 +477,37 @@ export default function Redshift() {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
when={
|
when={
|
||||||
utxos.state === "ready" &&
|
utxos.state ===
|
||||||
unredshiftedUtxos()?.length === 0
|
"ready" &&
|
||||||
|
unredshiftedUtxos()
|
||||||
|
?.length === 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<code>No utxos (empty state)</code>
|
<code>
|
||||||
|
No utxos (empty
|
||||||
|
state)
|
||||||
|
</code>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
when={
|
when={
|
||||||
utxos.state === "ready" &&
|
utxos.state ===
|
||||||
|
"ready" &&
|
||||||
unredshiftedUtxos() &&
|
unredshiftedUtxos() &&
|
||||||
unredshiftedUtxos()!.length >= 0
|
unredshiftedUtxos()!
|
||||||
|
.length >= 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For each={unredshiftedUtxos()}>
|
<For
|
||||||
|
each={unredshiftedUtxos()}
|
||||||
|
>
|
||||||
{(utxo) => (
|
{(utxo) => (
|
||||||
<Utxo
|
<Utxo
|
||||||
item={utxo}
|
item={utxo}
|
||||||
onClick={() => setChosenUtxo(utxo)}
|
onClick={() =>
|
||||||
|
setChosenUtxo(
|
||||||
|
utxo
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -475,7 +519,10 @@ export default function Redshift() {
|
|||||||
<Card
|
<Card
|
||||||
titleElement={
|
titleElement={
|
||||||
<SmallHeader>
|
<SmallHeader>
|
||||||
<span class="text-m-red">Redshifted </span>UTXOs
|
<span class="text-m-red">
|
||||||
|
Redshifted{" "}
|
||||||
|
</span>
|
||||||
|
UTXOs
|
||||||
</SmallHeader>
|
</SmallHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -485,21 +532,34 @@ export default function Redshift() {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
when={
|
when={
|
||||||
utxos.state === "ready" &&
|
utxos.state ===
|
||||||
redshiftedUtxos()?.length === 0
|
"ready" &&
|
||||||
|
redshiftedUtxos()
|
||||||
|
?.length === 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<code>No utxos (empty state)</code>
|
<code>
|
||||||
|
No utxos (empty
|
||||||
|
state)
|
||||||
|
</code>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
when={
|
when={
|
||||||
utxos.state === "ready" &&
|
utxos.state ===
|
||||||
|
"ready" &&
|
||||||
redshiftedUtxos() &&
|
redshiftedUtxos() &&
|
||||||
redshiftedUtxos()!.length >= 0
|
redshiftedUtxos()!
|
||||||
|
.length >= 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For each={redshiftedUtxos()}>
|
<For
|
||||||
{(utxo) => <Utxo item={utxo} />}
|
each={redshiftedUtxos()}
|
||||||
|
>
|
||||||
|
{(utxo) => (
|
||||||
|
<Utxo
|
||||||
|
item={utxo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -507,19 +567,32 @@ export default function Redshift() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={shiftStage() === "observe" && chosenUtxo()}>
|
<Match
|
||||||
|
when={
|
||||||
|
shiftStage() === "observe" &&
|
||||||
|
chosenUtxo()
|
||||||
|
}
|
||||||
|
>
|
||||||
<ShiftObserver
|
<ShiftObserver
|
||||||
setShiftStage={setShiftStage}
|
setShiftStage={setShiftStage}
|
||||||
redshiftId="dummy-redshift"
|
redshiftId="dummy-redshift"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={shiftStage() === "success" && chosenUtxo()}>
|
<Match
|
||||||
|
when={
|
||||||
|
shiftStage() === "success" &&
|
||||||
|
chosenUtxo()
|
||||||
|
}
|
||||||
|
>
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
<RedshiftReport
|
<RedshiftReport
|
||||||
redshift={dummyRedshift}
|
redshift={dummyRedshift}
|
||||||
utxo={chosenUtxo()!}
|
utxo={chosenUtxo()!}
|
||||||
/>
|
/>
|
||||||
<Button intent="red" onClick={resetState}>
|
<Button
|
||||||
|
intent="red"
|
||||||
|
onClick={resetState}
|
||||||
|
>
|
||||||
Nice
|
Nice
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -538,5 +611,5 @@ export default function Redshift() {
|
|||||||
<NavBar activeTab="redshift" />
|
<NavBar activeTab="redshift" />
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
</MutinyWalletGuard>
|
</MutinyWalletGuard>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Match, Show, Switch, createEffect, createMemo, createSignal, onMount } from "solid-js";
|
import {
|
||||||
|
Match,
|
||||||
|
Show,
|
||||||
|
Switch,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
onMount
|
||||||
|
} from "solid-js";
|
||||||
import { Amount } from "~/components/Amount";
|
import { Amount } from "~/components/Amount";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import {
|
import {
|
||||||
@@ -58,18 +66,26 @@ export function MethodChooser(props: {
|
|||||||
|
|
||||||
const methods = createMemo(() => {
|
const methods = createMemo(() => {
|
||||||
const lnBalance = store.balance?.lightning || 0n;
|
const lnBalance = store.balance?.lightning || 0n;
|
||||||
const onchainBalance = (store.balance?.confirmed || 0n) + (store.balance?.unconfirmed || 0n);
|
const onchainBalance =
|
||||||
|
(store.balance?.confirmed || 0n) +
|
||||||
|
(store.balance?.unconfirmed || 0n);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: "lightning",
|
value: "lightning",
|
||||||
label: "Lightning Balance",
|
label: "Lightning Balance",
|
||||||
caption: lnBalance > 0n ? `${lnBalance.toLocaleString()} SATS` : "No balance",
|
caption:
|
||||||
|
lnBalance > 0n
|
||||||
|
? `${lnBalance.toLocaleString()} SATS`
|
||||||
|
: "No balance",
|
||||||
disabled: lnBalance === 0n
|
disabled: lnBalance === 0n
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "onchain",
|
value: "onchain",
|
||||||
label: "On-chain Balance",
|
label: "On-chain Balance",
|
||||||
caption: onchainBalance > 0n ? `${onchainBalance.toLocaleString()} SATS` : "No balance",
|
caption:
|
||||||
|
onchainBalance > 0n
|
||||||
|
? `${onchainBalance.toLocaleString()} SATS`
|
||||||
|
: "No balance",
|
||||||
disabled: onchainBalance === 0n
|
disabled: onchainBalance === 0n
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -115,11 +131,17 @@ function DestinationInput(props: {
|
|||||||
<SmallHeader>Destination</SmallHeader>
|
<SmallHeader>Destination</SmallHeader>
|
||||||
<textarea
|
<textarea
|
||||||
value={props.fieldDestination}
|
value={props.fieldDestination}
|
||||||
onInput={(e) => props.setFieldDestination(e.currentTarget.value)}
|
onInput={(e) =>
|
||||||
|
props.setFieldDestination(e.currentTarget.value)
|
||||||
|
}
|
||||||
placeholder="bitcoin:..."
|
placeholder="bitcoin:..."
|
||||||
class="p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
class="p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
||||||
/>
|
/>
|
||||||
<Button disabled={!props.fieldDestination} intent="blue" onClick={props.handleDecode}>
|
<Button
|
||||||
|
disabled={!props.fieldDestination}
|
||||||
|
intent="blue"
|
||||||
|
onClick={props.handleDecode}
|
||||||
|
>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -191,7 +213,9 @@ export default function Send() {
|
|||||||
const [sentDetails, setSentDetails] = createSignal<SentDetails>();
|
const [sentDetails, setSentDetails] = createSignal<SentDetails>();
|
||||||
|
|
||||||
// Tagging stuff
|
// Tagging stuff
|
||||||
const [selectedContacts, setSelectedContacts] = createSignal<Partial<MutinyTagItem>[]>([]);
|
const [selectedContacts, setSelectedContacts] = createSignal<
|
||||||
|
Partial<MutinyTagItem>[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
const [error, setError] = createSignal<string>();
|
const [error, setError] = createSignal<string>();
|
||||||
@@ -211,10 +235,19 @@ export default function Send() {
|
|||||||
const feeEstimate = () => {
|
const feeEstimate = () => {
|
||||||
if (source() === "lightning") return undefined;
|
if (source() === "lightning") return undefined;
|
||||||
|
|
||||||
if (source() === "onchain" && amountSats() && amountSats() > 0n && address()) {
|
if (
|
||||||
|
source() === "onchain" &&
|
||||||
|
amountSats() &&
|
||||||
|
amountSats() > 0n &&
|
||||||
|
address()
|
||||||
|
) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
try {
|
try {
|
||||||
return state.mutiny_wallet?.estimate_tx_fee(address()!, amountSats(), undefined);
|
return state.mutiny_wallet?.estimate_tx_fee(
|
||||||
|
address()!,
|
||||||
|
amountSats(),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(eify(e).message);
|
setError(eify(e).message);
|
||||||
}
|
}
|
||||||
@@ -239,8 +272,11 @@ export default function Send() {
|
|||||||
if (source.memo) setDescription(source.memo);
|
if (source.memo) setDescription(source.memo);
|
||||||
|
|
||||||
if (source.invoice) {
|
if (source.invoice) {
|
||||||
state.mutiny_wallet?.decode_invoice(source.invoice).then((invoice) => {
|
state.mutiny_wallet
|
||||||
if (invoice?.amount_sats) setAmountSats(invoice.amount_sats);
|
?.decode_invoice(source.invoice)
|
||||||
|
.then((invoice) => {
|
||||||
|
if (invoice?.amount_sats)
|
||||||
|
setAmountSats(invoice.amount_sats);
|
||||||
setInvoice(invoice);
|
setInvoice(invoice);
|
||||||
setSource("lightning");
|
setSource("lightning");
|
||||||
});
|
});
|
||||||
@@ -249,7 +285,9 @@ export default function Send() {
|
|||||||
setNodePubkey(source.node_pubkey);
|
setNodePubkey(source.node_pubkey);
|
||||||
setSource("lightning");
|
setSource("lightning");
|
||||||
} else if (source.lnurl) {
|
} else if (source.lnurl) {
|
||||||
state.mutiny_wallet?.decode_lnurl(source.lnurl).then((lnurlParams) => {
|
state.mutiny_wallet
|
||||||
|
?.decode_lnurl(source.lnurl)
|
||||||
|
.then((lnurlParams) => {
|
||||||
if (lnurlParams.tag === "payRequest") {
|
if (lnurlParams.tag === "payRequest") {
|
||||||
setAmountSats(source.amount_sats || 0n);
|
setAmountSats(source.amount_sats || 0n);
|
||||||
setLnurlp(source.lnurl);
|
setLnurlp(source.lnurl);
|
||||||
@@ -303,7 +341,8 @@ export default function Send() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePaste() {
|
function handlePaste() {
|
||||||
if (!navigator.clipboard.readText) return showToast(new Error("Clipboard not supported"));
|
if (!navigator.clipboard.readText)
|
||||||
|
return showToast(new Error("Clipboard not supported"));
|
||||||
|
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.readText()
|
.readText()
|
||||||
@@ -316,21 +355,31 @@ export default function Send() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processContacts(contacts: Partial<MutinyTagItem>[]): Promise<string[]> {
|
async function processContacts(
|
||||||
|
contacts: Partial<MutinyTagItem>[]
|
||||||
|
): Promise<string[]> {
|
||||||
console.log("Processing contacts", contacts);
|
console.log("Processing contacts", contacts);
|
||||||
|
|
||||||
if (contacts.length) {
|
if (contacts.length) {
|
||||||
const first = contacts![0];
|
const first = contacts![0];
|
||||||
|
|
||||||
if (!first.name) {
|
if (!first.name) {
|
||||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
console.error(
|
||||||
|
"Something went wrong with contact creation, proceeding anyway"
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!first.id && first.name) {
|
if (!first.id && first.name) {
|
||||||
console.error("Creating new contact", first.name);
|
console.error("Creating new contact", first.name);
|
||||||
const c = new Contact(first.name, undefined, undefined, undefined);
|
const c = new Contact(
|
||||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
first.name,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const newContactId =
|
||||||
|
await state.mutiny_wallet?.create_new_contact(c);
|
||||||
if (newContactId) {
|
if (newContactId) {
|
||||||
return [newContactId];
|
return [newContactId];
|
||||||
}
|
}
|
||||||
@@ -342,7 +391,9 @@ export default function Send() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
console.error(
|
||||||
|
"Something went wrong with contact creation, proceeding anyway"
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,10 +411,20 @@ export default function Send() {
|
|||||||
sentDetails.destination = bolt11;
|
sentDetails.destination = bolt11;
|
||||||
// If the invoice has sats use that, otherwise we pass the user-defined amount
|
// If the invoice has sats use that, otherwise we pass the user-defined amount
|
||||||
if (invoice()?.amount_sats) {
|
if (invoice()?.amount_sats) {
|
||||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, tags);
|
await state.mutiny_wallet?.pay_invoice(
|
||||||
|
firstNode,
|
||||||
|
bolt11,
|
||||||
|
undefined,
|
||||||
|
tags
|
||||||
|
);
|
||||||
sentDetails.amount = invoice()?.amount_sats;
|
sentDetails.amount = invoice()?.amount_sats;
|
||||||
} else {
|
} else {
|
||||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), tags);
|
await state.mutiny_wallet?.pay_invoice(
|
||||||
|
firstNode,
|
||||||
|
bolt11,
|
||||||
|
amountSats(),
|
||||||
|
tags
|
||||||
|
);
|
||||||
sentDetails.amount = amountSats();
|
sentDetails.amount = amountSats();
|
||||||
}
|
}
|
||||||
} else if (source() === "lightning" && nodePubkey()) {
|
} else if (source() === "lightning" && nodePubkey()) {
|
||||||
@@ -399,7 +460,11 @@ export default function Send() {
|
|||||||
}
|
}
|
||||||
} else if (source() === "onchain" && address()) {
|
} else if (source() === "onchain" && address()) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tags);
|
const txid = await state.mutiny_wallet?.send_to_address(
|
||||||
|
address()!,
|
||||||
|
amountSats(),
|
||||||
|
tags
|
||||||
|
);
|
||||||
sentDetails.amount = amountSats();
|
sentDetails.amount = amountSats();
|
||||||
sentDetails.destination = address();
|
sentDetails.destination = address();
|
||||||
sentDetails.txid = txid;
|
sentDetails.txid = txid;
|
||||||
@@ -427,12 +492,22 @@ export default function Send() {
|
|||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<Show when={address() || invoice() || nodePubkey() || lnurlp()} fallback={<BackLink />}>
|
<Show
|
||||||
<BackButton onClick={() => clearAll()} title="Start Over" />
|
when={
|
||||||
|
address() || invoice() || nodePubkey() || lnurlp()
|
||||||
|
}
|
||||||
|
fallback={<BackLink />}
|
||||||
|
>
|
||||||
|
<BackButton
|
||||||
|
onClick={() => clearAll()}
|
||||||
|
title="Start Over"
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<LargeHeader>Send Bitcoin</LargeHeader>
|
<LargeHeader>Send Bitcoin</LargeHeader>
|
||||||
<SuccessModal
|
<SuccessModal
|
||||||
title={sentDetails()?.amount ? "Sent" : "Payment Failed"}
|
title={
|
||||||
|
sentDetails()?.amount ? "Sent" : "Payment Failed"
|
||||||
|
}
|
||||||
confirmText={sentDetails()?.amount ? "Nice" : "Too Bad"}
|
confirmText={sentDetails()?.amount ? "Nice" : "Too Bad"}
|
||||||
open={!!sentDetails()}
|
open={!!sentDetails()}
|
||||||
setOpen={(open: boolean) => {
|
setOpen={(open: boolean) => {
|
||||||
@@ -445,16 +520,33 @@ export default function Send() {
|
|||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={sentDetails()?.failure_reason}>
|
<Match when={sentDetails()?.failure_reason}>
|
||||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[50vh]" />
|
<img
|
||||||
|
src={megaex}
|
||||||
|
alt="fail"
|
||||||
|
class="w-1/2 mx-auto max-w-[50vh]"
|
||||||
|
/>
|
||||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||||
{sentDetails()?.failure_reason}
|
{sentDetails()?.failure_reason}
|
||||||
</p>
|
</p>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh]" />
|
<img
|
||||||
<Amount amountSats={sentDetails()?.amount} showFiat centered />
|
src={megacheck}
|
||||||
|
alt="success"
|
||||||
|
class="w-1/2 mx-auto max-w-[50vh]"
|
||||||
|
/>
|
||||||
|
<Amount
|
||||||
|
amountSats={sentDetails()?.amount}
|
||||||
|
showFiat
|
||||||
|
centered
|
||||||
|
/>
|
||||||
<Show when={sentDetails()?.txid}>
|
<Show when={sentDetails()?.txid}>
|
||||||
<ExternalLink href={mempoolTxUrl(sentDetails()?.txid, network)}>
|
<ExternalLink
|
||||||
|
href={mempoolTxUrl(
|
||||||
|
sentDetails()?.txid,
|
||||||
|
network
|
||||||
|
)}
|
||||||
|
>
|
||||||
View Transaction
|
View Transaction
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -463,7 +555,14 @@ export default function Send() {
|
|||||||
</SuccessModal>
|
</SuccessModal>
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={address() || invoice() || nodePubkey() || lnurlp()}>
|
<Match
|
||||||
|
when={
|
||||||
|
address() ||
|
||||||
|
invoice() ||
|
||||||
|
nodePubkey() ||
|
||||||
|
lnurlp()
|
||||||
|
}
|
||||||
|
>
|
||||||
<MethodChooser
|
<MethodChooser
|
||||||
source={source()}
|
source={source()}
|
||||||
setSource={setSource}
|
setSource={setSource}
|
||||||
@@ -483,7 +582,9 @@ export default function Send() {
|
|||||||
<SmallHeader>Private tags</SmallHeader>
|
<SmallHeader>Private tags</SmallHeader>
|
||||||
<TagEditor
|
<TagEditor
|
||||||
selectedValues={selectedContacts()}
|
selectedValues={selectedContacts()}
|
||||||
setSelectedValues={setSelectedContacts}
|
setSelectedValues={
|
||||||
|
setSelectedContacts
|
||||||
|
}
|
||||||
placeholder="Add the receiver for your records"
|
placeholder="Add the receiver for your records"
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -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,7 +2,12 @@ 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";
|
||||||
@@ -19,5 +24,5 @@ export default function Admin() {
|
|||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="none" />
|
<NavBar activeTab="none" />
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
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";
|
||||||
@@ -49,7 +56,8 @@ export default function Swap() {
|
|||||||
|
|
||||||
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()) {
|
||||||
@@ -69,11 +77,16 @@ export default function Swap() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasLsp = () => {
|
const hasLsp = () => {
|
||||||
return !!localStorage.getItem("MUTINY_SETTINGS_lsp") || !!import.meta.env.VITE_LSP;
|
return (
|
||||||
|
!!localStorage.getItem("MUTINY_SETTINGS_lsp") ||
|
||||||
|
!!import.meta.env.VITE_LSP
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -87,12 +100,17 @@ export default function Swap() {
|
|||||||
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 refetch();
|
await refetch();
|
||||||
|
|
||||||
// If peers list contains the peer we just connected to, select it
|
// If peers list contains the peer we just connected to, select it
|
||||||
const peer = peers()?.find((p) => p.pubkey === peerConnectString.split("@")[0]);
|
const peer = peers()?.find(
|
||||||
|
(p) => p.pubkey === peerConnectString.split("@")[0]
|
||||||
|
);
|
||||||
|
|
||||||
if (peer) {
|
if (peer) {
|
||||||
setSelectedPeer(peer.pubkey);
|
setSelectedPeer(peer.pubkey);
|
||||||
@@ -146,13 +164,23 @@ export default function Swap() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canSwap = () => {
|
const canSwap = () => {
|
||||||
const balance = (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
|
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") {
|
if (network === "bitcoin") {
|
||||||
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 50000n && amountSats() <= balance;
|
return (
|
||||||
|
(!!selectedPeer() || !!hasLsp()) &&
|
||||||
|
amountSats() >= 50000n &&
|
||||||
|
amountSats() <= balance
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 10000n && amountSats() <= balance;
|
return (
|
||||||
|
(!!selectedPeer() || !!hasLsp()) &&
|
||||||
|
amountSats() >= 10000n &&
|
||||||
|
amountSats() <= balance
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,7 +196,9 @@ export default function Swap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
amountSats() > (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n) ||
|
amountSats() >
|
||||||
|
(state.balance?.confirmed || 0n) +
|
||||||
|
(state.balance?.unconfirmed || 0n) ||
|
||||||
!feeEstimate()
|
!feeEstimate()
|
||||||
) {
|
) {
|
||||||
return "You don't have enough funds to make this channel";
|
return "You don't have enough funds to make this channel";
|
||||||
@@ -186,8 +216,14 @@ export default function Swap() {
|
|||||||
<BackLink />
|
<BackLink />
|
||||||
<LargeHeader>Swap to Lightning</LargeHeader>
|
<LargeHeader>Swap to Lightning</LargeHeader>
|
||||||
<SuccessModal
|
<SuccessModal
|
||||||
title={channelOpenResult()?.channel ? "Swap Success" : "Swap Failed"}
|
title={
|
||||||
confirmText={channelOpenResult()?.channel ? "Nice" : "Too Bad"}
|
channelOpenResult()?.channel
|
||||||
|
? "Swap Success"
|
||||||
|
: "Swap Failed"
|
||||||
|
}
|
||||||
|
confirmText={
|
||||||
|
channelOpenResult()?.channel ? "Nice" : "Too Bad"
|
||||||
|
}
|
||||||
open={!!channelOpenResult()}
|
open={!!channelOpenResult()}
|
||||||
setOpen={(open: boolean) => {
|
setOpen={(open: boolean) => {
|
||||||
if (!open) setChannelOpenResult(undefined);
|
if (!open) setChannelOpenResult(undefined);
|
||||||
@@ -199,22 +235,45 @@ export default function Swap() {
|
|||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={channelOpenResult()?.failure_reason}>
|
<Match when={channelOpenResult()?.failure_reason}>
|
||||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
<img
|
||||||
|
src={megaex}
|
||||||
|
alt="fail"
|
||||||
|
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||||
|
/>
|
||||||
|
|
||||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||||
{channelOpenResult()?.failure_reason?.message}
|
{
|
||||||
|
channelOpenResult()?.failure_reason
|
||||||
|
?.message
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
<img
|
||||||
<AmountCard
|
src={megacheck}
|
||||||
amountSats={channelOpenResult()?.channel?.balance?.toString() || ""}
|
alt="success"
|
||||||
reserve={channelOpenResult()?.channel?.reserve?.toString() || ""}
|
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||||
/>
|
/>
|
||||||
<Show when={channelOpenResult()?.channel?.outpoint}>
|
<AmountCard
|
||||||
|
amountSats={
|
||||||
|
channelOpenResult()?.channel?.balance?.toString() ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
reserve={
|
||||||
|
channelOpenResult()?.channel?.reserve?.toString() ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
channelOpenResult()?.channel?.outpoint
|
||||||
|
}
|
||||||
|
>
|
||||||
<ExternalLink
|
<ExternalLink
|
||||||
href={mempoolTxUrl(
|
href={mempoolTxUrl(
|
||||||
channelOpenResult()?.channel?.outpoint?.split(":")[0],
|
channelOpenResult()?.channel?.outpoint?.split(
|
||||||
|
":"
|
||||||
|
)[0],
|
||||||
network
|
network
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -226,13 +285,20 @@ export default function Swap() {
|
|||||||
</Switch>
|
</Switch>
|
||||||
</SuccessModal>
|
</SuccessModal>
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
<MethodChooser source={source()} setSource={setSource} both={false} />
|
<MethodChooser
|
||||||
|
source={source()}
|
||||||
|
setSource={setSource}
|
||||||
|
both={false}
|
||||||
|
/>
|
||||||
<VStack>
|
<VStack>
|
||||||
<Show when={!hasLsp()}>
|
<Show when={!hasLsp()}>
|
||||||
<Card>
|
<Card>
|
||||||
<VStack>
|
<VStack>
|
||||||
<div class="w-full flex flex-col gap-2">
|
<div class="w-full flex flex-col gap-2">
|
||||||
<label for="peerselect" class="uppercase font-semibold text-sm">
|
<label
|
||||||
|
for="peerselect"
|
||||||
|
class="uppercase font-semibold text-sm"
|
||||||
|
>
|
||||||
Use existing peer
|
Use existing peer
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@@ -241,19 +307,34 @@ export default function Swap() {
|
|||||||
onChange={handlePeerSelect}
|
onChange={handlePeerSelect}
|
||||||
value={selectedPeer()}
|
value={selectedPeer()}
|
||||||
>
|
>
|
||||||
<option value="" class="" selected>
|
<option
|
||||||
|
value=""
|
||||||
|
class=""
|
||||||
|
selected
|
||||||
|
>
|
||||||
Choose a peer
|
Choose a peer
|
||||||
</option>
|
</option>
|
||||||
<For each={peers()}>
|
<For each={peers()}>
|
||||||
{(peer) => (
|
{(peer) => (
|
||||||
<option value={peer.pubkey}>{peer.alias ?? peer.pubkey}</option>
|
<option
|
||||||
|
value={peer.pubkey}
|
||||||
|
>
|
||||||
|
{peer.alias ??
|
||||||
|
peer.pubkey}
|
||||||
|
</option>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<Show when={!selectedPeer()}>
|
<Show when={!selectedPeer()}>
|
||||||
<Form onSubmit={onSubmit} class="flex flex-col gap-4">
|
<Form
|
||||||
<Field name="peer" validate={[required("")]}>
|
onSubmit={onSubmit}
|
||||||
|
class="flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
name="peer"
|
||||||
|
validate={[required("")]}
|
||||||
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...props}
|
{...props}
|
||||||
@@ -264,8 +345,14 @@ export default function Swap() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Button layout="small" type="submit" disabled={isConnecting()}>
|
<Button
|
||||||
{isConnecting() ? "Connecting..." : "Connect"}
|
layout="small"
|
||||||
|
type="submit"
|
||||||
|
disabled={isConnecting()}
|
||||||
|
>
|
||||||
|
{isConnecting()
|
||||||
|
? "Connecting..."
|
||||||
|
: "Connect"}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
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 (
|
||||||
@@ -9,11 +14,11 @@ export default function NotFound() {
|
|||||||
<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.
|
|
||||||
</p>
|
|
||||||
<div class="h-full" />
|
<div class="h-full" />
|
||||||
<ButtonLink href="/" intent="red">Dangit</ButtonLink>
|
<ButtonLink href="/" intent="red">
|
||||||
|
Dangit
|
||||||
|
</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";
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
/* @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 {
|
||||||
|
ParentComponent,
|
||||||
|
createContext,
|
||||||
|
createEffect,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
useContext
|
||||||
|
} from "solid-js";
|
||||||
import { createStore, reconcile } from "solid-js/store";
|
import { createStore, reconcile } from "solid-js/store";
|
||||||
import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup";
|
import {
|
||||||
|
MutinyWalletSettingStrings,
|
||||||
|
setupMutinyWallet
|
||||||
|
} from "~/logic/mutinyWalletSetup";
|
||||||
import {
|
import {
|
||||||
MutinyBalance,
|
MutinyBalance,
|
||||||
MutinyWallet,
|
MutinyWallet,
|
||||||
@@ -64,7 +74,8 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
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:
|
||||||
|
localStorage.getItem("dismissed_restore_prompt") === "true",
|
||||||
wallet_loading: true,
|
wallet_loading: true,
|
||||||
nwc_enabled: localStorage.getItem("nwc_enabled") === "true",
|
nwc_enabled: localStorage.getItem("nwc_enabled") === "true",
|
||||||
activity: [] as MutinyActivity[]
|
activity: [] as MutinyActivity[]
|
||||||
@@ -98,7 +109,9 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
return "new_here";
|
return "new_here";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setupMutinyWallet(settings?: MutinyWalletSettingStrings): Promise<void> {
|
async setupMutinyWallet(
|
||||||
|
settings?: MutinyWalletSettingStrings
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
setState({ wallet_loading: true });
|
setState({ wallet_loading: true });
|
||||||
const mutinyWallet = await setupMutinyWallet(settings);
|
const mutinyWallet = await setupMutinyWallet(settings);
|
||||||
@@ -110,7 +123,11 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
const firstNode = (nodes[0] as string) || "";
|
const firstNode = (nodes[0] as string) || "";
|
||||||
await mutinyWallet.start_nostr_wallet_connect(firstNode);
|
await mutinyWallet.start_nostr_wallet_connect(firstNode);
|
||||||
}
|
}
|
||||||
setState({ mutiny_wallet: mutinyWallet, wallet_loading: false, balance });
|
setState({
|
||||||
|
mutiny_wallet: mutinyWallet,
|
||||||
|
wallet_loading: false,
|
||||||
|
balance
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@@ -133,7 +150,8 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
if (state.mutiny_wallet && !state.is_syncing) {
|
if (state.mutiny_wallet && !state.is_syncing) {
|
||||||
setState({ is_syncing: true });
|
setState({ is_syncing: true });
|
||||||
const newBalance = await state.mutiny_wallet?.get_balance();
|
const newBalance = await state.mutiny_wallet?.get_balance();
|
||||||
const price = await state.mutiny_wallet?.get_bitcoin_price();
|
const price =
|
||||||
|
await state.mutiny_wallet?.get_bitcoin_price();
|
||||||
setState({
|
setState({
|
||||||
balance: newBalance,
|
balance: newBalance,
|
||||||
last_sync: Date.now(),
|
last_sync: Date.now(),
|
||||||
@@ -181,9 +199,15 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
setState({ user_status: status });
|
setState({ user_status: status });
|
||||||
|
|
||||||
// Only load node manager when status is approved
|
// Only load node manager when status is approved
|
||||||
if (state.user_status === "approved" && !state.mutiny_wallet && !state.deleting) {
|
if (
|
||||||
|
state.user_status === "approved" &&
|
||||||
|
!state.mutiny_wallet &&
|
||||||
|
!state.deleting
|
||||||
|
) {
|
||||||
console.log("running setup node manager...");
|
console.log("running setup node manager...");
|
||||||
actions.setupMutinyWallet().then(() => console.log("node manager setup done"));
|
actions
|
||||||
|
.setupMutinyWallet()
|
||||||
|
.then(() => console.log("node manager setup done"));
|
||||||
|
|
||||||
// Setup an event listener to stop the mutiny wallet when the page unloads
|
// Setup an event listener to stop the mutiny wallet when the page unloads
|
||||||
window.onunload = async (_e) => {
|
window.onunload = async (_e) => {
|
||||||
@@ -214,14 +238,18 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
|
|
||||||
const store = [state, actions] as MegaStore;
|
const store = [state, actions] as MegaStore;
|
||||||
|
|
||||||
return <MegaStoreContext.Provider value={store}>{props.children}</MegaStoreContext.Provider>;
|
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,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,15 +1,24 @@
|
|||||||
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(
|
||||||
|
amount: number | undefined,
|
||||||
|
price: number,
|
||||||
|
formatted: boolean
|
||||||
|
): string {
|
||||||
if (typeof amount !== "number" || isNaN(amount)) {
|
if (typeof amount !== "number" || isNaN(amount)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const btc = MutinyWallet.convert_sats_to_btc(BigInt(Math.floor(amount)));
|
const btc = MutinyWallet.convert_sats_to_btc(
|
||||||
|
BigInt(Math.floor(amount))
|
||||||
|
);
|
||||||
const usd = btc * price;
|
const usd = btc * price;
|
||||||
|
|
||||||
if (formatted) {
|
if (formatted) {
|
||||||
return usd.toLocaleString("en-US", { style: "currency", currency: "USD" });
|
return usd.toLocaleString("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Some float fighting shenaningans
|
// Some float fighting shenaningans
|
||||||
const roundedUsd = Math.round(usd);
|
const roundedUsd = Math.round(usd);
|
||||||
@@ -25,7 +34,11 @@ export function satsToUsd(amount: number | undefined, price: number, formatted:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usdToSats(amount: number | undefined, price: number, formatted: boolean): string {
|
export function usdToSats(
|
||||||
|
amount: number | undefined,
|
||||||
|
price: number,
|
||||||
|
formatted: boolean
|
||||||
|
): string {
|
||||||
if (typeof amount !== "number" || isNaN(amount)) {
|
if (typeof amount !== "number" || isNaN(amount)) {
|
||||||
return "";
|
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>
|
||||||
|
>(obj: T): string {
|
||||||
|
return (
|
||||||
|
Object.entries(obj)
|
||||||
.filter(([_, value]) => value !== undefined)
|
.filter(([_, value]) => value !== undefined)
|
||||||
// Value shouldn't be null we just filtered it out but typescript is dumb
|
// Value shouldn't be null we just filtered it out but typescript is dumb
|
||||||
.map(([key, value]) => value ? `${encodeURIComponent(key)}=${encodeURIComponent(value)}` : "")
|
.map(([key, value]) =>
|
||||||
.join("&");
|
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': {
|
".safe-left": {
|
||||||
paddingLeft: 'constant(safe-area-inset-left)',
|
paddingLeft: "constant(safe-area-inset-left)",
|
||||||
paddingLeft: 'env(safe-area-inset-left)'
|
paddingLeft: "env(safe-area-inset-left)"
|
||||||
},
|
},
|
||||||
'.safe-right': {
|
".safe-right": {
|
||||||
paddingRight: 'constant(safe-area-inset-right)',
|
paddingRight: "constant(safe-area-inset-right)",
|
||||||
paddingRight: 'env(safe-area-inset-right)'
|
paddingRight: "env(safe-area-inset-right)"
|
||||||
},
|
},
|
||||||
'.safe-bottom': {
|
".safe-bottom": {
|
||||||
paddingBottom: 'constant(safe-area-inset-bottom)',
|
paddingBottom: "constant(safe-area-inset-bottom)",
|
||||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
paddingBottom: "env(safe-area-inset-bottom)"
|
||||||
},
|
},
|
||||||
'.h-screen-safe': {
|
".disable-scrollbars": {
|
||||||
height: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
scrollbarWidth: "none",
|
||||||
|
"-ms-overflow-style": "none",
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
width: "0px",
|
||||||
|
background: "transparent",
|
||||||
|
display: "none"
|
||||||
},
|
},
|
||||||
'.min-h-screen-safe': {
|
"& *::-webkit-scrollbar": {
|
||||||
minHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
width: "0px",
|
||||||
|
background: "transparent",
|
||||||
|
display: "none"
|
||||||
},
|
},
|
||||||
'.max-h-screen-safe': {
|
"& *": {
|
||||||
maxHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
scrollbarWidth: "none",
|
||||||
},
|
"-ms-overflow-style": "none"
|
||||||
'.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,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"include": ["global.d.ts", "src/**/*"],
|
"include": [
|
||||||
|
"global.d.ts",
|
||||||
|
"src/**/*",
|
||||||
|
"tailwind.config.cjs",
|
||||||
|
".eslintrc.cjs"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user