Compare commits

...

No commits in common. "gh-pages" and "master" have entirely different histories.

74 changed files with 8256 additions and 270 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

55
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Build & Deploy
on:
push:
branches:
- master
pull_request:
branches:
- master
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Upload artifact
if: success() && github.ref == 'refs/heads/master'
uses: actions/upload-pages-artifact@v3
with:
path: ./dist
deploy:
if: github.ref == 'refs/heads/master'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
node_modules
dist
.vite
*.local
# Environment files
.env
.env.*
# IDE
.idea
.vscode
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

View File

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. http://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// http://www.foo.tld/?p=/one/two&q=a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set segmentCount to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?p=/one/two&q=a=b~and~c=d#qwe
// Otherwise, leave segmentCount as 0.
var l = window.location;
var segmentCount = l.pathname.indexOf('/directory.btcpayserver.org/') === -1 ? 0 : 1;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice(segmentCount).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 BTCPay Server
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

115
README.md Normal file
View File

@ -0,0 +1,115 @@
# BTCPay Server Directory
Merchants, projects, and organizations accepting Bitcoin with [BTCPay Server](https://btcpayserver.org/).
Live at **[directory.btcpayserver.org](https://directory.btcpayserver.org/)**
## Development
```bash
npm install
npm run dev # Start dev server
npm run build # Type-check + production build
npm run preview # Preview the production build locally
```
Requires Node 20+.
## Adding a new entry
Entries live in `src/data/merchants.json`. Each entry follows this structure:
```jsonc
{
"name": "Store Name",
"url": "https://example.com",
"description": "Short description (max 250 chars).",
"type": "merchants", // required — see valid types below
"subType": "electronics", // required for type "merchants"
"country": "US", // optional — for type "hosted-btcpay" only
"twitter": "@handle", // optional
"github": "https://github.com/org", // optional
"onionUrl": "http://...onion" // optional
}
```
### Valid `type` values
| Type | Description |
|------|-------------|
| `merchants` | Stores and services (requires `subType`) |
| `apps` | Applications built on BTCPay Server |
| `hosted-btcpay` | BTCPay Server hosting providers (supports optional `country`) |
| `non-profits` | Non-profit organizations |
### Merchant subtypes
`3d-printing`, `adult`, `appliances-furniture`, `art`, `books`, `cryptocurrency-paraphernalia`, `domains-hosting-vpns`, `education`, `electronics`, `fashion`, `food`, `gambling`, `gift-cards`, `health-household`, `holiday-travel`, `jewelry`, `payment-services`, `pets`, `services`, `software-video-games`, `sports`, `tools`
### Country codes (for `hosted-btcpay`)
Use [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes (e.g. `US`, `BR`, `IT`) or `GLOBAL` for providers without a specific region. The UI converts these to flag emojis automatically.
### Public submissions
End users submit new entries via the **Submit Entry** button on the site, which opens a pre-filled GitHub Issue. Maintainers review the issue and manually add the entry to `merchants.json`.
## Link checker
A maintenance script to detect dead merchant URLs and optionally remove them:
```bash
npm run check-links
```
By default it runs interactively — checks all URLs, prints a report, and prompts before removing dead entries.
### Flags
| Flag | Description |
|------|-------------|
| `--no-interactive` | Skip the removal prompt (report only) |
| `--verbose`, `-v` | Also list alive entries in the report |
| `--timeout=N` | Request timeout in ms (default: `15000`) |
| `--concurrency=N` | Parallel requests (default: `5`) |
### Examples
```bash
# Report only, no changes
npm run check-links -- --no-interactive
# Verbose report with custom timeout
npm run check-links -- --verbose --timeout=20000
# Faster scan with more concurrency
npm run check-links -- --concurrency=10
```
### How classification works
Each URL is tried up to 3 times (with 3s delay between retries):
- **Dead** (auto-removable): DNS lookup failed, connection refused, request timeout after all retries, HTTP 404/410
- **Warning** (manual review): SSL certificate errors, Cloudflare blocks (HTTP 403, 5xx, 520-530), connect timeouts (TCP-level bot blocking)
- **Alive**: HTTP 2xx, redirects resolving to 2xx
## Project structure
```
src/
data/
merchants.json # All directory entries
categories.ts # Types, subtypes, country helpers, Merchant interface
supporters.ts # Foundation supporter logos
components/
DirectoryFilters.tsx # Sidebar category + subtype + country filters
MerchantCard.tsx # Individual entry card
SubmitForm.tsx # "Submit a Store" dialog form
Navbar.tsx # Top navigation bar
Footer.tsx # Site footer
pages/
Directory.tsx # Main directory page (filtering, search, infinite scroll)
scripts/
check-links.ts # Dead link checker script
```

View File

@ -1,25 +0,0 @@
{
"files": {
"main.css": "/static/css/main.4b42230b.chunk.css",
"main.js": "/static/js/main.c1aae364.chunk.js",
"main.js.map": "/static/js/main.c1aae364.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.b29dcf49.js",
"runtime-main.js.map": "/static/js/runtime-main.b29dcf49.js.map",
"static/js/2.704d4dc0.chunk.js": "/static/js/2.704d4dc0.chunk.js",
"static/js/2.704d4dc0.chunk.js.map": "/static/js/2.704d4dc0.chunk.js.map",
"index.html": "/index.html",
"static/css/main.4b42230b.chunk.css.map": "/static/css/main.4b42230b.chunk.css.map",
"static/js/2.704d4dc0.chunk.js.LICENSE.txt": "/static/js/2.704d4dc0.chunk.js.LICENSE.txt",
"static/media/btcpay-directory-logo-white.d809a188.png": "/static/media/btcpay-directory-logo-white.d809a188.png",
"static/media/btcpay-directory-logo.6a437bbe.svg": "/static/media/btcpay-directory-logo.6a437bbe.svg",
"static/media/icon-sprite.42be0321.svg": "/static/media/icon-sprite.42be0321.svg",
"static/media/moonFilled.fe8ddf47.svg": "/static/media/moonFilled.fe8ddf47.svg",
"static/media/sunFilled2.aaa47476.svg": "/static/media/sunFilled2.aaa47476.svg"
},
"entrypoints": [
"static/js/runtime-main.b29dcf49.js",
"static/js/2.704d4dc0.chunk.js",
"static/css/main.4b42230b.chunk.css",
"static/js/main.c1aae364.chunk.js"
]
}

View File

@ -1,27 +0,0 @@
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script checks to see if a redirect is present in the query string
// and converts it back into the correct url and adds it to the
// browser's history using window.history.replaceState(...),
// which won't cause the browser to attempt to load the new url.
// When the single page app is loaded further down in this file,
// the correct url will be waiting in the browser's history for
// the single page app to route accordingly.
(function (l) {
if (l.search) {
var q = {}
l.search.slice(1).split('&').forEach(function (v) {
var a = v.split('=')
q[a[0]] = a.slice(1).join('=').replace(/~and~/g, '&')
})
if (q.p !== undefined) {
window.history.replaceState(null, null,
l.pathname.slice(0, -1) + (q.p || '') +
(q.q ? ('?' + q.q) : '') +
l.hash
)
}
}
}(window.location))

View File

@ -1 +1,31 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>BTCPay Server Directory</title><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="description" content="Projects and organizations using BTCPay Server"/><link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800&display=swap" rel="stylesheet"><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"><link rel="manifest" href="/site.webmanifest"><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#51b13e"><meta name="msapplication-TileColor" content="#0f3b21"><meta name="theme-color" content="#ffffff"><script src="/gh-pages-spa.js"></script><link href="/static/css/main.4b42230b.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,i,l=r[0],a=r[1],c=r[2],f=0,s=[];f<l.length;f++)i=l[f],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,c||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var a=t[l];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/";var l=this["webpackJsonpbtcpayserver-directory"]=this["webpackJsonpbtcpayserver-directory"]||[],a=l.push.bind(l);l.push=r,l=l.slice();for(var c=0;c<l.length;c++)r(l[c]);var p=a;t()}([])</script><script src="/static/js/2.704d4dc0.chunk.js"></script><script src="/static/js/main.c1aae364.chunk.js"></script></body></html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self'; manifest-src 'self'; connect-src 'self' ws:; base-uri 'self'; form-action 'none';" />
<title>BTCPay Server Directory</title>
<meta name="description" content="The definitive list of merchants, creators, and organizations empowering the circular economy with BTCPay Server." />
<meta property="og:title" content="BTCPay Server Directory" />
<meta property="og:description" content="Merchants, projects and organizations using BTCPay Server" />
<meta property="og:image" content="https://directory.btcpayserver.org/opengraph.png" />
<meta property="og:url" content="https://directory.btcpayserver.org" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://directory.btcpayserver.org/opengraph.png" />
<meta name="theme-color" content="#51b13e" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#51b13e" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6
netlify.toml Normal file
View File

@ -0,0 +1,6 @@
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"

3582
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"private": true,
"name": "btcpayserver-directory",
"version": "2.0.0",
"license": "MIT",
"description": "BTCPay Server Directory — Merchants, projects and organizations using BTCPay Server",
"homepage": "https://directory.btcpayserver.org/",
"repository": "btcpayserver/directory.btcpayserver.org",
"bugs": "https://github.com/btcpayserver/directory.btcpayserver.org/issues",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"check-links": "tsx scripts/check-links.ts"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.0.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"tsx": "^4.21.0",
"tw-animate-css": "^1.2.5",
"typescript": "~5.7.0",
"vite": "^6.4.2"
}
}

View File

@ -0,0 +1,4 @@
Contact: https://github.com/btcpayserver/btcpayserver/security/policy
Expires: 2027-02-12T00:00:00.000Z
Preferred-Languages: en
Policy: https://github.com/btcpayserver/btcpayserver/security/policy

4
public/404-redirect.js Normal file
View File

@ -0,0 +1,4 @@
"use strict";
// Redirect all 404s to the root for SPA support on static hosting.
window.location.replace(window.location.origin);

10
public/404.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self';" />
<title>BTCPay Server Directory</title>
<script src="/404-redirect.js"></script>
</head>
<body></body>
</html>

View File

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1007 B

After

Width:  |  Height:  |  Size: 1007 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

11
public/frame-bust.js Normal file
View File

@ -0,0 +1,11 @@
"use strict";
// Fallback clickjacking mitigation for hosts where response headers
// cannot be enforced. Header-based frame-ancestors/XFO is still preferred.
if (window.top && window.top !== window.self) {
try {
window.top.location = window.self.location.href;
} catch {
window.self.location = window.self.location.href;
}
}

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/opengraph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Allow: /

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

426
scripts/check-links.ts Normal file
View File

@ -0,0 +1,426 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createInterface } from "node:readline/promises";
// ── Types ──────────────────────────────────────────────────────────
interface Merchant {
name: string;
url: string;
description: string;
type: string;
subType?: string;
twitter?: string;
github?: string;
onionUrl?: string;
}
type LinkStatus = "alive" | "warning" | "dead";
interface CheckResult {
merchant: Merchant;
status: LinkStatus;
statusCode?: number;
reason: string;
finalUrl?: string;
responseTime: number;
}
// ── ANSI Colors ────────────────────────────────────────────────────
const c = {
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
};
// ── CLI Args ───────────────────────────────────────────────────────
const args = process.argv.slice(2);
const verbose = args.includes("--verbose") || args.includes("-v");
const noInteractive = args.includes("--no-interactive");
const timeoutArg = args.find((a) => a.startsWith("--timeout="));
const TIMEOUT = timeoutArg ? parseInt(timeoutArg.split("=")[1], 10) : 15_000;
const concurrencyArg = args.find((a) => a.startsWith("--concurrency="));
const CONCURRENCY = concurrencyArg
? parseInt(concurrencyArg.split("=")[1], 10)
: 5;
const MAX_RETRIES = 2;
const RETRY_DELAY = 3000;
// ── Paths ──────────────────────────────────────────────────────────
const __dirname = dirname(fileURLToPath(import.meta.url));
const MERCHANTS_PATH = resolve(__dirname, "../src/data/merchants.json");
// ── Concurrency Pool ───────────────────────────────────────────────
function createPool(concurrency: number) {
let active = 0;
const queue: (() => void)[] = [];
async function run<T>(fn: () => Promise<T>): Promise<T> {
if (active >= concurrency) {
await new Promise<void>((resolve) => queue.push(resolve));
}
active++;
try {
return await fn();
} finally {
active--;
if (queue.length > 0) queue.shift()!();
}
}
return { run };
}
// ── Helpers ────────────────────────────────────────────────────────
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
// Mimic a real browser to avoid bot-detection false positives
const USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
// ── Fetch with timeout ─────────────────────────────────────────────
interface FetchResult {
statusCode?: number;
error?: string;
finalUrl?: string;
responseTime: number;
}
async function fetchUrl(
url: string,
method: "HEAD" | "GET" = "GET"
): Promise<FetchResult> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT);
const start = Date.now();
try {
const response = await fetch(url, {
method,
signal: controller.signal,
headers: {
"User-Agent": USER_AGENT,
Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
},
redirect: "follow",
});
clearTimeout(timer);
return {
statusCode: response.status,
finalUrl: response.url !== url ? response.url : undefined,
responseTime: Date.now() - start,
};
} catch (err: unknown) {
clearTimeout(timer);
const responseTime = Date.now() - start;
const error = err instanceof Error ? err : new Error(String(err));
// Node's native fetch wraps errors in TypeError with cause — check both
const msg = error.message;
const causeMsg =
error.cause instanceof Error ? error.cause.message : String(error.cause ?? "");
const causeCode =
error.cause instanceof Error ? (error.cause as NodeJS.ErrnoException).code ?? "" : "";
const fullMsg = `${msg} ${causeMsg} ${causeCode}`;
if (error.name === "AbortError") {
return { error: `Timeout (${TIMEOUT / 1000}s)`, responseTime };
}
if (fullMsg.includes("ENOTFOUND") || fullMsg.includes("getaddrinfo")) {
return { error: "DNS lookup failed", responseTime };
}
if (fullMsg.includes("ECONNREFUSED")) {
return { error: "Connection refused", responseTime };
}
if (fullMsg.includes("ECONNRESET")) {
return { error: "Connection reset", responseTime };
}
if (
fullMsg.includes("CERT_") ||
fullMsg.includes("SSL") ||
fullMsg.includes("certificate")
) {
return { error: "SSL certificate error", responseTime };
}
if (
fullMsg.includes("UND_ERR_CONNECT_TIMEOUT") ||
fullMsg.includes("Connect Timeout")
) {
return { error: "Connect timeout", responseTime };
}
return { error: error.message, responseTime };
}
}
// ── Classify result ────────────────────────────────────────────────
function classify(merchant: Merchant, result: FetchResult): CheckResult {
const base = {
merchant,
finalUrl: result.finalUrl,
responseTime: result.responseTime,
statusCode: result.statusCode,
};
if (result.error) {
// SSL errors → server exists but cert is broken → warning (no retry)
if (result.error === "SSL certificate error") {
return { ...base, status: "warning", reason: result.error };
}
// Connect timeout → server's IP resolves but TCP/TLS connect is blocked
// (common anti-bot measure, e.g. Cloudflare) → warning (no retry)
if (result.error === "Connect timeout") {
return { ...base, status: "warning", reason: result.error };
}
// Everything else (DNS failure, abort timeout, connection refused, reset)
// → dead. The retry loop in checkMerchant will retry these; if they
// persist after all attempts, the site is genuinely unreachable.
return { ...base, status: "dead", reason: result.error };
}
const code = result.statusCode!;
// 2xx = alive
if (code >= 200 && code < 300) {
return { ...base, status: "alive", reason: "OK" };
}
// 3xx that somehow didn't redirect (shouldn't happen with redirect: follow)
if (code >= 300 && code < 400) {
return { ...base, status: "alive", reason: `Redirect (${code})` };
}
// Clearly dead: 404 Not Found, 410 Gone
if (code === 404 || code === 410) {
return { ...base, status: "dead", reason: `HTTP ${code}` };
}
// Everything else is a warning — could be bot protection, rate limiting,
// temporary issues, Cloudflare challenges (403, 429, 5xx, 520-530, etc.)
return { ...base, status: "warning", reason: `HTTP ${code}` };
}
// ── Check a single merchant (with retries) ─────────────────────────
async function checkMerchant(merchant: Merchant): Promise<CheckResult> {
let lastResult: CheckResult | null = null;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
if (attempt > 0) await sleep(RETRY_DELAY);
const result = await fetchUrl(merchant.url, "GET");
lastResult = classify(merchant, result);
// If alive or just a warning, stop retrying
if (lastResult.status !== "dead") {
return lastResult;
}
}
return lastResult!;
}
// ── Progress display ───────────────────────────────────────────────
function printProgress(current: number, total: number, name: string) {
const padded = `[${current}/${total}]`.padEnd(10);
const truncated = name.length > 40 ? name.slice(0, 37) + "..." : name;
process.stdout.write(
`\r${c.dim("Checking...")} ${c.cyan(padded)} ${truncated}${"".padEnd(20)}`
);
}
// ── Report printing ────────────────────────────────────────────────
function printReport(
results: CheckResult[],
skipped: number,
durationMs: number
) {
const dead = results.filter((r) => r.status === "dead");
const warnings = results.filter((r) => r.status === "warning");
const alive = results.filter((r) => r.status === "alive");
// Clear progress line
process.stdout.write("\r" + "".padEnd(80) + "\r");
console.log("");
console.log(c.bold("=== BTCPay Server Directory Link Check ==="));
console.log(
`Checked ${results.length} URLs${skipped ? ` (skipped ${skipped} .onion)` : ""}`
);
console.log(`Duration: ${(durationMs / 1000).toFixed(1)}s`);
console.log("");
if (dead.length > 0) {
console.log(c.red(c.bold(`--- DEAD (${dead.length}) ---`)));
console.log(c.dim(" Unreachable after 3 attempts — DNS gone, refused, timeout, or HTTP 404/410"));
console.log("");
for (const r of dead) {
console.log(` ${c.red("❌")} ${c.bold(r.merchant.name)}`);
console.log(` ${c.dim(r.merchant.url)}`);
console.log(
` ${r.reason}${r.statusCode ? ` [${r.statusCode}]` : ""} ${c.dim(`(${r.responseTime}ms)`)}`
);
console.log("");
}
}
if (warnings.length > 0) {
console.log(c.yellow(c.bold(`--- WARNINGS (${warnings.length}) ---`)));
console.log(c.dim(" May be alive but blocking bots — verify manually"));
console.log("");
for (const r of warnings) {
console.log(` ${c.yellow("⚠️ ")} ${c.bold(r.merchant.name)}`);
console.log(` ${c.dim(r.merchant.url)}`);
let detail = r.reason;
if (r.statusCode) detail += ` [${r.statusCode}]`;
if (r.finalUrl) detail += `${r.finalUrl}`;
console.log(` ${detail} ${c.dim(`(${r.responseTime}ms)`)}`);
console.log("");
}
}
if (verbose && alive.length > 0) {
console.log(c.green(c.bold(`--- ALIVE (${alive.length}) ---`)));
for (const r of alive) {
let detail = ` ${c.green("✅")} ${r.merchant.name}`;
if (r.finalUrl) detail += ` ${c.dim(`${r.finalUrl}`)}`;
detail += ` ${c.dim(`(${r.responseTime}ms)`)}`;
console.log(detail);
}
console.log("");
} else if (alive.length > 0) {
console.log(
c.green(`--- ALIVE (${alive.length}) --- `) +
c.dim("(use --verbose to list)")
);
console.log("");
}
// Summary line
console.log(
c.bold("Summary: ") +
c.green(`${alive.length} alive`) +
", " +
c.yellow(`${warnings.length} warnings`) +
", " +
c.red(`${dead.length} dead`)
);
}
// ── Interactive removal (bulk) ─────────────────────────────────────
async function promptRemoval(deadResults: CheckResult[]): Promise<boolean> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
console.log("");
console.log(
c.bold(`${deadResults.length} confirmed dead (unreachable after 3 attempts):`)
);
for (const r of deadResults) {
console.log(` - ${r.merchant.name} ${c.dim(`(${r.reason})`)}`);
}
console.log("");
const answer = await rl.question(
c.yellow(
`Remove these ${deadResults.length} entries from merchants.json? (y/N) `
)
);
rl.close();
return answer.trim().toLowerCase() === "y";
}
// ── JSON rewrite ───────────────────────────────────────────────────
function rewriteMerchants(merchants: Merchant[]) {
const json = JSON.stringify(merchants, null, 2) + "\n";
writeFileSync(MERCHANTS_PATH, json, "utf8");
}
// ── Main ───────────────────────────────────────────────────────────
async function main() {
const raw = readFileSync(MERCHANTS_PATH, "utf8");
const merchants: Merchant[] = JSON.parse(raw);
// Skip .onion URLs
const checkable = merchants.filter((m) => !m.url.includes(".onion"));
const skipped = merchants.length - checkable.length;
console.log("");
console.log(
c.bold(`BTCPay Directory Link Checker`) +
c.dim(
`${checkable.length} URLs, concurrency ${CONCURRENCY}, timeout ${TIMEOUT / 1000}s, ${MAX_RETRIES} retries`
)
);
console.log("");
const pool = createPool(CONCURRENCY);
let completed = 0;
const startTime = Date.now();
const results = await Promise.all(
checkable.map((merchant) =>
pool.run(async () => {
const result = await checkMerchant(merchant);
completed++;
printProgress(completed, checkable.length, merchant.name);
return result;
})
)
);
const duration = Date.now() - startTime;
// Sort: dead first, then warnings, then alive
const order: Record<LinkStatus, number> = { dead: 0, warning: 1, alive: 2 };
results.sort((a, b) => order[a.status] - order[b.status]);
printReport(results, skipped, duration);
const dead = results.filter((r) => r.status === "dead");
if (dead.length > 0 && !noInteractive) {
const confirmed = await promptRemoval(dead);
if (confirmed) {
const deadUrls = new Set(dead.map((r) => r.merchant.url));
const survivors = merchants.filter((m) => !deadUrls.has(m.url));
rewriteMerchants(survivors);
console.log(
c.green(
`\n✅ Removed ${dead.length} entries. ${survivors.length} remaining.`
)
);
} else {
console.log(c.dim("\nNo changes made."));
}
} else if (dead.length > 0 && noInteractive) {
console.log(
c.dim(
`\n${dead.length} dead link(s) found. Run without --no-interactive to remove.`
)
);
}
process.exit(dead.length > 0 ? 1 : 0);
}
main().catch((err) => {
console.error(c.red("Script error:"), err);
process.exit(2);
});

13
scripts/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["./**/*.ts"]
}

12
src/App.tsx Normal file
View File

@ -0,0 +1,12 @@
import { ThemeProvider } from "@/components/ThemeProvider";
import DirectoryPage from "@/pages/Directory";
function App() {
return (
<ThemeProvider defaultTheme="system" storageKey="btcpay-directory-theme">
<DirectoryPage />
</ThemeProvider>
);
}
export default App;

View File

@ -0,0 +1,108 @@
import { mainTypes, merchantSubTypes, subTypeLabels, hostedBtcpayCountries, countryFlag } from "@/data/categories";
import { cn } from "@/lib/utils";
interface DirectoryFiltersProps {
selectedType: string;
setSelectedType: (type: string) => void;
selectedSubType: string | null;
setSelectedSubType: (subType: string | null) => void;
onFilterChange?: () => void;
className?: string;
}
export default function DirectoryFilters({
selectedType,
setSelectedType,
selectedSubType,
setSelectedSubType,
onFilterChange,
className,
}: DirectoryFiltersProps) {
const handleTypeClick = (type: string) => {
setSelectedType(type);
setSelectedSubType(null);
onFilterChange?.();
};
const handleSubTypeClick = (subType: string) => {
setSelectedSubType(selectedSubType === subType ? null : subType);
onFilterChange?.();
};
return (
<div className={cn("space-y-6", className)}>
<div className="bg-card/60 backdrop-blur-xl border border-border/40 rounded-3xl p-6 shadow-sm">
<h3 className="font-display font-bold text-lg mb-6 px-2">Categories</h3>
<div className="space-y-1">
{mainTypes.map((type) => (
<button
key={type}
onClick={() => handleTypeClick(type)}
className={cn(
"w-full text-left px-4 py-3 rounded-2xl text-sm font-medium transition-all duration-200 flex items-center justify-between group",
selectedType === type
? "bg-primary text-primary-foreground shadow-md shadow-primary/20 scale-[1.02]"
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
)}
>
{type}
{selectedType === type && (
<span className="w-1.5 h-1.5 rounded-full bg-white animate-pulse" />
)}
</button>
))}
</div>
{/* Merchant subtypes - shown when Merchants is selected */}
{selectedType === "Merchants" && (
<div className="mt-4 pt-4 border-t border-border/40">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-2 mb-3">
Subcategories
</p>
<div className="space-y-0.5 max-h-[40vh] sm:max-h-64 overflow-y-auto">
{merchantSubTypes.map((subType) => (
<button
key={subType}
onClick={() => handleSubTypeClick(subType)}
className={cn(
"w-full text-left px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
selectedSubType === subType
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted/30 hover:text-foreground"
)}
>
{subTypeLabels[subType] || subType}
</button>
))}
</div>
</div>
)}
{/* Country filter - shown when Hosted BTCPay is selected */}
{selectedType === "Hosted BTCPay" && (
<div className="mt-4 pt-4 border-t border-border/40">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-2 mb-3">
Country
</p>
<div className="space-y-0.5 max-h-[40vh] sm:max-h-64 overflow-y-auto">
{Object.entries(hostedBtcpayCountries).map(([code, name]) => (
<button
key={code}
onClick={() => handleSubTypeClick(code)}
className={cn(
"w-full text-left px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200",
selectedSubType === code
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted/30 hover:text-foreground"
)}
>
{countryFlag(code)} {name}
</button>
))}
</div>
</div>
)}
</div>
</div>
);
}

62
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,62 @@
import { Github, Twitter } from "lucide-react";
import SupporterSprite from "@/components/SupporterSprite";
import { supporters } from "@/data/supporters";
import { safeUrl } from "@/lib/url";
export default function Footer() {
return (
<footer className="border-t mt-auto">
{/* Supporters */}
<div>
<div className="container mx-auto px-4 sm:px-6 py-8 sm:py-10">
<SupporterSprite />
<div className="grid grid-cols-2 sm:grid-cols-5 lg:grid-cols-9 gap-3 sm:gap-6 items-center justify-items-center">
{supporters.map((s) => (
<a
key={s.svgId}
href={safeUrl(s.url)}
target="_blank"
rel="noreferrer"
title={s.name}
className="flex items-center justify-center w-full h-8 sm:h-10 opacity-60 hover:opacity-100 transition-opacity duration-200"
>
<svg
role="img"
width={s.width}
height={s.height}
className="max-h-6 sm:max-h-8 w-auto max-w-full shrink-0"
style={s.fillCurrentColor ? { fill: "currentColor" } : undefined}
>
<use href={`#${s.svgId}`} />
</svg>
</a>
))}
</div>
</div>
</div>
{/* Bottom bar */}
<div className="border-t border-border/40">
<div className="container mx-auto px-4 sm:px-6 py-4 sm:py-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div className="flex items-center gap-3 min-w-0">
<a href="/" className="shrink-0">
<img src="/btcpay-directory-logo.svg" alt="BTCPay Directory" className="h-5 sm:h-6 dark:hidden" />
<img src="/btcpay-directory-logo-white.svg" alt="BTCPay Directory" className="h-5 sm:h-6 hidden dark:block" />
</a>
<span className="text-[11px] sm:text-xs text-muted-foreground leading-tight">
Community-maintained listing. Inclusion does not imply endorsement.
</span>
</div>
<div className="flex items-center gap-3 shrink-0">
<a href="https://github.com/btcpayserver" target="_blank" rel="noreferrer" className="text-muted-foreground hover:text-foreground active:text-foreground transition-colors p-2 -m-2">
<Github className="h-4 w-4" />
</a>
<a href="https://x.com/BtcpayServer" target="_blank" rel="noreferrer" className="text-muted-foreground hover:text-foreground active:text-foreground transition-colors p-2 -m-2">
<Twitter className="h-4 w-4" />
</a>
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,73 @@
import type { Merchant } from "@/data/categories";
import { Badge } from "@/components/ui/badge";
import { ExternalLink } from "lucide-react";
import { subTypeLabels, countryFlag, hostedBtcpayCountries } from "@/data/categories";
import { safeUrl } from "@/lib/url";
interface MerchantCardProps {
merchant: Merchant;
}
export default function MerchantCard({ merchant }: MerchantCardProps) {
const displayCategory = merchant.subType
? subTypeLabels[merchant.subType] || merchant.subType
: merchant.type.charAt(0).toUpperCase() + merchant.type.slice(1);
return (
<div className="group relative flex flex-col h-full bg-card/60 hover:bg-card/80 backdrop-blur-md border border-border/40 hover:border-primary/20 rounded-2xl sm:rounded-3xl p-4 sm:p-6 transition-all duration-300 hover:shadow-xl hover:shadow-primary/5 hover:-translate-y-1">
{/* Category Pill */}
<div className="mb-3 sm:mb-4 flex justify-between items-start">
<Badge variant="secondary" className="rounded-full px-2.5 sm:px-3 py-0.5 sm:py-1 text-xs font-medium bg-muted/60 border border-border/40 backdrop-blur-sm group-hover:bg-primary/10 group-hover:text-primary transition-colors">
{displayCategory}
</Badge>
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-muted/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ExternalLink className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-primary" />
</div>
</div>
{/* Content */}
<div className="flex-1">
<h3 className="font-display font-bold text-lg sm:text-xl mb-1.5 sm:mb-2 group-hover:text-primary transition-colors duration-300">
{merchant.name}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed line-clamp-3 mb-4 sm:mb-6">
{merchant.description}
</p>
</div>
{/* Tags - show type + country flag + optional social indicators */}
<div className="flex flex-wrap gap-1.5 sm:gap-2 mb-4 sm:mb-6">
<span className="text-[10px] uppercase tracking-wider font-semibold text-muted-foreground bg-muted/50 px-2 py-0.5 sm:py-1 rounded-md border border-border/40">
{merchant.type}
</span>
{merchant.country && (
<span className="text-[10px] tracking-wider font-semibold text-muted-foreground bg-muted/50 px-2 py-0.5 sm:py-1 rounded-md border border-border/40">
{countryFlag(merchant.country)} {hostedBtcpayCountries[merchant.country] || merchant.country}
</span>
)}
{merchant.twitter && (
<span className="inline-flex items-center text-muted-foreground bg-muted/50 px-2 py-0.5 sm:py-1 rounded-md border border-border/40">
<svg viewBox="0 0 24 24" fill="currentColor" className="w-3 h-3">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.748l7.73-8.835L1.254 2.25H8.08l4.253 5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
</span>
)}
{merchant.onionUrl && (
<span className="text-[10px] uppercase tracking-wider font-semibold text-muted-foreground bg-muted/50 px-2 py-0.5 sm:py-1 rounded-md border border-border/40">
Tor
</span>
)}
</div>
{/* Action */}
<a
href={safeUrl(merchant.url)}
target="_blank"
rel="noreferrer"
className="absolute inset-0 z-10 focus:outline-none focus:ring-2 focus:ring-primary/50 rounded-2xl sm:rounded-3xl"
aria-label={`Visit ${merchant.name}`}
/>
</div>
);
}

55
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,55 @@
import { Button } from "@/components/ui/button";
import { Search } from "lucide-react";
import { ThemeToggle } from "@/components/ThemeToggle";
interface NavbarProps {
searchQuery: string;
setSearchQuery: (query: string) => void;
onSubmitClick: () => void;
}
export default function Navbar({ searchQuery, setSearchQuery, onSubmitClick }: NavbarProps) {
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-xl border-b border-border/40 supports-[backdrop-filter]:bg-background/80 transition-all duration-300">
<div className="container mx-auto px-4 sm:px-6 h-16 sm:h-20 flex items-center justify-between gap-3">
<a href="/" className="flex items-center group shrink-0">
<img
src="/btcpay-directory-logo.svg"
alt="BTCPay Directory"
className="h-8 sm:h-10 dark:hidden transition-transform duration-500 group-hover:scale-105"
/>
<img
src="/btcpay-directory-logo-white.svg"
alt="BTCPay Directory"
className="h-8 sm:h-10 hidden dark:block transition-transform duration-500 group-hover:scale-105"
/>
</a>
<div className="flex-1 max-w-md mx-4 sm:mx-8 hidden md:block">
<div className="relative group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors duration-300" />
<input
type="text"
placeholder="Search merchants, categories..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-10 pl-10 pr-4 rounded-full bg-muted/50 border border-transparent focus:bg-background focus:border-primary/20 focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300 text-sm placeholder:text-muted-foreground"
/>
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
<ThemeToggle />
<Button
size="sm"
className="rounded-full px-3 sm:px-5 text-xs sm:text-sm font-semibold text-primary-foreground hover:opacity-90 transition-all shadow-lg shadow-primary/20 bg-primary hover:bg-primary/90"
onClick={onSubmitClick}
>
<span className="hidden sm:inline">Submit Entry</span>
<span className="sm:hidden">Submit</span>
</Button>
</div>
</div>
</nav>
);
}

View File

@ -0,0 +1,304 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { typeMap, merchantSubTypes, subTypeLabels, countryFlag } from "@/data/categories";
import { countries } from "@/data/countries";
import { ExternalLink, CheckCircle2, AlertCircle, Globe, AtSign, Tag, Layers, MapPin, FileText } from "lucide-react";
const REPO_URL =
"https://github.com/btcpayserver/directory.btcpayserver.org/issues/new";
interface SubmitFormProps {
onSuccess?: () => void;
}
export default function SubmitForm({ onSuccess }: SubmitFormProps) {
const [name, setName] = useState("");
const [url, setUrl] = useState("");
const [description, setDescription] = useState("");
const [type, setType] = useState("");
const [subType, setSubType] = useState("");
const [country, setCountry] = useState("");
const [twitter, setTwitter] = useState("");
const [submitted, setSubmitted] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const isMerchant = type === "merchants";
const isHostedBtcpay = type === "hosted-btcpay";
function validate() {
const newErrors: Record<string, string> = {};
if (!name.trim()) newErrors.name = "Name is required";
if (!url.trim()) {
newErrors.url = "URL is required";
} else if (!/^https?:\/\//i.test(url.trim())) {
newErrors.url = "URL must start with https:// or http://";
}
if (!description.trim()) newErrors.description = "Description is required";
if (!type) newErrors.type = "Category is required";
if (isMerchant && !subType) newErrors.subType = "Subcategory is required";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!validate()) return;
const lines = [
"New submission:",
"",
`Name: ${name.trim()}`,
`Url: ${url.trim()}`,
];
if (twitter.trim()) lines.push(`Twitter: ${twitter.trim()}`);
lines.push(`Type: ${type}`);
if (isMerchant && subType) lines.push(`SubType: ${subType}`);
if (isHostedBtcpay && country) lines.push(`Country: ${country}`);
lines.push(`Description: ${description.trim()}`);
const issueTitle = `New entry submission - ${name.trim()}`;
const issueBody = lines.join("\n");
const params = new URLSearchParams({
title: issueTitle,
body: issueBody,
});
window.open(
`${REPO_URL}?${params.toString()}`,
"_blank",
"noopener,noreferrer"
);
setSubmitted(true);
setTimeout(() => onSuccess?.(), 2000);
}
if (submitted) {
return (
<div className="flex flex-col items-center justify-center py-12 px-5 text-center space-y-4 sm:py-16 sm:px-6">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-500 sm:h-16 sm:w-16">
<CheckCircle2 className="h-7 w-7 sm:h-8 sm:w-8" />
</div>
<div className="space-y-1.5">
<h3 className="font-display text-lg font-bold sm:text-xl">Submission opened!</h3>
<p className="text-sm text-muted-foreground max-w-xs leading-relaxed">
A GitHub issue has been opened in a new tab. The team will review your
submission.
</p>
</div>
<p className="text-xs text-muted-foreground/60">This window will close automatically</p>
</div>
);
}
const baseInput =
"w-full h-12 pl-9 pr-3 rounded-xl bg-muted/40 border text-base sm:text-sm outline-none transition-all duration-200 placeholder:text-muted-foreground/50 focus:bg-background focus:border-primary/40 focus:ring-4 focus:ring-primary/8";
const baseSelect =
"w-full h-12 pl-9 pr-3 rounded-xl bg-muted/40 border text-base sm:text-sm outline-none transition-all duration-200 appearance-none cursor-pointer focus:bg-background focus:border-primary/40 focus:ring-4 focus:ring-primary/8";
const errorBorder = "border-red-500/50 bg-red-500/5";
const okBorder = "border-transparent";
const inputClass = (field: string) =>
`${baseInput} ${errors[field] ? errorBorder : okBorder}`;
const selectClass = (field: string) =>
`${baseSelect} ${errors[field] ? errorBorder : okBorder}`;
const FieldError = ({ field }: { field: string }) =>
errors[field] ? (
<p className="flex items-center gap-1 text-xs text-red-500 mt-1">
<AlertCircle className="h-3 w-3 shrink-0" />
{errors[field]}
</p>
) : null;
const fieldLabel = (text: string, required?: boolean) => (
<label className="text-sm font-medium text-foreground/80">
{text}
{required && <span className="ml-0.5 text-red-500">*</span>}
</label>
);
return (
<>
<div className="px-4 pt-4 pb-4 border-b border-border/60 sm:px-6 sm:pt-6 sm:pb-5">
<DialogHeader>
<DialogTitle>Submit a new entry</DialogTitle>
<DialogDescription className="mt-1">
Fill in the details below this will open a pre-filled GitHub issue
for the team to review.
</DialogDescription>
</DialogHeader>
</div>
<form onSubmit={handleSubmit} className="px-4 py-4 space-y-3 sm:px-6 sm:py-5 sm:space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
{fieldLabel("Name", true)}
<div className="relative">
<FileText className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/50" />
<input
type="text"
placeholder="Your project or store name"
value={name}
onChange={(e) => setName(e.target.value)}
className={inputClass("name")}
/>
</div>
<FieldError field="name" />
</div>
<div className="space-y-1.5">
{fieldLabel("URL", true)}
<div className="relative">
<Globe className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/50" />
<input
type="text"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
className={inputClass("url")}
/>
</div>
<FieldError field="url" />
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
{fieldLabel("Category", true)}
<div className="relative">
<Tag className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/50" />
<select
value={type}
onChange={(e) => {
setType(e.target.value);
if (e.target.value !== "merchants") setSubType("");
if (e.target.value !== "hosted-btcpay") setCountry("");
}}
className={selectClass("type")}
>
<option value="">Select a category</option>
{Object.entries(typeMap).map(([display, value]) => (
<option key={value} value={value}>
{display}
</option>
))}
</select>
</div>
<FieldError field="type" />
</div>
{isMerchant && (
<div className="space-y-1.5">
{fieldLabel("Subcategory", true)}
<div className="relative">
<Layers className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/50" />
<select
value={subType}
onChange={(e) => setSubType(e.target.value)}
className={selectClass("subType")}
>
<option value="">Select a subcategory</option>
{merchantSubTypes.map((st) => (
<option key={st} value={st}>
{subTypeLabels[st] || st}
</option>
))}
</select>
</div>
<FieldError field="subType" />
</div>
)}
{isHostedBtcpay && (
<div className="space-y-1.5">
{fieldLabel("Country")}
<div className="relative">
<MapPin className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/50" />
<select
value={country}
onChange={(e) => setCountry(e.target.value)}
className={selectClass("country")}
>
<option value="">Select a country (optional)</option>
{Object.entries(countries).map(([code, countryName]) => (
<option key={code} value={code}>
{countryFlag(code)} {countryName}
</option>
))}
</select>
</div>
<FieldError field="country" />
</div>
)}
{!isMerchant && !isHostedBtcpay && (
<div className="hidden sm:block" />
)}
</div>
<div className="space-y-1.5">
<div className="flex items-center justify-between">
{fieldLabel("Description", true)}
<span
className={`text-xs tabular-nums ${
description.length > 180
? "text-red-500 font-medium"
: description.length > 150
? "text-amber-500"
: "text-muted-foreground"
}`}
>
{description.length}/180
</span>
</div>
<textarea
placeholder="Brief description of your project or store…"
value={description}
onChange={(e) => setDescription(e.target.value.slice(0, 180))}
rows={3}
className={`w-full px-3 py-2.5 rounded-xl bg-muted/40 border text-base sm:text-sm outline-none transition-all duration-200 placeholder:text-muted-foreground/50 focus:bg-background focus:border-primary/40 focus:ring-4 focus:ring-primary/8 resize-none leading-relaxed ${
errors.description ? errorBorder : okBorder
}`}
/>
<FieldError field="description" />
</div>
{/* Twitter */}
<div className="space-y-1.5">
{fieldLabel("Twitter / X")}
<div className="relative">
<AtSign className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/50" />
<input
type="text"
placeholder="yourhandle (optional)"
value={twitter}
onChange={(e) => setTwitter(e.target.value)}
className={inputClass("twitter")}
/>
</div>
</div>
{/* Submit */}
<div className="pt-1 pb-safe sm:pb-0">
<Button
type="submit"
className="w-full h-12 rounded-xl font-semibold text-base gap-2"
>
Submit to GitHub
<ExternalLink className="h-4 w-4" />
</Button>
<p className="mt-2 text-center text-xs text-muted-foreground/60">
Opens a pre-filled GitHub issue in a new tab
</p>
</div>
</form>
</>
);
}

View File

@ -0,0 +1,155 @@
export default function SupporterSprite() {
return (
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg" className="absolute">
<defs>
<linearGradient
id="spiral-gradient"
x1="81.36"
y1="311.35"
x2="541.35"
y2="311.35"
gradientUnits="userSpaceOnUse"
>
<stop offset=".18" stopColor="#00f" />
<stop offset="1" stopColor="#f0f" />
</linearGradient>
</defs>
<symbol id="supporter-spiral" viewBox="0 0 629 629">
<path
d="M326.4 572.09C201.2 572.09 141 503 112.48 445c-28.22-57.53-30.59-114.56-30.79-122.69-4.85-77 41-231.78 249.58-271.2a28.05 28.05 0 0 1 10.41 55.13c-213.12 40.28-204.44 206-204 213 0 .53.06 1.06.07 1.6.15 7.9 5.1 195.16 188.65 195.16 68.34 0 116.6-29.4 143.6-87.37 24.48-52.74 19.29-112.45-13.52-155.83-22.89-30.27-52.46-45-90.38-45-34.46 0-63.47 9.88-86.21 29.37A91.5 91.5 0 0 0 248 322.3c-1.41 25.4 7.14 49.36 24.07 67.49C287.27 406 305 413.9 326.4 413.9c27.46 0 45.52-9 53.66-26.81 8.38-18.3 3.61-38.93-.19-43.33-9.11-10-18.69-13.68-22.48-13-2.53.43-5.78 4.61-8.48 10.92a28 28 0 0 1-51.58-22c14.28-33.44 37.94-42 50.76-44.2 24.78-4.18 52.17 7.3 73.34 30.65s25.51 68.55 10.15 103.22C421.54 432 394.52 470 326.4 470c-36.72 0-69.67-14.49-95.29-41.92-27.47-29.4-41.34-68.08-39.11-108.89a149.1 149.1 0 0 1 51.31-104.6c33.19-28.45 74.48-42.87 122.71-42.87 55.12 0 101.85 23.25 135.12 67.23 45.36 60 52.9 141.71 19.66 213.3-25.35 54.67-79.68 119.84-194.4 119.84Z"
fill="url(#spiral-gradient)"
/>
</symbol>
<symbol id="supporter-opensats" viewBox="0 0 5220 720">
<path
d="M0 435.197L229.609 291.597V288.121L0 144.259V29.0508L334.901 245.894V333.824L0 550.798V435.197Z"
fill="#FF3300"
/>
<path
d="M486.969 623.844H902.627V719.643H486.969V623.844Z"
fill="#FF3300"
/>
<path
d="M993.879 291.2C993.879 106.422 1084.61 0 1214.37 0C1344.13 0 1434.86 106.422 1434.86 291.2C1434.86 479.061 1344.13 587.581 1214.37 587.581C1084.61 587.581 993.879 479.061 993.879 291.2ZM1345.12 291.2C1345.12 155.01 1293.16 75.9967 1214.37 75.9967C1135.58 75.9967 1083.62 155.01 1083.62 291.2C1083.62 430.473 1135.58 511.584 1214.37 511.584C1293.16 511.584 1344.85 430.473 1344.85 291.2H1345.12Z"
fill="#FF3300"
/>
<path
d="M1593.29 154.29H1663.81L1670.37 205.37H1673.13C1711.31 169.634 1764.71 144.258 1814.44 144.258C1925.96 144.258 1988.02 228.713 1988.02 359.855C1988.02 504.111 1897.95 587.911 1797.77 587.911C1759.13 587.911 1713.54 568.829 1677.39 535.454H1675.29L1679.43 612.237V749.936H1593.29V154.29ZM1899.65 359.855C1899.65 271.269 1867.44 215.599 1791.21 215.599C1756.57 215.599 1717.93 232.713 1679.69 272.121V472.112C1714.79 503.914 1754.61 515.455 1781.57 515.455C1848.75 515.717 1899.65 459.851 1899.65 359.855Z"
fill="#FF3300"
/>
<path
d="M2118.96 365.035C2118.96 227.336 2222.75 143.93 2335.98 143.93C2460.16 143.93 2530.82 225.434 2530.82 343.527C2530.67 359.209 2529.35 374.858 2526.88 390.345H2178.73V327.2H2473.22L2454.52 348.249C2454.52 256.449 2410.17 210.55 2338.47 210.55C2264.41 210.55 2203.66 265.17 2203.66 364.904C2203.66 468.833 2268.8 520.044 2359.79 520.044C2407.09 520.044 2445.08 505.75 2483.39 482.8L2513.56 537.29C2464.7 569.886 2407.32 587.378 2348.57 587.582C2220.39 587.582 2118.96 505.947 2118.96 365.035Z"
fill="#FF3300"
/>
<path
d="M2658.41 154.29H2729.07L2735.63 221.697H2739.04C2781.55 178.289 2829.83 144.258 2895.17 144.258C2994.1 144.258 3039.17 205.042 3039.17 315.201V577.026H2952.9V326.152C2952.9 252.319 2928.11 218.222 2865.39 218.222C2819.47 218.222 2788.31 240.844 2744.68 285.563V577.026H2658.41V154.29Z"
fill="#FF3300"
/>
<path
d="M3208.36 504.308L3259.46 444.376C3303 486.486 3360.93 510.468 3421.5 511.455C3493.27 511.455 3533.03 478.669 3533.03 432.77C3533.03 377.362 3491.63 361.953 3435.41 338.217L3355.57 303.333C3297.64 280.514 3234.21 238.614 3234.21 155.143C3234.21 66.8186 3313.65 0.001814 3425.64 0.001814C3492.01 -0.442962 3555.93 25.0654 3603.75 71.0807L3558.87 126.554C3521.22 93.051 3472.3 74.9951 3421.9 75.9985C3362.2 75.9985 3322.11 103.604 3322.11 150.028C3322.11 199.206 3371.05 217.173 3420.98 236.516L3497.93 270.416C3569.04 298.087 3622.18 339.528 3622.18 422.344C3622.18 513.356 3545.36 587.583 3416.78 587.583C3339.11 587.998 3264.34 558.123 3208.36 504.308V504.308Z"
fill="currentColor"
/>
<path
d="M3762.96 465.557C3762.96 370.741 3849.04 324.055 4062.7 308.186C4059.62 255.73 4031.54 213.896 3956.3 213.896C3905.13 213.896 3854.68 237.304 3813.28 261.5L3780.48 203.994C3827.98 174.684 3898.57 144.062 3971.78 144.062C4088.42 144.062 4148.91 210.945 4148.91 322.678V577.027H4077.73L4070.84 522.144H4068.08C4022.82 557.553 3963.97 587.715 3906.04 587.715C3825.16 587.584 3762.96 540.045 3762.96 465.557ZM4062.7 462.278V363.266C3896.79 375.134 3847.26 408.576 3847.26 459C3847.26 501.489 3885.38 519.063 3930.65 519.063C3975.91 519.063 4018.88 497.883 4062.7 462.278Z"
fill="currentColor"
/>
<path
d="M4388.81 409.884V222.941H4272.17V158.813L4392.16 154.289L4403.44 20.2617H4475.02V154.289H4674.64V222.941H4475.02V410.146C4475.02 482.864 4500.73 518.076 4577.29 518.076C4610.27 517.981 4642.96 511.847 4673.73 499.979L4691.24 562.992C4648.39 578.951 4603.07 587.274 4557.35 587.581C4430.86 587.581 4388.81 516.043 4388.81 409.884Z"
fill="currentColor"
/>
<path
d="M4818.71 521.815L4857.09 466.014C4909.9 502.948 4972.98 522.353 5037.43 521.487C5102.31 521.487 5133.34 495.259 5133.34 463.85C5133.34 433.95 5115.76 415 5009.22 393.034C4897.7 369.887 4845.21 331.2 4845.21 267.203C4845.21 196.518 4908.85 143.93 5025.95 143.93C5093.26 143.93 5157.55 170.158 5199.93 198.878L5159.45 252.646C5117.79 224.668 5068.79 209.592 5018.6 209.304C4955.62 209.304 4931.28 234.155 4931.28 263.138C4931.28 295.923 4965.07 309.037 5048.12 326.938C5185.89 357.101 5220.33 392.509 5220.33 458.736C5220.33 529.487 5151.71 587.582 5026.8 587.582C4952.47 586.641 4880.07 563.76 4818.71 521.815V521.815Z"
fill="currentColor"
/>
</symbol>
<symbol id="supporter-tether" viewBox="0 0 111 90">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M24.4825 0.862305H88.0496C89.5663 0.862305 90.9675 1.64827 91.7239 2.92338L110.244 34.1419C111.204 35.7609 110.919 37.8043 109.549 39.1171L58.5729 87.9703C56.9216 89.5528 54.2652 89.5528 52.6139 87.9703L1.70699 39.1831C0.305262 37.8398 0.0427812 35.7367 1.07354 34.1077L20.8696 2.82322C21.6406 1.60483 23.0087 0.862305 24.4825 0.862305ZM79.8419 14.8003V23.5597H61.7343V29.6329C74.4518 30.2819 83.9934 32.9475 84.0642 36.1425L84.0638 42.803C83.993 45.998 74.4518 48.6635 61.7343 49.3125V64.2168H49.7105V49.3125C36.9929 48.6635 27.4513 45.998 27.3805 42.803L27.381 36.1425C27.4517 32.9475 36.9929 30.2819 49.7105 29.6329V23.5597H31.6028V14.8003H79.8419ZM55.7224 44.7367C69.2943 44.7367 80.6382 42.4827 83.4143 39.4727C81.0601 36.9202 72.5448 34.9114 61.7343 34.3597V40.7183C59.7966 40.8172 57.7852 40.8693 55.7224 40.8693C53.6595 40.8693 51.6481 40.8172 49.7105 40.7183V34.3597C38.8999 34.9114 30.3846 36.9202 28.0304 39.4727C30.8066 42.4827 42.1504 44.7367 55.7224 44.7367Z"
fill="#009393"
/>
</symbol>
<symbol id="supporter-hrf" viewBox="0 0 3000 987.6">
<path
d="M1137.09 103.9v773.45h-51.44V515.96h-953.6v361.38H80.62V103.9h51.44v361.2h953.6V103.9h51.43zm-102.77 0h-51.44v258.19H234.94V103.9H183.5v309.05h850.82V103.9zm-696.29 0h-50.87v205.84h50.87V103.9zm593.05 0h-51.44v205.84h51.44V103.9zM183.5 877.34h51.44V619.16h747.94v258.19h51.44V567.72H183.5v309.62zm695.72 0h51.44V670.93h-51.44v206.41zm-592.47 0h51.44V670.93h-51.44v206.41z"
fill="#e12991"
/>
<path
d="M1422.94 103.88V331.3h-44.51v-94.22h-92.2v94.22h-44.83V103.88h44.83v90.32h92.2v-90.32h44.51zM1605.81 168.85V331.3h-41.91v-18.19c-9.75 14.62-26.64 22.74-48.41 22.74-34.44 0-61.4-24.04-61.4-67.25v-99.74H1496v94.54c0 22.1 13.32 33.47 32.16 33.47 20.47 0 35.74-12.02 35.74-40.29v-87.72h41.91zM1885.19 231.23V331.3h-41.91v-97.14c0-19.17-9.75-30.86-27.29-30.86-18.52 0-30.22 12.35-30.22 36.71v91.29h-41.91v-97.14c0-19.17-9.75-30.86-27.29-30.86-17.87 0-30.54 12.35-30.54 36.71v91.29h-41.91V168.85h41.91v17.22c9.1-13.64 24.37-21.77 45.16-21.77 20.14 0 35.09 8.45 44.18 23.39 10.07-14.62 26.32-23.39 48.41-23.39 37.04.01 61.41 26.32 61.41 66.93zM2086.24 168.85V331.3h-41.91v-19.17c-11.7 14.62-29.24 23.72-52.96 23.72-43.21 0-78.95-37.36-78.95-85.77s35.74-85.77 78.95-85.77c23.72 0 41.26 9.1 52.96 23.72v-19.17h41.91zm-41.91 81.23c0-27.29-19.17-45.81-45.16-45.81-25.66 0-44.83 18.52-44.83 45.81 0 27.29 19.17 45.81 44.83 45.81 25.99 0 45.16-18.52 45.16-45.81zM2275.93 231.56v99.74h-41.91v-94.54c0-22.09-13.32-33.46-32.16-33.46-20.47 0-35.74 12.02-35.74 40.29v87.72h-41.91V168.85h41.91v18.19c9.75-14.62 26.64-22.74 48.41-22.74 34.44.01 61.4 24.05 61.4 67.26zM1316.47 525.36h-30.25v78.95h-44.83V376.89h94.84c41.91 0 75.7 33.79 75.7 75.37 0 28.59-17.87 54.26-43.86 66.28l50.36 85.77h-48.41l-53.55-78.95zm-30.25-39.31h50c16.89 0 30.86-14.95 30.86-33.79s-13.97-33.46-30.86-33.46h-50v67.25zM1437.88 396.71c0-13.97 15.57-25.99 29.54-25.99 14.29 0 22.12 12.02 22.12 25.99s-11.7 25.67-25.99 25.67c-13.97 0-25.67-11.7-25.67-25.67zm4.88 45.16h41.91v162.45h-41.91V441.87zM1681.86 441.87v154.65c0 53.28-41.91 77.33-84.8 77.33-34.77 0-62.7-13.32-77-39.64l35.74-20.47c6.82 12.67 17.54 22.74 42.56 22.74 26.31 0 42.56-14.29 42.56-39.96v-17.54c-11.37 15.27-28.92 24.69-51.98 24.69-46.14 0-80.9-37.36-80.9-83.17 0-45.48 34.76-83.17 80.9-83.17 23.07 0 40.61 9.42 51.98 24.69v-20.14h40.94zm-40.94 78.62c0-25.67-19.17-44.18-45.49-44.18-26.31 0-45.48 18.52-45.48 44.18 0 25.99 19.17 44.51 45.48 44.51 26.32 0 45.49-18.52 45.49-44.51zM1871.55 504.57v99.74h-41.91v-94.54c0-22.09-13.32-33.46-32.16-33.46-20.47 0-35.74 12.02-35.74 40.29v87.72h-41.91V376.89h41.91v83.17c9.75-14.62 26.64-22.74 48.41-22.74 34.44 0 61.4 24.04 61.4 67.25zM1963.46 482.15v67.58c0 17.54 12.67 17.87 36.71 16.57v38.01c-58.81 6.5-78.62-10.72-78.62-54.58v-67.58h-28.27v-40.29h28.27v-32.81l41.91-12.67v45.48h36.71v40.29h-36.71zM2148.63 556.88c0 35.09-30.54 51.98-65.31 51.98-32.49 0-56.53-13.64-68.22-38.66l36.39-20.47c4.55 13.32 15.6 21.12 31.84 21.12 13.32 0 22.42-4.55 22.42-13.97 0-23.72-83.82-10.72-83.82-67.9 0-33.14 28.27-51.66 61.73-51.66 26.32 0 49.06 12.02 61.73 34.44l-35.74 19.49c-4.88-10.4-13.97-16.57-25.99-16.57-10.4 0-18.84 4.55-18.84 13.32-.01 24.04 83.81 9.1 83.81 68.88zM1286.22 692.79v53.93h96.11v42.89h-96.11v87.72h-44.83V649.9h143.54v42.88h-98.71zM1400.82 796.1c0-48.41 38.01-85.77 85.77-85.77s86.1 37.36 86.1 85.77-38.34 85.77-86.1 85.77c-47.76 0-85.77-37.36-85.77-85.77zm129.96 0c0-26.31-19.17-44.83-44.19-44.83-24.69 0-43.86 18.52-43.86 44.83 0 26.32 19.17 44.83 43.86 44.83 25.02.01 44.19-18.51 44.19-44.83zM1744.84 714.88v162.45h-41.91v-18.19c-9.75 14.62-26.64 22.74-48.41 22.74-34.44 0-61.4-24.04-61.4-67.25v-99.74h41.91v94.54c0 22.1 13.32 33.47 32.16 33.47 20.47 0 35.74-12.02 35.74-40.29v-87.72h41.91zM1934.86 777.58v99.74h-41.91v-94.54c0-22.09-13.32-33.46-32.16-33.46-20.47 0-35.74 12.02-35.74 40.29v87.72h-41.91V714.88h41.91v18.19c9.75-14.62 26.64-22.74 48.41-22.74 34.44 0 61.4 24.04 61.4 67.25zM2135.61 649.9v227.42h-41.91v-19.17c-11.7 14.95-28.92 23.72-52.63 23.72-43.54 0-79.27-37.36-79.27-85.77s35.74-85.77 79.27-85.77c23.72 0 40.94 8.77 52.63 23.72V649.9h41.91zm-41.91 146.2c0-27.29-19.17-45.81-44.84-45.81-25.99 0-45.16 18.52-45.16 45.81 0 27.29 19.17 45.81 45.16 45.81 25.67.01 44.84-18.51 44.84-45.81zM2337.35 714.88v162.45h-41.91v-19.17c-11.7 14.62-29.24 23.72-52.96 23.72-43.21 0-78.95-37.36-78.95-85.77s35.74-85.77 78.95-85.77c23.72 0 41.26 9.1 52.96 23.72v-19.17h41.91zm-41.91 81.22c0-27.29-19.17-45.81-45.16-45.81-25.66 0-44.83 18.52-44.83 45.81 0 27.29 19.17 45.81 44.83 45.81 25.99.01 45.16-18.51 45.16-45.81zM2433.46 755.17v67.58c0 17.54 12.67 17.87 36.71 16.57v38.01c-58.81 6.5-78.62-10.72-78.62-54.58v-67.58h-28.26v-40.29h28.26v-32.81l41.91-12.67v45.48h36.71v40.29h-36.71zM2494.84 669.72c0-13.97 11.7-25.99 25.67-25.99 14.29 0 25.99 12.02 25.99 25.99s-11.7 25.66-25.99 25.66c-13.97.01-25.67-11.69-25.67-25.66zm4.87 45.16h41.91v162.45h-41.91V714.88zM2565 796.1c0-48.41 38.01-85.77 85.77-85.77s86.1 37.36 86.1 85.77-38.34 85.77-86.1 85.77c-47.76 0-85.77-37.36-85.77-85.77zm129.96 0c0-26.31-19.17-44.83-44.19-44.83-24.69 0-43.86 18.52-43.86 44.83 0 26.32 19.17 44.83 43.86 44.83 25.02.01 44.19-18.51 44.19-44.83zM2911.62 777.58v99.74h-41.91v-94.54c0-22.09-13.32-33.46-32.16-33.46-20.47 0-35.74 12.02-35.74 40.29v87.72h-41.91V714.88h41.91v18.19c9.75-14.62 26.64-22.74 48.41-22.74 34.44 0 61.4 24.04 61.4 67.25z"
fill="currentColor"
/>
</symbol>
<symbol id="supporter-lunanode" viewBox="0 0 194.219 193.977">
<path
d="M3185.89 2995.8c-1.77 21.49-2.76 43.2-2.76 65.16 0 411.03 319.09 747.36 723.13 774.95l-618.54-641.7c-54.62-56.68-88.55-126.08-101.83-198.41M3960 2284.09c-270.37 0-508.4 138.15-647.57 347.65l23.25-22.42c76.82-74.06 176.93-109.95 276.2-108.13 99 1.77 197.53 41.2 271.5 117.59l-177.95 171.52c-26.66-27.31-62.22-41.38-98.02-42.14-36.12-.65-72.43 12.41-100.16 39.15l-37.98 36.6c-27.69 26.66-42.04 62.45-42.7 98.57-.65 36.07 12.36 72.48 39.11 100.21l745.68 773.56c305.71-104.45 525.52-394.17 525.52-735.29 0-29.89-1.73-59.34-5.04-88.32-19.44 54.57-51.41 105.56-95.79 148.35l-37.93 36.58c-76.86 74.07-176.93 110.05-276.16 108.18-99.32-1.77-198.13-41.38-272.19-118.25l-290.74-301.59 177.95-171.53 290.74 301.61c26.71 27.73 62.64 42.04 98.72 42.74 36.12.69 72.38-12.35 100.16-39.1l37.89-36.59c27.69-26.66 42.09-62.45 42.74-98.58.61-36.03-12.4-72.48-39.1-100.21l-440.73-457.23c-22.23-1.9-44.69-2.93-67.4-2.93"
transform="matrix(.125 0 0 -.125 -397.891 479.489)"
fill="#004581"
/>
<path
d="M4376.22 2292.8h360.66v433.41c-17.35-55.88-47.59-108.64-90.81-153.48l-269.85-279.93"
transform="matrix(.125 0 0 -.125 -397.891 479.489)"
fill="#3384b9"
/>
</symbol>
<symbol id="supporter-walletofsatoshi" viewBox="0 0 313.1 76.32">
<path
d="M110.47 44.8H121c.84 0 1.22-.64.9-1.48l-17.6-42A2 2 0 0 0 102.22 0H87.63a2 2 0 0 0-2 1.34L66 48.11c-.32.84.06 1.48.83 1.48h13.7a1.42 1.42 0 0 1 1.32 1.93l-9.7 24.8 30.55-32.63A1 1 0 0 0 102 42H84.73a1.42 1.42 0 0 1-1.32-2l5.06-12.91 6.86-17.47 6.78 17.51h-7.54a1.42 1.42 0 0 0-1.32.9l-2.83 7.22a1.42 1.42 0 0 0 1.32 1.93H105a1.42 1.42 0 0 1 1.33.91l2.08 5.36a1.92 1.92 0 0 0 2.06 1.35Zm62.65 0h37.42a1.3 1.3 0 0 0 1.46-1.41V35.9a1.3 1.3 0 0 0-1.47-1.41h-26V1.41A1.35 1.35 0 0 0 183 0h-9.92a1.3 1.3 0 0 0-1.47 1.41v42a1.3 1.3 0 0 0 1.51 1.39Zm45.36 0h42a1.3 1.3 0 0 0 1.52-1.41V35.9a1.31 1.31 0 0 0-1.47-1.41h-30.59v-7.36h25.59a1.33 1.33 0 0 0 1.48-1.4v-7a1.33 1.33 0 0 0-1.48-1.41h-25.59v-7h30.59A1.3 1.3 0 0 0 262 8.89V1.41A1.3 1.3 0 0 0 260.53 0h-42A1.3 1.3 0 0 0 217 1.41v42a1.3 1.3 0 0 0 1.48 1.39ZM71.79 0H61.61a1.71 1.71 0 0 0-1.85 1.41L52.08 34.3 44.91 1.41A1.65 1.65 0 0 0 43.12 0H30.38a1.71 1.71 0 0 0-1.85 1.41L21.36 34.3 13.68 1.41A1.65 1.65 0 0 0 11.89 0H1.14C.24 0-.14.51.05 1.41l10.88 42a1.68 1.68 0 0 0 1.79 1.41H28.4a1.65 1.65 0 0 0 1.79-1.41l6.27-28.31 6.34 28.29a1.65 1.65 0 0 0 1.79 1.41H60.2a1.66 1.66 0 0 0 1.8-1.41l10.87-42C73.07.51 72.68 0 71.79 0Zm239.84 0h-43.52a1.3 1.3 0 0 0-1.47 1.41v7.48a1.3 1.3 0 0 0 1.47 1.41h15.29v33.09a1.3 1.3 0 0 0 1.48 1.41h10a1.33 1.33 0 0 0 1.47-1.41V10.3h15.3a1.3 1.3 0 0 0 1.47-1.41V1.41A1.3 1.3 0 0 0 311.63 0ZM127.76 44.8h37.42a1.3 1.3 0 0 0 1.47-1.41V35.9a1.3 1.3 0 0 0-1.47-1.41h-26V1.41a1.35 1.35 0 0 0-1.5-1.41h-9.92a1.3 1.3 0 0 0-1.47 1.41v42a1.3 1.3 0 0 0 1.47 1.39Zm-3.84 9.6h-11.53c-3.13 0-4.53 1.31-4.53 4.36v10.37c0 3.05 1.4 4.36 4.53 4.36h11.53c3.16 0 4.51-1.31 4.51-4.36V58.76c0-3.05-1.35-4.36-4.51-4.36Zm-1 12.95c0 1.48-.29 1.75-2.07 1.75h-5.51c-1.76 0-2.08-.27-2.08-1.75v-6.81c0-1.47.32-1.75 2.08-1.75h5.51c1.78 0 2.07.28 2.07 1.75Zm51.87-5.59h-8.75c-.89 0-1.16-.27-1.16-.95v-1.06c0-.68.27-1 1.16-1h6.7c.65 0 .89.28.89.85v.16a.55.55 0 0 0 .62.6h4a.55.55 0 0 0 .62-.6v-1.08c0-3.21-1.11-4.28-4.4-4.28H164c-3.19 0-4.51 1.31-4.51 4.36v2.84c0 3.06 1.32 4.36 4.51 4.36h8.74c.9 0 1.17.28 1.17 1v1.23c0 .68-.27.95-1.17.95h-7.34c-.62 0-.86-.27-.86-.85v-.16a.56.56 0 0 0-.62-.6h-4a.55.55 0 0 0-.62.6v1.12c0 3.22 1.08 4.28 4.4 4.28h11.2c3.19 0 4.51-1.31 4.51-4.36v-3c-.06-3.1-1.41-4.41-4.57-4.41Zm85.43 0h-8.75c-.89 0-1.16-.27-1.16-.95v-1.06c0-.68.27-1 1.16-1h6.7c.64 0 .89.28.89.85v.16a.55.55 0 0 0 .62.6h4a.55.55 0 0 0 .62-.6v-1.08c0-3.21-1.11-4.28-4.4-4.28h-10.48c-3.19 0-4.51 1.31-4.51 4.36v2.84c0 3.06 1.32 4.36 4.51 4.36h8.74c.89 0 1.16.28 1.16 1v1.23c0 .68-.27.95-1.16.95h-7.34c-.62 0-.86-.27-.86-.85v-.16a.57.57 0 0 0-.62-.6h-4.05a.55.55 0 0 0-.62.6v1.12c0 3.22 1.08 4.28 4.4 4.28h11.2c3.18 0 4.51-1.31 4.51-4.36v-3c0-3.1-1.33-4.41-4.51-4.41Zm26.65-7.36h-4.21a.56.56 0 0 0-.63.6v6.66h-9.2V55a.57.57 0 0 0-.65-.6H268a.55.55 0 0 0-.62.6v17.89a.55.55 0 0 0 .62.6h4.18a.57.57 0 0 0 .65-.6v-6.84h9.2v6.84a.56.56 0 0 0 .63.6h4.21a.55.55 0 0 0 .62-.6V55a.55.55 0 0 0-.57-.6Zm-137.62 0h-17.07a.55.55 0 0 0-.62.6v17.89a.55.55 0 0 0 .62.6h4.19a.58.58 0 0 0 .65-.6v-6.52h10.15a.57.57 0 0 0 .64-.6v-3.19a.57.57 0 0 0-.64-.6H137v-3.19h12.3a.55.55 0 0 0 .62-.6V55a.55.55 0 0 0-.62-.6Zm146.47 0h-4.18a.55.55 0 0 0-.62.6v17.89a.55.55 0 0 0 .62.6h4.18a.57.57 0 0 0 .65-.6V55a.57.57 0 0 0-.6-.6Zm-100.28.6a.83.83 0 0 0-.86-.57h-6.16a.83.83 0 0 0-.89.57l-7.42 17.89c-.14.36 0 .63.38.63h4.45a.8.8 0 0 0 .86-.57l1-2.68h9.1l1 2.68a.8.8 0 0 0 .87.57h4.69c.33 0 .49-.27.35-.63Zm-7 11 2.89-7.52 2.92 7.52Zm30.9-11.6H201a.55.55 0 0 0-.62.6v3.19a.55.55 0 0 0 .62.6h6.45v14.1a.55.55 0 0 0 .62.6h4.21a.56.56 0 0 0 .62-.6v-14.1h6.46a.55.55 0 0 0 .62-.6V55a.55.55 0 0 0-.64-.6Zm18.46 0h-11.52c-3.13 0-4.54 1.31-4.54 4.36v10.37c0 3.05 1.41 4.36 4.54 4.36h11.52c3.16 0 4.51-1.31 4.51-4.36V58.76c0-3.05-1.31-4.36-4.51-4.36Zm-.94 12.95c0 1.48-.3 1.75-2.08 1.75h-5.51c-1.75 0-2.07-.27-2.07-1.75v-6.81c0-1.47.32-1.75 2.07-1.75h5.51c1.78 0 2.08.28 2.08 1.75Z"
fill="#fad228"
stroke="#1e2127"
strokeWidth="2"
/>
</symbol>
<symbol id="supporter-coincards" viewBox="0 0 64 32">
<g fill="none">
<path
d="M32.7 5.9c-.2-1-1.3-1.7-2.3-1.4L7.7 9.9c-1 .2-1.7 1.3-1.4 2.3l3.1 12.9c.2 1 1.3 1.7 2.3 1.4l22.7-5.4c1-.2 1.7-1.3 1.4-2.3L32.7 5.9Z"
fill="#EF8022"
/>
<path
d="M12.6 30.3c-.2.2-.5.3-.7.3l.8.5c.9.6 2.1.4 2.7-.5l3.1-4.4-5.9 4.1ZM2.3 19.5l-1 1.4c-.6.9-.4 2.1.5 2.7L8.1 28l-5.8-8.5ZM12.9 8.1l7.2-5-2.7-1.9c-.9-.6-2.1-.4-2.7.5l-5 7.2 3.2-.8Z"
fill="#F9F185"
/>
<path
d="M9.7 29.4c.6.9 1.8 1.1 2.7.5l6.7-4.6-7.4 1.8c-1.3.3-2.6-.5-2.9-1.8L6 13.5l-3.3 2.3c-.9.6-1.1 1.8-.5 2.7l7.5 10.9Zm4.5-21.6L25.9 5l-1.3-2c-.6-.9-1.8-1.1-2.7-.5l-7.7 5.3Z"
fill="#FFC214"
/>
<path
d="M11.9 24.8c-.7 0-1.4-.5-1.7-1.1l-1.5-3.2 1.1 4.6c.2.6.7 1.1 1.4 1.1h.3l17-4.1-16.2 2.7h-.4Z"
fill="#FFC214"
/>
<path
d="M16 17.5s-1.1 1.2-2.5 1.2c-1.7 0-2.6-1.4-2.6-2.8 0-1.3.9-2.7 2.6-2.7 1.3 0 2.3 1 2.3 1l1.1-1.7s-.6-.7-1.9-1.1v-1.2h-1.1v1h-.6v-1h-1.1v1.1c-2.2.5-3.7 2.4-3.7 4.7 0 2.4 1.5 4.2 3.7 4.7v1.2h1.1v-1h.6V22H15v-1.3c1.4-.4 2.1-1.3 2.1-1.3L16 17.5ZM21 13.7c2.1 0 3.8 1.4 3.8 3.6 0 2.1-1.7 3.5-3.8 3.5-2.1 0-3.8-1.4-3.8-3.5s1.7-3.6 3.8-3.6Zm0 5.2c.8 0 1.5-.6 1.5-1.6s-.7-1.7-1.5-1.7-1.5.6-1.5 1.7c0 1 .7 1.6 1.5 1.6Zm4.3-5h2.3v6.7h-2.3v-6.7Zm0-2.6h2.2v1.8h-2.2v-1.8Zm3.2 2.6h2.2v1c.3-.5 1-1.2 2.1-1.2 1.4 0 2.4.6 2.4 2.5v4.4h-2.3v-4c0-.6-.2-.9-.7-.9-.7 0-1.1.4-1.3 1-.1.3-.1.6-.1.9v3h-2.3v-6.7Z"
fill="#FFF"
/>
<path
d="M39.3 13.9c1.7 0 2.5 1 2.5 1l-.6.9s-.7-.8-1.8-.8c-1.3 0-2.3 1-2.3 2.4 0 1.3 1 2.4 2.3 2.4 1.2 0 2-.9 2-.9l.5.9s-.9 1.1-2.6 1.1c-2.1 0-3.5-1.5-3.5-3.5-.1-2 1.4-3.5 3.5-3.5Zm6.8 2.6h.3v-.1c0-1.1-.6-1.5-1.5-1.5-1 0-1.8.6-1.8.6l-.5-.9s1-.8 2.5-.8c1.7 0 2.6.9 2.6 2.6v4.2h-1.2v-1.1s-.5 1.3-2.1 1.3c-1.1 0-2.3-.7-2.3-2 0-2.2 2.9-2.3 4-2.3Zm-1.4 3.3c1.1 0 1.8-1.1 1.8-2.1v-.2h-.3c-1 0-2.7.1-2.7 1.3-.1.5.3 1 1.2 1Zm3.8-5.8h1.2v1.7c.3-1 1.1-1.7 2.1-1.7h.3v1.3h-.4c-.8 0-1.6.6-1.9 1.6-.1.4-.2.8-.2 1.2v2.7h-1.3V14h.2Zm6.8-.1c1.5 0 2 1 2 1v-3.5h1.3v9.2h-1.2v-1s-.5 1.2-2.2 1.2c-1.8 0-2.9-1.4-2.9-3.5s1.3-3.4 3-3.4Zm.2 5.8c1 0 1.9-.7 1.9-2.4 0-1.2-.6-2.4-1.9-2.4-1 0-1.9.9-1.9 2.4s.8 2.4 1.9 2.4Zm4.2-.8s.7.8 1.9.8c.5 0 1.1-.3 1.1-.8 0-1.2-3.4-1-3.4-3.1 0-1.2 1.1-1.9 2.4-1.9 1.5 0 2.1.7 2.1.7l-.5 1s-.6-.6-1.6-.6c-.5 0-1.1.2-1.1.8 0 1.2 3.4.9 3.4 3.1 0 1.1-.9 1.9-2.4 1.9-1.6 0-2.5-1-2.5-1l.6-.9Z"
fill="#EF8022"
/>
</g>
</symbol>
<symbol id="supporter-ivpn" viewBox="0 0 84 29">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.75 0h6.03c.07 0 .15.03.2.1.04.05.06.13.05.2L8.6 27.43a.26.26 0 0 1-.24.22l-6.63.38H1.7a.25.25 0 0 1-.19-.08.26.26 0 0 1-.06-.22L5.2 5.05C5.12 1.67 1 .85.49.73-.06.59 0 0 0 0h6.75Zm32.32.12a.25.25 0 0 0-.22-.12h-6.5c-.1 0-.18.05-.23.14l-8.98 17.4L20.08.2a.25.25 0 0 0-.24-.21h-6.55a.25.25 0 0 0-.2.1.26.26 0 0 0-.05.2l4.85 26.05a.25.25 0 0 0 .26.2l7.57-.43c.08 0 .16-.06.2-.14L39.08.38a.26.26 0 0 0-.01-.26Zm20.27 5.5a6.8 6.8 0 0 0-.53-2.08c-.27-.6-.61-1.1-1.01-1.5-.4-.41-.8-.75-1.23-1A8.23 8.23 0 0 0 52.9 0H40.47a.25.25 0 0 0-.25.2l-4.01 24.6c-.01.07.01.14.06.2.05.05.11.08.18.08h.02l6.25-.36c.11 0 .2-.1.22-.21l.75-4.63h6.08c1.39-.09 2.7-.43 3.89-1.03a9.75 9.75 0 0 0 2.99-2.46 9.9 9.9 0 0 0 2-4.76l.56-3.3c.17-1.02.21-1.93.13-2.71Zm-7.21 5.87a2.53 2.53 0 0 1-1.1 1.66c-.27.18-.64.27-1.1.27H44.7l1.1-7h5.3c.45 0 .78.09.97.27.22.2.38.41.47.65.1.27.13.6.1.95l-.52 3.2ZM83.75 0h-6.32c-.12 0-.23.1-.25.22l-2.25 14.34L70.04.17a.25.25 0 0 0-.23-.17H63.5c-.12 0-.23.1-.25.22l-3.86 24.56c-.01.07.01.16.06.22.05.05.12.08.19.08l6.43-.39c.12 0 .21-.1.23-.22l1.62-10.36 3.4 10.08c.04.1.14.17.25.17l8.58-.52c.11 0 .2-.1.23-.22L84 .3a.27.27 0 0 0-.06-.22.24.24 0 0 0-.19-.09Z"
fill="#F34"
/>
</symbol>
<symbol id="supporter-unbank" viewBox="0 0 766 132" fill="none">
<path
d="M133.125 66.24v41.145c-.03 10.77-7.051 19.926-17.376 22.792-2.053.571-4.156.831-6.279.831-27.55 0-55.09.03-82.641-.01-13.209-.02-23.625-10.429-23.645-23.623v-82.52c0-13.194 10.425-23.593 23.635-23.613h82.641c13.209.02 23.605 10.429 23.645 23.623l.01 41.385.01-.01zM68.38 33.16H31.987c-.361 0-.721-.02-1.082 0-2.694.17-4.657 2.044-4.717 4.508-.07 2.805 1.662 4.779 4.517 5.019.711.06.851.321.851.952v34.904c0 .661-.19.952-.871 1.162-2.183.661-2.694 3.206-1.042 4.769.751.711 1.632.731 2.574.731h72.316.721c1.472-.05 2.594-1.012 2.844-2.434a2.77 2.77 0 0 0-1.853-3.016c-.641-.21-.771-.501-.771-1.092V43.76c0-.741.19-.972.982-1.082 3.595-.491 5.347-4.348 3.385-7.363-1.062-1.633-2.704-2.154-4.557-2.154H68.41h-.03zm-.12 53.026H28.261c-.881 0-1.763-.04-2.644.01-3.245.19-5.208 3.446-3.715 6.171.701 1.272 1.933 1.853 3.175 2.435l27.37 12.824 12.478 5.88c1.703.822 3.325.972 5.037.081 1.352-.692 2.754-1.293 4.136-1.914l23.144-10.519 14.952-6.822c1.542-.701 2.564-1.783 2.684-3.516.2-2.755-1.672-4.638-4.577-4.638H68.26v.01zm-.13-65.329H26.218c-1.933 0-2.574.631-2.604 2.585-.02 1.122 0 2.234 0 3.356 0 2.615.561 3.176 3.155 3.176h82.741c.281 0 .561.01.841 0 1.432-.08 2.133-.761 2.153-2.174v-4.198c0-2.294-.45-2.755-2.713-2.755H68.12l.01.01zM593.247 49.67c-.27 1.703-.11 3.426-.1 5.129l.13 13.665.241 25.777.23 16.18c.18 5.169-4.006 8.155-7.321 8.145-4.696 0-7.911-3.156-7.971-7.995l-.15-17.261-.261-25.777-.22-22.18-.231-19.415c-.03-3.827 1.903-6.622 5.248-7.714s6.58.05 8.813 3.136l46.859 64.517c.29.401.5.892 1.131 1.262v-1.473-59.849c0-4.378 2.524-7.463 6.61-8.125 4.356-.711 8.542 2.755 8.693 7.183.01.321 0 .641 0 .962v84.554c0 3.897-1.933 6.792-5.248 7.864-3.355 1.082-6.64-.11-8.913-3.246l-46.708-64.327c-.251-.351-.421-.782-.832-.992-.04-.05-.08-.09-.12-.14 0 .03-.01.06-.02.1.05 0 .11.01.16.02h-.02zm-288.231-.21v12.383l.17 12.823.321 31.768c.02 1.753.22 3.526.03 5.269-.481 4.368-3.936 7.113-8.453 6.833-3.715-.231-6.739-3.637-6.79-7.704l-.14-15.589-.24-26.017-.24-22.18-.231-21.218c-.03-3.737 1.933-6.562 5.218-7.624 3.325-1.082 6.61.07 8.833 3.126l47.069 64.808c.25.351.51.691.971 1.292v-1.523-60.45c0-4.087 2.584-7.123 6.56-7.774 4.156-.671 8.182 2.394 8.622 6.572a12.05 12.05 0 0 1 .06 1.192v85.396c0 6.071-6.239 9.697-11.476 6.702-1.062-.611-1.863-1.483-2.574-2.465l-47.009-64.707-.681-.912h-.02zm97.402 22.421l-.02-37.178.02-8.626c.141-2.585.641-5.019 2.855-6.702 1.522-1.162 3.334-1.573 5.197-1.583l20.66.01c11.617.26 21.502 7.153 25.257 17.602 2.955 8.225 1.703 16.109-2.413 23.653-1.102 2.004-1.122 2.004.591 3.436 5.287 4.428 9.504 9.608 11.376 16.37 4.317 15.628-4.026 35.375-24.355 38.861-3.636.621-7.341.471-11.016.511-5.759.06-11.527 0-17.296-.05-1.442-.01-2.894 0-4.306-.21-3.946-.592-6.129-3.246-6.469-7.725a27.22 27.22 0 0 1-.061-2.033V71.871l-.02.01zm15.864 15.598v14.156c0 .491-.171 1.062.701 1.052 6.119-.1 12.258.32 18.366-.171 10.806-.871 17.206-11.921 12.619-21.729-2.163-4.628-5.718-7.634-11.026-8.135a86.36 86.36 0 0 0-9.104-.361l-10.675.17c-.801 0-.891.321-.891.982l.01 14.035zm0-42.537l-.02 10.78c0 .731.19.942.941.942l11.156-.03c7.461-.15 12.619-5.891 11.988-13.274-.431-5.009-4.006-8.796-8.903-9.537-4.767-.721-9.564-.571-14.351-.701-.751-.02-.831.311-.831.922v10.9h.02zM704.27 68.014c.581-.17.811-.571 1.111-.872l21.692-21.76 24.676-24.875c3.165-3.196 7.992-3.286 11.076-.21 3.015 3.005 2.915 7.814-.22 10.97l-24.886 25.016-6.259 6.301c-.601.581-.551.942-.071 1.563l31.026 40.153 1.462 1.903c2.604 3.456 2.073 8.095-1.212 10.72-3.315 2.644-8.072 2.123-10.796-1.323l-11.016-14.226-20.299-26.307c-.431-.561-.691-.822-1.332-.16l-14.281 14.406c-.541.541-.681 1.092-.681 1.813v18.954c-.02 3.166-1.492 5.53-4.347 6.893-2.764 1.312-5.468 1.002-7.921-.832-1.893-1.412-2.935-3.366-2.935-5.77V25.446c0-3.997 3.305-7.273 7.341-7.384 4.086-.12 7.631 3.026 7.812 7.033.1 2.194.05 4.398.05 6.592v36.336l.01-.01zM258.548 50.873l-.03 24.955c-.361 19.586-13.54 37.278-32.838 41.355-16.464 3.486-30.154-1.823-40.83-14.687-5.688-6.852-8.642-14.927-9.484-23.793-.17-1.803-.13-3.587-.13-5.38V24.745c0-3.807 2.063-6.682 5.488-7.674 4.978-1.443 9.765 2.254 9.765 7.584v33.591l.06 17.872c.33 12.232 8.843 23.423 20.66 26.228 10.045 2.374 20.64-1.803 26.879-10.86 3.275-4.749 5.068-10.028 5.078-15.839l.07-50.852c0-3.787 2.083-6.702 5.478-7.704 4.937-1.463 9.784 2.184 9.794 7.413v26.388l.04-.02zm292.788 67.683c-3.295 0-5.898-1.824-7.15-5.04l-7.131-18.413c-.35-.922-.811-1.152-1.742-1.152h-33.75c-.871 0-1.312.19-1.642 1.092l-7.01 18.584c-1.322 3.436-4.297 5.199-8.072 4.909-3.105-.241-5.859-2.786-6.56-6.102-.33-1.572-.07-3.075.501-4.568l11.657-30.906 20.37-53.968c1.272-3.386 3.996-5.4 7.301-5.41 3.254-.01 5.998 1.963 7.28 5.28l32.948 84.904c1.853 4.789-.45 9.447-5.207 10.639-.591.151-1.192.131-1.783.151h-.01zm-20.66-39.843l-12.438-32.068-.271.611-11.547 30.546c-.39 1.032.151.912.802.912h21.842 1.612z"
fill="#3cce49"
/>
<path
d="M68.38 33.16h36.874c1.863 0 3.495.511 4.557 2.154 1.962 3.005.21 6.873-3.385 7.363-.792.11-.982.341-.982 1.082v34.903c0 .591.13.892.771 1.092a2.76 2.76 0 0 1 1.853 3.016c-.24 1.423-1.372 2.384-2.844 2.434h-.721-72.316c-.931 0-1.823-.02-2.574-.731-1.652-1.563-1.142-4.118 1.042-4.769.681-.21.881-.501.871-1.162V43.639c0-.631-.14-.892-.851-.952-2.854-.24-4.587-2.214-4.517-5.019.06-2.455 2.023-4.338 4.717-4.508.361-.02.721 0 1.082 0H68.35h.03zM52.647 61.201l.02-17.271c0-.892-.16-1.252-1.172-1.232h-9.724c-.891-.01-1.152.23-1.152 1.132v34.543c0 .932.31 1.132 1.172 1.122h9.604c1.002.02 1.282-.27 1.272-1.272l-.02-17.031v.01zm42.973-.19l.02-17.141c0-.862-.18-1.192-1.122-1.172a249.45 249.45 0 0 1-9.484 0c-.921-.02-1.132.291-1.132 1.162v34.403c0 .902.18 1.262 1.182 1.242h9.364c.991.02 1.192-.321 1.192-1.232l-.02-17.261zm-21.561.08l.02-17.131c0-1.012-.29-1.282-1.282-1.262h-9.244c-.841-.01-1.082.24-1.082 1.082v34.633c0 .832.24 1.092 1.082 1.082h9.244c1.001.02 1.282-.281 1.272-1.272l-.02-17.131h.01zM68.26 86.187h42.041c2.905 0 4.777 1.883 4.577 4.638-.12 1.733-1.152 2.815-2.684 3.516l-14.952 6.823-23.144 10.519-4.136 1.914c-1.722.881-3.335.741-5.037-.081l-12.478-5.88-27.37-12.824c-1.242-.581-2.474-1.162-3.175-2.434-1.482-2.725.471-5.981 3.715-6.171.881-.05 1.762-.01 2.644-.01H68.26v-.01zm-.13-65.329h41.671c2.263 0 2.714.461 2.714 2.755v4.198c-.021 1.412-.722 2.094-2.154 2.174h-.841-82.741c-2.594 0-3.155-.561-3.155-3.176v-3.356c.03-1.954.661-2.585 2.604-2.585H68.14l-.01-.01z"
fill="#0e4160"
/>
<path
d="M52.647 61.201l.02 17.031c0 1.002-.27 1.292-1.272 1.272-3.205-.06-6.409-.05-9.604 0-.861.01-1.172-.18-1.172-1.122V43.84c0-.902.26-1.152 1.152-1.132h9.724c1.001-.02 1.172.341 1.172 1.232l-.02 17.271v-.01zm42.973-.19l.02 17.261c0 .912-.2 1.252-1.192 1.232a243.59 243.59 0 0 0-9.364 0c-1.002.02-1.182-.351-1.182-1.242V43.86c0-.871.21-1.182 1.132-1.162a249.45 249.45 0 0 0 9.484 0c.941-.02 1.122.311 1.122 1.172l-.02 17.141zm-21.562.08l.02 17.131c0 .992-.27 1.292-1.272 1.272-3.074-.06-6.159-.05-9.243 0-.841.01-1.081-.25-1.081-1.082V43.78c0-.842.24-1.092 1.081-1.082h9.243c.992-.02 1.292.25 1.282 1.262l-.02 17.131h-.01z"
fill="#3cce49"
/>
</symbol>
</svg>
);
}

View File

@ -0,0 +1,75 @@
import React, { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
} & React.ComponentProps<"div">
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem(storageKey);
const valid: string[] = ["dark", "light", "system"];
return stored && valid.includes(stored) ? (stored as Theme) : defaultTheme;
})
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider value={value} {...props}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}

View File

@ -0,0 +1,25 @@
import { Moon, Sun } from "lucide-react"
import { useTheme } from "@/components/ThemeProvider"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const toggle = () => {
const isDark =
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
setTheme(isDark ? "light" : "dark")
}
return (
<button
onClick={toggle}
className="relative flex items-center justify-center w-9 h-9 rounded-full hover:bg-accent transition-colors"
aria-label="Toggle theme"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all duration-300 dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all duration-300 dark:rotate-0 dark:scale-100" />
</button>
)
}

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"whitespace-nowrap inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow-sm",
secondary:
"border-transparent bg-secondary text-secondary-foreground",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow-sm",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,180 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => {
// Swipe-to-close, mobile only
const closeRef = React.useRef<HTMLButtonElement>(null)
const dragStartY = React.useRef<number | null>(null)
const panelRef = React.useRef<HTMLDivElement | null>(null)
function onDragStart(clientY: number) {
dragStartY.current = clientY
if (panelRef.current) panelRef.current.style.transition = "none"
}
function onDragMove(clientY: number) {
if (dragStartY.current === null || !panelRef.current) return
const delta = Math.max(0, clientY - dragStartY.current)
panelRef.current.style.transform = `translateY(${delta}px)`
}
function onDragEnd(clientY: number) {
if (dragStartY.current === null || !panelRef.current) return
const delta = clientY - dragStartY.current
panelRef.current.style.transition = ""
panelRef.current.style.transform = ""
dragStartY.current = null
if (delta > 80) closeRef.current?.click()
}
function handlePointerDown(e: React.PointerEvent) {
if (!(e.target as HTMLElement).closest("[data-drag-handle]")) return
e.currentTarget.setPointerCapture(e.pointerId)
onDragStart(e.clientY)
}
function handlePointerMove(e: React.PointerEvent) {
if (dragStartY.current === null) return
onDragMove(e.clientY)
}
function handlePointerUp(e: React.PointerEvent) {
if (dragStartY.current === null) return
onDragEnd(e.clientY)
}
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={(node) => {
panelRef.current = node
if (typeof ref === "function") ref(node)
else if (ref) ref.current = node
}}
className={cn(
// Mobile: bottom sheet
"fixed bottom-0 left-0 right-0 z-50 w-full border-t border-x-0 border-b-0 bg-background shadow-2xl duration-300",
"rounded-t-2xl",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
// sm+: centered dialog
"sm:bottom-auto sm:left-[50%] sm:right-auto sm:top-[50%] sm:translate-x-[-50%] sm:translate-y-[-50%]",
"sm:border sm:rounded-2xl",
"sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95",
"sm:data-[state=closed]:slide-out-to-bottom-[0%] sm:data-[state=open]:slide-in-from-bottom-[0%]",
"sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=open]:slide-in-from-left-1/2",
"sm:data-[state=closed]:slide-out-to-top-[48%] sm:data-[state=open]:slide-in-from-top-[48%]",
className
)}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
{...props}
>
{/* Drag handle */}
<div
data-drag-handle
className="mx-auto flex justify-center pt-3 pb-1 cursor-grab active:cursor-grabbing touch-none sm:hidden"
>
<div className="h-1 w-10 rounded-full bg-border pointer-events-none" />
</div>
{children}
<DialogPrimitive.Close
ref={closeRef}
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-muted/60 text-muted-foreground opacity-80 ring-offset-background transition-all hover:bg-muted hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
})
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"font-display text-xl font-bold leading-tight tracking-tight sm:text-2xl",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground leading-relaxed", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
}

138
src/components/ui/sheet.tsx Normal file
View File

@ -0,0 +1,138 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

112
src/data/categories.ts Normal file
View File

@ -0,0 +1,112 @@
import merchantsData from "@/data/merchants.json";
import { countries } from "@/data/countries";
export const mainTypes = [
"All",
"Merchants",
"Apps",
"Hosted BTCPay",
"Non-Profits",
] as const;
export type MainType = (typeof mainTypes)[number];
// Maps display name → JSON type value
export const typeMap: Record<string, string> = {
Merchants: "merchants",
Apps: "apps",
"Hosted BTCPay": "hosted-btcpay",
"Non-Profits": "non-profits",
};
export const merchantSubTypes = [
"3d-printing",
"adult",
"appliances-furniture",
"art",
"books",
"cryptocurrency-paraphernalia",
"domains-hosting-vpns",
"education",
"electronics",
"fashion",
"food",
"gambling",
"gift-cards",
"health-household",
"holiday-travel",
"jewelry",
"payment-services",
"pets",
"services",
"software-video-games",
"sports",
"tools",
] as const;
export type MerchantSubType = (typeof merchantSubTypes)[number];
// Pretty display names for subtypes
export const subTypeLabels: Record<string, string> = {
"3d-printing": "3D Printing",
adult: "Adult",
"appliances-furniture": "Appliances & Furniture",
art: "Art",
books: "Books",
"cryptocurrency-paraphernalia": "Crypto Paraphernalia",
"domains-hosting-vpns": "Domains, Hosting & VPNs",
education: "Education",
electronics: "Electronics",
fashion: "Fashion",
food: "Food & Drink",
gambling: "Gambling",
"gift-cards": "Gift Cards",
"health-household": "Health & Household",
"holiday-travel": "Holiday & Travel",
jewelry: "Jewelry",
"payment-services": "Payment Services",
pets: "Pets",
services: "Services",
"software-video-games": "Software & Video Games",
sports: "Sports",
tools: "Tools",
};
export interface Merchant {
name: string;
url: string;
description: string;
type: string;
subType?: string;
country?: string;
twitter?: string;
github?: string;
onionUrl?: string;
}
// ── Country helpers (for Hosted BTCPay entries) ──────────────────────
/** Convert ISO 3166-1 alpha-2 code to flag emoji (e.g. "US" → "🇺🇸") */
export function countryFlag(code: string): string {
if (code === "GLOBAL") return "🌍";
return [...code.toUpperCase()].map((c) =>
String.fromCodePoint(0x1f1e6 - 65 + c.charCodeAt(0))
).join("");
}
/** Countries available in the Hosted BTCPay dropdown — derived from hosted entries */
export const hostedBtcpayCountries: Record<string, string> = Object.fromEntries(
Array.from(
new Set(
(merchantsData as Merchant[])
.filter((merchant) => merchant.type === "hosted-btcpay" && merchant.country)
.map((merchant) => merchant.country!.toUpperCase())
)
)
.sort((a, b) => {
if (a === "GLOBAL") return -1;
if (b === "GLOBAL") return 1;
return (countries[a] || a).localeCompare(countries[b] || b);
})
.map((code) => [code, countries[code] || code])
);

253
src/data/countries.ts Normal file
View File

@ -0,0 +1,253 @@
/** Full ISO 3166-1 alpha-2 country list */
export const countries: Record<string, string> = {
GLOBAL: "Global",
AF: "Afghanistan",
AX: "Åland Islands",
AL: "Albania",
DZ: "Algeria",
AS: "American Samoa",
AD: "Andorra",
AO: "Angola",
AI: "Anguilla",
AQ: "Antarctica",
AG: "Antigua & Barbuda",
AR: "Argentina",
AM: "Armenia",
AW: "Aruba",
AU: "Australia",
AT: "Austria",
AZ: "Azerbaijan",
BS: "Bahamas",
BH: "Bahrain",
BD: "Bangladesh",
BB: "Barbados",
BY: "Belarus",
BE: "Belgium",
BZ: "Belize",
BJ: "Benin",
BM: "Bermuda",
BT: "Bhutan",
BO: "Bolivia",
BA: "Bosnia & Herzegovina",
BW: "Botswana",
BV: "Bouvet Island",
BR: "Brazil",
IO: "British Indian Ocean Territory",
VG: "British Virgin Islands",
BN: "Brunei",
BG: "Bulgaria",
BF: "Burkina Faso",
BI: "Burundi",
KH: "Cambodia",
CM: "Cameroon",
CA: "Canada",
CV: "Cape Verde",
BQ: "Caribbean Netherlands",
KY: "Cayman Islands",
CF: "Central African Republic",
TD: "Chad",
CL: "Chile",
CN: "China",
CX: "Christmas Island",
CC: "Cocos (Keeling) Islands",
CO: "Colombia",
KM: "Comoros",
CG: "Congo - Brazzaville",
CD: "Congo - Kinshasa",
CK: "Cook Islands",
CR: "Costa Rica",
CI: "Côte dIvoire",
HR: "Croatia",
CU: "Cuba",
CW: "Curaçao",
CY: "Cyprus",
CZ: "Czechia",
DK: "Denmark",
DJ: "Djibouti",
DM: "Dominica",
DO: "Dominican Republic",
EC: "Ecuador",
EG: "Egypt",
SV: "El Salvador",
GQ: "Equatorial Guinea",
ER: "Eritrea",
EE: "Estonia",
SZ: "Eswatini",
ET: "Ethiopia",
FK: "Falkland Islands",
FO: "Faroe Islands",
FJ: "Fiji",
FI: "Finland",
FR: "France",
GF: "French Guiana",
PF: "French Polynesia",
TF: "French Southern Territories",
GA: "Gabon",
GM: "Gambia",
GE: "Georgia",
DE: "Germany",
GH: "Ghana",
GI: "Gibraltar",
GR: "Greece",
GL: "Greenland",
GD: "Grenada",
GP: "Guadeloupe",
GU: "Guam",
GT: "Guatemala",
GG: "Guernsey",
GN: "Guinea",
GW: "Guinea-Bissau",
GY: "Guyana",
HT: "Haiti",
HM: "Heard & McDonald Islands",
HN: "Honduras",
HK: "Hong Kong SAR China",
HU: "Hungary",
IS: "Iceland",
IN: "India",
ID: "Indonesia",
IR: "Iran",
IQ: "Iraq",
IE: "Ireland",
IM: "Isle of Man",
IL: "Israel",
IT: "Italy",
JM: "Jamaica",
JP: "Japan",
JE: "Jersey",
JO: "Jordan",
KZ: "Kazakhstan",
KE: "Kenya",
KI: "Kiribati",
KW: "Kuwait",
KG: "Kyrgyzstan",
LA: "Laos",
LV: "Latvia",
LB: "Lebanon",
LS: "Lesotho",
LR: "Liberia",
LY: "Libya",
LI: "Liechtenstein",
LT: "Lithuania",
LU: "Luxembourg",
MO: "Macao SAR China",
MG: "Madagascar",
MW: "Malawi",
MY: "Malaysia",
MV: "Maldives",
ML: "Mali",
MT: "Malta",
MH: "Marshall Islands",
MQ: "Martinique",
MR: "Mauritania",
MU: "Mauritius",
YT: "Mayotte",
MX: "Mexico",
FM: "Micronesia",
MD: "Moldova",
MC: "Monaco",
MN: "Mongolia",
ME: "Montenegro",
MS: "Montserrat",
MA: "Morocco",
MZ: "Mozambique",
MM: "Myanmar (Burma)",
NA: "Namibia",
NR: "Nauru",
NP: "Nepal",
NL: "Netherlands",
NC: "New Caledonia",
NZ: "New Zealand",
NI: "Nicaragua",
NE: "Niger",
NG: "Nigeria",
NU: "Niue",
NF: "Norfolk Island",
KP: "North Korea",
MK: "North Macedonia",
MP: "Northern Mariana Islands",
NO: "Norway",
OM: "Oman",
PK: "Pakistan",
PW: "Palau",
PS: "Palestinian Territories",
PA: "Panama",
PG: "Papua New Guinea",
PY: "Paraguay",
PE: "Peru",
PH: "Philippines",
PN: "Pitcairn Islands",
PL: "Poland",
PT: "Portugal",
PR: "Puerto Rico",
QA: "Qatar",
RE: "Réunion",
RO: "Romania",
RU: "Russia",
RW: "Rwanda",
WS: "Samoa",
SM: "San Marino",
ST: "São Tomé & Príncipe",
SA: "Saudi Arabia",
SN: "Senegal",
RS: "Serbia",
SC: "Seychelles",
SL: "Sierra Leone",
SG: "Singapore",
SX: "Sint Maarten",
SK: "Slovakia",
SI: "Slovenia",
SB: "Solomon Islands",
SO: "Somalia",
ZA: "South Africa",
GS: "South Georgia & South Sandwich Islands",
KR: "South Korea",
SS: "South Sudan",
ES: "Spain",
LK: "Sri Lanka",
BL: "St. Barthélemy",
SH: "St. Helena",
KN: "St. Kitts & Nevis",
LC: "St. Lucia",
MF: "St. Martin",
PM: "St. Pierre & Miquelon",
VC: "St. Vincent & Grenadines",
SD: "Sudan",
SR: "Suriname",
SJ: "Svalbard & Jan Mayen",
SE: "Sweden",
CH: "Switzerland",
SY: "Syria",
TW: "Taiwan",
TJ: "Tajikistan",
TZ: "Tanzania",
TH: "Thailand",
TL: "Timor-Leste",
TG: "Togo",
TK: "Tokelau",
TO: "Tonga",
TT: "Trinidad & Tobago",
TN: "Tunisia",
TR: "Türkiye",
TM: "Turkmenistan",
TC: "Turks & Caicos Islands",
TV: "Tuvalu",
UM: "U.S. Outlying Islands",
VI: "U.S. Virgin Islands",
UG: "Uganda",
UA: "Ukraine",
AE: "United Arab Emirates",
GB: "United Kingdom",
US: "United States",
UY: "Uruguay",
UZ: "Uzbekistan",
VU: "Vanuatu",
VA: "Vatican City",
VE: "Venezuela",
VN: "Vietnam",
WF: "Wallis & Futuna",
EH: "Western Sahara",
YE: "Yemen",
ZM: "Zambia",
ZW: "Zimbabwe",
};

1587
src/data/merchants.json Normal file

File diff suppressed because it is too large Load Diff

21
src/data/supporters.ts Normal file
View File

@ -0,0 +1,21 @@
export interface Supporter {
name: string;
url: string;
svgId: string;
width: number;
height: number;
fillCurrentColor?: boolean;
}
// width/height reflect actual viewBox aspect ratios so SVGs render at correct proportions
export const supporters: Supporter[] = [
{ name: "Spiral", url: "https://spiral.xyz/", svgId: "supporter-spiral", width: 100, height: 100 },
{ name: "OpenSats", url: "https://opensats.org/", svgId: "supporter-opensats", width: 200, height: 28 },
{ name: "Tether", url: "https://tether.to/", svgId: "supporter-tether", width: 111, height: 90, fillCurrentColor: true },
{ name: "HRF", url: "https://hrf.org/", svgId: "supporter-hrf", width: 180, height: 60, fillCurrentColor: true },
{ name: "LunaNode", url: "https://www.lunanode.com/", svgId: "supporter-lunanode", width: 100, height: 100 },
{ name: "Wallet of Satoshi", url: "https://walletofsatoshi.com/", svgId: "supporter-walletofsatoshi", width: 200, height: 38 },
{ name: "Coincards", url: "https://coincards.com/", svgId: "supporter-coincards", width: 128, height: 64 },
{ name: "IVPN", url: "https://www.ivpn.net/", svgId: "supporter-ivpn", width: 120, height: 42 },
{ name: "Unbank", url: "https://www.unbank.com/", svgId: "supporter-unbank", width: 180, height: 32, fillCurrentColor: true },
];

252
src/index.css Normal file
View File

@ -0,0 +1,252 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans: 'Inter', sans-serif;
--font-display: 'Space Grotesk', sans-serif;
/* Primary Brand Colors - as per design.btcpayserver.org */
--color-btcpay-primary-100: #fef3e6;
--color-btcpay-primary-200: #fcdcb5;
--color-btcpay-primary-300: #fbc584;
--color-btcpay-primary-400: #f9ae53;
--color-btcpay-primary-500: #f79621;
--color-btcpay-primary-600: #de7d08;
--color-btcpay-primary-700: #ac6106;
--color-btcpay-primary-800: #7b4504;
--color-btcpay-primary-900: #4a2a03;
/* Neutral Colors (Light) */
--color-btcpay-neutral-light-100: #f8f9fa;
--color-btcpay-neutral-light-200: #e9ecef;
--color-btcpay-neutral-light-300: #dee2e6;
--color-btcpay-neutral-light-400: #ced4da;
--color-btcpay-neutral-light-500: #8f979e;
--color-btcpay-neutral-light-600: #6c757d;
--color-btcpay-neutral-light-700: #495057;
--color-btcpay-neutral-light-800: #343a40;
--color-btcpay-neutral-light-900: #292929;
/* Neutral Colors (Dark) */
--color-btcpay-neutral-dark-100: #F0F6FC;
--color-btcpay-neutral-dark-200: #C9D1D9;
--color-btcpay-neutral-dark-300: #B1BAC4;
--color-btcpay-neutral-dark-400: #8B949E;
--color-btcpay-neutral-dark-500: #6E7681;
--color-btcpay-neutral-dark-600: #484F58;
--color-btcpay-neutral-dark-700: #30363D;
--color-btcpay-neutral-dark-800: #21262D;
--color-btcpay-neutral-dark-900: #0D1117;
/* Core Brand Colors */
--color-btcpay-brand-primary: #51b13e; /* Green - "Jungle Green" */
--color-btcpay-brand-secondary: #CEDC21; /* Lime/Yellow */
--color-btcpay-brand-tertiary: #1e7a44; /* Dark Green */
--color-btcpay-brand-dark: #0F3B21; /* Very Dark Green */
--color-btcpay-white: #ffffff;
--color-btcpay-black: #000000;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--color-sidebar: hsl(var(--sidebar));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
}
@layer base {
:root {
/* Light Mode - Uses Neutral Light scale */
/* Background: Neutral Light 100 (#f8f9fa) */
--background: 210 20% 98%;
/* Foreground: Neutral Light 900 (#292929) */
--foreground: 0 0% 16%;
/* Card: White */
--card: 0 0% 100%;
--card-foreground: 0 0% 16%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 16%;
/* Primary: BTCPay Green (#51b13e) - HSL: 110 48% 47% */
--primary: 110 48% 47%;
--primary-foreground: 0 0% 100%;
/* Secondary: Neutral Light 200 (#e9ecef) */
--secondary: 210 16% 93%;
--secondary-foreground: 0 0% 16%;
/* Muted: Neutral Light 200 */
--muted: 210 16% 93%;
--muted-foreground: 210 7% 40%; /* Neutral 600 */
/* Accent: Primary 100 or Brand Secondary */
--accent: 110 48% 95%;
--accent-foreground: 110 48% 30%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
/* Border: Neutral Light 300 (#dee2e6) */
--border: 210 14% 89%;
--input: 210 14% 89%;
--ring: 110 48% 47%;
--radius: 0.5rem;
--chart-1: 110 48% 47%;
--chart-2: 65 74% 50%;
--chart-3: 139 60% 30%;
--chart-4: 110 48% 70%;
--chart-5: 27 87% 67%;
--sidebar: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.6);
}
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
.dark {
/* Dark Mode - Uses Neutral Dark scale */
/* Background: Neutral Dark 900 (#0D1117) */
--background: 216 28% 7%;
/* Foreground: Neutral Dark 100 (#F0F6FC) */
--foreground: 210 40% 96%;
/* Card: Neutral Dark 800 (#21262D) */
--card: 215 15% 15%;
--card-foreground: 210 40% 96%;
--popover: 215 15% 15%;
--popover-foreground: 210 40% 96%;
/* Primary: BTCPay Green (#51b13e) */
--primary: 110 48% 47%;
--primary-foreground: 0 0% 100%;
/* Secondary: Neutral Dark 700 (#30363D) */
--secondary: 210 12% 21%;
--secondary-foreground: 210 40% 96%;
/* Muted: Neutral Dark 700 */
--muted: 210 12% 21%;
--muted-foreground: 215 12% 58%; /* Neutral Dark 400 */
/* Accent: Neutral Dark 600 or Dark Green tint */
--accent: 213 10% 28%;
--accent-foreground: 210 40% 96%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 96%;
/* Border: Neutral Dark 700 (#30363D) */
--border: 210 12% 21%;
--input: 210 12% 21%;
--ring: 110 48% 47%;
--chart-1: 110 48% 47%;
--chart-2: 65 74% 50%;
--chart-3: 139 60% 30%;
--chart-4: 110 48% 70%;
--chart-5: 340 75% 55%;
--sidebar: 216 28% 7%;
--sidebar-foreground: 210 40% 96%;
--sidebar-primary: 110 48% 47%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 210 12% 21%;
--sidebar-accent-foreground: 210 40% 96%;
--sidebar-border: 210 12% 21%;
--sidebar-ring: 110 48% 47%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply font-sans antialiased bg-background text-foreground;
}
h1, h2, h3, h4, h5, h6 {
@apply font-display font-bold;
}
}
@layer utilities {
.pb-safe {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
}

15
src/lib/url.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* Validates that a URL uses a safe protocol (http: or https:).
* Returns the URL unchanged if safe, or "#" as a safe fallback.
* Prevents javascript:, data:, vbscript:, and other dangerous protocol URLs.
*/
export function safeUrl(url: string): string {
try {
const parsed = new URL(url, window.location.origin);
return parsed.protocol === "https:" || parsed.protocol === "http:"
? url
: "#";
} catch {
return "#";
}
}

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

10
src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);

335
src/pages/Directory.tsx Normal file
View File

@ -0,0 +1,335 @@
import { useState, useMemo, useEffect, useCallback, useLayoutEffect, useRef } from "react";
import merchantsData from "@/data/merchants.json";
import type { Merchant } from "@/data/categories";
import { typeMap, mainTypes, merchantSubTypes, hostedBtcpayCountries } from "@/data/categories";
import MerchantCard from "@/components/MerchantCard";
import DirectoryFilters from "@/components/DirectoryFilters";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import SubmitForm from "@/components/SubmitForm";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Search, SlidersHorizontal, Loader2 } from "lucide-react";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
const INITIAL_BATCH = 24;
const BATCH_SIZE = 24;
// Fisher-Yates shuffle
function shuffle<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
// URL hash helpers for shareable filter state
const validTypes = new Set<string>(mainTypes);
const validSubs = new Set<string>([
...merchantSubTypes,
...Object.keys(hostedBtcpayCountries),
]);
function parseHash(): { type: string; sub: string | null; q: string } {
const params = new URLSearchParams(window.location.hash.slice(1));
const rawType = params.get("type") || "All";
const type = validTypes.has(rawType) ? rawType : "All";
const rawSub = params.get("sub") || null;
const sub = rawSub && validSubs.has(rawSub) ? rawSub : null;
const q = params.get("q") || "";
return { type, sub, q };
}
function updateHash(type: string, sub: string | null, q: string) {
const params = new URLSearchParams();
if (type !== "All") params.set("type", type);
if (sub) params.set("sub", sub);
if (q) params.set("q", q);
const hash = params.toString();
history.replaceState(null, "", hash ? `#${hash}` : window.location.pathname);
}
const merchants: Merchant[] = merchantsData as Merchant[];
export default function DirectoryPage() {
const initial = useMemo(() => parseHash(), []);
const [selectedType, setSelectedType] = useState(initial.type);
const [selectedSubType, setSelectedSubType] = useState<string | null>(initial.sub);
const [searchQuery, setSearchQuery] = useState(initial.q);
const [visibleCount, setVisibleCount] = useState(INITIAL_BATCH);
// Shuffle once on mount so no merchant is permanently buried
const shuffledMerchants = useMemo(() => shuffle(merchants), []);
const filteredMerchants = useMemo(() => {
const query = searchQuery.toLowerCase().trim();
return shuffledMerchants.filter((merchant) => {
// Type filter
const typeValue = typeMap[selectedType];
const matchesType = selectedType === "All" || merchant.type === typeValue;
// Subtype / country filter (country is used for Hosted BTCPay)
const isHostedBtcpay = selectedType === "Hosted BTCPay";
const matchesSubType =
!selectedSubType ||
(isHostedBtcpay
? merchant.country === selectedSubType
: merchant.subType === selectedSubType);
// Search filter
const matchesSearch =
!query ||
merchant.name.toLowerCase().includes(query) ||
merchant.description.toLowerCase().includes(query);
return matchesType && matchesSubType && matchesSearch;
});
}, [shuffledMerchants, selectedType, selectedSubType, searchQuery]);
// Reset visible count when filters change
useEffect(() => {
setVisibleCount(INITIAL_BATCH);
}, [selectedType, selectedSubType, searchQuery]);
// Sync filter state → URL hash
useEffect(() => {
updateHash(selectedType, selectedSubType, searchQuery);
}, [selectedType, selectedSubType, searchQuery]);
const visibleMerchants = filteredMerchants.slice(0, visibleCount);
const hasMore = visibleCount < filteredMerchants.length;
const [isLoadingMore, setIsLoadingMore] = useState(false);
const scrollAnchorRef = useRef<number>(0);
// useLayoutEffect fires synchronously after DOM mutation but BEFORE the browser
// paints — the only reliable place to clamp scroll position when new cards are
// inserted into the grid.
useLayoutEffect(() => {
if (scrollAnchorRef.current > 0) {
window.scrollTo({ top: scrollAnchorRef.current, behavior: "instant" });
scrollAnchorRef.current = 0;
}
}, [visibleCount]); // ← keyed on visibleCount, not on the loading flag
const loadMore = useCallback(() => {
scrollAnchorRef.current = window.scrollY;
setIsLoadingMore(true);
// Double rAF: first frame lets the loading button paint so the user sees
// feedback; second frame commits the new cards and the useLayoutEffect above
// restores scroll before the browser paints that frame.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setVisibleCount((prev) => prev + BATCH_SIZE);
setIsLoadingMore(false);
});
});
}, []);
const scrollToDirectory = () => {
document.getElementById("directory")?.scrollIntoView({ behavior: "smooth" });
};
const clearFilters = useCallback(() => {
setSelectedType("All");
setSelectedSubType(null);
setSearchQuery("");
}, []);
const [submitOpen, setSubmitOpen] = useState(false);
const openSubmit = useCallback(() => setSubmitOpen(true), []);
return (
<div className="min-h-screen flex flex-col bg-background font-sans selection:bg-primary/30">
<Navbar searchQuery={searchQuery} setSearchQuery={setSearchQuery} onSubmitClick={openSubmit} />
{/* Hero Section */}
<div className="relative pt-24 sm:pt-32 pb-12 sm:pb-20 overflow-hidden bg-background">
{/* Subtle Mesh Gradient Background */}
<div className="absolute inset-0 pointer-events-none" aria-hidden="true" style={{ background: "radial-gradient(ellipse at 50% 0%, hsl(var(--primary) / 0.08) 0%, transparent 70%)" }} />
<div className="container mx-auto px-4 sm:px-6 relative z-10 text-center">
<h1 className="text-4xl sm:text-5xl md:text-7xl font-display font-bold mb-4 sm:mb-6 tracking-tight bg-clip-text text-transparent bg-gradient-to-br from-foreground to-foreground/70 animate-in fade-in slide-in-from-bottom-6 duration-700 delay-100">
itcoin is <br className="hidden md:block" />
<span className="bg-gradient-to-r from-primary to-emerald-600 bg-clip-text text-transparent">Money.</span>
</h1>
<p className="text-base sm:text-xl text-muted-foreground mb-8 sm:mb-10 max-w-2xl mx-auto leading-relaxed animate-in fade-in slide-in-from-bottom-8 duration-700 delay-200 px-2">
Discover merchants, creators, and organizations accepting Bitcoin with BTCPay Server, and support circular economies.
</p>
<div className="flex flex-col sm:flex-row flex-wrap justify-center gap-3 sm:gap-4 animate-in fade-in slide-in-from-bottom-10 duration-700 delay-300 px-4 sm:px-0">
<Button
size="lg"
className="rounded-full px-8 h-11 sm:h-12 text-sm sm:text-base shadow-lg shadow-primary/20 hover:shadow-primary/40 transition-all duration-300 hover:scale-105"
onClick={scrollToDirectory}
>
Browse Directory
</Button>
<Button
size="lg"
variant="outline"
className="rounded-full px-8 h-11 sm:h-12 text-sm sm:text-base backdrop-blur-sm bg-background/50 border-input/50 hover:bg-background/80 transition-all duration-300"
onClick={openSubmit}
>
Submit Entry
</Button>
</div>
</div>
</div>
{/* Main Content */}
<main id="directory" className="flex-1 container mx-auto px-4 sm:px-6 pb-16 sm:pb-24 scroll-mt-16 sm:scroll-mt-20 pt-4 sm:pt-6">
<div className="flex flex-col lg:flex-row gap-6 sm:gap-8">
{/* Mobile Filter + Search Bar */}
<div className="lg:hidden sticky top-16 sm:top-20 z-40 -mx-1 bg-background/80 backdrop-blur-md p-3 sm:p-4 rounded-2xl border border-border/50 shadow-sm space-y-2 sm:space-y-3 mb-4 sm:mb-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
type="text"
placeholder="Search merchants..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-11 pl-9 pr-4 rounded-full bg-muted/50 border border-transparent focus:bg-background focus:border-primary/20 focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300 text-base sm:text-sm placeholder:text-muted-foreground"
/>
</div>
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2 font-medium w-full justify-start">
<SlidersHorizontal className="w-4 h-4" />
Filters
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[280px] sm:w-[400px] overflow-y-auto">
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
</SheetHeader>
<DirectoryFilters
selectedType={selectedType}
setSelectedType={setSelectedType}
selectedSubType={selectedSubType}
setSelectedSubType={setSelectedSubType}
onFilterChange={scrollToDirectory}
/>
</SheetContent>
</Sheet>
</div>
{/* Desktop Sidebar */}
<aside className="hidden lg:block w-64 flex-shrink-0">
<div className="sticky top-28 max-h-[calc(100vh-8rem)] overflow-y-auto pb-4 space-y-8 animate-in fade-in slide-in-from-left-6 duration-700 delay-500 scrollbar-hide [ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<DirectoryFilters
selectedType={selectedType}
setSelectedType={setSelectedType}
selectedSubType={selectedSubType}
setSelectedSubType={setSelectedSubType}
onFilterChange={scrollToDirectory}
/>
<div className="p-6 rounded-3xl bg-gradient-to-br from-primary/10 to-emerald-500/5 border border-primary/10 backdrop-blur-xl">
<h3 className="font-bold text-lg mb-2 text-primary">Submit a new entry</h3>
<p className="text-sm text-muted-foreground mb-4">Are you using BTCPay Server? Get listed in the directory.</p>
<Button className="w-full rounded-xl bg-primary/90 hover:bg-primary shadow-lg shadow-primary/10" size="sm" onClick={openSubmit}>
Submit Now
</Button>
</div>
</div>
</aside>
{/* Content Area */}
<div className="flex-1 min-w-0">
<div className="flex justify-between items-end mb-6 sm:mb-8 px-1 sm:px-2">
<div>
<h2 className="text-2xl sm:text-3xl font-display font-bold tracking-tight">
{selectedType === "All" ? "Discover" : selectedType}
</h2>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{visibleMerchants.map((merchant, i) => {
// First batch on initial load: staggered slide-up entrance
const isInitialBatch = visibleCount <= INITIAL_BATCH;
// Cards added by "Load More": simple fade-in, no slide
const isNewCard = !isInitialBatch && i >= visibleCount - BATCH_SIZE;
return (
<div
key={merchant.url}
style={isInitialBatch ? { animationDelay: `${i * 50}ms` } : undefined}
className={
isInitialBatch
? "animate-in fade-in slide-in-from-bottom-4 duration-500 fill-mode-backwards"
: isNewCard
? "animate-in fade-in duration-300"
: ""
}
>
<MerchantCard merchant={merchant} />
</div>
);
})}
</div>
{(hasMore || isLoadingMore) && (
<div className="flex justify-center mt-8 sm:mt-12">
<Button
variant="outline"
size="lg"
disabled={isLoadingMore}
className="rounded-full px-8 sm:px-10 h-11 sm:h-12 gap-3 font-semibold border-border/50 hover:bg-muted/50 hover:border-primary/30 transition-all duration-300 disabled:opacity-70"
onClick={loadMore}
>
{isLoadingMore ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Loading
</>
) : (
"Load More"
)}
</Button>
</div>
)}
{filteredMerchants.length === 0 && (
<div className="text-center py-16 sm:py-32 bg-card/60 rounded-2xl sm:rounded-3xl border border-dashed border-muted-foreground/30 backdrop-blur-sm">
<div className="bg-muted/50 w-12 h-12 sm:w-16 sm:h-16 rounded-full flex items-center justify-center mx-auto mb-4">
<SlidersHorizontal className="w-6 h-6 sm:w-8 sm:h-8 text-muted-foreground" />
</div>
<h3 className="text-lg sm:text-xl font-bold mb-2">No merchants found</h3>
<p className="text-sm sm:text-base text-muted-foreground mb-6 max-w-sm mx-auto px-4">We couldn't find any merchants matching your current filters.</p>
<Button variant="outline" className="rounded-full" onClick={clearFilters}>
Clear filters
</Button>
</div>
)}
</div>
</div>
</main>
<Footer />
<Dialog open={submitOpen} onOpenChange={setSubmitOpen}>
<DialogContent className="max-h-[92dvh] overflow-y-auto p-0 sm:max-w-2xl sm:max-h-[90vh]" onOpenAutoFocus={(e) => e.preventDefault()}>
<SubmitForm onSuccess={() => setSubmitOpen(false)} />
</DialogContent>
</Dialog>
</div>
);
}

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,58 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/**
* A better abstraction over CSS.
*
* @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present
* @website https://github.com/cssinjs/jss
* @license MIT
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.12.0
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
!function(e){function r(r){for(var n,i,l=r[0],a=r[1],c=r[2],f=0,s=[];f<l.length;f++)i=l[f],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,c||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var a=t[l];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={1:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="/";var l=this["webpackJsonpbtcpayserver-directory"]=this["webpackJsonpbtcpayserver-directory"]||[],a=l.push.bind(l);l.push=r,l=l.slice();for(var c=0;c<l.length;c++)r(l[c]);var p=a;t()}([]);
//# sourceMappingURL=runtime-main.b29dcf49.js.map

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,46 +0,0 @@
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<symbol id="note" viewBox="0 0 16 16"><path d="M14.2 16H1.8C.808 16 0 15.192 0 14.2V1.8C0 .808.808 0 1.8 0h12.4c.992 0 1.8.808 1.8 1.8v12.4c0 .992-.808 1.8-1.8 1.8zM1.8 1.2a.6.6 0 00-.6.6v12.4c0 .33.269.6.6.6h12.4a.6.6 0 00.6-.6V1.8a.6.6 0 00-.6-.6H1.8z" fill="currentColor"/><path d="M12 5.312H4a.6.6 0 010-1.2h8a.6.6 0 110 1.2zM12 8.6H4a.6.6 0 010-1.2h8a.6.6 0 010 1.2zm-4 3.288H4a.6.6 0 110-1.2h4a.6.6 0 010 1.2z" fill="currentColor"/></symbol>
<symbol id="onion" viewBox="0 0 61 91"><g fill="currentColor"><path d="m34.9 6.8-2.4 9.6C35.9 9.6 41.4 4.5 47.7 0c-4.6 5.3-8.8 10.6-11.3 16C40.7 9.9 46.5 6.6 53 4.3 44.3 12 37.4 20.4 32.2 28.7L28 26.9c.7-6.7 3.2-13.5 6.9-20.1z"/><path d="m31.7 28.3 2.9 1.5c-.3 1.9.1 6.1 2 7.1 8.4 5.2 16.2 10.8 19.3 16.5 11 19.9-7.7 38.4-24 36.6 8.8-6.5 11.4-19.9 8.1-34.6-1.3-5.7-3.4-10.9-7.1-16.8-1.6-2.7-1-6.3-1.2-10.3zM28.5 37.8c-.6 3.1-1.3 8.7-4 10.8-1.1.8-2.3 1.6-3.5 2.4-4.9 3.3-9.7 6.4-11.9 14.3-.5 1.7-.1 3.5.3 5.2 1.2 4.9 4.6 10.1 7.3 13.2 0 .1.5.5.5.6 2.2 2.6 2.9 3.4 11.3 5.3l-.2.9c-5.1-1.3-9.2-2.6-11.9-5.6 0-.1-.5-.5-.5-.5-2.8-3.2-6.3-8.6-7.5-13.7-.5-2-.9-3.6-.3-5.7 2.3-8.2 7.3-11.5 12.3-14.9 1.1-.7 2.5-1.4 3.6-2.3 2.3-1.5 3.4-6.2 4.5-10z"/><path d="M30.7 50.8c.1 3.5-.3 5.3.6 7.8.5 1.5 2.4 3.5 2.9 5.5.7 2.6 1.5 5.5 1.5 7.3 0 2-.1 5.8-1 9.8-.7 3.3-2.2 6.2-4.8 7.8-2.7-.5-5.8-1.5-7.6-3.1-3.6-3.1-6.7-8.3-7.1-12.8-.3-3.7 3.1-9.2 7.9-11.9 4-2.4 5-5 5.9-9.4-1.2 3.8-2.4 6.9-6.3 9-5.7 3-8.6 7.9-8.3 12.7.4 6.1 2.8 10.2 7.6 13.5 2 1.4 5.8 2.9 8.2 3.3V90c1.8-.3 4.1-3.3 5.3-7.2 1-3.6 1.4-8.1 1.3-11-.1-1.7-.8-5.3-2.2-8.6-.7-1.8-1.9-3.6-2.6-4.9-.9-1.5-.9-4.3-1.3-7.5z"/><path d="M30.1 64.5c.1 2.4 1 5.4 1.4 8.5.3 2.3.2 4.6.1 6.6-.1 2.3-.8 6.5-1.9 8.6-1-.5-1.4-1-2.1-1.8-.8-1.1-1.4-2.3-1.9-3.6-.4-1-.9-2.2-1.1-3.5-.3-2-.2-5.2 2.1-8.4 1.8-2.6 2.2-2.8 2.8-5.7-.8 2.6-1.4 2.9-3.3 5.1-2.1 2.4-2.4 6-2.4 8.9 0 1.2.5 2.6 1 3.8.5 1.3 1 2.7 1.7 3.7 1.1 1.6 2.5 2.6 3.2 2.7v-.1c1.3-1.5 2.1-2.9 2.4-4.4.3-1.8.4-3.5.6-5.6.2-1.8.1-4.1-.4-6.5-.6-3-1.7-6.1-2.2-8.3z"/><path d="M30.5 35c.1 3.5.3 10 1.3 12.6.3.9 2.8 4.7 4.5 9.4 1.2 3.2 1.5 6.2 1.7 7.1.8 3.8-.2 10.3-1.5 16.4-.7 3.3-3 7.4-5.6 9l-.5.9c1.5-.1 5.1-3.6 6.4-8.1 2.2-7.5 3-11 2-19.4-.1-.8-.5-3.6-1.8-6.5-1.9-4.5-4.6-8.8-4.9-9.7-.7-1.4-1.5-7.6-1.6-11.7z"/><path d="M31.7 28.6c-.2 3.6-.2 6.4.4 9.1.7 2.9 4.5 7.1 6.1 11.9 3 9.2 2.2 21.2.1 30.5-.8 3.3-4.6 8.1-8.5 9.6l2.8.7c1.5-.1 5.5-3.8 7.1-8 2.5-6.7 3-14.6 2-23-.1-.8-1.4-8-2.7-11-1.8-4.5-4.7-7.7-5.7-10.5-.8-2.1-1.1-7.7-.6-8.8l-1-.5z"/><path d="M51.7 46.3c-2.9-2.6-6.5-4.8-10.3-6.9-1.7-.9-6.9-5-5.1-10.8l-13.1-5.4-.9.7c4.4 7.9 2.1 12.1-.1 13.5-4.4 3-10.8 6.8-13.9 10.1C2.2 53.8.4 59.8 1 67.6c.6 10.1 7.9 18.5 17.8 21.8 4.3 1.4 8.3 1.6 12.7 1.6 7.1 0 14.5-1.9 19.8-6.3 5.7-4.7 9-11.8 9-19.1 0-7.3-3.1-14.3-8.6-19.3zm-1.9 36.9c-4.9 4-13.7 6.8-18.4 6.6-5.2-.3-10.3-1.1-14.8-3.3C8.7 82.7 3.5 74.4 3.1 67.7 2.4 54 9 50.1 15.1 45.1c3.4-2.8 8.2-4.2 10.9-9.2.5-1.1.8-3.5.2-6-.3-.9-1.5-3.9-2-4.6l9.8 4.3c-1.2 4.5 2.5 9.2 5.5 10.9 3 1.7 7.7 4.9 10.6 7.5 5.1 4.5 7.7 10.9 7.7 17.6 0 6.7-2.8 13.3-8 17.6z"/></g></symbol>
<symbol id="back" viewBox="0 0 21 18"><path d="M7.63754 1.10861L0.578503 8.16764C0.119666 8.62648 0.119666 9.37121 0.578503 9.83122L7.63754 16.8902C8.09637 17.3491 8.8411 17.3491 9.30111 16.8902C9.53053 16.6608 9.64583 16.3608 9.64583 16.0585C9.64583 15.7561 9.53053 15.4561 9.30111 15.2267L4.25038 10.1759H19.0579C19.7085 10.1759 20.2344 9.65004 20.2344 8.99943C20.2344 8.34882 19.7085 7.82293 19.0579 7.82293L4.25038 7.82293L9.30111 2.77219C9.53053 2.54277 9.64583 2.24276 9.64583 1.9404C9.64583 1.63804 9.53053 1.33803 9.30111 1.10861C8.84228 0.649771 8.09755 0.649771 7.63754 1.10861Z" fill="currentColor" /></symbol>
<symbol id="close" viewBox="0 0 16 16"><path d="M9.38526 8.08753L15.5498 1.85558C15.9653 1.43545 15.9653 0.805252 15.5498 0.385121C15.1342 -0.0350102 14.5108 -0.0350102 14.0952 0.385121L7.93072 6.61707L1.76623 0.315098C1.35065 -0.105033 0.727273 -0.105033 0.311688 0.315098C-0.103896 0.73523 -0.103896 1.36543 0.311688 1.78556L6.47618 8.0175L0.311688 14.2495C-0.103896 14.6696 -0.103896 15.2998 0.311688 15.7199C0.519481 15.93 0.796499 16 1.07355 16C1.35061 16 1.62769 15.93 1.83548 15.7199L7.99997 9.48797L14.1645 15.7199C14.3722 15.93 14.6493 16 14.9264 16C15.2034 16 15.4805 15.93 15.6883 15.7199C16.1039 15.2998 16.1039 14.6696 15.6883 14.2495L9.38526 8.08753Z" fill="currentColor"/></symbol>
<symbol id="copy" viewBox="0 0 24 24" fill="none"><path d="M20 6H8a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2Zm0 13a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v10Z" fill="currentColor"/><path d="M4 5a1 1 0 0 1 1-1h12a1 1 0 1 0 0-2H4a2 2 0 0 0-2 2v13a1 1 0 1 0 2 0V5Z" fill="currentColor"/></symbol>
<symbol id="caret-right" viewBox="0 0 24 24"><path d="M9.5 17L14.5 12L9.5 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></symbol>
<symbol id="caret-down" viewBox="0 0 24 24"><path d="M7 9.5L12 14.5L17 9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></symbol>
<symbol id="new-store" viewBox="0 0 32 32"><path d="M16 10V22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 16H10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle fill="none" cx="16" cy="16" r="15" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="new-wallet" viewBox="0 0 32 32"><path d="M16 10V22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 16H10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle fill="none" cx="16" cy="16" r="15" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="existing-wallet" viewBox="0 0 32 32"><g clip-path="url(#clip0)"><path d="M26.5362 7.08746H25.9614V3.25512C25.9614 2.10542 25.3865 1.14734 24.6201 0.572488C23.8536 -0.00236247 22.7039 -0.193979 21.7458 -0.00236246L4.11707 5.36291C2.00929 5.93776 0.667969 7.85392 0.667969 9.96171V12.0695V12.836V27.3988C0.667969 30.0815 2.77575 32.1893 5.45839 32.1893H26.5362C29.2189 32.1893 31.3267 30.0815 31.3267 27.3988V12.0695C31.3267 9.38686 29.2189 7.08746 26.5362 7.08746ZM4.69192 7.08746L22.129 1.91381C22.5123 1.72219 23.0871 1.91381 23.4704 2.10542C23.8536 2.29704 24.0452 2.87189 24.0452 3.25512V7.08746H5.45839C4.88354 7.08746 4.5003 7.27908 3.92545 7.47069C4.11707 7.27908 4.5003 7.08746 4.69192 7.08746ZM29.4105 27.2072C29.4105 28.7402 28.0692 30.0815 26.5362 30.0815H5.45839C3.92545 30.0815 2.58414 28.7402 2.58414 27.2072V12.836V11.8779C2.58414 10.3449 3.92545 9.00362 5.45839 9.00362H26.5362C28.0692 9.00362 29.4105 10.3449 29.4105 11.8779V27.2072Z" fill="currentColor"/><path d="M25.9591 21.6487C27.0174 21.6487 27.8753 20.7908 27.8753 19.7326C27.8753 18.6743 27.0174 17.8164 25.9591 17.8164C24.9009 17.8164 24.043 18.6743 24.043 19.7326C24.043 20.7908 24.9009 21.6487 25.9591 21.6487Z" fill="currentColor"/></g><defs><clipPath id="clip0"><rect width="32" height="32" fill="white"/></clipPath></defs></symbol>
<symbol id="hot-wallet" viewBox="0 0 32 32"><g clip-path="url(#clip0)"><path d="M26.5362 7.08746H25.9614V3.25512C25.9614 2.10542 25.3865 1.14734 24.6201 0.572488C23.8536 -0.00236247 22.7039 -0.193979 21.7458 -0.00236246L4.11707 5.36291C2.00929 5.93776 0.667969 7.85392 0.667969 9.96171V12.0695V12.836V27.3988C0.667969 30.0815 2.77575 32.1893 5.45839 32.1893H26.5362C29.2189 32.1893 31.3267 30.0815 31.3267 27.3988V12.0695C31.3267 9.38686 29.2189 7.08746 26.5362 7.08746ZM4.69192 7.08746L22.129 1.91381C22.5123 1.72219 23.0871 1.91381 23.4704 2.10542C23.8536 2.29704 24.0452 2.87189 24.0452 3.25512V7.08746H5.45839C4.88354 7.08746 4.5003 7.27908 3.92545 7.47069C4.11707 7.27908 4.5003 7.08746 4.69192 7.08746ZM29.4105 27.2072C29.4105 28.7402 28.0692 30.0815 26.5362 30.0815H5.45839C3.92545 30.0815 2.58414 28.7402 2.58414 27.2072V12.836V11.8779C2.58414 10.3449 3.92545 9.00362 5.45839 9.00362H26.5362C28.0692 9.00362 29.4105 10.3449 29.4105 11.8779V27.2072Z" fill="currentColor"/><path d="M25.9591 21.6487C27.0174 21.6487 27.8753 20.7908 27.8753 19.7326C27.8753 18.6743 27.0174 17.8164 25.9591 17.8164C24.9009 17.8164 24.043 18.6743 24.043 19.7326C24.043 20.7908 24.9009 21.6487 25.9591 21.6487Z" fill="currentColor"/></g><defs><clipPath id="clip0"><rect width="32" height="32" fill="white"/></clipPath></defs></symbol>
<symbol id="watchonly-wallet" viewBox="0 0 32 32"><g clip-path="url(#clip0)"><path d="M26.5362 7.08746H25.9614V3.25512C25.9614 2.10542 25.3865 1.14734 24.6201 0.572488C23.8536 -0.00236247 22.7039 -0.193979 21.7458 -0.00236246L4.11707 5.36291C2.00929 5.93776 0.667969 7.85392 0.667969 9.96171V12.0695V12.836V27.3988C0.667969 30.0815 2.77575 32.1893 5.45839 32.1893H26.5362C29.2189 32.1893 31.3267 30.0815 31.3267 27.3988V12.0695C31.3267 9.38686 29.2189 7.08746 26.5362 7.08746ZM4.69192 7.08746L22.129 1.91381C22.5123 1.72219 23.0871 1.91381 23.4704 2.10542C23.8536 2.29704 24.0452 2.87189 24.0452 3.25512V7.08746H5.45839C4.88354 7.08746 4.5003 7.27908 3.92545 7.47069C4.11707 7.27908 4.5003 7.08746 4.69192 7.08746ZM29.4105 27.2072C29.4105 28.7402 28.0692 30.0815 26.5362 30.0815H5.45839C3.92545 30.0815 2.58414 28.7402 2.58414 27.2072V12.836V11.8779C2.58414 10.3449 3.92545 9.00362 5.45839 9.00362H26.5362C28.0692 9.00362 29.4105 10.3449 29.4105 11.8779V27.2072Z" fill="currentColor"/><path d="M25.9591 21.6487C27.0174 21.6487 27.8753 20.7908 27.8753 19.7326C27.8753 18.6743 27.0174 17.8164 25.9591 17.8164C24.9009 17.8164 24.043 18.6743 24.043 19.7326C24.043 20.7908 24.9009 21.6487 25.9591 21.6487Z" fill="currentColor"/></g><defs><clipPath id="clip0"><rect width="32" height="32" fill="white"/></clipPath></defs></symbol>
<symbol id="hardware-wallet" viewBox="0 0 32 32"><rect x="18.9767" y="6.57031" width="6" height="8" rx="1" transform="rotate(-45 18.9767 6.57031)" fill="none" stroke="currentColor" stroke-width="2"/><path d="M3.8871 21.1057C2.71552 19.9341 2.71552 18.0346 3.8871 16.8631L15.888 4.86213C16.2785 4.4716 16.9117 4.4716 17.3022 4.86212L25.7898 13.3497C26.1804 13.7402 26.1804 14.3734 25.7898 14.7639L13.7889 26.7649C12.6173 27.9364 10.7178 27.9364 9.54626 26.7649L3.8871 21.1057Z" fill="none" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="xpub" viewBox="0 0 32 32"><path d="M21.3911 14.0298C20.4238 14.0396 19.4831 13.713 18.73 13.1059C17.9769 12.4988 17.4581 11.649 17.2622 10.7017C17.0664 9.75436 17.2057 8.76844 17.6564 7.91249C18.1071 7.05655 18.8412 6.38377 19.733 6.00919C20.6249 5.6346 21.6192 5.58148 22.5459 5.85891C23.4726 6.13634 24.2742 6.72709 24.8134 7.53015C25.3528 8.33319 25.5964 9.29866 25.5026 10.2614C25.4088 11.2242 24.9834 12.1246 24.2992 12.8084C23.5288 13.5829 22.4836 14.022 21.3911 14.0298ZM21.3911 7.5228C20.9277 7.52249 20.4746 7.65927 20.0888 7.91592C19.703 8.17258 19.4017 8.53764 19.223 8.96514C19.0442 9.39264 18.9959 9.86347 19.0842 10.3184C19.1724 10.7733 19.3933 11.1919 19.7189 11.5215C20.1653 11.9482 20.759 12.1863 21.3765 12.1863C21.9941 12.1863 22.5878 11.9482 23.0342 11.5215C23.359 11.1928 23.5796 10.7755 23.6683 10.3219C23.7571 9.86838 23.71 9.39874 23.5329 8.97182C23.356 8.54491 23.057 8.1797 22.6734 7.92194C22.2898 7.66419 21.8387 7.52534 21.3765 7.5228H21.3911Z" fill="currentColor"/><path d="M11.3293 29.9927C10.6744 29.9903 10.0472 29.7289 9.58436 29.2657L7.81038 27.4844L7.71586 27.608C7.18174 28.1431 6.45693 28.444 5.70089 28.4448C4.94485 28.4454 4.2195 28.1458 3.68441 27.6117C3.14933 27.0776 2.84834 26.3527 2.84766 25.5967C2.84698 24.8406 3.14666 24.1153 3.68078 23.5802L14.172 13.0672C13.4303 11.3826 13.301 9.49181 13.8065 7.722C14.312 5.9522 15.4204 4.41487 16.9399 3.37617C18.4594 2.33747 20.2942 1.8628 22.1268 2.03435C23.9594 2.20589 25.6743 3.01285 26.9746 4.31551C28.2749 5.61816 29.0787 7.3345 29.2469 9.16737C29.4152 11.0002 28.9372 12.8343 27.8957 14.3519C26.8543 15.8695 25.315 16.9751 23.5443 17.4774C21.7736 17.9797 19.883 17.847 18.1998 17.1023L15.0954 20.2067L16.3241 21.4354C16.5544 21.6639 16.7373 21.9357 16.8621 22.2352C16.9868 22.5346 17.0511 22.8559 17.0511 23.1803C17.0511 23.5048 16.9868 23.826 16.8621 24.1255C16.7373 24.425 16.5544 24.6968 16.3241 24.9252C15.8548 25.3728 15.2312 25.6225 14.5828 25.6225C13.9343 25.6225 13.3107 25.3728 12.8415 24.9252L11.6128 23.6893L11.2929 24.0092L13.0742 25.7904C13.4162 26.1364 13.6484 26.5757 13.742 27.0532C13.8354 27.5307 13.7859 28.0252 13.5996 28.4746C13.4132 28.9241 13.0984 29.3086 12.6946 29.5799C12.2908 29.8512 11.8158 29.9974 11.3293 30V29.9927ZM7.81038 25.296C7.92899 25.2954 8.04656 25.3182 8.15636 25.3631C8.26615 25.408 8.36599 25.4742 8.45017 25.5578L10.8712 27.9861C10.9961 28.1011 11.1596 28.1649 11.3293 28.1649C11.4989 28.1649 11.6624 28.1011 11.7873 27.9861C11.8474 27.9259 11.8949 27.8545 11.9274 27.7759C11.9598 27.6973 11.9764 27.613 11.9763 27.5281C11.9769 27.443 11.9604 27.3587 11.928 27.28C11.8955 27.2013 11.8477 27.1299 11.7873 27.07L9.36624 24.649C9.27688 24.5611 9.2068 24.4557 9.16049 24.3393C9.11417 24.2228 9.09263 24.098 9.09724 23.9728C9.09677 23.8536 9.12035 23.7354 9.16656 23.6255C9.21278 23.5156 9.2807 23.4161 9.36624 23.333L10.9948 21.7917C11.0792 21.707 11.1795 21.6399 11.2899 21.594C11.4003 21.5482 11.5187 21.5247 11.6383 21.5247C11.7578 21.5247 11.8762 21.5482 11.9865 21.594C12.0969 21.6399 12.1973 21.707 12.2817 21.7917L14.1575 23.6675C14.2802 23.7835 14.4428 23.8481 14.6119 23.8481C14.7808 23.8481 14.9434 23.7835 15.0663 23.6675C15.1276 23.6078 15.1766 23.5367 15.2102 23.4581C15.2439 23.3795 15.2618 23.2949 15.2626 23.2094C15.2605 23.0381 15.1929 22.8742 15.0735 22.7514L13.176 20.8465C13.0041 20.675 12.9073 20.4423 12.907 20.1995C12.9065 20.0802 12.93 19.9621 12.9763 19.8521C13.0225 19.7423 13.0904 19.6427 13.176 19.5597L17.3855 15.3501C17.5244 15.2094 17.7056 15.1183 17.9014 15.0906C18.0971 15.063 18.2965 15.1006 18.4688 15.1974C19.7515 15.9077 21.2475 16.131 22.6816 15.8261C24.1158 15.5214 25.3917 14.7091 26.2747 13.5387C27.1577 12.3681 27.5884 10.9182 27.4877 9.45553C27.3869 7.99281 26.7614 6.61566 25.7262 5.57732C24.691 4.539 23.3158 3.90933 21.8534 3.8041C20.391 3.69889 18.9398 4.1252 17.7666 5.00464C16.5935 5.88408 15.7773 7.15751 15.4681 8.59074C15.1591 10.024 15.3777 11.5206 16.0841 12.8055C16.1792 12.977 16.2157 13.1749 16.1881 13.369C16.1606 13.5632 16.0705 13.7432 15.9314 13.8815L4.96764 24.8307C4.8026 25.0286 4.71754 25.281 4.72917 25.5385C4.74081 25.796 4.84829 26.0397 5.0305 26.2219C5.21272 26.4041 5.4565 26.5117 5.71392 26.5233C5.97135 26.5349 6.22383 26.4499 6.42173 26.2848L7.14877 25.5578C7.32593 25.3863 7.56388 25.2922 7.81038 25.296Z" fill="currentColor"/></symbol>
<symbol id="wallet-file" viewBox="0 0 32 32"><path d="M5 1H20.8479L27 6.90258V31H5V1Z" fill="none" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="scan-qr" viewBox="0 0 32 32"><path d="M20 .875h10c.621 0 1.125.504 1.125 1.125v10m0 8v10c0 .621-.504 1.125-1.125 1.125H20m-8 0H2A1.125 1.125 0 01.875 30V20m0-8V2C.875 1.379 1.379.875 2 .875h10" stroke="currentColor" stroke-width="1.75" fill="none" fill-rule="evenodd"/></symbol>
<symbol id="seed" viewBox="0 0 32 32"><rect x="0.875" y="2.875" width="30.25" height="26.25" rx="1.125" fill="none" stroke="currentColor" stroke-width="1.75"/><rect x="5" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="7" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="14" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="5" y="21" width="9" height="4" rx="0.5" fill="currentColor"/><rect x="18" y="21" width="9" height="4" rx="0.5" fill="currentColor"/></symbol>
<symbol id="warning" viewBox="0 0 24 24"><path d="M12.337 3.101a.383.383 0 00-.674 0l-9.32 17.434a.383.383 0 00.338.564h18.638a.384.384 0 00.337-.564L12.337 3.101zM9.636 2.018c1.01-1.89 3.719-1.89 4.728 0l9.32 17.434a2.681 2.681 0 01-2.365 3.945H2.681a2.68 2.68 0 01-2.364-3.945L9.636 2.018zm3.896 15.25a1.532 1.532 0 11-3.064 0 1.532 1.532 0 013.064 0zm-.383-8.044a1.15 1.15 0 00-2.298 0v3.83a1.15 1.15 0 002.298 0v-3.83z" fill="currentColor"/></symbol>
<symbol id="github" viewBox="0 0 25 24"><path clip-rule="evenodd" d="M12.75.3c-6.6 0-12 5.4-12 12 0 5.325 3.45 9.825 8.175 11.4.6.075.825-.225.825-.6v-2.025C6.375 21.825 5.7 19.5 5.7 19.5c-.525-1.35-1.35-1.725-1.35-1.725-1.125-.75.075-.75.075-.75 1.2.075 1.875 1.2 1.875 1.2 1.05 1.8 2.775 1.275 3.525.975a2.59 2.59 0 0 1 .75-1.575c-2.7-.3-5.475-1.35-5.475-5.925 0-1.275.45-2.4 1.2-3.225-.15-.3-.525-1.5.15-3.15 0 0 .975-.3 3.3 1.2.975-.3 1.95-.375 3-.375s2.025.15 3 .375c2.325-1.575 3.3-1.275 3.3-1.275.675 1.65.225 2.85.15 3.15.75.825 1.2 1.875 1.2 3.225 0 4.575-2.775 5.625-5.475 5.925.45.375.825 1.125.825 2.25v3.3c0 .3.225.675.825.6a12.015 12.015 0 0 0 8.175-11.4c0-6.6-5.4-12-12-12z" fill="currentColor" fill-rule="evenodd"/></symbol>
<symbol id="twitter" viewBox="0 0 37 37"><path d="M36 18c0 9.945-8.055 18-18 18S0 27.945 0 18 8.055 0 18 0s18 8.055 18 18zm-21.294 9.495c7.983 0 12.348-6.615 12.348-12.348 0-.189 0-.378-.009-.558a8.891 8.891 0 0 0 2.169-2.25 8.808 8.808 0 0 1-2.493.684 4.337 4.337 0 0 0 1.908-2.403 8.788 8.788 0 0 1-2.754 1.053 4.319 4.319 0 0 0-3.168-1.368 4.34 4.34 0 0 0-4.338 4.338c0 .342.036.675.117.99a12.311 12.311 0 0 1-8.946-4.536 4.353 4.353 0 0 0-.585 2.178 4.32 4.32 0 0 0 1.935 3.609 4.263 4.263 0 0 1-1.962-.54v.054a4.345 4.345 0 0 0 3.483 4.257 4.326 4.326 0 0 1-1.962.072 4.333 4.333 0 0 0 4.05 3.015 8.724 8.724 0 0 1-6.426 1.791 12.091 12.091 0 0 0 6.633 1.962z" fill="currentColor"/></symbol>
<symbol id="telegram" viewBox="0 0 496 512" fill="currentColor"><path d="M248,8C111.033,8,0,119.033,0,256S111.033,504,248,504,496,392.967,496,256,384.967,8,248,8ZM362.952,176.66c-3.732,39.215-19.881,134.378-28.1,178.3-3.476,18.584-10.322,24.816-16.948,25.425-14.4,1.326-25.338-9.517-39.287-18.661-21.827-14.308-34.158-23.215-55.346-37.177-24.485-16.135-8.612-25,5.342-39.5,3.652-3.793,67.107-61.51,68.335-66.746.153-.655.3-3.1-1.154-4.384s-3.59-.849-5.135-.5q-3.283.746-104.608,69.142-14.845,10.194-26.894,9.934c-8.855-.191-25.888-5.006-38.551-9.123-15.531-5.048-27.875-7.717-26.8-16.291q.84-6.7,18.45-13.7,108.446-47.248,144.628-62.3c68.872-28.647,83.183-33.623,92.511-33.789,2.052-.034,6.639.474,9.61,2.885a10.452,10.452,0,0,1,3.53,6.716A43.765,43.765,0,0,1,362.952,176.66Z"/></symbol>
<symbol id="mattermost" viewBox="0 0 206 206"><path fill="currentColor" d="m163.012 19.596 1.082 21.794c17.667 19.519 24.641 47.161 15.846 73.14-13.129 38.782-56.419 59.169-96.693 45.535-40.272-13.633-62.278-56.124-49.15-94.905 8.825-26.066 31.275-43.822 57.276-48.524L105.422.038C61.592-1.15 20.242 26.056 5.448 69.76c-18.178 53.697 10.616 111.963 64.314 130.142 53.698 18.178 111.964-10.617 130.143-64.315 14.77-43.633-1.474-90.283-36.893-115.99"/><path fill="currentColor" d="m137.097 53.436-.596-17.531-.404-15.189s.084-7.322-.17-9.043a2.776 2.776 0 0 0-.305-.914l-.05-.109-.06-.094a2.378 2.378 0 0 0-1.293-1.07 2.382 2.382 0 0 0-1.714.078l-.033.014-.18.092a2.821 2.821 0 0 0-.75.518c-1.25 1.212-5.63 7.08-5.63 7.08l-9.547 11.82-11.123 13.563-19.098 23.75s-8.763 10.938-6.827 24.4c1.937 13.464 11.946 20.022 19.71 22.65 7.765 2.63 19.7 3.5 29.417-6.019 9.716-9.518 9.397-23.53 9.397-23.53l-.744-30.466z"/></symbol>
<symbol id="notifications" viewBox="0 0 24 24"><path d="M12.1933 0.992188C17.152 0.992188 19.2346 5.35582 19.7305 7.04178C20.3255 9.12442 19.8297 10.017 20.3255 12.893C20.6231 14.7773 21.6148 16.3641 22.4082 17.2567C22.7057 17.5542 22.4082 18.05 22.0115 18.05H13.2842H12.1933H2.07762C1.68092 18.05 1.3834 17.5542 1.68092 17.2567C2.37514 16.3641 3.46605 14.7773 3.76357 12.893C4.16026 10.017 3.76357 9.12442 4.35861 7.04178C4.85448 5.35582 7.03629 0.992188 12.1933 0.992188Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M16.2595 18.0488C16.2595 18.2472 16.3586 18.5447 16.3586 18.743C16.3586 21.1232 14.4743 23.0075 12.0942 23.0075C9.71401 23.0075 7.82971 21.1232 7.82971 18.743C7.82971 18.5447 7.82971 18.3463 7.82971 18.148" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /></symbol>
<symbol id="crowdfund" viewBox="0 0 24 24"><path d="M9.1638 12.4922C11.339 12.4922 13.1023 10.7288 13.1023 8.5537C13.1023 6.37854 11.339 4.61523 9.1638 4.61523C6.98865 4.61523 5.22534 6.37854 5.22534 8.5537C5.22534 10.7288 6.98865 12.4922 9.1638 12.4922Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M15.9331 18.0307C17.4965 18.0307 18.7638 16.7633 18.7638 15.1999C18.7638 13.6365 17.4965 12.3691 15.9331 12.3691C14.3697 12.3691 13.1023 13.6365 13.1023 15.1999C13.1023 16.7633 14.3697 18.0307 15.9331 18.0307Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M8.24067 19.3839C9.49818 19.3839 10.5176 18.3645 10.5176 17.107C10.5176 15.8495 9.49818 14.8301 8.24067 14.8301C6.98316 14.8301 5.96375 15.8495 5.96375 17.107C5.96375 18.3645 6.98316 19.3839 8.24067 19.3839Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="pointofsale" viewBox="0 0 24 24"><path d="M16.88 19.4303H7.12002C6.79748 19.4322 6.47817 19.3659 6.18309 19.2356C5.88802 19.1054 5.62385 18.9141 5.40795 18.6745C5.19206 18.4349 5.02933 18.1522 4.93046 17.8452C4.83159 17.5382 4.79882 17.2137 4.83431 16.8931L5.60002 10.1617C5.6311 9.88087 5.76509 9.62152 5.97615 9.43368C6.1872 9.24584 6.46035 9.14284 6.74288 9.14455H17.2572C17.5397 9.14284 17.8129 9.24584 18.0239 9.43368C18.235 9.62152 18.369 9.88087 18.4 10.1617L19.1429 16.8931C19.1782 17.2118 19.146 17.5343 19.0485 17.8398C18.951 18.1452 18.7903 18.4267 18.5769 18.666C18.3634 18.9053 18.1021 19.097 17.8097 19.2286C17.5174 19.3603 17.2006 19.429 16.88 19.4303Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M7.42859 9.14369C7.42859 7.93128 7.91022 6.76852 8.76753 5.91121C9.62484 5.0539 10.7876 4.57227 12 4.57227C13.2124 4.57227 14.3752 5.0539 15.2325 5.91121C16.0898 6.76852 16.5714 7.93128 16.5714 9.14369" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M9.14282 12.5723H14.8571" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="account" viewBox="0 0 24 24"><path d="M11.9336 12.777C14.1707 12.777 15.9843 10.9635 15.9843 8.72641C15.9843 6.48931 14.1707 4.67578 11.9336 4.67578C9.69653 4.67578 7.883 6.48931 7.883 8.72641C7.883 10.9635 9.69653 12.777 11.9336 12.777Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M5.38513 19.3247C5.38513 17.4345 6.87036 15.9492 8.76066 15.9492H15.2417C17.0645 15.9492 18.6172 17.4345 18.6172 19.3247" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="settings" viewBox="0 0 24 24"><path d="M9.81672 6.15385L10.3459 4.78769C10.4352 4.55639 10.5922 4.35743 10.7965 4.21688C11.0007 4.07632 11.2426 4.00073 11.4906 4H12.4998C12.7477 4.00073 12.9896 4.07632 13.1939 4.21688C13.3981 4.35743 13.5552 4.55639 13.6444 4.78769L14.1736 6.15385L15.9706 7.18769L17.4229 6.96615C17.6647 6.93333 17.9108 6.97314 18.13 7.08052C18.3491 7.1879 18.5314 7.35801 18.6536 7.56923L19.1459 8.43077C19.2721 8.64535 19.3302 8.89314 19.3126 9.14144C19.2951 9.38974 19.2026 9.62687 19.0475 9.82154L18.149 10.9662V13.0338L19.0721 14.1785C19.2272 14.3731 19.3197 14.6103 19.3373 14.8586C19.3548 15.1069 19.2967 15.3546 19.1706 15.5692L18.6783 16.4308C18.556 16.642 18.3737 16.8121 18.1546 16.9195C17.9354 17.0269 17.6893 17.0667 17.4475 17.0338L15.9952 16.8123L14.1983 17.8462L13.669 19.2123C13.5798 19.4436 13.4227 19.6426 13.2185 19.7831C13.0143 19.9237 12.7723 19.9993 12.5244 20H11.4906C11.2426 19.9993 11.0007 19.9237 10.7965 19.7831C10.5922 19.6426 10.4352 19.4436 10.3459 19.2123L9.81672 17.8462L8.01979 16.8123L6.56749 17.0338C6.32566 17.0667 6.07954 17.0269 5.86039 16.9195C5.64124 16.8121 5.45896 16.642 5.33672 16.4308L4.84441 15.5692C4.71826 15.3546 4.66013 15.1069 4.67771 14.8586C4.69529 14.6103 4.78774 14.3731 4.94287 14.1785L5.84133 13.0338V10.9662L4.91826 9.82154C4.76313 9.62687 4.67068 9.38974 4.6531 9.14144C4.63552 8.89314 4.69364 8.64535 4.81979 8.43077L5.3121 7.56923C5.43435 7.35801 5.61662 7.1879 5.83577 7.08052C6.05492 6.97314 6.30105 6.93333 6.54287 6.96615L7.99518 7.18769L9.81672 6.15385ZM9.53364 12C9.53364 12.4868 9.67801 12.9628 9.94848 13.3676C10.219 13.7724 10.6034 14.0879 11.0532 14.2742C11.503 14.4605 11.9979 14.5092 12.4754 14.4142C12.9529 14.3193 13.3915 14.0848 13.7357 13.7406C14.08 13.3963 14.3144 12.9577 14.4094 12.4802C14.5044 12.0027 14.4557 11.5078 14.2693 11.058C14.083 10.6082 13.7675 10.2238 13.3627 9.95331C12.9579 9.68283 12.482 9.53846 11.9952 9.53846C11.3423 9.53846 10.7162 9.7978 10.2546 10.2594C9.79298 10.7211 9.53364 11.3472 9.53364 12V12Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="server-settings" viewBox="0 0 24 24"><rect x="4.75" y="4.75" width="14.5" height="14.5" rx="3.25" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="m8 8 1.6 1.6L8 11.2" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="new" viewBox="0 0 24 24"><path d="M17 11H13V7C13 6.45 12.55 6 12 6C11.45 6 11 6.45 11 7V11H7C6.45 11 6 11.45 6 12C6 12.55 6.45 13 7 13H11V17C11 17.55 11.45 18 12 18C12.55 18 13 17.55 13 17V13H17C17.55 13 18 12.55 18 12C18 11.45 17.55 11 17 11Z" fill="currentColor"/></symbol>
<symbol id="wallet-onchain" viewBox="0 0 24 24"><path fill-rule="evenodd" clip-rule="evenodd" d="m16.05 12.26 1.08-1.08a3.05 3.05 0 0 0-4.31-4.32l-2.7 2.7a2.28 2.28 0 0 0 0 3.23l.54.54 1.08-1.07a1.52 1.52 0 0 1 0-2.15l2.15-2.16a1.52 1.52 0 0 1 2.6 1.08 1.52 1.52 0 0 1-.44 1.07l-1.08 1.08 1.08 1.08Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7.97 11.72 6.89 12.8a3.05 3.05 0 0 0 4.3 4.31l2.7-2.7a2.28 2.28 0 0 0 0-3.23l-.54-.54-1.07 1.08a1.52 1.52 0 0 1 0 2.15l-2.16 2.16a1.52 1.52 0 0 1-2.6-1.08 1.52 1.52 0 0 1 .45-1.08l1.08-1.08-1.08-1.07Z" fill="currentColor"/></symbol>
<symbol id="wallet-lightning" viewBox="0 0 24 24"><path d="M17.57 10.7c-.1-.23-.27-.34-.5-.34h-4.3l.5-3.76a.48.48 0 0 0-.33-.55.52.52 0 0 0-.66.17l-5.45 6.54a.59.59 0 0 0-.05.6c.1.17.27.28.49.28h4.3l-.49 3.76c-.05.22.11.5.33.55.06.05.17.05.22.05a.5.5 0 0 0 .44-.22l5.45-6.54c.1-.17.16-.39.05-.55Z" fill="currentColor"/></symbol>
<symbol id="payment-requests" viewBox="0 0 24 24"><path d="M12 19.3845C16.0784 19.3845 19.3846 16.0783 19.3846 11.9999C19.3846 7.92144 16.0784 4.61523 12 4.61523C7.92156 4.61523 4.61536 7.92144 4.61536 11.9999C4.61536 16.0783 7.92156 19.3845 12 19.3845Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M9.53845 14.216L14.2769 9.41602" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /><path d="M9.53845 10.707V14.2147H13.2308" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="pull-payments" viewBox="0 0 24 24"><path d="M12 20a8 8 0 1 1 0-16 8 8 0 0 1 0 16Zm0-15.19a7.2 7.2 0 0 0 0 14.38A7.2 7.2 0 0 0 12 4.8Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M9.48 14.85a.44.44 0 0 1-.3-.14c-.14-.16-.14-.43.05-.57l5.02-4.31c.16-.14.43-.14.57.05.14.17.14.44-.05.57l-5.05 4.29c-.05.08-.16.1-.24.1Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/><path d="M14.39 14.28a.4.4 0 0 1-.41-.4l.1-3.42-3.08-.17a.4.4 0 0 1-.38-.43c0-.22.19-.4.43-.38l3.47.19c.22 0 .38.19.38.4l-.13 3.83c.02.19-.17.38-.38.38Z" fill="currentColor" stroke="currentColor" stroke-width=".6"/></symbol>
<symbol id="payouts" viewBox="0 0 24 24"><path d="M8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M8.30774 8.92383H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /><path d="M8.30774 12H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /><path d="M8.30774 15.0156H12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="pay-button" viewBox="0 0 24 24"><path d="M8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M12 15.0777C13.6994 15.0777 15.0769 13.7001 15.0769 12.0008C15.0769 10.3014 13.6994 8.92383 12 8.92383C10.3007 8.92383 8.9231 10.3014 8.9231 12.0008C8.9231 13.7001 10.3007 15.0777 12 15.0777Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="invoice" viewBox="0 0 24 24"><path d="M8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M8.30774 8.92383H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /><path d="M8.30774 12H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /><path d="M8.30774 15.0156H12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="shopify" viewBox="0 0 32 32"><path transform="scale(.7) translate(5, 5)" d="m20.45 31.97 9.62-2.08-3.5-23.64c-.03-.16-.15-.26-.28-.26l-2.57-.18s-1.7-1.7-1.92-1.88a.41.41 0 0 0-.16-.1l-1.22 28.14zm-4.83-16.9s-1.09-.56-2.37-.56c-1.93 0-2 1.2-2 1.52 0 1.64 4.31 2.29 4.31 6.17 0 3.06-1.92 5.01-4.54 5.01-3.14 0-4.72-1.95-4.72-1.95l.86-2.78s1.66 1.42 3.04 1.42c.9 0 1.3-.72 1.3-1.24 0-2.16-3.54-2.26-3.54-5.81-.04-2.98 2.1-5.9 6.44-5.9 1.68 0 2.5.49 2.5.49l-1.26 3.62zM14.9 1.1c.17 0 .36.06.53.19-1.31.62-2.75 2.18-3.34 5.32-.88.28-1.73.54-2.52.77.69-2.38 2.36-6.26 5.33-6.26zm1.64 3.94v.18l-3.2.98c.63-2.37 1.79-3.53 2.79-3.96.26.67.41 1.57.41 2.8zm.72-2.98c.92.1 1.52 1.15 1.9 2.34-.46.15-.98.3-1.54.49v-.34c0-1-.13-1.82-.36-2.5zm3.99 1.72-.1.03c-.03 0-.39.1-.96.28-.56-1.65-1.56-3.16-3.34-3.16h-.16C16.2.28 15.56 0 15.02 0 10.88 0 8.9 5.17 8.28 7.8c-1.6.48-2.75.84-2.88.9-.9.28-.93.3-1.03 1.15-.1.62-2.44 18.75-2.44 18.75L20.01 32z" fill="currentColor"/></symbol>
<symbol id="done" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="12" fill="#51B13E"/><path d="m7 12.14 3.55 3.54L17.5 9" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="store" viewBox="0 0 24 24" fill="none"><path d="M19.049 10.2637V16.5294C19.049 17.7602 18.042 18.7672 16.8112 18.7672H7.24478C6.01401 18.7672 5.00702 17.7602 5.00702 16.5294V10.2637" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M9.45456 5.25649V9.08866C9.45456 10.2635 8.50351 11.2425 7.32868 11.2425H6.9091C5.00701 11.2425 3.74826 9.31243 4.50351 7.57817L5.06295 6.26348C5.34267 5.62012 5.95805 5.22852 6.62938 5.22852L9.45456 5.25649Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M14.5455 5.25781V9.08998C14.5455 10.2648 15.4965 11.2438 16.6713 11.2438H17.0909C18.993 11.2438 20.2518 9.31376 19.4965 7.57949L18.9371 6.26481C18.6574 5.64942 18.042 5.25781 17.3706 5.25781H14.5455Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M12 11.4949C10.6014 11.4949 9.48254 10.3481 9.48254 8.97746V5.28516H14.5455V8.97746C14.5455 10.3761 13.3986 11.4949 12 11.4949Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /></symbol>
<symbol id="home" viewBox="0 0 24 24" fill="none"><path d="M15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" /><path d="M7.56921 11.938H9.04614L10.5846 14.1534L13.3538 9.72266L14.8923 11.938H16.2461" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" /></symbol>
<symbol id="spark" viewBox="0 0 24 24"><path d="M17.57 10.7c-.1-.23-.27-.34-.5-.34h-4.3l.5-3.76a.48.48 0 0 0-.33-.55.52.52 0 0 0-.66.17l-5.45 6.54a.59.59 0 0 0-.05.6c.1.17.27.28.49.28h4.3l-.49 3.76c-.05.22.11.5.33.55.06.05.17.05.22.05a.5.5 0 0 0 .44-.22l5.45-6.54c.1-.17.16-.39.05-.55Z" fill="currentColor"/></symbol>
<symbol id="rtl" viewBox="0 0 610 524"><defs><path id="a" d="M.45.26h52.23V52H.45z"/></defs><g fill="none" fill-rule="evenodd"><path d="M418.62 107.6c1.95 4.6 2.73 8.24 2.63 10.24-7.56-4.56-18.93-8.43-25.14-9.54-4.03-.72 9.18-3.93 22.51-.7m10.99 169.33a476.42 476.42 0 0 1 14.43-2.8 447.25 447.25 0 0 0-5.98-3.71c-25.45-15.5-52.58-28.64-79.4-41.37-16.91-8.03-34.15-16.02-51.7-23.22 15.31-20.19 34.91-37.32 55.86-52 .02 0 .1-.07.23-.16 4.59 6.26 10.38 10.51 13.29 12.11 5.68 3.13 12.84 6.06 19.41 6.95a110.45 110.45 0 0 0 35.44-.55c1.06-.18 2.14-.3 3.2-.49a105.01 105.01 0 0 1 16.87-1.54c5.88-.1 8.79 1.14 9.48 1.32 2.1.54 3.89 1.44 4.88 2.64a39.71 39.71 0 0 0 3.7 4.13c3.96 3.93 7.89 6.11 13.58 6.74 7.34.8 12.53-2.07 16.2-6.83 1.74-2.25.9-5.78.71-6.47-.96-3.55-3.18-8.7-4.8-13.5a27.3 27.3 0 0 0-4.86-7.64l-2.42-2.56c-3.78-4-7.59-7.96-11.36-11.95-15.06-15.92-31.38-30.58-48.21-44.59l-5.24-4.36-3.82-3.16a1032.74 1032.74 0 0 0-6.06-4.96l-1.63-1.31c-.86-.7-2.81-2.34-4.46-3.66 5.46-5.44 9.86-8.83 17.68-13.95 1.12-.74 7.16-3.96 6.95-4.78-.13-.49-10.05-.38-21.4 1.09-4 .51-26.38 3.81-41.74 7.16-16.35 3.56-33.62 8.55-49.53 13.55-45.94 14.47-89.81 34.07-129.99 60.7-23.09 15.32-44.7 32.21-65.56 50.42a1066.8 1066.8 0 0 0-28.81 25.95 696.64 696.64 0 0 0-4.41 4.19c2 .37 4.01.76 6.02 1.18 16.6 3.4 33.04 7.97 49.02 12.53 19.53 5.59 39.3 11.6 58.41 19a583.18 583.18 0 0 0-31.63 32.21 765.5 765.5 0 0 0-53.37 66.63c-21.6 30.15-40.02 62.67-56.09 96.17a765.04 765.04 0 0 0-4.5 9.54c2.97-2.38 5.96-4.74 8.97-7.08 26.01-20.17 53.44-38.82 80.82-56.84 38.53-25.35 79.88-46.3 122.78-63.16 51.73-20.32 104.56-40.03 159.04-51.57" fill="currentColor"/></g></symbol>
<symbol id="thunderhub" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><g transform="translate(4, 4)"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><path d="M9 9h6v6H9zM9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></g></symbol>
<symbol id="lightningterminal" viewBox="0 0 28 55"><g fill="currentColor"><path d="m27.25 30.5-15.9 23.2a.84.84 0 1 1-1.38-.96l15.9-23.19a.84.84 0 1 1 1.38.96zm-2.09-4.13L9.63 49.08a.84.84 0 0 1-1.39-.95l15.54-22.71a.84.84 0 0 1 1.38.95zm-4.72-24.8L2.43 27.9h16.9l-1.14 1.68H.36a.84.84 0 0 1-.22-1.15L19 .62A.84.84 0 0 1 20.16.4c.4.26.52.78.28 1.19z"/><path d="M22.12 6.62 10.24 23.99H22l-1.15 1.68H7.05l1.14-1.68 12.53-18.3a.84.84 0 0 1 1.39.93z"/></g></symbol>
</svg>

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 312.812 312.812" style="enable-background:new 0 0 312.812 312.812;" xml:space="preserve">
<g>
<g>
<path d="M305.2,178.159c-3.2-0.8-6.4,0-9.2,2c-10.4,8.8-22.4,16-35.6,20.8c-12.4,4.8-26,7.2-40.4,7.2c-32.4,0-62-13.2-83.2-34.4
c-21.2-21.2-34.4-50.8-34.4-83.2c0-13.6,2.4-26.8,6.4-38.8c4.4-12.8,10.8-24.4,19.2-34.4c3.6-4.4,2.8-10.8-1.6-14.4
c-2.8-2-6-2.8-9.2-2c-34,9.2-63.6,29.6-84.8,56.8c-20.4,26.8-32.4,60-32.4,96c0,43.6,17.6,83.2,46.4,112s68,46.4,112,46.4
c36.8,0,70.8-12.8,98-34c27.6-21.6,47.6-52.4,56-87.6C314,184.959,310.8,179.359,305.2,178.159z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,16 +0,0 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 302.4 302.4" style="enable-background:new 0 0 302.4 302.4;" xml:space="preserve" width="512px" height="512px"><g><g>
<g>
<g>
<path d="M204.8,97.6C191.2,84,172,75.2,151.2,75.2s-40,8.4-53.6,22.4c-13.6,13.6-22.4,32.8-22.4,53.6s8.8,40,22.4,53.6 c13.6,13.6,32.8,22.4,53.6,22.4s40-8.4,53.6-22.4c13.6-13.6,22.4-32.8,22.4-53.6S218.8,111.2,204.8,97.6z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M151.2,51.6c5.6,0,10.4-4.8,10.4-10.4V10.4c0-5.6-4.8-10.4-10.4-10.4c-5.6,0-10.4,4.8-10.4,10.4v30.8 C140.8,46.8,145.6,51.6,151.2,51.6z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M236.4,80.8l22-22c4-4,4-10.4,0-14.4s-10.4-4-14.4,0l-22,22c-4,4-4,10.4,0,14.4C225.6,84.8,232,84.8,236.4,80.8z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M292,140.8h-30.8c-5.6,0-10.4,4.8-10.4,10.4c0,5.6,4.8,10.4,10.4,10.4H292c5.6,0,10.4-4.8,10.4-10.4 C302.4,145.6,297.6,140.8,292,140.8z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M236,221.6c-4-4-10.4-4-14.4,0s-4,10.4,0,14.4l22,22c4,4,10.4,4,14.4,0s4-10.4,0-14.4L236,221.6z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M151.2,250.8c-5.6,0-10.4,4.8-10.4,10.4V292c0,5.6,4.8,10.4,10.4,10.4c5.6,0,10.4-4.8,10.4-10.4v-30.8 C161.6,255.6,156.8,250.8,151.2,250.8z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M66,221.6l-22,22c-4,4-4,10.4,0,14.4s10.4,4,14.4,0l22-22c4-4,4-10.4,0-14.4C76.8,217.6,70.4,217.6,66,221.6z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M51.6,151.2c0-5.6-4.8-10.4-10.4-10.4H10.4c-5.6,0-10.4,4.8-10.4,10.4s4.8,10.4,10.4,10.4h30.8 C46.8,161.6,51.6,156.8,51.6,151.2z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
<path d="M66,80.8c4,4,10.4,4,14.4,0s4-10.4,0-14.4l-22-22c-4-4-10.4-4-14.4,0s-4,10.4,0,14.4L66,80.8z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/>
</g>
</g>
</g></g> </svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

27
tsconfig.app.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsBuildInfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"resolveJsonModule": true
},
"include": ["src"]
}

6
tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" }
]
}

13
vite.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});