Clean up login page

This commit is contained in:
Jon Staab
2026-02-26 14:15:07 -08:00
parent e9f56295de
commit 93e9a714cf
9 changed files with 321 additions and 225 deletions
+3 -87
View File
@@ -9,14 +9,16 @@
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/solid-query": "^5.90.23",
"@types/qrcode": "^1.5.6",
"applesauce-accounts": "^5.1.0",
"applesauce-common": "^5.1.0",
"applesauce-core": "^5.1.0",
"applesauce-relay": "^5.1.0",
"applesauce-signers": "^5.1.0",
"applesauce-wallet-connect": "^5.0.1",
"nonboard": "^1.0.8",
"preline": "^4.1.1",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"solid-js": "^1.9.10",
"tailwindcss": "^4.2.1",
},
@@ -29,8 +31,6 @@
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
@@ -67,8 +67,6 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@capacitor/core": ["@capacitor/core@7.5.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-4Y4trISe2Bp3lwsoGFoQIvgX4hiZO8S1Slmbz6oFaMxAuEc4noipQGCQx974PF4glwVVe/8+H3P9iEmCXtrUgA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@@ -203,8 +201,6 @@
"@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
"@svgdotjs/svg.draggable.js": ["@svgdotjs/svg.draggable.js@3.0.6", "", { "peerDependencies": { "@svgdotjs/svg.js": "^3.2.4" } }, "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA=="],
"@svgdotjs/svg.filter.js": ["@svgdotjs/svg.filter.js@3.0.9", "", { "dependencies": { "@svgdotjs/svg.js": "^3.2.4" } }, "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw=="],
@@ -265,36 +261,14 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/events": ["@types/events@3.0.3", "", {}, "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g=="],
"@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="],
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
"@types/throttle-debounce": ["@types/throttle-debounce@5.0.2", "", {}, "sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@welshman/lib": ["@welshman/lib@0.4.7", "", { "dependencies": { "@scure/base": "^1.1.6", "@types/events": "^3.0.3", "events": "^3.3.0" } }, "sha512-VP3WO2ROo5pf2vHwnrdt6lQVTc8Eo52Ie+1/9ZzfTrSxtLrreSSxW3H+1oPDbHl3FXbDnQWdFWbxys6OxzKZWw=="],
"@welshman/net": ["@welshman/net@0.4.7", "", { "dependencies": { "@welshman/lib": "0.4.7", "@welshman/relay": "0.4.7", "@welshman/util": "0.4.7", "events": "^3.3.0", "isomorphic-ws": "^5.0.0" } }, "sha512-S0dGVqNAfo5cvBxIuaI2oEX0JUs2FuzkOcYXkMNB1plWzi1mBcskNCBWfFf/zHJYaqUoYjZ/tDARy64lge9m/w=="],
"@welshman/relay": ["@welshman/relay@0.4.7", "", { "dependencies": { "@welshman/lib": "0.4.7", "@welshman/util": "0.4.7" } }, "sha512-FkqYswNA3uT1NeJVHdEZ7p9jEPGCFMx4ci2y+h9o25rCDdxg3WUhqDSdc5d85sGTO0qG2pNnvNMfS/Du/nFlOA=="],
"@welshman/signer": ["@welshman/signer@0.4.7", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "@welshman/lib": "0.4.7", "@welshman/net": "0.4.7", "@welshman/util": "0.4.7", "nostr-tools": "^2.14.2" }, "peerDependencies": { "nostr-signer-capacitor-plugin": "~0.0.4" } }, "sha512-V5Jdmblb2kPO5bAv1CzVrodZiwKpYYotmS6MFXfWmyrKKp+9B5KoMWzXt6yfd4HWYbEKEjLhbm1gzbckupW8Nw=="],
"@welshman/store": ["@welshman/store@0.4.7", "", { "dependencies": { "@welshman/lib": "0.4.7", "@welshman/relay": "0.4.7", "@welshman/util": "0.4.7", "svelte": "^4.2.18" } }, "sha512-8PniW1AOOYFtLRYMuay62taumW7zgwtBmouwoMh08fBQjLb+c90V4g2cEGVWoyvKXSLzQkQppPlaqYzdSDqgwg=="],
"@welshman/util": ["@welshman/util@0.4.7", "", { "dependencies": { "@types/ws": "^8.5.13", "@welshman/lib": "0.4.7", "js-base64": "^3.7.7", "nostr-tools": "^2.14.2", "nostr-wasm": "^0.1.0" } }, "sha512-FlmBiZeKlAEAAwyhu7cWtlfAxU3CWX7WQGn0NkCZaAjgGV3n8LIDjT1u9m1PmXirBT0+qFNGLWas3p72IQMLgg=="],
"@yr/monotone-cubic-spline": ["@yr/monotone-cubic-spline@1.0.3", "", {}, "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -313,42 +287,26 @@
"applesauce-wallet-connect": ["applesauce-wallet-connect@5.0.1", "", { "dependencies": { "@noble/hashes": "^1.7.1", "applesauce-common": "^5.0.0", "applesauce-core": "^5.0.0", "rxjs": "^7.8.1" } }, "sha512-k/Gl2IIjfQelW4deN/0M9/I3uznUMZalGAP9/wPgwmAtUyaEHb8YJpOdxqLwCQ98vZMTAcwgK6hmXkAPqA6NTg=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="],
"babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"blueimp-canvas-to-blob": ["blueimp-canvas-to-blob@3.29.0", "", {}, "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"compressorjs": ["compressorjs@1.2.1", "", { "dependencies": { "blueimp-canvas-to-blob": "^3.29.0", "is-blob": "^2.1.0" } }, "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="],
@@ -363,8 +321,6 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="],
"dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
"dropzone": ["dropzone@6.0.0-beta.2", "", { "dependencies": { "@swc/helpers": "^0.2.13", "just-extend": "^5.0.0" } }, "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ=="],
@@ -381,14 +337,6 @@
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -407,22 +355,14 @@
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
"is-blob": ["is-blob@2.1.0", "", {}, "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jquery": ["jquery@4.0.0", "", {}, "sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg=="],
"js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -457,32 +397,22 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
"mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
"mithril": ["mithril@2.3.8", "", {}, "sha512-za/Yo7qXEckjm5syrSfaaI9Utf4tCUT3T1IOIYqH6Lrj7G0OZuYYLAY9SV4ygoaAf0+CNqU92MBt+7pmo53JVQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"nonboard": ["nonboard@1.0.8", "", { "dependencies": { "@types/qrcode": "^1.5.5", "@types/throttle-debounce": "^5.0.2", "@welshman/lib": "^0.4.7", "@welshman/net": "^0.4.7", "@welshman/signer": "^0.4.7", "@welshman/store": "^0.4.7", "@welshman/util": "^0.4.7", "classnames": "^2.5.1", "compressorjs": "^1.2.1", "mithril": "^2.3.7", "nostr-tools": "^2.16.2", "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", "svelte": "^5.38.10", "throttle-debounce": "^5.0.2" } }, "sha512-yMGESyQs4M1Fcv4xolhWUvWAxXesw5tClJMS89R5r2Fh4d3XFoG6uCylQjPg0miAMG0Uu1p6EmOIhIoPx8ADlg=="],
"nostr-signer-capacitor-plugin": ["nostr-signer-capacitor-plugin@0.0.5", "", { "peerDependencies": { "@capacitor/core": "^7.0.0" } }, "sha512-/EvqWz71HZ5cWmzvfXWTm48AWZtbeZDbOg3vLwXyXPjnIp1DR7Wurww/Mo41ORNu1DNPlqH20l7kIXKO6vR5og=="],
"nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="],
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
@@ -499,8 +429,6 @@
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
@@ -541,14 +469,10 @@
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"svelte": ["svelte@5.53.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -571,8 +495,6 @@
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -581,8 +503,6 @@
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
@@ -607,10 +527,6 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@welshman/signer/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="],
"@welshman/store/svelte": ["svelte@4.2.20", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q=="],
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
"light-bolt11-decoder/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
+2
View File
@@ -13,6 +13,7 @@
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/solid-query": "^5.90.23",
"@types/qrcode": "^1.5.6",
"applesauce-accounts": "^5.1.0",
"applesauce-common": "^5.1.0",
"applesauce-core": "^5.1.0",
@@ -21,6 +22,7 @@
"applesauce-wallet-connect": "^5.0.1",
"preline": "^4.1.1",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"solid-js": "^1.9.10",
"tailwindcss": "^4.2.1"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

+36 -9
View File
@@ -1,23 +1,50 @@
import { Show } from "solid-js"
import { A } from "@solidjs/router"
import { PLATFORM_NAME } from "../lib/nostr"
import { PLATFORM_NAME, useActiveAccount, useProfilePicture } from "../lib/nostr"
export default function Navbar() {
const account = useActiveAccount()
const picture = useProfilePicture(() => account()?.pubkey)
return (
<nav class="bg-white border-b border-gray-200">
<div class="max-w-5xl mx-auto px-4 h-14 flex items-center justify-between">
<A href="/" class="font-bold text-gray-900 text-lg">
{PLATFORM_NAME}
<div class="max-w-4xl mx-auto px-4 h-14 flex items-center justify-between">
<A href="/" class="flex items-center gap-2">
<img src="/caravel.png" alt={PLATFORM_NAME} class="h-8 w-8 rounded-full object-cover" />
<span class="font-bold text-gray-900 text-lg">{PLATFORM_NAME}</span>
</A>
<div class="flex items-center gap-4">
<A href="/relays" class="text-sm text-gray-600 hover:text-gray-900">
Dashboard
</A>
<A
href="/login"
class="text-sm py-1.5 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
<Show
when={account()}
fallback={
<A
href="/login"
class="text-sm py-1.5 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Log In
</A>
}
>
Log In
</A>
<A href="/account">
<Show
when={picture()}
fallback={
<div class="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center">
<svg class="w-4 h-4 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
}
>
<img
src={picture()}
alt="Profile"
class="h-8 w-8 rounded-full object-cover"
/>
</Show>
</A>
</Show>
</div>
</div>
</nav>
+41
View File
@@ -1,4 +1,6 @@
import { createEffect, createSignal, onCleanup } from "solid-js"
import { EventStore } from "applesauce-core"
import { getProfilePicture } from "applesauce-core/helpers/profile"
import { RelayPool } from "applesauce-relay"
import { AccountManager } from "applesauce-accounts"
import type { IAccount, SerializedAccount } from "applesauce-accounts"
@@ -8,6 +10,8 @@ import { NostrConnectSigner } from "applesauce-signers"
export const API_URL = import.meta.env.VITE_API_URL
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME || "Caravel"
const PROFILE_RELAYS = ["wss://purplepag.es", "wss://relay.damus.io", "wss://nos.lol"]
export const eventStore = new EventStore()
export const pool = new RelayPool()
export const accounts = new AccountManager()
@@ -51,3 +55,40 @@ export function persistAccounts() {
localStorage.removeItem(ACTIVE_ACCOUNT_STORAGE_KEY)
}
}
export function useActiveAccount() {
const [account, setAccount] = createSignal(accounts.active)
const sub = accounts.active$.subscribe(setAccount)
onCleanup(() => sub.unsubscribe())
return account
}
export function useProfilePicture(pubkey: () => string | undefined) {
const [picture, setPicture] = createSignal<string | undefined>()
createEffect(() => {
const pk = pubkey()
if (!pk) {
setPicture(undefined)
return
}
// Subscribe to profile changes in the event store
const profileSub = eventStore.profile(pk).subscribe(profile => {
setPicture(getProfilePicture(profile))
})
// Fetch the kind 0 from relays and add to the event store
const reqSub = pool.request(PROFILE_RELAYS, { kinds: [0], authors: [pk] }).subscribe(event => {
eventStore.add(event)
})
onCleanup(() => {
profileSub.unsubscribe()
reqSub.unsubscribe()
})
})
return picture
}
+2 -2
View File
@@ -4,7 +4,7 @@ export default function Home() {
return (
<div class="min-h-screen bg-white">
{/* Hero */}
<div class="max-w-5xl mx-auto px-4 py-24 text-center">
<div class="max-w-4xl mx-auto px-4 py-24 text-center">
<h1 class="text-5xl font-bold text-gray-900 mb-4">
Host Your Own Nostr Community Relay
</h1>
@@ -21,7 +21,7 @@ export default function Home() {
</div>
{/* Pricing */}
<div class="max-w-5xl mx-auto px-4 pb-24">
<div class="max-w-4xl mx-auto px-4 pb-24">
<h2 class="text-3xl font-bold text-center text-gray-900 mb-12">Pricing</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="border border-gray-200 rounded-xl p-6">
+235 -125
View File
@@ -1,14 +1,18 @@
import { Show, createSignal, onCleanup, onMount } from "solid-js"
import { Show, createSignal, onCleanup } from "solid-js"
import { useNavigate } from "@solidjs/router"
import { ExtensionAccount, NostrConnectAccount, PasswordAccount, PrivateKeyAccount } from "applesauce-accounts/accounts"
import { PasswordSigner } from "applesauce-signers"
import QrScanner from "qr-scanner"
import QRCode from "qrcode"
import { activateAccount } from "../lib/nostr"
import { PLATFORM_NAME } from "../lib/nostr"
const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc']
type Tab = "nip07" | "nip46" | "key"
type Screen = "tabs" | "nip46" | "key"
type SignerTab = "qr" | "paste"
type KeyTab = "plaintext" | "encrypted"
function normalizeBunkerUrl(value: string): string {
const trimmed = value.trim()
@@ -37,8 +41,12 @@ export default function Login() {
const [tab, setTab] = createSignal<Tab>(window.nostr ? "nip07" : "nip46")
const [loading, setLoading] = createSignal(false)
const [error, setError] = createSignal("")
const [screen, setScreen] = createSignal<Screen>("tabs")
const [signerTab, setSignerTab] = createSignal<SignerTab>("qr")
const [keyTab, setKeyTab] = createSignal<KeyTab>("plaintext")
const [nostrConnectUri, setNostrConnectUri] = createSignal("")
const [qrDataUrl, setQrDataUrl] = createSignal("")
const [showScanner, setShowScanner] = createSignal(false)
const [bunkerUrl, setBunkerUrl] = createSignal("")
const [nsecValue, setNsecValue] = createSignal("")
@@ -79,6 +87,7 @@ export default function Login() {
url: window.location.origin,
})
setNostrConnectUri(uri)
setQrDataUrl(await QRCode.toDataURL(uri, { width: 256, margin: 2 }))
abortController = new AbortController()
const timeout = window.setTimeout(() => abortController?.abort(), 60_000)
@@ -141,24 +150,25 @@ export default function Login() {
}
}
async function toggleScanner() {
const next = !showScanner()
setShowScanner(next)
if (!next) {
scanner?.destroy()
scanner = undefined
return
}
function closeScanner() {
setShowScanner(false)
scanner?.destroy()
scanner = undefined
}
async function openScanner() {
setError("")
setShowScanner(true)
// Wait a tick for the modal video element to mount
await new Promise(r => setTimeout(r, 0))
if (!scannerVideo) return
scanner = new QrScanner(
scannerVideo,
(result) => {
setBunkerUrl(result.data)
setShowScanner(false)
scanner?.destroy()
scanner = undefined
closeScanner()
},
{ returnDetailedScanResult: true },
)
@@ -166,12 +176,21 @@ export default function Login() {
await scanner.start()
} catch (e) {
setError(e instanceof Error ? e.message : "Unable to access camera")
setShowScanner(false)
scanner?.destroy()
scanner = undefined
closeScanner()
}
}
function enterSignerScreen() {
setError("")
setScreen("nip46")
setSignerTab("qr")
if (!nostrConnectUri()) void startNostrConnect()
}
function copyUri() {
void navigator.clipboard.writeText(nostrConnectUri())
}
onCleanup(() => {
abortController?.abort()
scanner?.destroy()
@@ -204,111 +223,187 @@ export default function Login() {
</div>
<div class="flex items-start gap-3">
<span class="mt-1 h-2 w-2 rounded-full bg-indigo-500" />
<p>Fast access to your relays and invoices.</p>
<p>Get fast access to your relays.</p>
</div>
</div>
</div>
<div class="rounded-xl border border-gray-200 bg-white/80 p-6">
<h2 class="text-lg font-semibold text-gray-900">Log in / Sign up</h2>
<p class="mt-2 text-xs text-gray-500">
Use any Nostr signer method. New users are automatically onboarded.
</p>
<div class="mt-6 space-y-4">
<div class="flex gap-2 border border-gray-200 rounded-lg p-1">
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${tab() === "nip07" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setTab("nip07")}
disabled={!window.nostr}
>
NIP-07
</button>
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${tab() === "nip46" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setTab("nip46")}
>
NIP-46
</button>
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${tab() === "key" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setTab("key")}
>
Key
</button>
</div>
<Show when={screen() === "tabs"}>
<h2 class="text-lg font-semibold text-gray-900">Log in / Sign up</h2>
<p class="mt-2 text-xs text-gray-500">
Use any Nostr signer method. New users are automatically onboarded.
</p>
<div class="mt-6 space-y-4">
<div class="flex gap-2 border border-gray-200 rounded-lg p-1">
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${tab() === "nip07" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setTab("nip07")}
disabled={!window.nostr}
>
Extension
</button>
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${tab() === "nip46" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setTab("nip46")}
>
Signer
</button>
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${tab() === "key" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setTab("key")}
>
Key
</button>
</div>
<Show when={tab() === "nip07"}>
<div class="space-y-3">
<p class="text-xs text-gray-500">
{window.nostr
? "Detected browser extension signer."
: "No extension detected in this browser."}
</p>
<Show when={tab() === "nip07"}>
<button
type="button"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium disabled:opacity-50"
disabled={!window.nostr || loading()}
onClick={loginWithNip07}
>
{loading() ? "Connecting..." : "Continue with extension"}
{loading() ? "Connecting..." : <>Continue with extension <svg class="inline-block w-4 h-4 ml-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg></>}
</button>
</Show>
<Show when={tab() === "nip46"}>
<button
type="button"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium"
onClick={enterSignerScreen}
>
Continue with signer <svg class="inline-block w-4 h-4 ml-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</Show>
<Show when={tab() === "key"}>
<button
type="button"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium"
onClick={() => { setError(""); setScreen("key") }}
>
Continue with key <svg class="inline-block w-4 h-4 ml-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</Show>
</div>
</Show>
<Show when={screen() === "nip46"}>
<button
type="button"
class="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-900"
onClick={() => { setError(""); setScreen("tabs") }}
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Back
</button>
<h2 class="mt-3 text-lg font-semibold text-gray-900">Log in with signer</h2>
<div class="mt-4 space-y-4">
<div class="flex gap-2 border border-gray-200 rounded-lg p-1">
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${signerTab() === "qr" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setSignerTab("qr")}
>
Use QR Code
</button>
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${signerTab() === "paste" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setSignerTab("paste")}
>
Paste Link
</button>
</div>
</Show>
<Show when={tab() === "nip46"}>
<div class="space-y-3">
<Show when={signerTab() === "qr"}>
<Show when={qrDataUrl()} fallback={
<div class="flex items-center justify-center py-8 text-sm text-gray-400">
{loading() ? "Generating..." : "Loading QR code..."}
</div>
}>
<img src={qrDataUrl()} alt="Nostrconnect QR code" class="mx-auto rounded-lg" />
</Show>
<div class="flex rounded-lg border border-gray-300">
<input
type="text"
readOnly
value={nostrConnectUri()}
class="min-w-0 flex-1 rounded-l-lg border-0 px-3 py-2 text-xs text-gray-500 bg-transparent focus:outline-none"
/>
<button
type="button"
class="flex items-center px-3 text-gray-400 hover:text-gray-700"
onClick={copyUri}
title="Copy link"
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
</div>
</Show>
<Show when={signerTab() === "paste"}>
<div class="flex rounded-lg border border-gray-300">
<input
value={bunkerUrl()}
onInput={(e) => setBunkerUrl(e.currentTarget.value)}
placeholder="bunker://..."
class="min-w-0 flex-1 rounded-l-lg border-0 px-3 py-2 text-sm bg-transparent focus:outline-none"
/>
<button
type="button"
class="flex items-center px-3 text-gray-400 hover:text-gray-700"
onClick={openScanner}
title="Scan QR code"
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
</button>
</div>
<button
type="button"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium disabled:opacity-50"
disabled={loading()}
onClick={startNostrConnect}
disabled={loading() || !bunkerUrl().trim()}
onClick={loginWithBunker}
>
Generate nostrconnect QR
Connect to Signer
</button>
</Show>
</div>
</Show>
<Show when={nostrConnectUri()}>
<a
href={nostrConnectUri()}
class="block rounded-lg border border-gray-200 p-3 text-xs text-gray-600 break-all"
target="_parent"
>
{nostrConnectUri()}
</a>
</Show>
<input
value={bunkerUrl()}
onInput={(e) => setBunkerUrl(e.currentTarget.value)}
placeholder="bunker://... or nostrconnect://..."
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
/>
<div class="flex gap-2">
<button
type="button"
class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium disabled:opacity-50"
disabled={loading() || !bunkerUrl().trim()}
onClick={loginWithBunker}
>
Connect bunker URL
</button>
<button
type="button"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm"
onClick={toggleScanner}
>
{showScanner() ? "Stop" : "Scan"}
</button>
</div>
<Show when={showScanner()}>
<video ref={scannerVideo} class="w-full rounded-lg border border-gray-200" />
</Show>
<Show when={screen() === "key"}>
<button
type="button"
class="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-900"
onClick={() => { setError(""); setScreen("tabs") }}
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Back
</button>
<h2 class="mt-3 text-lg font-semibold text-gray-900">Log in with key</h2>
<div class="mt-4 space-y-4">
<div class="flex gap-2 border border-gray-200 rounded-lg p-1">
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${keyTab() === "plaintext" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setKeyTab("plaintext")}
>
Plaintext
</button>
<button
type="button"
class={`flex-1 rounded-md px-3 py-2 text-sm ${keyTab() === "encrypted" ? "bg-gray-900 text-white" : "text-gray-700"}`}
onClick={() => setKeyTab("encrypted")}
>
Encrypted
</button>
</div>
</Show>
<Show when={tab() === "key"}>
<form
class="space-y-3"
onSubmit={(e) => {
@@ -316,42 +411,43 @@ export default function Login() {
void loginWithKeyMaterial()
}}
>
<input
value={nsecValue()}
onInput={(e) => setNsecValue(e.currentTarget.value)}
placeholder="nsec1..."
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
/>
<input
value={ncryptsecValue()}
onInput={(e) => setNcryptsecValue(e.currentTarget.value)}
placeholder="ncryptsec1..."
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
/>
<input
type="password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
placeholder="Password (for ncryptsec)"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
/>
<Show when={keyTab() === "plaintext"}>
<input
value={nsecValue()}
onInput={(e) => setNsecValue(e.currentTarget.value)}
placeholder="nsec1..."
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
/>
</Show>
<Show when={keyTab() === "encrypted"}>
<input
value={ncryptsecValue()}
onInput={(e) => setNcryptsecValue(e.currentTarget.value)}
placeholder="ncryptsec1..."
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
/>
<input
type="password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
placeholder="Password"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
/>
</Show>
<button
type="submit"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium disabled:opacity-50"
disabled={loading()}
>
Continue with key
Log in
</button>
</form>
</Show>
</div>
</Show>
<Show when={error()}>
<p class="text-xs text-red-600">{error()}</p>
</Show>
</div>
<p class="mt-6 text-xs text-gray-500">
By continuing, you agree to use NIP-98 authentication for secure access.
</p>
<Show when={error()}>
<p class="mt-4 text-xs text-red-600">{error()}</p>
</Show>
</div>
</div>
</div>
@@ -359,6 +455,20 @@ export default function Login() {
Having trouble? Make sure your signer is unlocked and connected.
</p>
</div>
<Show when={showScanner()}>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-6" onClick={closeScanner}>
<div class="relative w-full max-w-sm rounded-2xl bg-white p-4 shadow-xl" onClick={(e) => e.stopPropagation()}>
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-900">Scan QR Code</h3>
<button type="button" class="text-gray-400 hover:text-gray-700" onClick={closeScanner}>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<video ref={scannerVideo} class="w-full rounded-lg" />
</div>
</div>
</Show>
</div>
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
export default function AdminRelays() {
return (
<div class="max-w-5xl mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-2xl font-bold text-gray-900 mb-6">All Relays</h1>
<input
type="search"
+1 -1
View File
@@ -1,6 +1,6 @@
export default function AdminTenants() {
return (
<div class="max-w-5xl mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Tenants</h1>
<input
type="search"