diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 9999a3e..0000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -.next/ -.github/ -.DS_Store diff --git a/.env.example b/.env.example deleted file mode 100644 index de165b7..0000000 --- a/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -NEXT_PUBLIC_SHOW_GEYSER_FUND=false - -NEXT_PUBLIC_ENABLE_UMAMI=false -NEXT_PUBLIC_UMAMI_URL=https://your-umami-url.com -NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-umami-website-id - -NEXT_PUBLIC_ENABLE_IMGPROXY=false -NEXT_PUBLIC_IMGPROXY_URL=https://your-imgproxy-url.com \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 5990d9c..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - -version: 2 -updates: - - package-ecosystem: "" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "weekly" diff --git a/.github/prompts/alby-js-sdk-nwc-client.prompt.md b/.github/prompts/alby-js-sdk-nwc-client.prompt.md deleted file mode 100644 index 2664e5d..0000000 --- a/.github/prompts/alby-js-sdk-nwc-client.prompt.md +++ /dev/null @@ -1,253 +0,0 @@ -# Nostr Wallet Connect Documentation - -[Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect their existing wallets to your application allowing developers to easily integrate bitcoin lightning functionality. - -The Alby JS SDK allows you to easily integrate Nostr Wallet Connect into any JavaScript based application. - -There are two interfaces you can use to access NWC: - -- The `NWCClient` exposes the [NWC](https://nwc.dev/) interface directly, which is more powerful than the WebLN interface and is recommended if you plan to create an application outside of the web (e.g. native mobile/command line/server backend etc.). You can explore all the examples [here](../examples/nwc/client/). -- The `NostrWebLNProvider` exposes the [WebLN](https://webln.guide/) interface to execute lightning wallet functionality through Nostr Wallet Connect, such as sending payments, making invoices and getting the node balance. You can explore all the examples [here](../examples/nwc/). See also [Bitcoin Connect](https://github.com/getAlby/bitcoin-connect/) if you are developing a frontend web application. - -> See [NWCClient class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWCClient.html) - -## NWCClient - -### Initialization Options - -- `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) -- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://relay.getalby.com/v1) -- `walletPubkey`: pubkey of the Nostr Wallet Connect app -- `secret`: secret key to sign the request event (if not available window.nostr will be used) - -### NWCClient Quick start example - -```js -import { nwc } from "@getalby/sdk"; -const nwcClient = new nwc.NWCClient({ - nostrWalletConnectUrl: loadNWCUrl(), -}); // loadNWCUrl is some function to get the NWC URL from some (encrypted) storage - -// now you can send payments by passing in the invoice in an object -const response = await nwcClient.payInvoice({ invoice }); -``` - -### `static fromAuthorizationUrl()` - -Initialized a new `NWCClient` instance but generates a new random secret. The pubkey of that secret then needs to be authorized by the user (this can be initiated by redirecting the user to the `getAuthorizationUrl()` URL or calling `fromAuthorizationUrl()` to open an authorization popup. - -```js -const nwcClient = await nwc.NWCClient.fromAuthorizationUrl( - "https://my.albyhub.com/apps/new", - { - name: "My app name", - }, -); -``` - -The same options can be provided to getAuthorizationUrl() as fromAuthorizationUrl() - see [Manual Auth example](../examples/nwc/client/auth_manual.html) - -### Examples - -See [the NWC client examples directory](../examples/nwc/client) for a full list of examples. - -## NostrWebLNProvider - -> See [NostrWebLNProvider class documentation](https://getalby.github.io/js-sdk/classes/webln.NostrWebLNProvider.html) - -### Initialization Options - -- `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) -- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://relay.getalby.com/v1) -- `walletPubkey`: pubkey of the Nostr Wallet Connect app -- `secret`: secret key to sign the request event (if not available window.nostr will be used) -- `client`: initialize using an existing NWC client - -### WebLN Quick start example - -```js -import { webln } from "@getalby/sdk"; -const nwc = new webln.NostrWebLNProvider({ - nostrWalletConnectUrl: loadNWCUrl(), -}); // loadNWCUrl is some function to get the NWC URL from some (encrypted) storage -// or use the short version -const nwc = new webln.NWC({ nostrWalletConnectUrl: loadNWCUrl }); - -// connect to the relay -await nwc.enable(); - -// now you can send payments by passing in the invoice -const response = await nwc.sendPayment(invoice); -``` - -You can use NWC as a webln compatible object in your web app: - -```js -// you can set the window.webln object to use the universal API to send payments: -if (!window.webln) { - // prompt the user to connect to NWC - window.webln = new webln.NostrWebLNProvider({ - nostrWalletConnectUrl: loadNWCUrl, - }); - // now use any webln code -} -``` - -## NostrWebLNProvider Functions - -The goal of the Nostr Wallet Connect provider is to be API compatible with [webln](https://www.webln.guide/). Currently not all methods are supported - see the examples/nwc directory for a list of supported methods. - -### sendPayment(invoice: string) - -Takes a bolt11 invoice and calls the NWC `pay_invoice` function. -It returns a promise object that is resolved with an object with the preimage or is rejected with an error - -#### Payment Example - -```js -import { webln } from "@getalby/sdk"; -const nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: loadNWCUrl }); -await nwc.enable(); -const response = await nwc.sendPayment(invoice); -console.log(response); -``` - -#### getNostrWalletConnectUrl() - -Returns the `nostr+walletconnect://` URL which includes all the connection information (`walletPubkey`, `relayUrl`, `secret`) -This can be used to get and persist the string for later use. - -#### fromAuthorizationUrl(url: string, {name: string}) - -Opens a new window prompt with at the provided authorization URL to ask the user to authorize the app connection. -The promise resolves when the connection is authorized and the popup sends a `nwc:success` message or rejects when the prompt is closed. -Pass a `name` to the NWC provider describing the application. - -```js -import { webln } from "@getalby/sdk"; - -try { - const nwc = await webln.NostrWebLNProvider.fromAuthorizationUrl( - "https://my.albyhub.com/apps/new", - { - name: "My app name", - }, - ); -} catch (e) { - console.error(e); -} -await nwc.enable(); -let response; -try { - response = await nwc.sendPayment(invoice); - // if success then the response.preimage will be only - console.info(`payment successful, the preimage is ${response.preimage}`); -} catch (e) { - console.error(e.error || e); -} -``` - -#### React Native (Expo) - -Look at our [NWC React Native Expo Demo app](https://github.com/getAlby/nwc-react-native-expo) for how to use NWC in a React Native expo project. - -#### For Node.js - -To use this on Node.js you first must install `websocket-polyfill@0.0.3` and import it: - -```js -import "websocket-polyfill"; -// or: require('websocket-polyfill'); -``` - -if you get an `crypto is not defined` error, either upgrade to node.js 20 or above, or import it manually: - -```js -import * as crypto from 'crypto'; // or 'node:crypto' -globalThis.crypto = crypto as any; -//or: global.crypto = require('crypto'); -``` - -### Examples - -#### Defaults - -```js -import { webln } from "@getalby/sdk"; - -const nwc = new webln.NostrWebLNProvider(); // use defaults (connects to Alby's relay, will use window.nostr to sign the request) -await nwc.enable(); // connect to the relay -const response = await nwc.sendPayment(invoice); -console.log(response.preimage); - -nwc.close(); // close the websocket connection -``` - -#### Use a custom, user provided Nostr Wallet Connect URL - -```js -import { webln } from "@getalby/sdk"; - -const nwc = new webln.NostrWebLNProvider({ - nostrWalletConnectUrl: - "nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss://nostr.bitcoiner.social&secret=c60320b3ecb6c15557510d1518ef41194e9f9337c82621ddef3f979f668bfebd", -}); // use defaults -await nwc.enable(); // connect to the relay -const response = await nwc.sendPayment(invoice); -console.log(response.preimage); - -nwc.close(); // close the websocket connection -``` - -#### Generate a new NWC connect url using a locally-generated secret - -```js -// use the `fromAuthorizationUrl` helper which opens a popup to initiate the connection flow. -// the promise resolves once the NWC app returned. -const nwc = await webln.NostrWebLNProvider.fromAuthorizationUrl( - "https://my.albyhub.com/apps/new", - { - name: "My app name", - }, -); - -// ... enable and send a payment - -// if you want to get the connect url with the secret: -// const nostrWalletConnectUrl nwc.getNostrWalletConnectUrl(true) -``` - -The same options can be provided to getAuthorizationUrl() as fromAuthorizationUrl() - see [Manual Auth example](../examples/nwc/auth_manual.html) - -### Nostr Wallet Auth - -NWA is an alternative flow for lightning apps to easily initialize an NWC connection to mobile-first or self-custodial wallets, using a client-created secret. - -The app will generate an NWA URI which should be opened in the wallet, where the user can approve the connection. - -> See [NWAClient class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWAClient.html) - -#### Generating an NWA URI (For Client apps) - -```js -import { nwa } from "@getalby/sdk"; -const connectionUri = new nwa.NWAClient({ - relayUrl, - requestMethods: ["get_info"], -}).connectionUri; - -// then allow the user to copy it / display it as a QR code to the user -``` - -See full [NWA example](../examples/nwc/client/nwa.js) - -### Accepting and creating a connection from an NWA URI (For Wallet services) - -```js -import { nwa } from "@getalby/sdk"; -const nwaOptions = nwa.NWAClient.parseWalletAuthUrl(nwaUrl); - -// then use `nwaOptions` to display a confirmation page to the user and create a connection. -``` - -See full [NWA accept example](../examples/nwc/client/nwa-accept.js) for NWA URI parsing and handling. The implementation of actually creating the connection and showing a confirmation page to the user is wallet-specific. In the example, a connection will be created via the `create_connection` NWC command. \ No newline at end of file diff --git a/.github/prompts/alby-js-sdk.prompt.md b/.github/prompts/alby-js-sdk.prompt.md deleted file mode 100644 index dcdfaa7..0000000 --- a/.github/prompts/alby-js-sdk.prompt.md +++ /dev/null @@ -1,112 +0,0 @@ -# Alby JS SDK - -## Introduction - -Build zero-custody bitcoin payments into apps with a few lines of code. - -This JavaScript SDK is for interacting with a bitcoin lightning wallet via Nostr Wallet Connect or the Alby Wallet API. - -## Installing - -```bash -npm install @getalby/sdk -``` - -or - -```bash -yarn add @getalby/sdk -``` - -or for use without any build tools: - -```html - -``` - -## Lightning Network Client (LN) Documentation - -Quickly get started adding lightning payments to your app. - -> The easiest way to provide credentials is with an [NWC connection secret](https://nwc.dev). Get one in minutes by connecting to [Alby Hub](https://albyhub.com/), [coinos](https://coinos.io/apps/new), [Primal](https://primal.net/downloads), [lnwallet.app](https://lnwallet.app/), [Yakihonne](https://yakihonne.com/), [or other NWC-enabled wallets](https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets). - -For example, to make a payment: - -```js -import { LN, USD } from "@getalby/sdk"; -const credentials = "nostr+walletconnect://..."; // the NWC connection credentials -await new LN(credentials).pay("lnbc..."); // pay a lightning invoice -await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address -``` - -Or to request to receive a payment: - -```js -const request = await new LN(credentials).requestPayment(USD(1.0)); -// give request.invoice to someone... -request.onPaid(giveAccess); -``` - -[Read more](./docs/lnclient.md) - -For more flexibility you can access the underlying NWC wallet directly. Continue to read the Nostr Wallet Connect documentation below. - -## Nostr Wallet Connect Documentation - -[Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect their existing wallets to your application allowing developers to easily integrate bitcoin lightning functionality. - -For apps, see [NWC client and NWA client documentation](./docs/nwc.md) - -For wallet services, see [NWC wallet service documentation](./docs/nwc-wallet-service.md) - -## Alby Wallet API Documentation - -The [Alby OAuth API](https://guides.getalby.com/alby-wallet-api/reference/getting-started) allows you to integrate bitcoin lightning functionality provided by the Alby Wallet into your applications, with the Alby Wallet API. Send & receive payments, create invoices, setup payment webhooks, access Podcasting 2.0 and more! - -[Read more](./docs/oauth.md) - -### NodeJS - -#### Fetch - -**This library relies on a global fetch() function which will work in browsers and node v18.x or newer.** (In older versions you have to use a polyfill.) - -#### Websocket polyfill - -To use this on Node.js you first must install `websocket-polyfill@0.0.3` and import it: - -```js -import "websocket-polyfill"; -// or: require('websocket-polyfill'); -``` - -## WebLN Documentation - -The JS SDK also has some implementations for [WebLN](https://webln.guide). -See the [NostrWebLNProvider documentation](./docs/nwc.md) and [OAuthWebLNProvider documentation](./docs/oauth.md). - -## More Documentation - -Read the [auto-generated documentation](https://getalby.github.io/js-sdk/modules.html) - -## Need help? - -We are happy to help, please contact us or create an issue. - -- [Twitter: @getAlby](https://twitter.com/getAlby) -- [Telegram Community Chat](https://t.me/getAlby) -- e-mail to support@getalby.com -- [bitcoin.design](https://bitcoin.design/) Slack community [#lightning-browser-extension](https://bitcoindesign.slack.com/archives/C02591ADXM2) -- Read the [Alby developer guide](https://guides.getalby.com/developer-guide) to better understand how Alby packages and APIs can be used to power your app. - -## Thanks - -The client and the setup is inspired and based on the [twitter-api-typescript-sdk](https://github.com/twitterdev/twitter-api-typescript-sdk). - -## License - -MIT \ No newline at end of file diff --git a/.github/prompts/basics.prompt.md b/.github/prompts/basics.prompt.md deleted file mode 100644 index 34817a5..0000000 --- a/.github/prompts/basics.prompt.md +++ /dev/null @@ -1,15 +0,0 @@ -nostr-vibed is a modern web application starter built for experimenting with the Nostr protocol and related technologies. - -This project uses: -- **Next.js (App Router):** React-based framework for server-side rendering, routing, and API routes. -- **TailwindCSS:** Utility-first CSS framework for rapid UI development. -- **shadcn/ui:** Accessible, customizable React UI components built on top of Radix UI primitives. -- **next-themes:** Theme switching (light/dark) with system preference support. -- **lucide-react:** Icon library for modern SVG icons. -- **nostr-tools:** Utilities for working with the Nostr protocol (events, keys, relays, etc). - -Features: -- Clean, responsive layout with a customizable header and theme toggle. -- Ready-to-use component structure for rapid prototyping. -- Utility helpers and sensible defaults for styling and state management. -- Easily extendable for building Nostr dashboards or experiments. \ No newline at end of file diff --git a/.github/prompts/nostr-nip01.prompt.md b/.github/prompts/nostr-nip01.prompt.md deleted file mode 100644 index 08a6d98..0000000 --- a/.github/prompts/nostr-nip01.prompt.md +++ /dev/null @@ -1,177 +0,0 @@ -NIP-01 -====== - -Basic protocol flow description -------------------------------- - -`draft` `mandatory` - -This NIP defines the basic protocol that should be implemented by everybody. New NIPs may add new optional (or mandatory) fields and messages and features to the structures and flows described here. - -## Events and signatures - -Each user has a keypair. Signatures, public key, and encodings are done according to the [Schnorr signatures standard for the curve `secp256k1`](https://bips.xyz/340). - -The only object type that exists is the `event`, which has the following format on the wire: - -```jsonc -{ - "id": <32-bytes lowercase hex-encoded sha256 of the serialized event data>, - "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, - "created_at": , - "kind": , - "tags": [ - [...], - // ... - ], - "content": , - "sig": <64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field> -} -``` - -To obtain the `event.id`, we `sha256` the serialized event. The serialization is done over the UTF-8 JSON-serialized string (which is described below) of the following structure: - -``` -[ - 0, - , - , - , - , - -] -``` - -To prevent implementation differences from creating a different event ID for the same event, the following rules MUST be followed while serializing: -- UTF-8 should be used for encoding. -- Whitespace, line breaks or other unnecessary formatting should not be included in the output JSON. -- The following characters in the content field must be escaped as shown, and all other characters must be included verbatim: - - A line break (`0x0A`), use `\n` - - A double quote (`0x22`), use `\"` - - A backslash (`0x5C`), use `\\` - - A carriage return (`0x0D`), use `\r` - - A tab character (`0x09`), use `\t` - - A backspace, (`0x08`), use `\b` - - A form feed, (`0x0C`), use `\f` - -### Tags - -Each tag is an array of one or more strings, with some conventions around them. Take a look at the example below: - -```jsonc -{ - "tags": [ - ["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com"], - ["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"], - ["a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com"], - ["alt", "reply"], - // ... - ], - // ... -} -``` - -The first element of the tag array is referred to as the tag _name_ or _key_ and the second as the tag _value_. So we can safely say that the event above has an `e` tag set to `"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"`, an `alt` tag set to `"reply"` and so on. All elements after the second do not have a conventional name. - -This NIP defines 3 standard tags that can be used across all event kinds with the same meaning. They are as follows: - -- The `e` tag, used to refer to an event: `["e", <32-bytes lowercase hex of the id of another event>, , <32-bytes lowercase hex of the author's pubkey, optional>]` -- The `p` tag, used to refer to another user: `["p", <32-bytes lowercase hex of a pubkey>, ]` -- The `a` tag, used to refer to an addressable or replaceable event - - for an addressable event: `["a", ":<32-bytes lowercase hex of a pubkey>:", ]` - - for a normal replaceable event: `["a", ":<32-bytes lowercase hex of a pubkey>:", ]` (note: include the trailing colon) - -As a convention, all single-letter (only english alphabet letters: a-z, A-Z) key tags are expected to be indexed by relays, such that it is possible, for example, to query or subscribe to events that reference the event `"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"` by using the `{"#e": ["5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"]}` filter. Only the first value in any given tag is indexed. - -### Kinds - -Kinds specify how clients should interpret the meaning of each event and the other fields of each event (e.g. an `"r"` tag may have a meaning in an event of kind 1 and an entirely different meaning in an event of kind 10002). Each NIP may define the meaning of a set of kinds that weren't defined elsewhere. [NIP-10](10.md), for instance, especifies the `kind:1` text note for social media applications. - -This NIP defines one basic kind: - -- `0`: **user metadata**: the `content` is set to a stringified JSON object `{name: , about: , picture: }` describing the user who created the event. [Extra metadata fields](24.md#kind-0) may be set. A relay may delete older events once it gets a new one for the same pubkey. - -And also a convention for kind ranges that allow for easier experimentation and flexibility of relay implementation: - -- for kind `n` such that `1000 <= n < 10000 || 4 <= n < 45 || n == 1 || n == 2`, events are **regular**, which means they're all expected to be stored by relays. -- for kind `n` such that `10000 <= n < 20000 || n == 0 || n == 3`, events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event MUST be stored by relays, older versions MAY be discarded. -- for kind `n` such that `20000 <= n < 30000`, events are **ephemeral**, which means they are not expected to be stored by relays. -- for kind `n` such that `30000 <= n < 40000`, events are **addressable** by their `kind`, `pubkey` and `d` tag value -- which means that, for each combination of `kind`, `pubkey` and the `d` tag value, only the latest event MUST be stored by relays, older versions MAY be discarded. - -In case of replaceable events with the same timestamp, the event with the lowest id (first in lexical order) should be retained, and the other discarded. - -When answering to `REQ` messages for replaceable events such as `{"kinds":[0],"authors":[]}`, even if the relay has more than one version stored, it SHOULD return just the latest one. - -These are just conventions and relay implementations may differ. - -## Communication between clients and relays - -Relays expose a websocket endpoint to which clients can connect. Clients SHOULD open a single websocket connection to each relay and use it for all their subscriptions. Relays MAY limit number of connections from specific IP/client/etc. - -### From client to relay: sending events and creating subscriptions - -Clients can send 3 types of messages, which must be JSON arrays, according to the following patterns: - - * `["EVENT", ]`, used to publish events. - * `["REQ", , , , ...]`, used to request events and subscribe to new updates. - * `["CLOSE", ]`, used to stop previous subscriptions. - -`` is an arbitrary, non-empty string of max length 64 chars. It represents a subscription per connection. Relays MUST manage ``s independently for each WebSocket connection. ``s are not guaranteed to be globally unique. - -`` is a JSON object that determines what events will be sent in that subscription, it can have the following attributes: - -```json -{ - "ids": , - "authors": , - "kinds": , - "#": , - "since": = to this to pass>, - "until": , - "limit": -} -``` - -Upon receiving a `REQ` message, the relay SHOULD return events that match the filter. Any new events it receives SHOULD be sent to that same websocket until the connection is closed, a `CLOSE` event is received with the same ``, or a new `REQ` is sent using the same `` (in which case a new subscription is created, replacing the old one). - -Filter attributes containing lists (`ids`, `authors`, `kinds` and tag filters like `#e`) are JSON arrays with one or more values. At least one of the arrays' values must match the relevant field in an event for the condition to be considered a match. For scalar event attributes such as `authors` and `kind`, the attribute from the event must be contained in the filter list. In the case of tag attributes such as `#e`, for which an event may have multiple values, the event and filter condition values must have at least one item in common. - -The `ids`, `authors`, `#e` and `#p` filter lists MUST contain exact 64-character lowercase hex values. - -The `since` and `until` properties can be used to specify the time range of events returned in the subscription. If a filter includes the `since` property, events with `created_at` greater than or equal to `since` are considered to match the filter. The `until` property is similar except that `created_at` must be less than or equal to `until`. In short, an event matches a filter if `since <= created_at <= until` holds. - -All conditions of a filter that are specified must match for an event for it to pass the filter, i.e., multiple conditions are interpreted as `&&` conditions. - -A `REQ` message may contain multiple filters. In this case, events that match any of the filters are to be returned, i.e., multiple filters are to be interpreted as `||` conditions. - -The `limit` property of a filter is only valid for the initial query and MUST be ignored afterwards. When `limit: n` is present it is assumed that the events returned in the initial query will be the last `n` events ordered by the `created_at`. Newer events should appear first, and in the case of ties the event with the lowest id (first in lexical order) should be first. It is safe to return less events than `limit` specifies, but it is expected that relays do not return (much) more events than requested so clients don't get unnecessarily overwhelmed by data. - -### From relay to client: sending events and notices - -Relays can send 5 types of messages, which must also be JSON arrays, according to the following patterns: - - * `["EVENT", , ]`, used to send events requested by clients. - * `["OK", , , ]`, used to indicate acceptance or denial of an `EVENT` message. - * `["EOSE", ]`, used to indicate the _end of stored events_ and the beginning of events newly received in real-time. - * `["CLOSED", , ]`, used to indicate that a subscription was ended on the server side. - * `["NOTICE", ]`, used to send human-readable error messages or other things to clients. - -This NIP defines no rules for how `NOTICE` messages should be sent or treated. - -- `EVENT` messages MUST be sent only with a subscription ID related to a subscription previously initiated by the client (using the `REQ` message above). -- `OK` messages MUST be sent in response to `EVENT` messages received from clients, they must have the 3rd parameter set to `true` when an event has been accepted by the relay, `false` otherwise. The 4th parameter MUST always be present, but MAY be an empty string when the 3rd is `true`, otherwise it MUST be a string formed by a machine-readable single-word prefix followed by a `:` and then a human-readable message. Some examples: - * `["OK", "b1a649ebe8...", true, ""]` - * `["OK", "b1a649ebe8...", true, "pow: difficulty 25>=24"]` - * `["OK", "b1a649ebe8...", true, "duplicate: already have this event"]` - * `["OK", "b1a649ebe8...", false, "blocked: you are banned from posting here"]` - * `["OK", "b1a649ebe8...", false, "blocked: please register your pubkey at https://my-expensive-relay.example.com"]` - * `["OK", "b1a649ebe8...", false, "rate-limited: slow down there chief"]` - * `["OK", "b1a649ebe8...", false, "invalid: event creation date is too far off from the current time"]` - * `["OK", "b1a649ebe8...", false, "pow: difficulty 26 is less than 30"]` - * `["OK", "b1a649ebe8...", false, "restricted: not allowed to write."]` - * `["OK", "b1a649ebe8...", false, "error: could not connect to the database"]` -- `CLOSED` messages MUST be sent in response to a `REQ` when the relay refuses to fulfill it. It can also be sent when a relay decides to kill a subscription on its side before a client has disconnected or sent a `CLOSE`. This message uses the same pattern of `OK` messages with the machine-readable prefix and human-readable message. Some examples: - * `["CLOSED", "sub1", "unsupported: filter contains unknown elements"]` - * `["CLOSED", "sub1", "error: could not connect to the database"]` - * `["CLOSED", "sub1", "error: shutting down idle subscription"]` -- The standardized machine-readable prefixes for `OK` and `CLOSED` are: `duplicate`, `pow`, `blocked`, `rate-limited`, `invalid`, `restricted`, and `error` for when none of that fits. \ No newline at end of file diff --git a/.github/prompts/nostr-nip02.prompt.md b/.github/prompts/nostr-nip02.prompt.md deleted file mode 100644 index 8354bf0..0000000 --- a/.github/prompts/nostr-nip02.prompt.md +++ /dev/null @@ -1,78 +0,0 @@ -NIP-02 -====== - -Follow List ------------ - -`final` `optional` - -A special event with kind `3`, meaning "follow list" is defined as having a list of `p` tags, one for each of the followed/known profiles one is following. - -Each tag entry should contain the key for the profile, a relay URL where events from that key can be found (can be set to an empty string if not needed), and a local name (or "petname") for that profile (can also be set to an empty string or not provided), i.e., `["p", <32-bytes hex key>,
, ]`. - -The `.content` is not used. - -For example: - -```jsonc -{ - "kind": 3, - "tags": [ - ["p", "91cf9..4e5ca", "wss://alicerelay.com/", "alice"], - ["p", "14aeb..8dad4", "wss://bobrelay.com/nostr", "bob"], - ["p", "612ae..e610f", "ws://carolrelay.com/ws", "carol"] - ], - "content": "", - // other fields... -} -``` - -Every new following list that gets published overwrites the past ones, so it should contain all entries. Relays and clients SHOULD delete past following lists as soon as they receive a new one. - -Whenever new follows are added to an existing list, clients SHOULD append them to the end of the list, so they are stored in chronological order. - -## Uses - -### Follow list backup - -If one believes a relay will store their events for sufficient time, they can use this kind-3 event to backup their following list and recover on a different device. - -### Profile discovery and context augmentation - -A client may rely on the kind-3 event to display a list of followed people by profiles one is browsing; make lists of suggestions on who to follow based on the follow lists of other people one might be following or browsing; or show the data in other contexts. - -### Relay sharing - -A client may publish a follow list with good relays for each of their follows so other clients may use these to update their internal relay lists if needed, increasing censorship-resistance. - -### Petname scheme - -The data from these follow lists can be used by clients to construct local ["petname"](http://www.skyhunter.com/marcs/petnames/IntroPetNames.html) tables derived from other people's follow lists. This alleviates the need for global human-readable names. For example: - -A user has an internal follow list that says - -```json -[ - ["p", "21df6d143fb96c2ec9d63726bf9edc71", "", "erin"] -] -``` - -And receives two follow lists, one from `21df6d143fb96c2ec9d63726bf9edc71` that says - -```json -[ - ["p", "a8bb3d884d5d90b413d9891fe4c4e46d", "", "david"] -] -``` - -and another from `a8bb3d884d5d90b413d9891fe4c4e46d` that says - -```json -[ - ["p", "f57f54057d2a7af0efecc8b0b66f5708", "", "frank"] -] -``` - -When the user sees `21df6d143fb96c2ec9d63726bf9edc71` the client can show _erin_ instead; -When the user sees `a8bb3d884d5d90b413d9891fe4c4e46d` the client can show _david.erin_ instead; -When the user sees `f57f54057d2a7af0efecc8b0b66f5708` the client can show _frank.david.erin_ instead. diff --git a/.github/prompts/nostr-nip07.prompt.md b/.github/prompts/nostr-nip07.prompt.md deleted file mode 100644 index ae566bb..0000000 --- a/.github/prompts/nostr-nip07.prompt.md +++ /dev/null @@ -1,32 +0,0 @@ -NIP-07 -====== - -`window.nostr` capability for web browsers ------------------------------------------- - -`draft` `optional` - -The `window.nostr` object may be made available by web browsers or extensions and websites or web-apps may make use of it after checking its availability. - -That object must define the following methods: - -``` -async window.nostr.getPublicKey(): string // returns a public key as hex -async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event // takes an event object, adds `id`, `pubkey` and `sig` and returns it -``` - -Aside from these two basic above, the following functions can also be implemented optionally: -``` -async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated) -async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated) -async window.nostr.nip44.encrypt(pubkey, plaintext): string // returns ciphertext as specified in nip-44 -async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes ciphertext as specified in nip-44 -``` - -### Recommendation to Extension Authors -To make sure that the `window.nostr` is available to nostr clients on page load, the authors who create Chromium and Firefox extensions should load their scripts by specifying `"run_at": "document_end"` in the extension's manifest. - - -### Implementation - -See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions. \ No newline at end of file diff --git a/.github/prompts/nostr-nip09.prompt.md b/.github/prompts/nostr-nip09.prompt.md deleted file mode 100644 index 23ffeab..0000000 --- a/.github/prompts/nostr-nip09.prompt.md +++ /dev/null @@ -1,53 +0,0 @@ -NIP-09 -====== - -Event Deletion Request ----------------------- - -`draft` `optional` - -A special event with kind `5`, meaning "deletion request" is defined as having a list of one or more `e` or `a` tags, each referencing an event the author is requesting to be deleted. Deletion requests SHOULD include a `k` tag for the kind of each event being requested for deletion. - -The event's `content` field MAY contain a text note describing the reason for the deletion request. - -For example: - -```jsonc -{ - "kind": 5, - "pubkey": <32-bytes hex-encoded public key of the event creator>, - "tags": [ - ["e", "dcd59..464a2"], - ["e", "968c5..ad7a4"], - ["a", "::"], - ["k", "1"], - ["k", "30023"] - ], - "content": "these posts were published by accident", - // other fields... -} -``` - -Relays SHOULD delete or stop publishing any referenced events that have an identical `pubkey` as the deletion request. Clients SHOULD hide or otherwise indicate a deletion request status for referenced events. - -Relays SHOULD continue to publish/share the deletion request events indefinitely, as clients may already have the event that's intended to be deleted. Additionally, clients SHOULD broadcast deletion request events to other relays which don't have it. - -When an `a` tag is used, relays SHOULD delete all versions of the replaceable event up to the `created_at` timestamp of the deletion request event. - -## Client Usage - -Clients MAY choose to fully hide any events that are referenced by valid deletion request events. This includes text notes, direct messages, or other yet-to-be defined event kinds. Alternatively, they MAY show the event along with an icon or other indication that the author has "disowned" the event. The `content` field MAY also be used to replace the deleted events' own content, although a user interface should clearly indicate that this is a deletion request reason, not the original content. - -A client MUST validate that each event `pubkey` referenced in the `e` tag of the deletion request is identical to the deletion request `pubkey`, before hiding or deleting any event. Relays can not, in general, perform this validation and should not be treated as authoritative. - -Clients display the deletion request event itself in any way they choose, e.g., not at all, or with a prominent notice. - -Clients MAY choose to inform the user that their request for deletion does not guarantee deletion because it is impossible to delete events from all relays and clients. - -## Relay Usage - -Relays MAY validate that a deletion request event only references events that have the same `pubkey` as the deletion request itself, however this is not required since relays may not have knowledge of all referenced events. - -## Deletion Request of a Deletion Request - -Publishing a deletion request event against a deletion request has no effect. Clients and relays are not obliged to support "unrequest deletion" functionality. diff --git a/.github/prompts/nostr-nip19.prompt.md b/.github/prompts/nostr-nip19.prompt.md deleted file mode 100644 index 3ea8e11..0000000 --- a/.github/prompts/nostr-nip19.prompt.md +++ /dev/null @@ -1,69 +0,0 @@ -NIP-19 -====== - -bech32-encoded entities ------------------------ - -`draft` `optional` - -This NIP standardizes bech32-formatted strings that can be used to display keys, ids and other information in clients. These formats are not meant to be used anywhere in the core protocol, they are only meant for displaying to users, copy-pasting, sharing, rendering QR codes and inputting data. - -It is recommended that ids and keys are stored in either hex or binary format, since these formats are closer to what must actually be used the core protocol. - -## Bare keys and ids - -To prevent confusion and mixing between private keys, public keys and event ids, which are all 32 byte strings. bech32-(not-m) encoding with different prefixes can be used for each of these entities. - -These are the possible bech32 prefixes: - - - `npub`: public keys - - `nsec`: private keys - - `note`: note ids - -Example: the hex public key `3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d` translates to `npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6`. - -The bech32 encodings of keys and ids are not meant to be used inside the standard NIP-01 event formats or inside the filters, they're meant for human-friendlier display and input only. Clients should still accept keys in both hex and npub format for now, and convert internally. - -## Shareable identifiers with extra metadata - -When sharing a profile or an event, an app may decide to include relay information and other metadata such that other apps can locate and display these entities more easily. - -For these events, the contents are a binary-encoded list of `TLV` (type-length-value), with `T` and `L` being 1 byte each (`uint8`, i.e. a number in the range of 0-255), and `V` being a sequence of bytes of the size indicated by `L`. - -These are the possible bech32 prefixes with `TLV`: - - - `nprofile`: a nostr profile - - `nevent`: a nostr event - - `naddr`: a nostr _replaceable event_ coordinate - - `nrelay`: a nostr relay (deprecated) - -These possible standardized `TLV` types are indicated here: - -- `0`: `special` - - depends on the bech32 prefix: - - for `nprofile` it will be the 32 bytes of the profile public key - - for `nevent` it will be the 32 bytes of the event id - - for `naddr`, it is the identifier (the `"d"` tag) of the event being referenced. For normal replaceable events use an empty string. -- `1`: `relay` - - for `nprofile`, `nevent` and `naddr`, _optionally_, a relay in which the entity (profile or event) is more likely to be found, encoded as ascii - - this may be included multiple times -- `2`: `author` - - for `naddr`, the 32 bytes of the pubkey of the event - - for `nevent`, _optionally_, the 32 bytes of the pubkey of the event -- `3`: `kind` - - for `naddr`, the 32-bit unsigned integer of the kind, big-endian - - for `nevent`, _optionally_, the 32-bit unsigned integer of the kind, big-endian - -## Examples - -- `npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg` should decode into the public key hex `7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e` and vice-versa -- `nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5` should decode into the private key hex `67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa` and vice-versa -- `nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p` should decode into a profile with the following TLV items: - - pubkey: `3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d` - - relay: `wss://r.x.com` - - relay: `wss://djbas.sadkb.com` - -## Notes - -- `npub` keys MUST NOT be used in NIP-01 events or in NIP-05 JSON responses, only the hex format is supported there. -- When decoding a bech32-formatted string, TLVs that are not recognized or supported should be ignored, rather than causing an error. diff --git a/.github/prompts/nostr-nip38.prompt.md b/.github/prompts/nostr-nip38.prompt.md deleted file mode 100644 index f3d47c5..0000000 --- a/.github/prompts/nostr-nip38.prompt.md +++ /dev/null @@ -1,63 +0,0 @@ - -NIP-38 -====== - -User Statuses -------------- - -`draft` `optional` - -## Abstract - -This NIP enables a way for users to share live statuses such as what music they are listening to, as well as what they are currently doing: work, play, out of office, etc. - -## Live Statuses - -A special event with `kind:30315` "User Status" is defined as an *optionally expiring* _addressable event_, where the `d` tag represents the status type: - -For example: - -```json -{ - "kind": 30315, - "content": "Sign up for nostrasia!", - "tags": [ - ["d", "general"], - ["r", "https://nostr.world"] - ], -} -``` - -```json -{ - "kind": 30315, - "content": "Intergalatic - Beastie Boys", - "tags": [ - ["d", "music"], - ["r", "spotify:search:Intergalatic%20-%20Beastie%20Boys"], - ["expiration", "1692845589"] - ], -} -``` - -Two common status types are defined: `general` and `music`. `general` represent general statuses: "Working", "Hiking", etc. - -`music` status events are for live streaming what you are currently listening to. The expiry of the `music` status should be when the track will stop playing. - -Any other status types can be used but they are not defined by this NIP. - -The status MAY include an `r`, `p`, `e` or `a` tag linking to a URL, profile, note, or addressable event. - -The `content` MAY include emoji(s), or [NIP-30](30.md) custom emoji(s). If the `content` is an empty string then the client should clear the status. - -# Client behavior - -Clients MAY display this next to the username on posts or profiles to provide live user status information. - -# Use Cases - -* Calendar nostr apps that update your general status when you're in a meeting -* Nostr Nests that update your general status with a link to the nest when you join -* Nostr music streaming services that update your music status when you're listening -* Podcasting apps that update your music status when you're listening to a podcast, with a link for others to listen as well -* Clients can use the system media player to update playing music status \ No newline at end of file diff --git a/.github/prompts/nostr-nip46.prompt.md b/.github/prompts/nostr-nip46.prompt.md deleted file mode 100644 index 2f62f22..0000000 --- a/.github/prompts/nostr-nip46.prompt.md +++ /dev/null @@ -1,226 +0,0 @@ -NIP-46 -====== - -Nostr Remote Signing --------------------- - -## Changes - -`remote-signer-key` is introduced, passed in bunker url, clients must differentiate between `remote-signer-pubkey` and `user-pubkey`, must call `get_public_key` after connect, nip05 login is removed, create_account moved to another NIP. - -## Rationale - -Private keys should be exposed to as few systems - apps, operating systems, devices - as possible as each system adds to the attack surface. - -This NIP describes a method for 2-way communication between a remote signer and a Nostr client. The remote signer could be, for example, a hardware device dedicated to signing Nostr events, while the client is a normal Nostr client. - -## Terminology - -- **user**: A person that is trying to use Nostr. -- **client**: A user-facing application that _user_ is looking at and clicking buttons in. This application will send requests to _remote-signer_. -- **remote-signer**: A daemon or server running somewhere that will answer requests from _client_, also known as "bunker". -- **client-keypair/pubkey**: The keys generated by _client_. Used to encrypt content and communicate with _remote-signer_. -- **remote-signer-keypair/pubkey**: The keys used by _remote-signer_ to encrypt content and communicate with _client_. This keypair MAY be same as _user-keypair_, but not necessarily. -- **user-keypair/pubkey**: The actual keys representing _user_ (that will be used to sign events in response to `sign_event` requests, for example). The _remote-signer_ generally has control over these keys. - -All pubkeys specified in this NIP are in hex format. - -## Overview - -1. _client_ generates `client-keypair`. This keypair doesn't need to be communicated to _user_ since it's largely disposable. _client_ might choose to store it locally and they should delete it on logout; -2. A connection is established (see below), _remote-signer_ learns `client-pubkey`, _client_ learns `remote-signer-pubkey`. -3. _client_ uses `client-keypair` to send requests to _remote-signer_ by `p`-tagging and encrypting to `remote-signer-pubkey`; -4. _remote-signer_ responds to _client_ by `p`-tagging and encrypting to the `client-pubkey`. -5. _client_ requests `get_public_key` to learn `user-pubkey`. - -## Initiating a connection - -There are two ways to initiate a connection: - -### Direct connection initiated by _remote-signer_ - -_remote-signer_ provides connection token in the form: - -``` -bunker://?relay=&relay=&secret= -``` - -_user_ passes this token to _client_, which then sends `connect` request to _remote-signer_ via the specified relays. Optional secret can be used for single successfully established connection only, _remote-signer_ SHOULD ignore new attempts to establish connection with old secret. - -### Direct connection initiated by the _client_ - -_client_ provides a connection token using `nostrconnect://` as the protocol, and `client-pubkey` as the origin. Additional information should be passed as query parameters: - -- `relay` (required) - one or more relay urls on which the _client_ is listening for responses from the _remote-signer_. -- `secret` (required) - a short random string that the _remote-signer_ should return as the `result` field of its response. -- `perms` (optional) - a comma-separated list of permissions the _client_ is requesting be approved by the _remote-signer_ -- `name` (optional) - the name of the _client_ application -- `url` (optional) - the canonical url of the _client_ application -- `image` (optional) - a small image representing the _client_ application - -Here's an example: - -``` -nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay1.example.com&perms=nip44_encrypt%2Cnip44_decrypt%2Csign_event%3A13%2Csign_event%3A14%2Csign_event%3A1059&name=My+Client&secret=0s8j2djs&relay=wss%3A%2F%2Frelay2.example2.com -``` - -_user_ passes this token to _remote-signer_, which then sends `connect` *response* event to the `client-pubkey` via the specified relays. Client discovers `remote-signer-pubkey` from connect response author. `secret` value MUST be provided to avoid connection spoofing, _client_ MUST validate the `secret` returned by `connect` response. - -## Request Events `kind: 24133` - -```jsonc -{ - "kind": 24133, - "pubkey": , - "content": )>, - "tags": [["p", ]], -} -``` - -The `content` field is a JSON-RPC-like message that is [NIP-44](44.md) encrypted and has the following structure: - -```jsonc -{ - "id": , - "method": , - "params": [array_of_strings] -} -``` - -- `id` is a random string that is a request ID. This same ID will be sent back in the response payload. -- `method` is the name of the method/command (detailed below). -- `params` is a positional array of string parameters. - -### Methods/Commands - -Each of the following are methods that the _client_ sends to the _remote-signer_. - -| Command | Params | Result | -| ------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------- | -| `connect` | `[, , ]` | "ack" OR `` | -| `sign_event` | `[<{kind, content, tags, created_at}>]` | `json_stringified()` | -| `ping` | `[]` | "pong" | -| `get_public_key` | `[]` | `` | -| `nip04_encrypt` | `[, ]` | `` | -| `nip04_decrypt` | `[, ]` | `` | -| `nip44_encrypt` | `[<third_party_pubkey>, <plaintext_to_encrypt>]` | `<nip44_ciphertext>` | -| `nip44_decrypt` | `[<third_party_pubkey>, <nip44_ciphertext_to_decrypt>]` | `<plaintext>` | - -### Requested permissions - -The `connect` method may be provided with `optional_requested_permissions` for user convenience. The permissions are a comma-separated list of `method[:params]`, i.e. `nip44_encrypt,sign_event:4` meaning permissions to call `nip44_encrypt` and to call `sign_event` with `kind:4`. Optional parameter for `sign_event` is the kind number, parameters for other methods are to be defined later. Same permission format may be used for `perms` field of `metadata` in `nostrconnect://` string. - -## Response Events `kind:24133` - -```json -{ - "id": <id>, - "kind": 24133, - "pubkey": <remote-signer-pubkey>, - "content": <nip44(<response>)>, - "tags": [["p", <client-pubkey>]], - "created_at": <unix timestamp in seconds> -} -``` - -The `content` field is a JSON-RPC-like message that is [NIP-44](44.md) encrypted and has the following structure: - -```json -{ - "id": <request_id>, - "result": <results_string>, - "error": <optional_error_string> -} -``` - -- `id` is the request ID that this response is for. -- `results` is a string of the result of the call (this can be either a string or a JSON stringified object) -- `error`, _optionally_, it is an error in string form, if any. Its presence indicates an error with the request. - -## Example flow for signing an event - -- `remote-signer-pubkey` is `fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52` -- `user-pubkey` is also `fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52` -- `client-pubkey` is `eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86` - -### Signature request - -```jsonc -{ - "kind": 24133, - "pubkey": "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86", - "content": nip44({ - "id": <random_string>, - "method": "sign_event", - "params": [json_stringified(<{ - content: "Hello, I'm signing remotely", - kind: 1, - tags: [], - created_at: 1714078911 - }>)] - }), - "tags": [["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"]], // p-tags the remote-signer-pubkey -} -``` - -### Response event - -```jsonc -{ - "kind": 24133, - "pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", - "content": nip44({ - "id": <random_string>, - "result": json_stringified(<signed-event>) - }), - "tags": [["p", "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86"]], // p-tags the client-pubkey -} -``` - -### Diagram - -![signing-example](https://i.nostr.build/P3gW.png) - - -## Auth Challenges - -An Auth Challenge is a response that a _remote-signer_ can send back when it needs the _user_ to authenticate via other means. The response `content` object will take the following form: - -```json -{ - "id": <request_id>, - "result": "auth_url", - "error": <URL_to_display_to_end_user> -} -``` - -_client_ should display (in a popup or new tab) the URL from the `error` field and then subscribe/listen for another response from the _remote-signer_ (reusing the same request ID). This event will be sent once the user authenticates in the other window (or will never arrive if the user doesn't authenticate). - -### Example event signing request with auth challenge - -![signing-example-with-auth-challenge](https://i.nostr.build/W3aj.png) - -## Appendix - -### Announcing _remote-signer_ metadata - -_remote-signer_ MAY publish it's metadata by using [NIP-05](05.md) and [NIP-89](89.md). With NIP-05, a request to `<remote-signer>/.well-known/nostr.json?name=_` MAY return this: -```jsonc -{ - "names":{ - "_": <remote-signer-app-pubkey>, - }, - "nip46": { - "relays": ["wss://relay1","wss://relay2"...], - "nostrconnect_url": "https://remote-signer-domain.example/<nostrconnect>" - } -} -``` - -The `<remote-signer-app-pubkey>` MAY be used to verify the domain from _remote-signer_'s NIP-89 event (see below). `relays` SHOULD be used to construct a more precise `nostrconnect://` string for the specific `remote-signer`. `nostrconnect_url` template MAY be used to redirect users to _remote-signer_'s connection flow by replacing `<nostrconnect>` placeholder with an actual `nostrconnect://` string. - -### Remote signer discovery via NIP-89 - -_remote-signer_ MAY publish a NIP-89 `kind: 31990` event with `k` tag of `24133`, which MAY also include one or more `relay` tags and MAY include `nostrconnect_url` tag. The semantics of `relay` and `nostrconnect_url` tags are the same as in the section above. - -_client_ MAY improve UX by discovering _remote-signers_ using their `kind: 31990` events. _client_ MAY then pre-generate `nostrconnect://` strings for the _remote-signers_, and SHOULD in that case verify that `kind: 31990` event's author is mentioned in signer's `nostr.json?name=_` file as `<remote-signer-app-pubkey>`. \ No newline at end of file diff --git a/.github/prompts/nostr-nip47.prompt.md b/.github/prompts/nostr-nip47.prompt.md deleted file mode 100644 index 77d52e8..0000000 --- a/.github/prompts/nostr-nip47.prompt.md +++ /dev/null @@ -1,524 +0,0 @@ -NIP-47 -====== - -Nostr Wallet Connect (NWC) --------------------- - -`draft` `optional` - -## Rationale - -This NIP describes a way for clients to access a remote lightning wallet through a standardized protocol. Custodians may implement this, or the user may run a bridge that bridges their wallet/node and the Nostr Wallet Connect protocol. - -## Terms - -* **client**: Nostr app on any platform that wants to interact with a lightning wallet. -* **user**: The person using the **client**, and wants to connect their wallet to their **client**. -* **wallet service**: Nostr app that typically runs on an always-on computer (eg. in the cloud or on a Raspberry Pi). This app has access to the APIs of the wallets it serves. - -## Theory of Operation - -Fundamentally NWC is communication between a **client** and **wallet service** by the means of E2E-encrypted direct messages over a nostr relay. The relay knows the kinds and tags of notes, but not the content of the encrypted payloads. The **user**'s identity key is not used to avoid linking payment activity to the user. Ideally unique keys are used for each individual connection. - - 1. **Users** who wish to use this NIP to allow **client(s)** to interact with their wallet must first acquire a special "connection" URI from their NIP-47 compliant wallet application. The wallet application may provide this URI using a QR screen, or a pasteable string, or some other means. - - 2. The **user** should then copy this URI into their **client(s)** by pasting, or scanning the QR, etc. The **client(s)** should save this URI and use it later whenever the **user** (or the **client** on the user's behalf) wants to interact with the wallet. The **client** should then request an `info` (13194) event from the relay(s) specified in the URI. The **wallet service** will have sent that event to those relays earlier, and the relays will hold it as a replaceable event. - - 3. When the **user** initiates a payment their nostr **client** create a `pay_invoice` request, encrypts it using a token from the URI, and sends it (kind 23194) to the relay(s) specified in the connection URI. The **wallet service** will be listening on those relays and will decrypt the request and then contact the **user's** wallet application to send the payment. The **wallet service** will know how to talk to the wallet application because the connection URI specified relay(s) that have access to the wallet app API. - - 4. Once the payment is complete the **wallet service** will send an encrypted `response` (kind 23195) to the **user** over the relay(s) in the URI. - - 5. The **wallet service** may send encrypted notifications (kind 23196) of wallet events (such as a received payment) to the **client**. - -## Events - -There are four event kinds: -- `NIP-47 info event`: 13194 -- `NIP-47 request`: 23194 -- `NIP-47 response`: 23195 -- `NIP-47 notification event`: 23196 - -### Info Event - -The info event should be a replaceable event that is published by the **wallet service** on the relay to indicate which capabilities it supports. - -The content should be a plaintext string with the supported capabilities space-separated, eg. `pay_invoice get_balance notifications`. - -If the **wallet service** supports notifications, the info event SHOULD contain a `notifications` tag with the supported notification types space-separated, eg. `payment_received payment_sent`. - -### Request and Response Events - -Both the request and response events SHOULD contain one `p` tag, containing the public key of the **wallet service** if this is a request, and the public key of the **client** if this is a response. The response event SHOULD contain an `e` tag with the id of the request event it is responding to. -Optionally, a request can have an `expiration` tag that has a unix timestamp in seconds. If the request is received after this timestamp, it should be ignored. - -The content of requests and responses is encrypted with [NIP04](04.md), and is a JSON-RPCish object with a semi-fixed structure: - -Request: -```jsonc -{ - "method": "pay_invoice", // method, string - "params": { // params, object - "invoice": "lnbc50n1..." // command-related data - } -} -``` - -Response: -```jsonc -{ - "result_type": "pay_invoice", //indicates the structure of the result field - "error": { //object, non-null in case of error - "code": "UNAUTHORIZED", //string error code, see below - "message": "human readable error message" - }, - "result": { // result, object. null in case of error. - "preimage": "0123456789abcdef..." // command-related data - } -} -``` - -The `result_type` field MUST contain the name of the method that this event is responding to. -The `error` field MUST contain a `message` field with a human readable error message and a `code` field with the error code if the command was not successful. -If the command was successful, the `error` field must be null. - -### Notification Events - -The notification event SHOULD contain one `p` tag, the public key of the **client**. - -The content of notifications is encrypted with [NIP04](04.md), and is a JSON-RPCish object with a semi-fixed structure: - -```jsonc -{ - "notification_type": "payment_received", //indicates the structure of the notification field - "notification": { - "payment_hash": "0123456789abcdef..." // notification-related data - } -} -``` - - -### Error codes -- `RATE_LIMITED`: The client is sending commands too fast. It should retry in a few seconds. -- `NOT_IMPLEMENTED`: The command is not known or is intentionally not implemented. -- `INSUFFICIENT_BALANCE`: The wallet does not have enough funds to cover a fee reserve or the payment amount. -- `QUOTA_EXCEEDED`: The wallet has exceeded its spending quota. -- `RESTRICTED`: This public key is not allowed to do this operation. -- `UNAUTHORIZED`: This public key has no wallet connected. -- `INTERNAL`: An internal error. -- `OTHER`: Other error. - -## Nostr Wallet Connect URI - -Communication between the **client** and **wallet service** requires two keys in order to encrypt and decrypt messages. The connection URI includes the secret key of the **client** and only the public key of the **wallet service**. - -The **client** discovers **wallet service** by scanning a QR code, handling a deeplink or pasting in a URI. - -The **wallet service** generates this connection URI with protocol `nostr+walletconnect://` and base path its 32-byte hex-encoded `pubkey`, which SHOULD be unique per client connection. - -The connection URI contains the following query string parameters: - -- `relay` Required. URL of the relay where the **wallet service** is connected and will be listening for events. May be more than one. -- `secret` Required. 32-byte randomly generated hex encoded string. The **client** MUST use this to sign events and encrypt payloads when communicating with the **wallet service**. The **wallet service** MUST use the corresponding public key of this secret to communicate with the **client**. - - Authorization does not require passing keys back and forth. - - The user can have different keys for different applications. Keys can be revoked and created at will and have arbitrary constraints (eg. budgets). - - The key is harder to leak since it is not shown to the user and backed up. - - It improves privacy because the user's main key would not be linked to their payments. -- `lud16` Recommended. A lightning address that clients can use to automatically setup the `lud16` field on the user's profile if they have none configured. - -The **client** should then store this connection and use it when the user wants to perform actions like paying an invoice. Due to this NIP using ephemeral events, it is recommended to pick relays that do not close connections on inactivity to not drop events, and ideally retain the events until they are either consumed or become stale. - -- When the **client** sends or receives a message it will use the `secret` from the connection URI and **wallet service**'s `pubkey` to encrypt or decrypt. -- When the **wallet service** sends or receives a message it will use its own secret and the corresponding pubkey of the **client's** `secret` to encrypt or decrypt. The **wallet service** SHOULD NOT store the secret it generates for the client and MUST NOT rely on the knowing the **client** secret for general operation. - -### Example connection string -```sh -nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c -``` - -## Commands - -### `pay_invoice` - -Description: Requests payment of an invoice. - -Request: -```jsonc -{ - "method": "pay_invoice", - "params": { - "invoice": "lnbc50n1...", // bolt11 invoice - "amount": 123, // invoice amount in msats, optional - } -} -``` - -Response: -```jsonc -{ - "result_type": "pay_invoice", - "result": { - "preimage": "0123456789abcdef...", // preimage of the payment - "fees_paid": 123, // value in msats, optional - } -} -``` - -Errors: -- `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. - -### `multi_pay_invoice` - -Description: Requests payment of multiple invoices. - -Request: -```jsonc -{ - "method": "multi_pay_invoice", - "params": { - "invoices": [ - {"id":"4da52c32a1", "invoice": "lnbc1...", "amount": 123}, // bolt11 invoice and amount in msats, amount is optional - {"id":"3da52c32a1", "invoice": "lnbc50n1..."}, - ], - } -} -``` - -Response: - -For every invoice in the request, a separate response event is sent. To differentiate between the responses, each -response event contains a `d` tag with the id of the invoice it is responding to; if no id was given, then the -payment hash of the invoice should be used. - -```jsonc -{ - "result_type": "multi_pay_invoice", - "result": { - "preimage": "0123456789abcdef...", // preimage of the payment - "fees_paid": 123, // value in msats, optional - } -} -``` - -Errors: -- `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. - -### `pay_keysend` - -Request: -```jsonc -{ - "method": "pay_keysend", - "params": { - "amount": 123, // invoice amount in msats, required - "pubkey": "03...", // payee pubkey, required - "preimage": "0123456789abcdef...", // preimage of the payment, optional - "tlv_records": [ // tlv records, optional - { - "type": 5482373484, // tlv type - "value": "0123456789abcdef" // hex encoded tlv value - } - ] - } -} -``` - -Response: -```jsonc -{ - "result_type": "pay_keysend", - "result": { - "preimage": "0123456789abcdef...", // preimage of the payment - "fees_paid": 123, // value in msats, optional - } -} -``` - -Errors: -- `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. - -### `multi_pay_keysend` - -Description: Requests multiple keysend payments. - -Has an array of keysends, these follow the same semantics as `pay_keysend`, just done in a batch - -Request: -```jsonc -{ - "method": "multi_pay_keysend", - "params": { - "keysends": [ - {"id": "4c5b24a351", "pubkey": "03...", "amount": 123}, - {"id": "3da52c32a1", "pubkey": "02...", "amount": 567, "preimage": "abc123..", "tlv_records": [{"type": 696969, "value": "77616c5f6872444873305242454d353736"}]}, - ], - } -} -``` - -Response: - -For every keysend in the request, a separate response event is sent. To differentiate between the responses, each -response event contains a `d` tag with the id of the keysend it is responding to; if no id was given, then the -pubkey should be used. - -```jsonc -{ - "result_type": "multi_pay_keysend", - "result": { - "preimage": "0123456789abcdef...", // preimage of the payment - "fees_paid": 123, // value in msats, optional - } -} -``` - -Errors: -- `PAYMENT_FAILED`: The payment failed. This may be due to a timeout, exhausting all routes, insufficient capacity or similar. - -### `make_invoice` - -Request: -```jsonc -{ - "method": "make_invoice", - "params": { - "amount": 123, // value in msats - "description": "string", // invoice's description, optional - "description_hash": "string", // invoice's description hash, optional - "expiry": 213 // expiry in seconds from time invoice is created, optional - } -} -``` - -Response: -```jsonc -{ - "result_type": "make_invoice", - "result": { - "type": "incoming", // "incoming" for invoices, "outgoing" for payments - "invoice": "string", // encoded invoice, optional - "description": "string", // invoice's description, optional - "description_hash": "string", // invoice's description hash, optional - "preimage": "string", // payment's preimage, optional if unpaid - "payment_hash": "string", // Payment hash for the payment - "amount": 123, // value in msats - "fees_paid": 123, // value in msats - "created_at": unixtimestamp, // invoice/payment creation time - "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable - "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. - } -} -``` - -### `lookup_invoice` - -Request: -```jsonc -{ - "method": "lookup_invoice", - "params": { - "payment_hash": "31afdf1..", // payment hash of the invoice, one of payment_hash or invoice is required - "invoice": "lnbc50n1..." // invoice to lookup - } -} -``` - -Response: -```jsonc -{ - "result_type": "lookup_invoice", - "result": { - "type": "incoming", // "incoming" for invoices, "outgoing" for payments - "invoice": "string", // encoded invoice, optional - "description": "string", // invoice's description, optional - "description_hash": "string", // invoice's description hash, optional - "preimage": "string", // payment's preimage, optional if unpaid - "payment_hash": "string", // Payment hash for the payment - "amount": 123, // value in msats - "fees_paid": 123, // value in msats - "created_at": unixtimestamp, // invoice/payment creation time - "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable - "settled_at": unixtimestamp, // invoice/payment settlement time, optional if unpaid - "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. - } -} -``` - -Errors: -- `NOT_FOUND`: The invoice could not be found by the given parameters. - -### `list_transactions` - -Lists invoices and payments. If `type` is not specified, both invoices and payments are returned. -The `from` and `until` parameters are timestamps in seconds since epoch. If `from` is not specified, it defaults to 0. -If `until` is not specified, it defaults to the current time. Transactions are returned in descending order of creation -time. - -Request: -```jsonc -{ - "method": "list_transactions", - "params": { - "from": 1693876973, // starting timestamp in seconds since epoch (inclusive), optional - "until": 1703225078, // ending timestamp in seconds since epoch (inclusive), optional - "limit": 10, // maximum number of invoices to return, optional - "offset": 0, // offset of the first invoice to return, optional - "unpaid": true, // include unpaid invoices, optional, default false - "type": "incoming", // "incoming" for invoices, "outgoing" for payments, undefined for both - } -} -``` - -Response: -```jsonc -{ - "result_type": "list_transactions", - "result": { - "transactions": [ - { - "type": "incoming", // "incoming" for invoices, "outgoing" for payments - "invoice": "string", // encoded invoice, optional - "description": "string", // invoice's description, optional - "description_hash": "string", // invoice's description hash, optional - "preimage": "string", // payment's preimage, optional if unpaid - "payment_hash": "string", // Payment hash for the payment - "amount": 123, // value in msats - "fees_paid": 123, // value in msats - "created_at": unixtimestamp, // invoice/payment creation time - "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable - "settled_at": unixtimestamp, // invoice/payment settlement time, optional if unpaid - "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. - } - ], - }, -} -``` - -### `get_balance` - -Request: -```jsonc -{ - "method": "get_balance", - "params": {} -} -``` - -Response: -```jsonc -{ - "result_type": "get_balance", - "result": { - "balance": 10000, // user's balance in msats - } -} -``` - -### `get_info` - -Request: -```jsonc -{ - "method": "get_info", - "params": {} -} -``` - -Response: -```jsonc -{ - "result_type": "get_info", - "result": { - "alias": "string", - "color": "hex string", - "pubkey": "hex string", - "network": "string", // mainnet, testnet, signet, or regtest - "block_height": 1, - "block_hash": "hex string", - "methods": ["pay_invoice", "get_balance", "make_invoice", "lookup_invoice", "list_transactions", "get_info"], // list of supported methods for this connection - "notifications": ["payment_received", "payment_sent"], // list of supported notifications for this connection, optional. - } -} -``` - -## Notifications - -### `payment_received` - -Description: A payment was successfully received by the wallet. - -Notification: -```jsonc -{ - "notification_type": "payment_received", - "notification": { - "type": "incoming", - "invoice": "string", // encoded invoice - "description": "string", // invoice's description, optional - "description_hash": "string", // invoice's description hash, optional - "preimage": "string", // payment's preimage - "payment_hash": "string", // Payment hash for the payment - "amount": 123, // value in msats - "fees_paid": 123, // value in msats - "created_at": unixtimestamp, // invoice/payment creation time - "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable - "settled_at": unixtimestamp, // invoice/payment settlement time - "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. - } -} -``` - -### `payment_sent` - -Description: A payment was successfully sent by the wallet. - -Notification: -```jsonc -{ - "notification_type": "payment_sent", - "notification": { - "type": "outgoing", - "invoice": "string", // encoded invoice - "description": "string", // invoice's description, optional - "description_hash": "string", // invoice's description hash, optional - "preimage": "string", // payment's preimage - "payment_hash": "string", // Payment hash for the payment - "amount": 123, // value in msats - "fees_paid": 123, // value in msats - "created_at": unixtimestamp, // invoice/payment creation time - "expires_at": unixtimestamp, // invoice expiration time, optional if not applicable - "settled_at": unixtimestamp, // invoice/payment settlement time - "metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc. - } -} -``` - -## Example pay invoice flow - -0. The user scans the QR code generated by the **wallet service** with their **client** application, they follow a `nostr+walletconnect://` deeplink or configure the connection details manually. -1. **client** sends an event to the **wallet service** with kind `23194`. The content is a `pay_invoice` request. The private key is the secret from the connection string above. -2. **wallet service** verifies that the author's key is authorized to perform the payment, decrypts the payload and sends the payment. -3. **wallet service** responds to the event by sending an event with kind `23195` and content being a response either containing an error message or a preimage. - -## Using a dedicated relay -This NIP does not specify any requirements on the type of relays used. However, if the user is using a custodial service it might make sense to use a relay that is hosted by the custodial service. The relay may then enforce authentication to prevent metadata leaks. Not depending on a 3rd party relay would also improve reliability in this case. - -## Appendix - -### Example NIP-47 info event - -```jsonc -{ - "id": "df467db0a9f9ec77ffe6f561811714ccaa2e26051c20f58f33c3d66d6c2b4d1c", - "pubkey": "c04ccd5c82fc1ea3499b9c6a5c0a7ab627fbe00a0116110d4c750faeaecba1e2", - "created_at": 1713883677, - "kind": 13194, - "tags": [ - [ - "notifications", - "payment_received payment_sent" - ] - ], - "content": "pay_invoice pay_keysend get_balance get_info make_invoice lookup_invoice list_transactions multi_pay_invoice multi_pay_keysend sign_message notifications", - "sig": "31f57b369459b5306a5353aa9e03be7fbde169bc881c3233625605dd12f53548179def16b9fe1137e6465d7e4d5bb27ce81fd6e75908c46b06269f4233c845d8" -} -``` \ No newline at end of file diff --git a/.github/prompts/nostr-nip57.prompt.md b/.github/prompts/nostr-nip57.prompt.md deleted file mode 100644 index b52ee7a..0000000 --- a/.github/prompts/nostr-nip57.prompt.md +++ /dev/null @@ -1,188 +0,0 @@ -NIP-57 -====== - -Lightning Zaps --------------- - -`draft` `optional` - -This NIP defines two new event types for recording lightning payments between users. `9734` is a `zap request`, representing a payer's request to a recipient's lightning wallet for an invoice. `9735` is a `zap receipt`, representing the confirmation by the recipient's lightning wallet that the invoice issued in response to a `zap request` has been paid. - -Having lightning receipts on nostr allows clients to display lightning payments from entities on the network. These can be used for fun or for spam deterrence. - -## Protocol flow - -1. Client calculates a recipient's lnurl pay request url from the `zap` tag on the event being zapped (see Appendix G), or by decoding their lud06 or lud16 field on their profile according to the [lnurl specifications](https://github.com/lnurl/luds). The client MUST send a GET request to this url and parse the response. If `allowsNostr` exists and it is `true`, and if `nostrPubkey` exists and is a valid BIP 340 public key in hex, the client should associate this information with the user, along with the response's `callback`, `minSendable`, and `maxSendable` values. -2. Clients may choose to display a lightning zap button on each post or on a user's profile. If the user's lnurl pay request endpoint supports nostr, the client SHOULD use this NIP to request a `zap receipt` rather than a normal lnurl invoice. -3. When a user (the "sender") indicates they want to send a zap to another user (the "recipient"), the client should create a `zap request` event as described in Appendix A of this NIP and sign it. -4. Instead of publishing the `zap request`, the `9734` event should instead be sent to the `callback` url received from the lnurl pay endpoint for the recipient using a GET request. See Appendix B for details and an example. -5. The recipient's lnurl server will receive this `zap request` and validate it. See Appendix C for details on how to properly configure an lnurl server to support zaps, and Appendix D for details on how to validate the `nostr` query parameter. -6. If the `zap request` is valid, the server should fetch a description hash invoice where the description is this `zap request` note and this note only. No additional lnurl metadata is included in the description. This will be returned in the response according to [LUD06](https://github.com/lnurl/luds/blob/luds/06.md). -7. On receiving the invoice, the client MAY pay it or pass it to an app that can pay the invoice. -8. Once the invoice is paid, the recipient's lnurl server MUST generate a `zap receipt` as described in Appendix E, and publish it to the `relays` specified in the `zap request`. -9. Clients MAY fetch `zap receipt`s on posts and profiles, but MUST authorize their validity as described in Appendix F. If the `zap request` note contains a non-empty `content`, it may display a zap comment. Generally clients should show users the `zap request` note, and use the `zap receipt` to show "zap authorized by ..." but this is optional. - -## Reference and examples - -### Appendix A: Zap Request Event - -A `zap request` is an event of kind `9734` that is _not_ published to relays, but is instead sent to a recipient's lnurl pay `callback` url. This event's `content` MAY be an optional message to send along with the payment. The event MUST include the following tags: - -- `relays` is a list of relays the recipient's wallet should publish its `zap receipt` to. Note that relays should not be nested in an additional list, but should be included as shown in the example below. -- `amount` is the amount in _millisats_ the sender intends to pay, formatted as a string. This is recommended, but optional. -- `lnurl` is the lnurl pay url of the recipient, encoded using bech32 with the prefix `lnurl`. This is recommended, but optional. -- `p` is the hex-encoded pubkey of the recipient. - -In addition, the event MAY include the following tags: - -- `e` is an optional hex-encoded event id. Clients MUST include this if zapping an event rather than a person. -- `a` is an optional event coordinate that allows tipping addressable events such as NIP-23 long-form notes. - -Example: - -```json -{ - "kind": 9734, - "content": "Zap!", - "tags": [ - ["relays", "wss://nostr-pub.wellorder.com", "wss://anotherrelay.example.com"], - ["amount", "21000"], - ["lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"], - ["p", "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"], - ["e", "9ae37aa68f48645127299e9453eb5d908a0cbb6058ff340d528ed4d37c8994fb"] - ], - "pubkey": "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", - "created_at": 1679673265, - "id": "30efed56a035b2549fcaeec0bf2c1595f9a9b3bb4b1a38abaf8ee9041c4b7d93", - "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" -} -``` - -### Appendix B: Zap Request HTTP Request - -A signed `zap request` event is not published, but is instead sent using a HTTP GET request to the recipient's `callback` url, which was provided by the recipient's lnurl pay endpoint. This request should have the following query parameters defined: - -- `amount` is the amount in _millisats_ the sender intends to pay -- `nostr` is the `9734` `zap request` event, JSON encoded then URI encoded -- `lnurl` is the lnurl pay url of the recipient, encoded using bech32 with the prefix `lnurl` - -This request should return a JSON response with a `pr` key, which is the invoice the sender must pay to finalize their zap. Here is an example flow in javascript: - -```javascript -const senderPubkey // The sender's pubkey -const recipientPubkey = // The recipient's pubkey -const callback = // The callback received from the recipients lnurl pay endpoint -const lnurl = // The recipient's lightning address, encoded as a lnurl -const sats = 21 - -const amount = sats * 1000 -const relays = ['wss://nostr-pub.wellorder.net'] -const event = encodeURI(JSON.stringify(await signEvent({ - kind: 9734, - content: "", - pubkey: senderPubkey, - created_at: Math.round(Date.now() / 1000), - tags: [ - ["relays", ...relays], - ["amount", amount.toString()], - ["lnurl", lnurl], - ["p", recipientPubkey], - ], -}))) - -const {pr: invoice} = await fetchJson(`${callback}?amount=${amount}&nostr=${event}&lnurl=${lnurl}`) -``` - -### Appendix C: LNURL Server Configuration - -The lnurl server will need some additional pieces of information so that clients can know that zap invoices are supported: - -1. Add a `nostrPubkey` to the lnurl-pay static endpoint `/.well-known/lnurlp/<user>`, where `nostrPubkey` is the nostr pubkey your server will use to sign `zap receipt` events. Clients will use this to validate `zap receipt`s. -2. Add an `allowsNostr` field and set it to true. - -### Appendix D: LNURL Server Zap Request Validation - -When a client sends a `zap request` event to a server's lnurl-pay callback URL, there will be a `nostr` query parameter whose value is that event which is URI- and JSON-encoded. If present, the `zap request` event must be validated in the following ways: - -1. It MUST have a valid nostr signature -2. It MUST have tags -3. It MUST have only one `p` tag -4. It MUST have 0 or 1 `e` tags -5. There should be a `relays` tag with the relays to send the `zap receipt` to. -6. If there is an `amount` tag, it MUST be equal to the `amount` query parameter. -7. If there is an `a` tag, it MUST be a valid event coordinate -8. There MUST be 0 or 1 `P` tags. If there is one, it MUST be equal to the `zap receipt`'s `pubkey`. - -The event MUST then be stored for use later, when the invoice is paid. - -### Appendix E: Zap Receipt Event - -A `zap receipt` is created by a lightning node when an invoice generated by a `zap request` is paid. `Zap receipt`s are only created when the invoice description (committed to the description hash) contains a `zap request` note. - -When receiving a payment, the following steps are executed: - -1. Get the description for the invoice. This needs to be saved somewhere during the generation of the description hash invoice. It is saved automatically for you with CLN, which is the reference implementation used here. -2. Parse the bolt11 description as a JSON nostr event. This SHOULD be validated based on the requirements in Appendix D, either when it is received, or before the invoice is paid. -3. Create a nostr event of kind `9735` as described below, and publish it to the `relays` declared in the `zap request`. - -The following should be true of the `zap receipt` event: - -- The `content` SHOULD be empty. -- The `created_at` date SHOULD be set to the invoice `paid_at` date for idempotency. -- `tags` MUST include the `p` tag (zap recipient) AND optional `e` tag from the `zap request` AND optional `a` tag from the `zap request` AND optional `P` tag from the pubkey of the zap request (zap sender). -- The `zap receipt` MUST have a `bolt11` tag containing the description hash bolt11 invoice. -- The `zap receipt` MUST contain a `description` tag which is the JSON-encoded zap request. -- `SHA256(description)` SHOULD match the description hash in the bolt11 invoice. -- The `zap receipt` MAY contain a `preimage` tag to match against the payment hash of the bolt11 invoice. This isn't really a payment proof, there is no real way to prove that the invoice is real or has been paid. You are trusting the author of the `zap receipt` for the legitimacy of the payment. - -The `zap receipt` is not a proof of payment, all it proves is that some nostr user fetched an invoice. The existence of the `zap receipt` implies the invoice as paid, but it could be a lie given a rogue implementation. - -A reference implementation for a zap-enabled lnurl server can be found [here](https://github.com/jb55/cln-nostr-zapper). - -Example `zap receipt`: - -```json -{ - "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", - "pubkey": "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31", - "created_at": 1674164545, - "kind": 9735, - "tags": [ - ["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"], - ["P", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"], - ["e", "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"], - ["bolt11", "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"], - ["description", "{\"pubkey\":\"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\",\"wss://nostr-relay.wlvs.space\",\"wss://nostr.fmt.wiz.biz\",\"wss://relay.nostr.bg\",\"wss://nostr.oxtr.dev\",\"wss://nostr.v0l.io\",\"wss://brb.io\",\"wss://nostr.bitcoiner.social\",\"ws://monad.jb55.com:8080\",\"wss://relay.snort.social\"]]}"], - ["preimage", "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f"] - ], - "content": "", - } -``` - -### Appendix F: Validating Zap Receipts - -A client can retrieve `zap receipt`s on events and pubkeys using a NIP-01 filter, for example `{"kinds": [9735], "#e": [...]}`. Zaps MUST be validated using the following steps: - -- The `zap receipt` event's `pubkey` MUST be the same as the recipient's lnurl provider's `nostrPubkey` (retrieved in step 1 of the protocol flow). -- The `invoiceAmount` contained in the `bolt11` tag of the `zap receipt` MUST equal the `amount` tag of the `zap request` (if present). -- The `lnurl` tag of the `zap request` (if present) SHOULD equal the recipient's `lnurl`. - -### Appendix G: `zap` tag on other events - -When an event includes one or more `zap` tags, clients wishing to zap it SHOULD calculate the lnurl pay request based on the tags value instead of the event author's profile field. The tag's second argument is the `hex` string of the receiver's pub key and the third argument is the relay to download the receiver's metadata (Kind-0). An optional fourth parameter specifies the weight (a generalization of a percentage) assigned to the respective receiver. Clients should parse all weights, calculate a sum, and then a percentage to each receiver. If weights are not present, CLIENTS should equally divide the zap amount to all receivers. If weights are only partially present, receivers without a weight should not be zapped (`weight = 0`). - -```jsonc -{ - "tags": [ - [ "zap", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "wss://nostr.oxtr.dev", "1" ], // 25% - [ "zap", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", "wss://nostr.wine/", "1" ], // 25% - [ "zap", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "wss://nos.lol/", "2" ] // 50% - ] -} -``` - -Clients MAY display the zap split configuration in the note. - -## Future Work - -Zaps can be extended to be more private by encrypting `zap request` notes to the target user, but for simplicity it has been left out of this initial draft. \ No newline at end of file diff --git a/.github/prompts/nostr-nip65.prompt.md b/.github/prompts/nostr-nip65.prompt.md deleted file mode 100644 index 81b3cf4..0000000 --- a/.github/prompts/nostr-nip65.prompt.md +++ /dev/null @@ -1,42 +0,0 @@ -NIP-65 -====== - -Relay List Metadata -------------------- - -`draft` `optional` - -Defines a replaceable event using `kind:10002` to advertise relays where the user generally **writes** to and relays where the user generally **reads** mentions. - -The event MUST include a list of `r` tags with relay URLs as value and an optional `read` or `write` marker. If the marker is omitted, the relay is both **read** and **write**. - -```jsonc -{ - "kind": 10002, - "tags": [ - ["r", "wss://alicerelay.example.com"], - ["r", "wss://brando-relay.com"], - ["r", "wss://expensive-relay.example2.com", "write"], - ["r", "wss://nostr-relay.example.com", "read"] - ], - "content": "", - // other fields... -} -``` - -When downloading events **from** a user, clients SHOULD use the **write** relays of that user. - -When downloading events **about** a user, where the user was tagged (mentioned), clients SHOULD use the user's **read** relays. - -When publishing an event, clients SHOULD: - -- Send the event to the **write** relays of the author -- Send the event to all **read** relays of each tagged user - -### Size - -Clients SHOULD guide users to keep `kind:10002` lists small (2-4 relays of each category). - -### Discoverability - -Clients SHOULD spread an author's `kind:10002` event to as many relays as viable, paying attention to relays that, at any moment, serve naturally as well-known public indexers for these relay lists (where most other clients and users are connecting to in order to publish and fetch those). \ No newline at end of file diff --git a/.github/prompts/nostr-nip68.prompt.md b/.github/prompts/nostr-nip68.prompt.md deleted file mode 100644 index 918378a..0000000 --- a/.github/prompts/nostr-nip68.prompt.md +++ /dev/null @@ -1,92 +0,0 @@ -NIP-68 -====== - -Picture-first feeds -------------------- - -`draft` `optional` - -This NIP defines event kind `20` for picture-first clients. Images must be self-contained. They are hosted externally and referenced using `imeta` tags. - -The idea is for this type of event to cater to Nostr clients resembling platforms like Instagram, Flickr, Snapchat, or 9GAG, where the picture itself takes center stage in the user experience. - -## Picture Events - -Picture events contain a `title` tag and description in the `.content`. - -They may contain multiple images to be displayed as a single post. - -```jsonc -{ - "id": <32-bytes lowercase hex-encoded SHA-256 of the the serialized event data>, - "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, - "created_at": <Unix timestamp in seconds>, - "kind": 20, - "content": "<description of post>", - "tags": [ - ["title", "<short title of post>"], - - // Picture Data - [ - "imeta", - "url https://nostr.build/i/my-image.jpg", - "m image/jpeg", - "blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$", - "dim 3024x4032", - "alt A scenic photo overlooking the coast of Costa Rica", - "x <sha256 hash as specified in NIP 94>", - "fallback https://nostrcheck.me/alt1.jpg", - "fallback https://void.cat/alt1.jpg" - ], - [ - "imeta", - "url https://nostr.build/i/my-image2.jpg", - "m image/jpeg", - "blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$", - "dim 3024x4032", - "alt Another scenic photo overlooking the coast of Costa Rica", - "x <sha256 hash as specified in NIP 94>", - "fallback https://nostrcheck.me/alt2.jpg", - "fallback https://void.cat/alt2.jpg", - - "annotate-user <32-bytes hex of a pubkey>:<posX>:<posY>" // Tag users in specific locations in the picture - ], - - ["content-warning", "<reason>"], // if NSFW - - // Tagged users - ["p", "<32-bytes hex of a pubkey>", "<optional recommended relay URL>"], - ["p", "<32-bytes hex of a pubkey>", "<optional recommended relay URL>"], - - // Specify the media type for filters to allow clients to filter by supported kinds - ["m", "image/jpeg"], - - // Hashes of each image to make them queryable - ["x", "<sha256>"] - - // Hashtags - ["t", "<tag>"], - ["t", "<tag>"], - - // location - ["location", "<location>"], // city name, state, country - ["g", "<geohash>"], - - // When text is written in the image, add the tag to represent the language - ["L", "ISO-639-1"], - ["l", "en", "ISO-639-1"] - ] -} -``` - -The `imeta` tag `annotate-user` places a user link in the specific position in the image. - -Only the following media types are accepted: -- `image/apng`: Animated Portable Network Graphics (APNG) -- `image/avif`: AV1 Image File Format (AVIF) -- `image/gif`: Graphics Interchange Format (GIF) -- `image/jpeg`: Joint Photographic Expert Group image (JPEG) -- `image/png`: Portable Network Graphics (PNG) -- `image/webp`: Web Picture format (WEBP) - -Picture events might be used with [NIP-71](71.md)'s kind `22` to display short vertical videos in the same feed. diff --git a/.github/prompts/nostr-react.prompt.md b/.github/prompts/nostr-react.prompt.md deleted file mode 100644 index de1cf9f..0000000 --- a/.github/prompts/nostr-react.prompt.md +++ /dev/null @@ -1,152 +0,0 @@ -## Installation - -``` -npm install nostr-react -``` - -## Example usage: - -Wrap your app in the NostrProvider: - -```tsx -import { NostrProvider } from "nostr-react"; - -const relayUrls = [ - "wss://nostr-pub.wellorder.net", - "wss://relay.nostr.ch", -]; - -function MyApp() { - return ( - <NostrProvider relayUrls={relayUrls} debug={true}> - <App /> - </NostrProvider> - ); -}; -``` - -You can now use the `useNostr` and `useNostrEvents` hooks in your components! - -**Fetching all `text_note` events starting now:** - -```tsx -import { useRef } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; - -const GlobalFeed = () => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered - - const { events } = useNostrEvents({ - filter: { - since: dateToUnix(now.current), // all new events from now - kinds: [1], - }, - }); - - return ( - <> - {events.map((event) => ( - <p key={event.id}>{event.pubkey} posted: {event.content}</p> - ))} - </> - ); -}; -``` - -**Fetching all `text_note` events from a specific user, since the beginning of time:** - -```tsx -import { useNostrEvents } from "nostr-react"; - -const ProfileFeed = () => { - const { events } = useNostrEvents({ - filter: { - authors: [ - "9c2a6495b4e3de93f3e1cc254abe4078e17c64e5771abc676a5e205b62b1286c", - ], - since: 0, - kinds: [1], - }, - }); - - return ( - <> - {events.map((event) => ( - <p key={event.id}>{event.pubkey} posted: {event.content}</p> - ))} - </> - ); -}; -``` - -**Fetching user profiles** - -Use the `useProfile` hook to render user profiles. You can use this in multiple components at once (for example, rendering a name and avatar for each message in a chat), the hook will automatically use *batching* to prevent errors where a client sends too many requests at once. 🎉 - -```tsx -import { useProfile } from "nostr-react"; - -const Profile = () => { - const { data: userData } = useProfile({ - pubkey, - }); - - return ( - <> - <p>Name: {userData?.name}</p> - <p>Public key: {userData?.npub}</p> - <p>Picture URL: {userData?.picture}</p> - </> - ) -} -``` - -**Post a message:** - -```tsx -import { useNostr, dateToUnix } from "nostr-react"; - -import { - type Event as NostrEvent, - getEventHash, - getPublicKey, - signEvent, -} from "nostr-tools"; - -export default function PostButton() { - const { publish } = useNostr(); - - const onPost = async () => { - const privKey = prompt("Paste your private key:"); - - if (!privKey) { - alert("no private key provided"); - return; - } - - const message = prompt("Enter the message you want to send:"); - - if (!message) { - alert("no message provided"); - return; - } - - const event: NostrEvent = { - content: message, - kind: 1, - tags: [], - created_at: dateToUnix(), - pubkey: getPublicKey(privKey), - }; - - event.id = getEventHash(event); - event.sig = signEvent(event, privKey); - - publish(event); - }; - - return ( - <Button onClick={onPost}>Post a message!</Button> - ); -} -``` \ No newline at end of file diff --git a/.github/prompts/nostr-tools.prompt.md b/.github/prompts/nostr-tools.prompt.md deleted file mode 100644 index 13f3d88..0000000 --- a/.github/prompts/nostr-tools.prompt.md +++ /dev/null @@ -1,361 +0,0 @@ -# ![](https://img.shields.io/github/actions/workflow/status/nbd-wtf/nostr-tools/test.yml) [![JSR](https://jsr.io/badges/@nostr/tools)](https://jsr.io/@nostr/tools) nostr-tools - -Tools for developing [Nostr](https://github.com/fiatjaf/nostr) clients. - -Only depends on _@scure_ and _@noble_ packages. - -This package is only providing lower-level functionality. If you want more higher-level features, take a look at [Nostrify](https://nostrify.dev), or if you want an easy-to-use fully-fledged solution that abstracts the hard parts of Nostr and makes decisions on your behalf, take a look at [NDK](https://github.com/nostr-dev-kit/ndk) and [@snort/system](https://www.npmjs.com/package/@snort/system). - -## Installation - -```bash -# npm -npm install --save nostr-tools - -# jsr -npx jsr add @nostr/tools -``` - -If using TypeScript, this package requires TypeScript >= 5.0. - -## Documentation - -https://jsr.io/@nostr/tools/doc - -## Usage - -### Generating a private key and a public key - -```js -import { generateSecretKey, getPublicKey } from 'nostr-tools/pure' - -let sk = generateSecretKey() // `sk` is a Uint8Array -let pk = getPublicKey(sk) // `pk` is a hex string -``` - -To get the secret key in hex format, use - -```js -import { bytesToHex, hexToBytes } from '@noble/hashes/utils' // already an installed dependency - -let skHex = bytesToHex(sk) -let backToBytes = hexToBytes(skHex) -``` - -### Creating, signing and verifying events - -```js -import { finalizeEvent, verifyEvent } from 'nostr-tools/pure' - -let event = finalizeEvent({ - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: 'hello', -}, sk) - -let isGood = verifyEvent(event) -``` - -### Interacting with one or multiple relays - -Doesn't matter what you do, you always should be using a `SimplePool`: - -```js -import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure' -import { SimplePool } from 'nostr-tools/pool' - -const pool = new SimplePool() - -// let's query for an event that exists -const event = relay.get( - ['wss://relay.example.com'], - { - ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'], - }, -) -if (event) { - console.log('it exists indeed on this relay:', event) -} - -// let's publish a new event while simultaneously monitoring the relay for it -let sk = generateSecretKey() -let pk = getPublicKey(sk) - -pool.subscribe( - ['wss://a.com', 'wss://b.com', 'wss://c.com'], - { - kinds: [1], - authors: [pk], - }, - { - onevent(event) { - console.log('got event:', event) - } - } -) - -let eventTemplate = { - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: 'hello world', -} - -// this assigns the pubkey, calculates the event id and signs the event in a single step -const signedEvent = finalizeEvent(eventTemplate, sk) -await pool.publish(['wss://a.com', 'wss://b.com'], signedEvent) - -relay.close() -``` - -To use this on Node.js you first must install `ws` and call something like this: - -```js -import { useWebSocketImplementation } from 'nostr-tools/pool' -// or import { useWebSocketImplementation } from 'nostr-tools/relay' if you're using the Relay directly - -import WebSocket from 'ws' -useWebSocketImplementation(WebSocket) -``` - -### Parsing references (mentions) from a content based on NIP-27 - -```js -import * as nip27 from '@nostr/tools/nip27' - -for (let block of nip27.parse(evt.content)) { - switch (block.type) { - case 'text': - console.log(block.text) - break - case 'reference': { - if ('id' in block.pointer) { - console.log("it's a nevent uri", block.pointer) - } else if ('identifier' in block.pointer) { - console.log("it's a naddr uri", block.pointer) - } else { - console.log("it's an npub or nprofile uri", block.pointer) - } - break - } - case 'url': { - console.log("it's a normal url:", block.url) - break - } - case 'image': - case 'video': - case 'audio': - console.log("it's a media url:", block.url) - case 'relay': - console.log("it's a websocket url, probably a relay address:", block.url) - default: - break - } -} -``` - -### Connecting to a bunker using NIP-46 - -```js -import { generateSecretKey, getPublicKey } from '@nostr/tools/pure' -import { BunkerSigner, parseBunkerInput } from '@nostr/tools/nip46' -import { SimplePool } from '@nostr/tools/pool' - -// the client needs a local secret key (which is generally persisted) for communicating with the bunker -const localSecretKey = generateSecretKey() - -// parse a bunker URI -const bunkerPointer = await parseBunkerInput('bunker://abcd...?relay=wss://relay.example.com') -if (!bunkerPointer) { - throw new Error('Invalid bunker input') -} - -// create the bunker instance -const pool = new SimplePool() -const bunker = new BunkerSigner(localSecretKey, bunkerPointer, { pool }) -await bunker.connect() - -// and use it -const pubkey = await bunker.getPublicKey() -const event = await bunker.signEvent({ - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: 'Hello from bunker!' -}) - -// cleanup -await signer.close() -pool.close([]) -``` - -### Parsing thread from any note based on NIP-10 - -```js -import * as nip10 from '@nostr/tools/nip10' - -// event is a nostr event with tags -const refs = nip10.parse(event) - -// get the root event of the thread -if (refs.root) { - console.log('root event:', refs.root.id) - console.log('root event relay hints:', refs.root.relays) - console.log('root event author:', refs.root.author) -} - -// get the immediate parent being replied to -if (refs.reply) { - console.log('reply to:', refs.reply.id) - console.log('reply relay hints:', refs.reply.relays) - console.log('reply author:', refs.reply.author) -} - -// get any mentioned events -for (let mention of refs.mentions) { - console.log('mentioned event:', mention.id) - console.log('mention relay hints:', mention.relays) - console.log('mention author:', mention.author) -} - -// get any quoted events -for (let quote of refs.quotes) { - console.log('quoted event:', quote.id) - console.log('quote relay hints:', quote.relays) -} - -// get any referenced profiles -for (let profile of refs.profiles) { - console.log('referenced profile:', profile.pubkey) - console.log('profile relay hints:', profile.relays) -} -``` - -### Querying profile data from a NIP-05 address - -```js -import { queryProfile } from 'nostr-tools/nip05' - -let profile = await queryProfile('jb55.com') -console.log(profile.pubkey) -// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245 -console.log(profile.relays) -// prints: [wss://relay.damus.io] -``` - -To use this on Node.js < v18, you first must install `node-fetch@2` and call something like this: - -```js -import { useFetchImplementation } from 'nostr-tools/nip05' -useFetchImplementation(require('node-fetch')) -``` - -### Including NIP-07 types -```js -import type { WindowNostr } from 'nostr-tools/nip07' - -declare global { - interface Window { - nostr?: WindowNostr; - } -} -``` - -### Encoding and decoding NIP-19 codes - -```js -import { generateSecretKey, getPublicKey } from 'nostr-tools/pure' -import * as nip19 from 'nostr-tools/nip19' - -let sk = generateSecretKey() -let nsec = nip19.nsecEncode(sk) -let { type, data } = nip19.decode(nsec) -assert(type === 'nsec') -assert(data === sk) - -let pk = getPublicKey(generateSecretKey()) -let npub = nip19.npubEncode(pk) -let { type, data } = nip19.decode(npub) -assert(type === 'npub') -assert(data === pk) - -let pk = getPublicKey(generateSecretKey()) -let relays = ['wss://relay.nostr.example.mydomain.example.com', 'wss://nostr.banana.com'] -let nprofile = nip19.nprofileEncode({ pubkey: pk, relays }) -let { type, data } = nip19.decode(nprofile) -assert(type === 'nprofile') -assert(data.pubkey === pk) -assert(data.relays.length === 2) -``` - -### Using it with `nostr-wasm` - -[`nostr-wasm`](https://github.com/fiatjaf/nostr-wasm) is a thin wrapper over [libsecp256k1](https://github.com/bitcoin-core/secp256k1) compiled to WASM just for hashing, signing and verifying Nostr events. - -```js -import { setNostrWasm, generateSecretKey, finalizeEvent, verifyEvent } from 'nostr-tools/wasm' -import { initNostrWasm } from 'nostr-wasm' - -// make sure this promise resolves before your app starts calling finalizeEvent or verifyEvent -initNostrWasm().then(setNostrWasm) - -// or use 'nostr-wasm/gzipped' or even 'nostr-wasm/headless', -// see https://www.npmjs.com/package/nostr-wasm for options -``` - -If you're going to use `Relay` and `SimplePool` you must also import `nostr-tools/abstract-relay` and/or `nostr-tools/abstract-pool` instead of the defaults and then instantiate them by passing the `verifyEvent`: - -```js -import { setNostrWasm, verifyEvent } from 'nostr-tools/wasm' -import { AbstractRelay } from 'nostr-tools/abstract-relay' -import { AbstractSimplePool } from 'nostr-tools/abstract-pool' -import { initNostrWasm } from 'nostr-wasm' - -initNostrWasm().then(setNostrWasm) - -const relay = AbstractRelay.connect('wss://relayable.org', { verifyEvent }) -const pool = new AbstractSimplePool({ verifyEvent }) -``` - -This may be faster than the pure-JS [noble libraries](https://paulmillr.com/noble/) used by default and in `nostr-tools/pure`. Benchmarks: - -``` -benchmark time (avg) (min … max) p75 p99 p995 -------------------------------------------------- ----------------------------- -• relay read message and verify event (many events) -------------------------------------------------- ----------------------------- -wasm 34.94 ms/iter (34.61 ms … 35.73 ms) 35.07 ms 35.73 ms 35.73 ms -pure js 239.7 ms/iter (235.41 ms … 243.69 ms) 240.51 ms 243.69 ms 243.69 ms -trusted 402.71 µs/iter (344.57 µs … 2.98 ms) 407.39 µs 745.62 µs 812.59 µs - -summary for relay read message and verify event - wasm - 86.77x slower than trusted - 6.86x faster than pure js -``` - -### Using from the browser (if you don't want to use a bundler) - -```html -<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script> -<script> - window.NostrTools.generateSecretKey('...') // and so on -</script> -``` - -## Plumbing - -To develop `nostr-tools`, install [`just`](https://just.systems/) and run `just -l` to see commands available. - -## License - -This is free and unencumbered software released into the public domain. By submitting patches to this project, you agree to dedicate any and all copyright interest in this software to the public domain. - -## Contributing to this repository - -Use NIP-34 to send your patches to: - -``` -naddr1qq9kummnw3ez6ar0dak8xqg5waehxw309aex2mrp0yhxummnw3ezucn8qyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqpzemhxue69uhhyetvv9ujuurjd9kkzmpwdejhgq3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmejdv00jq -``` \ No newline at end of file diff --git a/.github/prompts/nostr.prompt.md b/.github/prompts/nostr.prompt.md deleted file mode 100644 index b566c36..0000000 --- a/.github/prompts/nostr.prompt.md +++ /dev/null @@ -1,44 +0,0 @@ -# Nostr -## Overview - -Nostr (Notes and Other Stuff Transmitted by Relays) is an open protocol for creating decentralized, censorship-resistant social networks. It is designed to be simple, robust, and easy to implement, enabling anyone to publish and receive messages without relying on a central authority. - -## Core Concepts - -- **Events:** The basic unit of data in Nostr. Events are signed messages containing content (such as text notes), metadata, or other information. -- **Public Key Cryptography:** Every user has a public/private key pair. Events are signed with the user's private key, and followers use the public key to verify authenticity. -- **Relays:** Servers that receive, store, and forward events. Anyone can run a relay, and users can publish to or read from any relay. -- **Clients:** Applications that allow users to create, sign, and read events. Clients connect to one or more relays. - -## How It Works - -1. **User Identity:** Users are identified by their public key. There is no central user directory. -2. **Publishing:** Users sign events with their private key and send them to one or more relays. -3. **Receiving:** Clients subscribe to relays for events from specific public keys or matching certain filters. -4. **Verification:** Clients verify event signatures to ensure authenticity and integrity. -5. **Decentralization:** There is no single point of failure. Users can switch relays or use multiple relays for redundancy. - -## Protocol Specification - -- The protocol is defined in a series of documents called [NIPs (Nostr Implementation Possibilities)](https://github.com/nostr-protocol/nips). -- [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) describes the core event structure and relay protocol. -- Additional NIPs define extensions for features like direct messages, reactions, lists, and more. - -## Getting Started - -- Explore the [Nostr protocol repository](https://github.com/nostr-protocol/nostr) for an introduction and links to resources. -- See [NIPs](https://github.com/nostr-protocol/nips) for protocol details and extensions. -- Try out Nostr clients or run your own relay. A list of apps is available at [nostrapps.com](https://nostrapps.com/) and [nostr.net](https://nostr.net/). - -## Key Benefits - -- **Censorship Resistance:** No central authority can block users or delete content globally. -- **Interoperability:** Any client can connect to any relay, and all use the same protocol. -- **Simplicity:** The protocol is intentionally minimal, making it easy to implement in any language. - -## Further Reading - -- [Animated protocol flow](https://how-nostr-works.pages.dev/#/outbox) -- [NIP-01: Basic protocol spec](https://github.com/nostr-protocol/nips/blob/master/01.md) -- [Nostr protocol GitHub](https://github.com/nostr-protocol/nostr) -- [NIPs repository](https://github.com/nostr-protocol/nips) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index b4e723d..0000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Continous Deplyoment - -on: - workflow_dispatch: - push: - branches: - - main - - master - -env: - REGISTRY_NAME: ghcr.io - IMAGE_NAME: lumina - MAIN_HOST: ${{ secrets.MAIN_HOST }} - MAIN_HOST_USERNAME: ${{ secrets.MAIN_HOST_USERNAME }} - -jobs: - # ci: - # name: CI - # uses: ./.github/workflows/ci.yml - - build_and_push: - # needs: [ci] - name: Build and Push - runs-on: ubuntu-latest - steps: - - name: Check out the repo 🛎️ - uses: actions/checkout@v4 - - - name: Set up QEMU 🐳 - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx 🐳 - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry 🎫 - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Collecting Metadata 🏷️ - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_NAME }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=branch - - - name: Building And Pushing Image 🚀 - id: docker_build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Image digest 📋 - run: echo ${{ steps.docker_build.outputs.digest }} - - # deploy: - # # needs: [build_and_push] - # name: Deployment - # runs-on: ubuntu-latest - - # steps: - # - name: Checkout code 🛎️ - # uses: actions/checkout@v4 - - # - name: Setup SSH 🔑 - # run: | - # mkdir -p ~/.ssh - # echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa - # chmod 600 ~/.ssh/id_rsa - # ssh-keyscan -H ${{ env.MAIN_HOST }} >> ~/.ssh/known_hosts - # env: - # SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - - # - name: Deploy 🚀 - # run: | - # ssh -o StrictHostKeyChecking=no ${{ env.MAIN_HOST_USERNAME }}@${{ env.MAIN_HOST }} ' - # cd lumina && - # git pull origin main && - # docker compose up -d --build - # ' diff --git a/.github/workflows/cd_beta.yml b/.github/workflows/cd_beta.yml deleted file mode 100644 index dd33d04..0000000 --- a/.github/workflows/cd_beta.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Continous Deplyoment Beta - -on: - workflow_dispatch: - push: - branches: - - beta - -env: - REGISTRY_NAME: ghcr.io - IMAGE_NAME: lumina - BETA_HOST: ${{ secrets.BETA_HOST }} - BETA_HOST_USERNAME: ${{ secrets.BETA_HOST_USERNAME }} - -jobs: - # ci: - # name: CI - # uses: ./.github/workflows/ci.yml - - build_and_push: - # needs: [ci] - name: Build and Push - runs-on: ubuntu-latest - steps: - - name: Check out the repo 🛎️ - uses: actions/checkout@v4 - - - name: Set up QEMU 🐳 - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx 🐳 - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry 🎫 - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Collecting Metadata 🏷️ - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_NAME }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=branch - - - name: Building And Pushing Image 🚀 - id: docker_build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Image digest 📋 - run: echo ${{ steps.docker_build.outputs.digest }} - - # deploy: - # needs: [build_and_push] - # name: Deployment - # runs-on: ubuntu-latest - - # steps: - # - name: Checkout code 🛎️ - # uses: actions/checkout@v4 - - # - name: Setup SSH 🔑 - # run: | - # mkdir -p ~/.ssh - # echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa - # chmod 600 ~/.ssh/id_rsa - # ssh-keyscan -H ${{ env.BETA_HOST }} >> ~/.ssh/known_hosts - # env: - # SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - - # - name: Deploy 🚀 - # run: | - # ssh -o StrictHostKeyChecking=no ${{ env.BETA_HOST_USERNAME }}@${{ env.BETA_HOST }} ' - # cd lumina && - # git pull origin beta && - # docker compose up -d --build - # ' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index e586994..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Continous Integration - -on: - workflow_dispatch: - workflow_call: - # when tag released - release: - types: - - created - -env: - REGISTRY_NAME: ghcr.io - IMAGE_NAME: lumina - -jobs: - build_and_push: - name: Build and Push - runs-on: ubuntu-latest - steps: - - name: Check out the repo 🛎️ - uses: actions/checkout@v4 - - - name: Set up QEMU 🐳 - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx 🐳 - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry 🎫 - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Collecting Metadata 🏷️ - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_NAME }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=branch - type=ref,event=tag - - - name: Building And Pushing Image 🚀 - id: docker_build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Image digest 📋 - run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml deleted file mode 100644 index c0eea94..0000000 --- a/.github/workflows/codacy.yml +++ /dev/null @@ -1,61 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow checks out code, performs a Codacy security scan -# and integrates the results with the -# GitHub Advanced Security code scanning feature. For more information on -# the Codacy security scan action usage and parameters, see -# https://github.com/codacy/codacy-analysis-cli-action. -# For more information on Codacy Analysis CLI in general, see -# https://github.com/codacy/codacy-analysis-cli. - -name: Codacy Security Scan - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '32 21 * * 4' - -permissions: - contents: read - -jobs: - codacy-security-scan: - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - name: Codacy Security Scan - runs-on: ubuntu-latest - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout code - uses: actions/checkout@v4 - - # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b - with: - # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository - # You can also omit the token and run the tools that support default configurations - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - verbose: true - output: results.sarif - format: sarif - # Adjust severity of non-security issues - gh-code-scanning-compat: true - # Force 0 exit code to allow SARIF file generation - # This will handover control about PR rejection to the GitHub side - max-allowed-issues: 2147483647 - - # Upload the SARIF file generated in the previous step - - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..19ef0a0 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,56 @@ +name: Deploy to GitHub Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies and build + run: | + npm install + npm run build + cp dist/index.html dist/404.html + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: dist + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..68d464d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Clean install dependencies + run: | + rm -rf node_modules package-lock.json + npm install + + - name: Run tests + run: npm run test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 466a532..b350e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,33 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -/.env - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug +# Logs +logs +*.log npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -# local env files -.env*.local -# vercel +node_modules +dist +dist-ssr +*.local +.ai/ +yarn.lock + +# Editor directories and files +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Vercel .vercel -# typescript -*.tsbuildinfo -next-env.d.ts - -# sw -/public/sw.js -/public/sw.js.map -/public/workbox-*.js -/public/workbox-*.js.map \ No newline at end of file +# Secrets +.env +.env.* +!.env.example diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..14b7efa --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,27 @@ +image: node:22 + +default: + interruptible: true + timeout: 1 minute + +stages: + - test + - deploy + +test: + stage: test + script: + - npm run test + +pages: + stage: deploy + script: + - npm run build + - rm -rf public + - mv dist public + artifacts: + paths: + - public + only: + variables: + - $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..bf437ae --- /dev/null +++ b/.mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "js-dev": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@soapbox.pub/js-dev-mcp@latest"] + }, + "nostr": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@nostrbook/mcp@latest"] + } + } +} \ No newline at end of file diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..9567ef7 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,14 @@ +{ + "servers": { + "js-dev": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@soapbox.pub/js-dev-mcp@latest"] + }, + "nostr": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@nostrbook/mcp@latest"] + } + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0a77011 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f3d9ace --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,1145 @@ +# Project Overview + +This project is a Nostr client application built with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify. + +## Technology Stack + +- **React 18.x**: Stable version of React with hooks, concurrent rendering, and improved performance +- **TailwindCSS 3.x**: Utility-first CSS framework for styling +- **Vite**: Fast build tool and development server +- **shadcn/ui**: Unstyled, accessible UI components built with Radix UI and Tailwind +- **Nostrify**: Nostr protocol framework for Deno and web +- **React Router**: For client-side routing with BrowserRouter and ScrollToTop functionality +- **TanStack Query**: For data fetching, caching, and state management +- **TypeScript**: For type-safe JavaScript development + +## Project Structure + +- `/docs/`: Specialized documentation for implementation patterns and features +- `/src/components/`: UI components including NostrProvider for Nostr integration + - `/src/components/ui/`: shadcn/ui components (48+ components available) + - `/src/components/auth/`: Authentication-related components (LoginArea, LoginDialog, etc.) + - `/src/components/dm/`: Direct messaging UI components (DMMessagingInterface, DMConversationList, DMChatArea) + - Zap components: `ZapButton`, `ZapDialog`, `WalletModal` for Lightning payments +- `/src/hooks/`: Custom hooks including: + - `useNostr`: Core Nostr protocol integration + - `useAuthor`: Fetch user profile data by pubkey + - `useCurrentUser`: Get currently logged-in user + - `useNostrPublish`: Publish events to Nostr + - `useUploadFile`: Upload files via Blossom servers + - `useAppContext`: Access global app configuration + - `useTheme`: Theme management + - `useToast`: Toast notifications + - `useLocalStorage`: Persistent local storage + - `useLoggedInAccounts`: Manage multiple accounts + - `useLoginActions`: Authentication actions + - `useIsMobile`: Responsive design helper + - `useZaps`: Lightning zap functionality with payment processing + - `useWallet`: Unified wallet detection (WebLN + NWC) + - `useNWC`: Nostr Wallet Connect connection management + - `useNWCContext`: Access NWC context provider + - `useShakespeare`: AI chat completions with Shakespeare AI API +- `/src/pages/`: Page components used by React Router (Index, NotFound) +- `/src/lib/`: Utility functions and shared logic +- `/src/contexts/`: React context providers (AppContext, NWCContext, DMContext) + - `useDMContext`: Hook exported from DMContext for direct messaging (NIP-04 & NIP-17) + - `useConversationMessages`: Hook exported from DMContext for paginated messages +- `/src/test/`: Testing utilities including TestApp component +- `/public/`: Static assets +- `App.tsx`: Main app component with provider setup (**CRITICAL**: this file is **already configured** with `QueryClientProvider`, `NostrProvider`, `UnheadProvider` and other important providers - **read this file before making changes**. Changes are usually not necessary unless adding new providers. Changing this file may break the application) +- `AppRouter.tsx`: React Router configuration + +**CRITICAL**: Always read the files mentioned above before making changes, as they contain important setup and configuration for the application. Never directly write to these files without first reading their contents. + +## UI Components + +The project uses shadcn/ui components located in `@/components/ui`. These are unstyled, accessible components built with Radix UI and styled with Tailwind CSS. Available components include: + +- **Accordion**: Vertically collapsing content panels +- **Alert**: Displays important messages to users +- **AlertDialog**: Modal dialog for critical actions requiring confirmation +- **AspectRatio**: Maintains consistent width-to-height ratio +- **Avatar**: User profile pictures with fallback support +- **Badge**: Small status descriptors for UI elements +- **Breadcrumb**: Navigation aid showing current location in hierarchy +- **Button**: Customizable button with multiple variants and sizes +- **Calendar**: Date picker component +- **Card**: Container with header, content, and footer sections +- **Carousel**: Slideshow for cycling through elements +- **Chart**: Data visualization component +- **Checkbox**: Selectable input element +- **Collapsible**: Toggle for showing/hiding content +- **Command**: Command palette for keyboard-first interfaces +- **ContextMenu**: Right-click menu component +- **Dialog**: Modal window overlay +- **Drawer**: Side-sliding panel (using vaul) +- **DropdownMenu**: Menu that appears from a trigger element +- **Form**: Form validation and submission handling +- **HoverCard**: Card that appears when hovering over an element +- **InputOTP**: One-time password input field +- **Input**: Text input field +- **Label**: Accessible form labels +- **Menubar**: Horizontal menu with dropdowns +- **NavigationMenu**: Accessible navigation component +- **Pagination**: Controls for navigating between pages +- **Popover**: Floating content triggered by a button +- **Progress**: Progress indicator +- **RadioGroup**: Group of radio inputs +- **Resizable**: Resizable panels and interfaces +- **ScrollArea**: Scrollable container with custom scrollbars +- **Select**: Dropdown selection component +- **Separator**: Visual divider between content +- **Sheet**: Side-anchored dialog component +- **Sidebar**: Navigation sidebar component +- **Skeleton**: Loading placeholder +- **Slider**: Input for selecting a value from a range +- **Switch**: Toggle switch control +- **Table**: Data table with headers and rows +- **Tabs**: Tabbed interface component +- **Textarea**: Multi-line text input +- **Toast**: Toast notification component +- **ToggleGroup**: Group of toggle buttons +- **Toggle**: Two-state button +- **Tooltip**: Informational text that appears on hover + +These components follow a consistent pattern using React's `forwardRef` and use the `cn()` utility for class name merging. Many are built on Radix UI primitives for accessibility and customized with Tailwind CSS. + +## Documentation + +The project includes a **`docs/`** directory containing specialized documentation for specific implementation tasks. You are encouraged to add new documentation files to help future development. + +- **`docs/AI_CHAT.md`**: Read when building any AI-powered chat interfaces, implementing streaming responses, or integrating with the Shakespeare API. + +- **`docs/NOSTR_COMMENTS.md`**: Read when implementing comment systems, adding discussion features to posts/articles, or building community interaction features. + +- **`docs/NOSTR_INFINITE_SCROLL.md`**: Read when building feed interfaces, implementing pagination for Nostr events, or creating social media-style infinite scroll experiences. + +- **`docs/NOSTR_DIRECT_MESSAGES.md`**: Read when implementing direct messaging features, building chat interfaces, or working with encrypted peer-to-peer communication (NIP-04 and NIP-17). + +## System Prompt Management + +The AI assistant's behavior and knowledge is defined by the AGENTS.md file, which serves as the system prompt. To modify the assistant's instructions or add new project-specific guidelines: + +1. Edit AGENTS.md directly +2. The changes take effect in the next session + +## "Vibed with MKStack" + +When building the site for the first time, include "Vibed with MKStack" somewhere in the UI, linked to this URL: https://soapbox.pub/mkstack + +## Nostr Protocol Integration + +This project comes with custom hooks for querying and publishing events on the Nostr network. + +### Nostr Implementation Guidelines + +- Always check the full list of existing NIPs before implementing any Nostr features to see what kinds are currently in use across all NIPs. +- If any existing kind or NIP might offer the required functionality, read the relevant NIPs to investigate thoroughly. Several NIPs may need to be read before making a decision. +- Only generate new kind numbers if no existing suitable kinds are found after comprehensive research. + +Knowing when to create a new kind versus reusing an existing kind requires careful judgement. Introducing new kinds means the project won't be interoperable with existing clients. But deviating too far from the schema of a particular kind can cause different interoperability issues. + +#### Choosing Between Existing NIPs and Custom Kinds + +When implementing features that could use existing NIPs, follow this decision framework: + +1. **Thorough NIP Review**: Before considering a new kind, always perform a comprehensive review of existing NIPs and their associated kinds. Get an overview of all NIPs, and then read specific NIPs and kind documentation to investigate any potentially relevant NIPs or kinds in detail. The goal is to find the closest existing solution. + +2. **Prioritize Existing NIPs**: Always prefer extending or using existing NIPs over creating custom kinds, even if they require minor compromises in functionality. + +3. **Interoperability vs. Perfect Fit**: Consider the trade-off between: + - **Interoperability**: Using existing kinds means compatibility with other Nostr clients + - **Perfect Schema**: Custom kinds allow perfect data modeling but create ecosystem fragmentation + +4. **Extension Strategy**: When existing NIPs are close but not perfect: + - Use the existing kind as the base + - Add domain-specific tags for additional metadata + - Document the extensions in `NIP.md` + +5. **When to Generate Custom Kinds**: + - No existing NIP covers the core functionality + - The data structure is fundamentally different from existing patterns + - The use case requires different storage characteristics (regular vs replaceable vs addressable) + - If you have a tool available to generate a kind, you **MUST** call the tool to generate a new kind rather than picking an arbitrary number + +6. **Custom Kind Publishing**: When publishing events with custom generated kinds, always include a NIP-31 "alt" tag with a human-readable description of the event's purpose. + +**Example Decision Process**: +``` +Need: Equipment marketplace for farmers +Options: +1. NIP-15 (Marketplace) - Too structured for peer-to-peer sales +2. NIP-99 (Classified Listings) - Good fit, can extend with farming tags +3. Custom kind - Perfect fit but no interoperability + +Decision: Use NIP-99 + farming-specific tags for best balance +``` + +#### Tag Design Principles + +When designing tags for Nostr events, follow these principles: + +1. **Kind vs Tags Separation**: + - **Kind** = Schema/structure (how the data is organized) + - **Tags** = Semantics/categories (what the data represents) + - Don't create different kinds for the same data structure + +2. **Use Single-Letter Tags for Categories**: + - **Relays only index single-letter tags** for efficient querying + - Use `t` tags for categorization, not custom multi-letter tags + - Multiple `t` tags allow items to belong to multiple categories + +3. **Relay-Level Filtering**: + - Design tags to enable efficient relay-level filtering with `#t: ["category"]` + - Avoid client-side filtering when relay-level filtering is possible + - Consider query patterns when designing tag structure + +4. **Tag Examples**: + ```json + // ❌ Wrong: Multi-letter tag, not queryable at relay level + ["product_type", "electronics"] + + // ✅ Correct: Single-letter tag, relay-indexed and queryable + ["t", "electronics"] + ["t", "smartphone"] + ["t", "android"] + ``` + +5. **Querying Best Practices**: + ```typescript + // ❌ Inefficient: Get all events, filter in JavaScript + const events = await nostr.query([{ kinds: [30402] }]); + const filtered = events.filter(e => hasTag(e, 'product_type', 'electronics')); + + // ✅ Efficient: Filter at relay level + const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]); + ``` + +#### `t` Tag Filtering for Community-Specific Content + +For applications focused on a specific community or niche, you can use `t` tags to filter events for the target audience. + +**When to Use:** +- ✅ Community apps: "farmers" → `t: "farming"`, "Poland" → `t: "poland"` +- ❌ Generic platforms: Twitter clones, general Nostr clients + +**Implementation:** +```typescript +// Publishing with community tag +createEvent({ + kind: 1, + content: data.content, + tags: [['t', 'farming']] +}); + +// Querying community content +const events = await nostr.query([{ + kinds: [1], + '#t': ['farming'], + limit: 20 +}], { signal }); +``` + +### Kind Ranges + +An event's kind number determines the event's behavior and storage characteristics: + +- **Regular Events** (1000 ≤ kind < 10000): Expected to be stored by relays permanently. Used for persistent content like notes, articles, etc. +- **Replaceable Events** (10000 ≤ kind < 20000): Only the latest event per pubkey+kind combination is stored. Used for profile metadata, contact lists, etc. +- **Addressable Events** (30000 ≤ kind < 40000): Identified by pubkey+kind+d-tag combination, only latest per combination is stored. Used for articles, long-form content, etc. + +Kinds below 1000 are considered "legacy" kinds, and may have different storage characteristics based on their kind definition. For example, kind 1 is regular, while kind 3 is replaceable. + +### Content Field Design Principles + +When designing new event kinds, the `content` field should be used for semantically important data that doesn't need to be queried by relays. **Structured JSON data generally shouldn't go in the content field** (kind 0 being an early exception). + +#### Guidelines + +- **Use content for**: Large text, freeform human-readable content, or existing industry-standard JSON formats (Tiled maps, FHIR, GeoJSON) +- **Use tags for**: Queryable metadata, structured data, anything that needs relay-level filtering +- **Empty content is valid**: Many events need only tags with `content: ""` +- **Relays only index tags**: If you need to filter by a field, it must be a tag + +#### Example + +**✅ Good - queryable data in tags:** +```json +{ + "kind": 30402, + "content": "", + "tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]] +} +``` + +**❌ Bad - structured data in content:** +```json +{ + "kind": 30402, + "content": "{\"title\":\"Camera\",\"price\":250,\"category\":\"photo\"}", + "tags": [["d", "product-123"]] +} +``` + +### NIP.md + +The file `NIP.md` is used by this project to define a custom Nostr protocol document. If the file doesn't exist, it means this project doesn't have any custom kinds associated with it. + +Whenever new kinds are generated, the `NIP.md` file in the project must be created or updated to document the custom event schema. Whenever the schema of one of these custom events changes, `NIP.md` must also be updated accordingly. + +### The `useNostr` Hook + +The `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively. + +```typescript +import { useNostr } from '@nostrify/react'; + +function useCustomHook() { + const { nostr } = useNostr(); + + // ... +} +``` + +### Connecting to Multiple Nostr Relays + +By default, the `nostr` object from `useNostr` uses a pool configuration that reads data from 1 relay and publishes to all configured relays. However, you can connect to specific relays or groups of relays for more granular control: + +#### Single Relay Connection + +To read and publish from one specific relay, use `nostr.relay()` with a WebSocket URL: + +```typescript +import { useNostr } from '@nostrify/react'; + +function useSpecificRelay() { + const { nostr } = useNostr(); + + // Connect to a specific relay + const relay = nostr.relay('wss://relay.damus.io'); + + // Query from this specific relay only + const events = await relay.query([{ kinds: [1], limit: 20 }], { signal }); + + // Publish to this specific relay only + await relay.event({ kind: 1, content: 'Hello from specific relay!' }); +} +``` + +#### Multiple Relay Group + +To read and publish from a specific set of relays, use `nostr.group()` with an array of relay URLs: + +```typescript +import { useNostr } from '@nostrify/react'; + +function useRelayGroup() { + const { nostr } = useNostr(); + + // Create a group of specific relays + const relayGroup = nostr.group([ + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol' + ]); + + // Query from all relays in the group + const events = await relayGroup.query([{ kinds: [1], limit: 20 }], { signal }); + + // Publish to all relays in the group + await relayGroup.event({ kind: 1, content: 'Hello from relay group!' }); +} +``` + +#### API Consistency + +Both `relay` and `group` objects have the same API as the main `nostr` object, including: + +- `.query()` - Query events with filters +- `.req()` - Create subscriptions +- `.event()` - Publish events +- All other Nostr protocol methods + +#### Use Cases + +**Single Relay (`nostr.relay()`):** +- Testing specific relay behavior +- Querying relay-specific content +- Debugging connectivity issues +- Working with specialized relays + +**Relay Group (`nostr.group()`):** +- Querying from trusted relay sets +- Publishing to specific communities +- Load balancing across relay subsets +- Geographic relay optimization + +**Default Pool (`nostr`):** +- General application queries +- Maximum reach for publishing +- Default user experience +- Simplified relay management + +### Query Nostr Data with `useNostr` and Tanstack Query + +When querying Nostr, the best practice is to create custom hooks that combine `useNostr` and `useQuery` to get the required data. + +```typescript +import { useNostr } from '@nostrify/react'; +import { useQuery } from '@tanstack/query'; + +function usePosts() { + const { nostr } = useNostr(); + + return useQuery({ + queryKey: ['posts'], + queryFn: async (c) => { + const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]); + const events = await nostr.query([{ kinds: [1], limit: 20 }], { signal }); + return events; // these events could be transformed into another format + }, + }); +} +``` + +### Efficient Query Design + +**Critical**: Always minimize the number of separate queries to avoid rate limiting and improve performance. Combine related queries whenever possible. + +**✅ Efficient - Single query with multiple kinds:** +```typescript +// Query multiple event types in one request +const events = await nostr.query([ + { + kinds: [1, 6, 16], // All repost kinds in one query + '#e': [eventId], + limit: 150, + } +], { signal }); + +// Separate by type in JavaScript +const notes = events.filter((e) => e.kind === 1); +const reposts = events.filter((e) => e.kind === 6); +const genericReposts = events.filter((e) => e.kind === 16); +``` + +**❌ Inefficient - Multiple separate queries:** +```typescript +// This creates unnecessary load and can trigger rate limiting +const [notes, reposts, genericReposts] = await Promise.all([ + nostr.query([{ kinds: [1], '#e': [eventId] }], { signal }), + nostr.query([{ kinds: [6], '#e': [eventId] }], { signal }), + nostr.query([{ kinds: [16], '#e': [eventId] }], { signal }), +]); +``` + +**Query Optimization Guidelines:** +1. **Combine kinds**: Use `kinds: [1, 6, 16]` instead of separate queries +2. **Use multiple filters**: When you need different tag filters, use multiple filter objects in a single query +3. **Adjust limits**: When combining queries, increase the limit appropriately +4. **Filter in JavaScript**: Separate event types after receiving results rather than making multiple requests +5. **Consider relay capacity**: Each query consumes relay resources and may count against rate limits + +The data may be transformed into a more appropriate format if needed, and multiple calls to `nostr.query()` may be made in a single queryFn. + +### Event Validation + +When querying events, if the event kind being returned has required tags or required JSON fields in the content, the events should be filtered through a validator function. This is not generally needed for kinds such as 1, where all tags are optional and the content is freeform text, but is especially useful for custom kinds as well as kinds with strict requirements. + +```typescript +// Example validator function for NIP-52 calendar events +function validateCalendarEvent(event: NostrEvent): boolean { + // Check if it's a calendar event kind + if (![31922, 31923].includes(event.kind)) return false; + + // Check for required tags according to NIP-52 + const d = event.tags.find(([name]) => name === 'd')?.[1]; + const title = event.tags.find(([name]) => name === 'title')?.[1]; + const start = event.tags.find(([name]) => name === 'start')?.[1]; + + // All calendar events require 'd', 'title', and 'start' tags + if (!d || !title || !start) return false; + + // Additional validation for date-based events (kind 31922) + if (event.kind === 31922) { + // start tag should be in YYYY-MM-DD format for date-based events + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(start)) return false; + } + + // Additional validation for time-based events (kind 31923) + if (event.kind === 31923) { + // start tag should be a unix timestamp for time-based events + const timestamp = parseInt(start); + if (isNaN(timestamp) || timestamp <= 0) return false; + } + + return true; +} + +function useCalendarEvents() { + const { nostr } = useNostr(); + + return useQuery({ + queryKey: ['calendar-events'], + queryFn: async (c) => { + const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]); + const events = await nostr.query([{ kinds: [31922, 31923], limit: 20 }], { signal }); + + // Filter events through validator to ensure they meet NIP-52 requirements + return events.filter(validateCalendarEvent); + }, + }); +} +``` + +### The `useAuthor` Hook + +To display profile data for a user by their Nostr pubkey (such as an event author), use the `useAuthor` hook. + +```tsx +import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; +import { useAuthor } from '@/hooks/useAuthor'; +import { genUserName } from '@/lib/genUserName'; + +function Post({ event }: { event: NostrEvent }) { + const author = useAuthor(event.pubkey); + const metadata: NostrMetadata | undefined = author.data?.metadata; + + const displayName = metadata?.name ?? genUserName(event.pubkey); + const profileImage = metadata?.picture; + + // ...render elements with this data +} +``` + +### `NostrMetadata` type + +```ts +/** Kind 0 metadata. */ +interface NostrMetadata { + /** A short description of the user. */ + about?: string; + /** A URL to a wide (~1024x768) picture to be optionally displayed in the background of a profile screen. */ + banner?: string; + /** A boolean to clarify that the content is entirely or partially the result of automation, such as with chatbots or newsfeeds. */ + bot?: boolean; + /** An alternative, bigger name with richer characters than `name`. `name` should always be set regardless of the presence of `display_name` in the metadata. */ + display_name?: string; + /** A bech32 lightning address according to NIP-57 and LNURL specifications. */ + lud06?: string; + /** An email-like lightning address according to NIP-57 and LNURL specifications. */ + lud16?: string; + /** A short name to be displayed for the user. */ + name?: string; + /** An email-like Nostr address according to NIP-05. */ + nip05?: string; + /** A URL to the user's avatar. */ + picture?: string; + /** A web URL related in any way to the event author. */ + website?: string; +} +``` + +### The `useNostrPublish` Hook + +To publish events, use the `useNostrPublish` hook in this project. This hook automatically adds a "client" tag to published events. + +```tsx +import { useState } from 'react'; + +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { useNostrPublish } from '@/hooks/useNostrPublish'; + +export function MyComponent() { + const [ data, setData] = useState<Record<string, string>>({}); + + const { user } = useCurrentUser(); + const { mutate: createEvent } = useNostrPublish(); + + const handleSubmit = () => { + createEvent({ kind: 1, content: data.content }); + }; + + if (!user) { + return <span>You must be logged in to use this form.</span>; + } + + return ( + <form onSubmit={handleSubmit} disabled={!user}> + {/* ...some input fields */} + </form> + ); +} +``` + +The `useCurrentUser` hook should be used to ensure that the user is logged in before they are able to publish Nostr events. + +### Nostr Login + +To enable login with Nostr, simply use the `LoginArea` component already included in this project. + +```tsx +import { LoginArea } from "@/components/auth/LoginArea"; + +function MyComponent() { + return ( + <div> + {/* other components ... */} + + <LoginArea className="max-w-60" /> + </div> + ); +} +``` + +The `LoginArea` component handles all the login-related UI and interactions, including displaying login dialogs, sign up functionality, and switching between accounts. It should not be wrapped in any conditional logic. + +`LoginArea` displays both "Log in" and "Sign Up" buttons when the user is logged out, and changes to an account switcher once the user is logged in. It is an inline-flex element by default. To make it expand to the width of its container, you can pass a className like `flex` (to make it a block element) or `w-full`. If it is left as inline-flex, it's recommended to set a max width. + +**Important**: Social applications should include a profile menu button in the main interface (typically in headers/navigation) to provide access to account settings, profile editing, and logout functionality. Don't only show `LoginArea` in logged-out states. + +### `npub`, `naddr`, and other Nostr addresses + +Nostr defines a set of bech32-encoded identifiers in NIP-19. Their prefixes and purposes: + +- `npub1`: **public keys** - Just the 32-byte public key, no additional metadata +- `nsec1`: **private keys** - Secret keys (should never be displayed publicly) +- `note1`: **event IDs** - Just the 32-byte event ID (hex), no additional metadata +- `nevent1`: **event pointers** - Event ID plus optional relay hints and author pubkey +- `nprofile1`: **profile pointers** - Public key plus optional relay hints and petname +- `naddr1`: **addressable event coordinates** - For parameterized replaceable events (kind 30000-39999) +- `nrelay1`: **relay references** - Relay URLs (deprecated) + +#### Key Differences Between Similar Identifiers + +**`note1` vs `nevent1`:** +- `note1`: Contains only the event ID (32 bytes) - specifically for kind:1 events (Short Text Notes) as defined in NIP-10 +- `nevent1`: Contains event ID plus optional relay hints and author pubkey - for any event kind +- Use `note1` for simple references to text notes and threads +- Use `nevent1` when you need to include relay hints or author context for any event type + +**`npub1` vs `nprofile1`:** +- `npub1`: Contains only the public key (32 bytes) +- `nprofile1`: Contains public key plus optional relay hints and petname +- Use `npub1` for simple user references +- Use `nprofile1` when you need to include relay hints or display name context + +#### NIP-19 Routing Implementation + +**Critical**: NIP-19 identifiers should be handled at the **root level** of URLs (e.g., `/note1...`, `/npub1...`, `/naddr1...`), NOT nested under paths like `/note/note1...` or `/profile/npub1...`. + +This project includes a boilerplate `NIP19Page` component that provides the foundation for handling all NIP-19 identifier types at the root level. The component is configured in the routing system and ready for AI agents to populate with specific functionality. + +**How it works:** + +1. **Root-Level Route**: The route `/:nip19` in `AppRouter.tsx` catches all NIP-19 identifiers +2. **Automatic Decoding**: The `NIP19Page` component automatically decodes the identifier using `nip19.decode()` +3. **Type-Specific Sections**: Different sections are rendered based on the identifier type: + - `npub1`/`nprofile1`: Profile section with placeholder for profile view + - `note1`: Note section with placeholder for kind:1 text note view + - `nevent1`: Event section with placeholder for any event type view + - `naddr1`: Addressable event section with placeholder for articles, marketplace items, etc. +4. **Error Handling**: Invalid, vacant, or unsupported identifiers show 404 NotFound page +5. **Ready for Population**: Each section includes comments indicating where AI agents should implement specific functionality + +**Example URLs that work automatically:** +- `/npub1abc123...` - User profile (needs implementation) +- `/note1def456...` - Kind:1 text note (needs implementation) +- `/nevent1ghi789...` - Any event with relay hints (needs implementation) +- `/naddr1jkl012...` - Addressable event (needs implementation) + +**Features included:** +- Basic NIP-19 identifier decoding and routing +- Type-specific sections for different identifier types +- Error handling for invalid identifiers +- Responsive container structure +- Comments indicating where to implement specific views + +**Error handling:** +- Invalid NIP-19 format → 404 NotFound +- Unsupported identifier types (like `nsec1`) → 404 NotFound +- Empty or missing identifiers → 404 NotFound + +To implement NIP-19 routing in your Nostr application: + +1. **The NIP19Page boilerplate is already created** - populate sections with specific functionality +2. **The route is already configured** in `AppRouter.tsx` +3. **Error handling is built-in** - all edge cases show appropriate 404 responses +4. **Add specific components** for profile views, event displays, etc. as needed + +#### Event Type Distinctions + +**`note1` identifiers** are specifically for **kind:1 events** (Short Text Notes) as defined in NIP-10: "Text Notes and Threads". These are the basic social media posts in Nostr. + +**`nevent1` identifiers** can reference any event kind and include additional metadata like relay hints and author pubkey. Use `nevent1` when: +- The event is not a kind:1 text note +- You need to include relay hints for better discoverability +- You want to include author context + +#### Use in Filters + +The base Nostr protocol uses hex string identifiers when filtering by event IDs and pubkeys. Nostr filters only accept hex strings. + +```ts +// ❌ Wrong: naddr is not decoded +const events = await nostr.query( + [{ ids: [naddr] }], + { signal } +); +``` + +Corrected example: + +```ts +// Import nip19 from nostr-tools +import { nip19 } from 'nostr-tools'; + +// Decode a NIP-19 identifier +const decoded = nip19.decode(value); + +// Optional: guard certain types (depending on the use-case) +if (decoded.type !== 'naddr') { + throw new Error('Unsupported Nostr identifier'); +} + +// Get the addr object +const naddr = decoded.data; + +// ✅ Correct: naddr is expanded into the correct filter +const events = await nostr.query( + [{ + kinds: [naddr.kind], + authors: [naddr.pubkey], + '#d': [naddr.identifier], + }], + { signal } +); +``` + +#### Implementation Guidelines + +1. **Always decode NIP-19 identifiers** before using them in queries +2. **Use the appropriate identifier type** based on your needs: + - Use `note1` for kind:1 text notes specifically + - Use `nevent1` when including relay hints or for non-kind:1 events + - Use `naddr1` for addressable events (always includes author pubkey for security) +3. **Handle different identifier types** appropriately: + - `npub1`/`nprofile1`: Display user profiles + - `note1`: Display kind:1 text notes specifically + - `nevent1`: Display any event with optional relay context + - `naddr1`: Display addressable events (articles, marketplace items, etc.) +4. **Security considerations**: Always use `naddr1` for addressable events instead of just the `d` tag value, as `naddr1` contains the author pubkey needed to create secure filters +5. **Error handling**: Gracefully handle invalid or unsupported NIP-19 identifiers with 404 responses + +### Nostr Edit Profile + +To include an Edit Profile form, place the `EditProfileForm` component in the project: + +```tsx +import { EditProfileForm } from "@/components/EditProfileForm"; + +function EditProfilePage() { + return ( + <div> + {/* you may want to wrap this in a layout or include other components depending on the project ... */} + + <EditProfileForm /> + </div> + ); +} +``` + +The `EditProfileForm` component displays just the form. It requires no props, and will "just work" automatically. + +### Direct Messaging (NIP-04 & NIP-17) + +The project includes a complete direct messaging system with real-time updates, encrypted storage, and support for both NIP-04 (legacy) and NIP-17 (modern private messaging) protocols. **The system is disabled by default** - enable it by passing `enabled: true` in the `DMProvider` config. + +For complete implementation guide including: +- Setup and configuration +- Sending messages and file attachments +- Using the `DMMessagingInterface` component +- Building custom messaging UIs +- Protocol comparison (NIP-04 vs NIP-17) +- Advanced features and architecture + +See **`docs/NOSTR_DIRECT_MESSAGES.md`** + +### Uploading Files on Nostr + +Use the `useUploadFile` hook to upload files. This hook uses Blossom servers for file storage and returns NIP-94 compatible tags. + +```tsx +import { useUploadFile } from "@/hooks/useUploadFile"; + +function MyComponent() { + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); + + const handleUpload = async (file: File) => { + try { + // Provides an array of NIP-94 compatible tags + // The first tag in the array contains the URL + const [[_, url]] = await uploadFile(file); + // ...use the url + } catch (error) { + // ...handle errors + } + }; + + // ...rest of component +} +``` + +To attach files to kind 1 events, each file's URL should be appended to the event's `content`, and an `imeta` tag should be added for each file. For kind 0 events, the URL by itself can be used in relevant fields of the JSON content. + +### Nostr Encryption and Decryption + +The logged-in user has a `signer` object (matching the NIP-07 signer interface) that can be used for encryption and decryption. The signer's nip44 methods handle all cryptographic operations internally, including key derivation and conversation key management, so you never need direct access to private keys. Always use the signer interface for encryption rather than requesting private keys from users, as this maintains security and follows best practices. + +```ts +// Get the current user +const { user } = useCurrentUser(); + +// Optional guard to check that nip44 is available +if (!user.signer.nip44) { + throw new Error("Please upgrade your signer extension to a version that supports NIP-44 encryption"); +} + +// Encrypt message to self +const encrypted = await user.signer.nip44.encrypt(user.pubkey, "hello world"); +// Decrypt message to self +const decrypted = await user.signer.nip44.decrypt(user.pubkey, encrypted) // "hello world" +``` + +### Rendering Rich Text Content + +Nostr text notes (kind 1, 11, and 1111) have a plaintext `content` field that may contain URLs, hashtags, and Nostr URIs. These events should render their content using the `NoteContent` component: + +```tsx +import { NoteContent } from "@/components/NoteContent"; + +export function Post(/* ...props */) { + // ... + + return ( + <CardContent className="pb-2"> + <div className="whitespace-pre-wrap break-words"> + <NoteContent event={post} className="text-sm" /> + </div> + </CardContent> + ); +} +``` + +## App Configuration + +The project includes an `AppProvider` that manages global application state including theme and NIP-65 relay configuration. The default configuration includes: + +```typescript +const defaultConfig: AppConfig = { + theme: "light", + relayMetadata: { + relays: [ + { url: 'wss://relay.ditto.pub', read: true, write: true }, + { url: 'wss://relay.nostr.band', read: true, write: true }, + { url: 'wss://relay.damus.io', read: true, write: true }, + ], + updatedAt: 0, + }, +}; +``` + +The app uses NIP-65 compatible relay management with automatic sync when users log in. Local storage persists user preferences and relay configurations. + +### Relay Management + +The project includes a complete NIP-65 relay management system: + +- **RelayListManager**: Component for managing multiple relays with read/write permissions +- **NostrSync**: Automatically syncs user's NIP-65 relay list when they log in +- **Automatic Publishing**: Changes to relay configuration are automatically published as NIP-65 events when the user is logged in + +Use the `RelayListManager` component to provide relay management interfaces: + +```tsx +import { RelayListManager } from '@/components/RelayListManager'; + +function SettingsPage() { + return ( + <div> + <h2>Relay Settings</h2> + <RelayListManager /> + </div> + ); +} +``` + +## Routing + +The project uses React Router with a centralized routing configuration in `AppRouter.tsx`. To add new routes: + +1. Create your page component in `/src/pages/` +2. Import it in `AppRouter.tsx` +3. Add the route above the catch-all `*` route: + +```tsx +<Route path="/your-path" element={<YourComponent />} /> +``` + +The router includes automatic scroll-to-top functionality and a 404 NotFound page for unmatched routes. + +## Development Practices + +- Uses React Query for data fetching and caching +- Follows shadcn/ui component patterns +- Implements Path Aliases with `@/` prefix for cleaner imports +- Uses Vite for fast development and production builds +- Component-based architecture with React hooks +- Default connection to one Nostr relay for best performance +- Comprehensive provider setup with NostrLoginProvider, QueryClientProvider, and custom AppProvider +- **Never use the `any` type**: Always use proper TypeScript types for type safety + +## Loading States + +**Use skeleton loading** for structured content (feeds, profiles, forms). **Use spinners** only for buttons or short operations. + +```tsx +// Skeleton example matching component structure +<Card> + <CardHeader> + <div className="flex items-center space-x-3"> + <Skeleton className="h-10 w-10 rounded-full" /> + <div className="space-y-1"> + <Skeleton className="h-4 w-24" /> + <Skeleton className="h-3 w-16" /> + </div> + </div> + </CardHeader> + <CardContent> + <div className="space-y-2"> + <Skeleton className="h-4 w-full" /> + <Skeleton className="h-4 w-4/5" /> + </div> + </CardContent> +</Card> +``` + +### Empty States and No Content Found + +When no content is found (empty search results, no data available, etc.), display a minimalist empty state with helpful messaging. The application uses NIP-65 relay management, so users can manage their relays through the settings or relay management interface. + +```tsx +import { Card, CardContent } from '@/components/ui/card'; + +// Empty state example +<div className="col-span-full"> + <Card className="border-dashed"> + <CardContent className="py-12 px-8 text-center"> + <div className="max-w-sm mx-auto space-y-6"> + <p className="text-muted-foreground"> + No results found. Try checking your relay connections or wait a moment for content to load. + </p> + </div> + </CardContent> + </Card> +</div> +``` + +## CRITICAL Design Standards + +- Create breathtaking, immersive designs that feel like bespoke masterpieces, rivaling the polish of Apple, Stripe, or luxury brands +- Designs must be production-ready, fully featured, with no placeholders unless explicitly requested, ensuring every element serves a functional and aesthetic purpose +- Avoid generic or templated aesthetics at all costs; every design must have a unique, brand-specific visual signature that feels custom-crafted +- Headers must be dynamic, immersive, and storytelling-driven, using layered visuals, motion, and symbolic elements to reflect the brand’s identity—never use simple “icon and text” combos +- Incorporate purposeful, lightweight animations for scroll reveals, micro-interactions (e.g., hover, click, transitions), and section transitions to create a sense of delight and fluidity + +### Design Principles + +- Achieve Apple-level refinement with meticulous attention to detail, ensuring designs evoke strong emotions (e.g., wonder, inspiration, energy) through color, motion, and composition +- Deliver fully functional interactive components with intuitive feedback states, ensuring every element has a clear purpose and enhances user engagement +- Use custom illustrations, 3D elements, or symbolic visuals instead of generic stock imagery to create a unique brand narrative; stock imagery, when required, must be sourced exclusively from Pexels (NEVER Unsplash) and align with the design’s emotional tone +- Ensure designs feel alive and modern with dynamic elements like gradients, glows, or parallax effects, avoiding static or flat aesthetics +- Before finalizing, ask: "Would this design make Apple or Stripe designers pause and take notice?" If not, iterate until it does + +### Avoid Generic Design + +- No basic layouts (e.g., text-on-left, image-on-right) without significant custom polish, such as dynamic backgrounds, layered visuals, or interactive elements +- No simplistic headers; they must be immersive, animated, and reflective of the brand’s core identity and mission +- No designs that could be mistaken for free templates or overused patterns; every element must feel intentional and tailored + +### Interaction Patterns + +- Use progressive disclosure for complex forms or content to guide users intuitively and reduce cognitive load +- Incorporate contextual menus, smart tooltips, and visual cues to enhance navigation and usability +- Implement drag-and-drop, hover effects, and transitions with clear, dynamic visual feedback to elevate the user experience +- Support power users with keyboard shortcuts, ARIA labels, and focus states for accessibility and efficiency +- Add subtle parallax effects or scroll-triggered animations to create depth and engagement without overwhelming the user + +### Technical Requirements + +- Curated color FRpalette (3-5 evocative colors + neutrals) that aligns with the brand’s emotional tone and creates a memorable impact +- Ensure a minimum 4.5:1 contrast ratio for all text and interactive elements to meet accessibility standards +- Use expressive, readable fonts (18px+ for body text, 40px+ for headlines) with a clear hierarchy; pair a modern sans-serif (e.g., Inter) with an elegant serif (e.g., Playfair Display) for personality +- Design for full responsiveness, ensuring flawless performance and aesthetics across all screen sizes (mobile, tablet, desktop) +- Adhere to WCAG 2.1 AA guidelines, including keyboard navigation, screen reader support, and reduced motion options +- Follow an 8px grid system for consistent spacing, padding, and alignment to ensure visual harmony +- Add depth with subtle shadows, gradients, glows, and rounded corners (e.g., 16px radius) to create a polished, modern aesthetic +- Optimize animations and interactions to be lightweight and performant, ensuring smooth experiences across devices + +### Components + +- Design reusable, modular components with consistent styling, behavior, and feedback states (e.g., hover, active, focus, error) +- Include purposeful animations (e.g., scale-up on hover, fade-in on scroll) to guide attention and enhance interactivity without distraction +- Ensure full accessibility support with keyboard navigation, ARIA labels, and visible focus states (e.g., a glowing outline in an accent color) +- Use custom icons or illustrations for components to reinforce the brand’s visual identity + +### Adding Fonts + +To add custom fonts, follow these steps: + +1. **Install a font package** using npm: + + **Any Google Font can be installed** using the @fontsource packages. Examples: + - For Inter Variable: `@fontsource-variable/inter` + - For Roboto: `@fontsource/roboto` + - For Outfit Variable: `@fontsource-variable/outfit` + - For Poppins: `@fontsource/poppins` + - For Open Sans: `@fontsource/open-sans` + + **Format**: `@fontsource/[font-name]` or `@fontsource-variable/[font-name]` (for variable fonts) + +2. **Import the font** in `src/main.tsx`: + ```typescript + import '@fontsource-variable/<font-name>'; + ``` + +3. **Update Tailwind configuration** in `tailwind.config.ts`: + ```typescript + export default { + theme: { + extend: { + fontFamily: { + sans: ['Inter Variable', 'Inter', 'system-ui', 'sans-serif'], + }, + }, + }, + } + ``` + +### Recommended Font Choices by Use Case + +- **Modern/Clean**: Inter Variable, Outfit Variable, or Manrope +- **Professional/Corporate**: Roboto, Open Sans, or Source Sans Pro +- **Creative/Artistic**: Poppins, Nunito, or Comfortaa +- **Technical/Code**: JetBrains Mono, Fira Code, or Source Code Pro (for monospace) + +### Theme System + +The project includes a complete light/dark theme system using CSS custom properties. The theme can be controlled via: + +- `useTheme` hook for programmatic theme switching +- CSS custom properties defined in `src/index.css` +- Automatic dark mode support with `.dark` class + +### Color Scheme Implementation + +When users specify color schemes: +- Update CSS custom properties in `src/index.css` (both `:root` and `.dark` selectors) +- Use Tailwind's color palette or define custom colors +- Ensure proper contrast ratios for accessibility +- Apply colors consistently across components (buttons, links, accents) +- Test both light and dark mode variants + +### Component Styling Patterns + +- Use `cn()` utility for conditional class merging +- Follow shadcn/ui patterns for component variants +- Implement responsive design with Tailwind breakpoints +- Add hover and focus states for interactive elements + +## Writing Tests vs Running Tests + +There is an important distinction between **writing new tests** and **running existing tests**: + +### Writing Tests (Creating New Test Files) + +**Do not write tests** unless the user explicitly requests them in plain language. Writing unnecessary tests wastes significant time and money. Only create tests when: + +1. **The user explicitly asks for tests** to be written in their message +2. **The user describes a specific bug in plain language** and requests tests to help diagnose it +3. **The user says they are still experiencing a problem** that you have already attempted to solve (tests can help verify the fix) + +**Never write tests because:** +- Tool results show test failures (these are not user requests) +- You think tests would be helpful +- New features or components are created +- Existing functionality needs verification + +### Running Tests (Executing the Test Suite) + +**ALWAYS run the test script** after making any code changes. This is mandatory regardless of whether you wrote new tests or not. + +- **You must run the test script** to validate your changes +- **Your task is not complete** until the test script passes without errors +- **This applies to all changes** - bug fixes, new features, refactoring, or any code modifications +- **The test script includes** TypeScript compilation, ESLint checks, and existing test validation + +### Test Setup + +The project uses Vitest with jsdom environment and includes comprehensive test setup: + +- **Testing Library**: React Testing Library with jest-dom matchers +- **Test Environment**: jsdom with mocked browser APIs (matchMedia, scrollTo, IntersectionObserver, ResizeObserver) +- **Test App**: `TestApp` component provides all necessary context providers for testing + +The project includes a `TestApp` component that provides all necessary context providers for testing. Wrap components with this component to provide required context providers: + +```tsx +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TestApp } from '@/test/TestApp'; +import { MyComponent } from './MyComponent'; + +describe('MyComponent', () => { + it('renders correctly', () => { + render( + <TestApp> + <MyComponent /> + </TestApp> + ); + + expect(screen.getByText('Expected text')).toBeInTheDocument(); + }); +}); +``` + +## Validating Your Changes + +**CRITICAL**: After making any code changes, you must validate your work by running available validation tools. + +**Your task is not considered finished until the code successfully type-checks and builds without errors.** + +### Validation Priority Order + +Run available tools in this priority order: + +1. **Type Checking** (Required): Ensure TypeScript compilation succeeds +2. **Building/Compilation** (Required): Verify the project builds successfully +3. **Linting** (Recommended): Check code style and catch potential issues +4. **Tests** (If Available): Run existing test suite +5. **Git Commit** (Required): Create a commit with your changes when finished + +**Minimum Requirements:** +- Code must type-check without errors +- Code must build/compile successfully +- Fix any critical linting errors that would break functionality +- Create a git commit when your changes are complete + +The validation ensures code quality and catches errors before deployment, regardless of the development environment. + +### Using Git + +If git is available in your environment (through a `shell` tool, or other git-specific tools), you should utilize `git log` to understand project history. Use `git status` and `git diff` to check the status of your changes, and if you make a mistake use `git checkout` to restore files. + +When your changes are complete and validated, create a git commit with a descriptive message summarizing your changes. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index fa7971e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -FROM node:22-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Install dependencies based on the preferred package manager -COPY ./package.json ./yarn.lock* ./package-lock.json* ./pnpm-lock.yaml* ./ -RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm ci; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ - else echo "Lockfile not found." && exit 1; \ - fi - - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry during the build. -ENV NEXT_TELEMETRY_DISABLED 1 - -RUN \ - if [ -f yarn.lock ]; then yarn run build; \ - elif [ -f package-lock.json ]; then npm run build; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ - else echo "Lockfile not found." && exit 1; \ - fi - -# Production image, copy all the files and run next -FROM base AS runner - -ENV NODE_ENV production -# Uncomment the following line in case you want to disable telemetry during runtime. -ENV NEXT_TELEMETRY_DISABLED 1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public - -# Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next - -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 - -# server.js is created by next build from the standalone output -# https://nextjs.org/docs/pages/api-reference/next-config-js/output -CMD HOSTNAME="0.0.0.0" node server.js \ No newline at end of file diff --git a/README.md b/README.md index 2a44e2b..e7e5690 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,238 @@ -# LUMINA.rocks 📸 +# MKStack -![LUMINA Version](https://img.shields.io/badge/version-0.1.28-blue) +**The Complete Framework for Building Nostr Clients with AI** -A modern, decentralized social media platform for images and pictures built on the Nostr protocol. +MKStack is an AI-powered framework for building Nostr applications with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify. Build powerful Nostr applications with AI-first development - from social feeds to private messaging, MKStack provides everything you need to create decentralized apps on the Nostr protocol. -## ✨ Features +## 🚀 Quick Start -- **Image-centric social experience** - Share and discover beautiful images -- **Decentralized architecture** - Powered by Nostr protocol -- **User profiles** - Customize your presence on the platform -- **Feeds** - Global, personalized, and tag-based image discovery -- **React and engage** - Like, comment, and interact with content -- **Lightning Network integration** - For tipping and monetization -- **Responsive design** - Optimized for mobile and desktop experiences - -## 🚀 Getting Started - -### Prerequisites - -- [Node.js](https://nodejs.org/) (v18 or newer) -- [Docker](https://www.docker.com/) (optional, for containerized deployment) - -### Local Development +Build your Nostr app in 3 simple steps: +### 1. Install & Create ```bash -# Clone the repository -git clone https://github.com/lumina-rocks/lumina.git -cd lumina - -# Install dependencies -npm install -# or -bun install - -# Start the development server -npm run dev -# or -bun dev +npm install -g @getstacks/stacks +stacks mkstack ``` -Your application will be available at http://localhost:3000. - -## 🐳 Docker Deployment - -### Quickstart - +### 2. Build with AI ```bash -docker run --rm -it -p 3000:3000 ghcr.io/lumina-rocks/lumina:main +stacks agent +# Tell Dork AI what you want: "Build a group chat application" ``` -### Using Docker Compose - +### 3. Deploy Instantly ```bash -docker compose up -d +npm run deploy +# ✅ App deployed to NostrDeploy.com! ``` -### Build from Source +## ✨ What Makes MKStack Special +- **🤖 AI-First Development**: Build complete Nostr apps with just one prompt using Dork AI agent +- **⚡ 8 Minutes Average**: From idea to deployed application in minutes, not months +- **🔗 50+ NIPs Supported**: Comprehensive Nostr protocol implementation +- **🎨 Beautiful UI**: 48+ shadcn/ui components with light/dark theme support +- **🔐 Built-in Security**: NIP-07 browser signing, NIP-44 encryption, event validation +- **💰 Payments Ready**: Lightning zaps (NIP-57), Cashu wallets (NIP-60), Wallet Connect (NIP-47) +- **📱 Production Ready**: TypeScript, testing, deployment, and responsive design included + +## 🛠 Technology Stack + +- **React 18.x**: Stable version with hooks, concurrent rendering, and improved performance +- **TailwindCSS 3.x**: Utility-first CSS framework for styling +- **Vite**: Fast build tool and development server +- **shadcn/ui**: 48+ unstyled, accessible UI components built with Radix UI +- **Nostrify**: Nostr protocol framework for Deno and web +- **React Router**: Client-side routing with BrowserRouter +- **TanStack Query**: Data fetching, caching, and state management +- **TypeScript**: Type-safe JavaScript development + +## 🎯 Real-World Examples + +### Built with One Prompt + +Each of these applications was created with just a single prompt to Dork AI: + +- **Group Chat Application**: `"Build me a group chat application"` + - [Live Demo](https://groupchat-74z9j26wq-mks-projects-1f1254c4.vercel.app/) + +- **Decentralized Goodreads**: `"Build a decentralized goodreads alternative. Use OpenLibrary API for book data."` + - [Live Demo](https://bookstr123-87phkwjcy-mks-projects-1f1254c4.vercel.app/) + +- **Chess Game**: `"Build a chess game with NIP 64"` + - [Live Demo](https://chess-l0d7ms7m3-mks-projects-1f1254c4.vercel.app/chess) + +### Production Apps + +Real Nostr applications built using MKStack: + +- **[Chorus](https://chorus.community/)**: Facebook-style groups on Nostr with built-in eCash wallet +- **[Blobbi](https://www.blobbi.pet/)**: Digital pet companions that live forever on the decentralized web +- **[Treasures](https://treasures.to/)**: Decentralized geocaching adventure powered by Nostr + +[Browse more apps made with MKStack →](https://nostrhub.io/apps/t/mkstack/) + +## 🔧 Core Features + +### Authentication & Users +- `LoginArea` component with account switching +- `useCurrentUser` hook for authentication state +- `useAuthor` hook for fetching user profiles +- NIP-07 browser signing support +- Multi-account management + +### Nostr Protocol Support +- **Social Features**: User profiles (NIP-01), follow lists (NIP-02), reactions (NIP-25), reposts (NIP-18) +- **Messaging**: Private DMs (NIP-17), public chat (NIP-28), group chat (NIP-29), encryption (NIP-44) +- **Payments**: Lightning zaps (NIP-57), Cashu wallets (NIP-60), Nutzaps (NIP-61), Wallet Connect (NIP-47) +- **Content**: Long-form articles (NIP-23), file metadata (NIP-94), live events (NIP-53), calendars (NIP-52) + +### Data Management +- `useNostr` hook for querying and publishing +- `useNostrPublish` hook with automatic client tagging +- Event validation and filtering +- Infinite scroll with TanStack Query +- Multi-relay support + +### UI Components +- 48+ shadcn/ui components (buttons, forms, dialogs, etc.) +- `NoteContent` component for rich text rendering +- `EditProfileForm` for profile management +- `RelaySelector` for relay switching +- `CommentsSection` for threaded discussions +- Light/dark theme system + +### Media & Files +- `useUploadFile` hook with Blossom server integration +- NIP-94 compatible file metadata +- Image and video support +- File attachment to events + +### Advanced Features +- NIP-19 identifier routing (`npub1`, `note1`, `nevent1`, `naddr1`) +- Cryptographic operations (encryption/decryption) +- Lightning payments and zaps +- Real-time event subscriptions +- Responsive design with mobile support + +## 🤖 AI Development with Dork + +MKStack includes Dork, a built-in AI agent that understands your codebase and Nostr protocols: + +### Supported AI Providers + +Configure your AI provider with `stacks configure`: + +- **OpenRouter** ([openrouter.ai](https://openrouter.ai/)): Enter your API key from settings +- **Routstr** ([routstr.com](https://www.routstr.com/)): Use Cashu tokens for payment +- **PayPerQ** ([ppq.ai](https://ppq.ai/)): OpenAI-compatible API + +### How Dork Works + +- **Context-Aware**: Understands your entire codebase and project structure +- **Nostr Expert**: Built-in knowledge of 50+ NIPs and best practices +- **Instant Implementation**: Makes changes directly to your code following React/TypeScript best practices + +Example prompts: ```bash -# Build the Docker image -docker build -t lumina . - -# Run the container -docker run -p 3000:3000 lumina +"Add user profiles with avatars and bio" +"Implement NIP-17 private messaging" +"Add a dark mode toggle" +"Create a marketplace with NIP-15" ``` -## ⚙️ Configuration +## 📁 Project Structure -### Image Proxy (imgproxy) +``` +src/ +├── components/ # UI components +│ ├── ui/ # shadcn/ui components (48+ available) +│ ├── auth/ # Authentication components +│ └── comments/ # Comment system components +├── hooks/ # Custom React hooks +│ ├── useNostr # Core Nostr integration +│ ├── useAuthor # User profile data +│ ├── useCurrentUser # Authentication state +│ ├── useNostrPublish # Event publishing +│ ├── useUploadFile # File uploads +│ └── useZaps # Lightning payments +├── pages/ # Page components +├── lib/ # Utility functions +├── contexts/ # React context providers +└── test/ # Testing utilities +``` -LUMINA supports image proxying via imgproxy to optimize image loading, resize images on-the-fly, and enhance privacy. To enable: +## 🎨 UI Components -1. Edit the `.env` file in the `lumina` directory: - ``` - NEXT_PUBLIC_ENABLE_IMGPROXY=true - NEXT_PUBLIC_IMGPROXY_URL=https://your-imgproxy-instance.com/ - ``` +MKStack includes 48+ shadcn/ui components: -2. Make sure your imgproxy instance is properly configured and accessible. +**Layout**: Card, Separator, Sheet, Sidebar, ScrollArea, Resizable +**Navigation**: Breadcrumb, NavigationMenu, Menubar, Tabs, Pagination +**Forms**: Button, Input, Textarea, Select, Checkbox, RadioGroup, Switch, Slider +**Feedback**: Alert, AlertDialog, Toast, Progress, Skeleton +**Overlay**: Dialog, Popover, HoverCard, Tooltip, ContextMenu, DropdownMenu +**Data Display**: Table, Avatar, Badge, Calendar, Chart, Carousel +**And many more... -3. Restart the application to apply changes. +## 🔐 Security & Best Practices -The imgproxy feature: -- Resizes images to appropriate dimensions for better performance -- Falls back to direct image URLs if the proxy fails -- Provides faster loading times for large images -- Can be disabled by setting `NEXT_PUBLIC_ENABLE_IMGPROXY=false` +- **Never use `any` type**: Always use proper TypeScript types +- **Event validation**: Filter events through validator functions for custom kinds +- **Efficient queries**: Minimize separate queries to avoid rate limiting +- **Proper error handling**: Graceful handling of invalid NIP-19 identifiers +- **Secure authentication**: Use signer interface, never request private keys directly -### Umami Analytics +## 📱 Responsive Design -Umami analytics is disabled by default. To enable: +- Mobile-first approach with Tailwind breakpoints +- `useIsMobile` hook for responsive behavior +- Touch-friendly interactions +- Optimized for all screen sizes -1. Edit the `.env` file in the `lumina` directory: - ``` - NEXT_PUBLIC_UMAMI_SCRIPT_URL=your-umami-script-url - NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-id - ``` +## 🧪 Testing -2. Rebuild and restart the container: - ```bash - docker compose up -d --build - ``` +- Vitest with jsdom environment +- React Testing Library with jest-dom matchers +- `TestApp` component provides all necessary context providers +- Mocked browser APIs (matchMedia, scrollTo, IntersectionObserver, ResizeObserver) -## 🛠️ Tech Stack +## 🚀 Deployment -- **Frontend**: Next.js, React, TypeScript, Tailwind CSS -- **UI Components**: Radix UI, Lucide icons, shadcn/ui -- **Protocols**: Nostr, Lightning Network +Built-in deployment to NostrDeploy.com: -## ⚡ Support LUMINA +```bash +npm run deploy +``` -LUMINA is an independent, community-focused project kickstarted and currently mostly developed by a single passionate developer. Your support helps keep this decentralized platform alive and growing! +Your app goes live instantly with: +- Automatic builds +- CDN distribution +- HTTPS support +- Custom domains available -### How to support: -- **Geyser Fund**: Donate at [geyser.fund/project/lumina](https://geyser.fund/project/lumina) -- **Lightning Address**: Send sats directly to `lumina@geyser.fund` -- **Code Contributions**: PRs are welcome! +## 📚 Documentation -Every contribution helps build a better, more open social media landscape. +For detailed documentation on building Nostr applications with MKStack: + +- [Tutorial](https://soapbox.pub/blog/mkstack-tutorial) +- [Nostr Protocol Documentation](https://nostr.com) +- [shadcn/ui Components](https://ui.shadcn.com) ## 🤝 Contributing -Contributions, issues, and feature requests are welcome! Feel free to check [issues page](https://github.com/lumina-rocks/lumina/issues). +MKStack is open source and welcomes contributions. The framework is designed to be: -## 🔗 Links +- **Extensible**: Easy to add new NIPs and features +- **Maintainable**: Clean architecture with TypeScript +- **Testable**: Comprehensive testing setup included +- **Documented**: Clear patterns and examples -- [Website](https://lumina.rocks) -- [GitHub Repository](https://github.com/lumina-rocks/lumina) -- [Support Us](https://geyser.fund/project/lumina) \ No newline at end of file +## 📄 License + +Open source - build amazing Nostr applications and help grow the decentralized web! + +--- + +**"Vibed with MKStack"** - [Learn more about MKStack](https://soapbox.pub/mkstack) + +*Build your Nostr app in minutes, not months. Start with AI, deploy instantly.* \ No newline at end of file diff --git a/app/dashboard/[pubkey]/page.tsx b/app/dashboard/[pubkey]/page.tsx deleted file mode 100644 index fe6fe86..0000000 --- a/app/dashboard/[pubkey]/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { useParams } from 'next/navigation' -import { nip19 } from "nostr-tools"; -import Statistics from '@/components/dashboard/Statistics'; -import { useEffect } from 'react'; - -const DashboardPage: React.FC = ({ }) => { - - useEffect(() => { - document.title = `Dashboard | LUMINA`; - }, []); - - const params = useParams() - let pubkey = params.pubkey - // check if pubkey contains "npub" - // if so, then we need to convert it to a pubkey - if (pubkey.includes("npub")) { - // convert npub to pubkey - pubkey = nip19.decode(pubkey.toString()).data.toString() - } - - return ( - <> - <Statistics pubkey={pubkey.toString()} /> - </> - ); -} - -export default DashboardPage; \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index e7a4399..0000000 Binary files a/app/favicon.ico and /dev/null differ diff --git a/app/feed/page.tsx b/app/feed/page.tsx deleted file mode 100644 index decd5ef..0000000 --- a/app/feed/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import Head from "next/head"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { SectionIcon, GridIcon } from '@radix-ui/react-icons' -import FollowerFeed from "@/components/FollowerFeed"; -import FollowerQuickViewFeed from "@/components/FollowerQuickViewFeed"; -import { useEffect } from "react"; - -export default function FeedPage() { - - useEffect(() => { - document.title = `Feed | LUMINA`; - }, []); - - let pubkey = null; - if (typeof window !== 'undefined') { - pubkey = window.localStorage.getItem('pubkey'); - } - - // check if pubkey contains "npub" - // if so, then we need to convert it to a pubkey - // if (pubkey.includes("npub")) { - // // convert npub to pubkey - // pubkey = nip19.decode(pubkey.toString()).data.toString() - // } - - return ( - <> - <Head> - <title>LUMINA.rocks - {pubkey}</title> - <meta name="description" content="Yet another nostr web ui" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/favicon.ico" /> - </Head> - <div className="py-4 px-2 md:py-6 md:px-6"> - {/* <h2 className="text-2xl font-bold mb-4 px-2 md:px-4">Follower Feed</h2> */} - <Tabs defaultValue="QuickView"> - <TabsList className="mb-4 w-full grid grid-cols-2"> - <TabsTrigger value="QuickView"><GridIcon /></TabsTrigger> - <TabsTrigger value="ExtendedFeed"><SectionIcon /></TabsTrigger> - </TabsList> - <TabsContent value="QuickView"> - <FollowerQuickViewFeed pubkey={pubkey || ''} /> - </TabsContent> - <TabsContent value="ExtendedFeed"> - <FollowerFeed pubkey={pubkey || ''} /> - </TabsContent> - </Tabs> - </div> - </> - ); -} diff --git a/app/global/page.tsx b/app/global/page.tsx deleted file mode 100644 index 8af8cbe..0000000 --- a/app/global/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import GlobalFeed from "@/components/GlobalFeed"; -import GlobalQuickViewFeed from "@/components/GlobalQuickViewFeed"; -import ReelFeed from "@/components/ReelFeed"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; -import { GridIcon, SectionIcon, PlayIcon } from "@radix-ui/react-icons"; -import { useEffect } from "react"; - -export default function GlobalFeedPage() { - - useEffect(() => { - document.title = `Global Feed | LUMINA`; - }, []); - - return ( - <div className="py-4 px-2 md:py-6 md:px-6"> - {/* <h2 className="text-2xl font-bold mb-4">Global Feed</h2> */} - <Tabs defaultValue="Feed"> - <TabsList className="mb-4 w-full grid grid-cols-3"> - <TabsTrigger value="Feed">Feed</TabsTrigger> - <TabsTrigger value="Reels"><PlayIcon /></TabsTrigger> - <TabsTrigger value="Extended"><SectionIcon /></TabsTrigger> - </TabsList> - <TabsContent value="Feed"> - <GlobalQuickViewFeed /> - </TabsContent> - <TabsContent value="Reels"> - <ReelFeed /> - </TabsContent> - <TabsContent value="Extended"> - <GlobalFeed /> - </TabsContent> - </Tabs> - </div> - ); -} diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index 418e63e..0000000 --- a/app/globals.css +++ /dev/null @@ -1,484 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 224 71.4% 4.1%; - --card: 0 0% 100%; - --card-foreground: 224 71.4% 4.1%; - --popover: 0 0% 100%; - --popover-foreground: 224 71.4% 4.1%; - --primary: 262.1 83.3% 57.8%; - --primary-foreground: 210 20% 98%; - --secondary: 220 14.3% 95.9%; - --secondary-foreground: 220.9 39.3% 11%; - --muted: 220 14.3% 95.9%; - --muted-foreground: 220 8.9% 46.1%; - --accent: 220 14.3% 95.9%; - --accent-foreground: 220.9 39.3% 11%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 20% 98%; - --border: 220 13% 91%; - --input: 220 13% 91%; - --ring: 262.1 83.3% 57.8%; - --radius: 0.5rem; - } - - .dark { - --background: 224 71.4% 4.1%; - --foreground: 210 20% 98%; - --card: 224 71.4% 4.1%; - --card-foreground: 210 20% 98%; - --popover: 224 71.4% 4.1%; - --popover-foreground: 210 20% 98%; - --primary: 263.4 70% 50.4%; - --primary-foreground: 210 20% 98%; - --secondary: 215 27.9% 16.9%; - --secondary-foreground: 210 20% 98%; - --muted: 215 27.9% 16.9%; - --muted-foreground: 217.9 10.6% 64.9%; - --accent: 215 27.9% 16.9%; - --accent-foreground: 210 20% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 20% 98%; - --border: 215 27.9% 16.9%; - --input: 215 27.9% 16.9%; - --ring: 263.4 70% 50.4%; - } - - .purple-light { - --background: 260 23.0769% 97.4510%; - --foreground: 243.1579 13.6691% 27.2549%; - --card: 0 0% 100%; - --card-foreground: 243.1579 13.6691% 27.2549%; - --popover: 0 0% 100%; - --popover-foreground: 243.1579 13.6691% 27.2549%; - --primary: 260.4000 22.9358% 57.2549%; - --primary-foreground: 260 23.0769% 97.4510%; - --secondary: 258.9474 33.3333% 88.8235%; - --secondary-foreground: 243.1579 13.6691% 27.2549%; - --muted: 258.0000 15.1515% 87.0588%; - --muted-foreground: 247.5000 10.3448% 45.4902%; - --accent: 342.4615 56.5217% 77.4510%; - --accent-foreground: 343.4483 23.9669% 23.7255%; - --destructive: 0 62.1891% 60.5882%; - --destructive-foreground: 260 23.0769% 97.4510%; - --border: 258.7500 17.3913% 81.9608%; - --input: 260.0000 23.0769% 92.3529%; - --ring: 260.4000 22.9358% 57.2549%; - --chart-1: 260.4000 22.9358% 57.2549%; - --chart-2: 342.4615 56.5217% 77.4510%; - --chart-3: 158.7692 31.4010% 59.4118%; - --chart-4: 35.7576 76.7442% 74.7059%; - --chart-5: 215.8209 54.4715% 75.8824%; - --sidebar: 260.0000 23.0769% 94.9020%; - --sidebar-foreground: 243.1579 13.6691% 27.2549%; - --sidebar-primary: 260.4000 22.9358% 57.2549%; - --sidebar-primary-foreground: 260 23.0769% 97.4510%; - --sidebar-accent: 342.4615 56.5217% 77.4510%; - --sidebar-accent-foreground: 343.4483 23.9669% 23.7255%; - --sidebar-border: 261.4286 18.4211% 85.0980%; - --sidebar-ring: 260.4000 22.9358% 57.2549%; - --font-sans: Geist, sans-serif; - --font-serif: "Lora", Georgia, serif; - --font-mono: "Fira Code", "Courier New", monospace; - --radius: 0.5rem; - --shadow-2xs: 1px 2px 5px 1px hsl(0 0% 0% / 0.03); - --shadow-xs: 1px 2px 5px 1px hsl(0 0% 0% / 0.03); - --shadow-sm: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 1px 2px 0px hsl(0 0% 0% / 0.06); - --shadow: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 1px 2px 0px hsl(0 0% 0% / 0.06); - --shadow-md: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 2px 4px 0px hsl(0 0% 0% / 0.06); - --shadow-lg: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 4px 6px 0px hsl(0 0% 0% / 0.06); - --shadow-xl: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 8px 10px 0px hsl(0 0% 0% / 0.06); - --shadow-2xl: 1px 2px 5px 1px hsl(0 0% 0% / 0.15); - } - - .purple-dark { - --background: 250.9091 18.6441% 11.5686%; - --foreground: 250.0000 36.0000% 90.1961%; - --card: 251.2500 20% 15.6863%; - --card-foreground: 250.0000 36.0000% 90.1961%; - --popover: 251.2500 20% 15.6863%; - --popover-foreground: 250.0000 36.0000% 90.1961%; - --primary: 263.0769 32.5000% 68.6275%; - --primary-foreground: 250.9091 18.6441% 11.5686%; - --secondary: 254.4828 14.8718% 38.2353%; - --secondary-foreground: 250.0000 36.0000% 90.1961%; - --muted: 254.1176 20.9877% 15.8824%; - --muted-foreground: 258.9474 10.3825% 64.1176%; - --accent: 271.7647 15.5963% 21.3725%; - --accent-foreground: 345.5172 69.0476% 83.5294%; - --destructive: 0 68.6747% 67.4510%; - --destructive-foreground: 250.9091 18.6441% 11.5686%; - --border: 252 18.5185% 21.1765%; - --input: 249.4737 19.5876% 19.0196%; - --ring: 263.0769 32.5000% 68.6275%; - --chart-1: 263.0769 32.5000% 68.6275%; - --chart-2: 345.5172 69.0476% 83.5294%; - --chart-3: 158.7692 31.4010% 59.4118%; - --chart-4: 35.7576 76.7442% 74.7059%; - --chart-5: 215.8209 54.4715% 75.8824%; - --sidebar: 252 20.0000% 9.8039%; - --sidebar-foreground: 250.0000 36.0000% 90.1961%; - --sidebar-primary: 263.0769 32.5000% 68.6275%; - --sidebar-primary-foreground: 250.9091 18.6441% 11.5686%; - --sidebar-accent: 271.7647 15.5963% 21.3725%; - --sidebar-accent-foreground: 345.5172 69.0476% 83.5294%; - --sidebar-border: 249.4737 19.5876% 19.0196%; - --sidebar-ring: 263.0769 32.5000% 68.6275%; - --font-sans: Geist, sans-serif; - --font-serif: "Lora", Georgia, serif; - --font-mono: "Fira Code", "Courier New", monospace; - --radius: 0.5rem; - --shadow-2xs: 1px 2px 5px 1px hsl(0 0% 0% / 0.03); - --shadow-xs: 1px 2px 5px 1px hsl(0 0% 0% / 0.03); - --shadow-sm: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 1px 2px 0px hsl(0 0% 0% / 0.06); - --shadow: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 1px 2px 0px hsl(0 0% 0% / 0.06); - --shadow-md: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 2px 4px 0px hsl(0 0% 0% / 0.06); - --shadow-lg: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 4px 6px 0px hsl(0 0% 0% / 0.06); - --shadow-xl: 1px 2px 5px 1px hsl(0 0% 0% / 0.06), 1px 8px 10px 0px hsl(0 0% 0% / 0.06); - --shadow-2xl: 1px 2px 5px 1px hsl(0 0% 0% / 0.15); - } - - .vintage-light { - --background: 44.0000 42.8571% 93.1373%; - --foreground: 28.5714 16.5354% 24.9020%; - --card: 42.0000 100.0000% 98.0392%; - --card-foreground: 28.5714 16.5354% 24.9020%; - --popover: 42.0000 100.0000% 98.0392%; - --popover-foreground: 28.5714 16.5354% 24.9020%; - --primary: 30.0000 33.8710% 48.6275%; - --primary-foreground: 0 0% 100%; - --secondary: 40.6452 34.8315% 82.5490%; - --secondary-foreground: 28.9655 18.7097% 30.3922%; - --muted: 39 34.4828% 88.6275%; - --muted-foreground: 32.3077 18.4834% 41.3725%; - --accent: 42.8571 32.8125% 74.9020%; - --accent-foreground: 28.5714 16.5354% 24.9020%; - --destructive: 9.8438 54.7009% 45.8824%; - --destructive-foreground: 0 0% 100%; - --border: 40.0000 31.4286% 79.4118%; - --input: 40.0000 31.4286% 79.4118%; - --ring: 30.0000 33.8710% 48.6275%; - --chart-1: 30.0000 33.8710% 48.6275%; - --chart-2: 31.3846 29.9539% 42.5490%; - --chart-3: 33.6842 32.9480% 33.9216%; - --chart-4: 29.1176 30.9091% 56.8627%; - --chart-5: 30 33.6842% 62.7451%; - --sidebar: 39 34.4828% 88.6275%; - --sidebar-foreground: 28.5714 16.5354% 24.9020%; - --sidebar-primary: 30.0000 33.8710% 48.6275%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 42.8571 32.8125% 74.9020%; - --sidebar-accent-foreground: 28.5714 16.5354% 24.9020%; - --sidebar-border: 40.0000 31.4286% 79.4118%; - --sidebar-ring: 30.0000 33.8710% 48.6275%; - --font-sans: Libre Baskerville, serif; - --font-serif: Lora, serif; - --font-mono: IBM Plex Mono, monospace; - --radius: 0.25rem; - --shadow-2xs: 2px 3px 5px 0px hsl(28 13% 20% / 0.06); - --shadow-xs: 2px 3px 5px 0px hsl(28 13% 20% / 0.06); - --shadow-sm: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 1px 2px -1px hsl(28 13% 20% / 0.12); - --shadow: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 1px 2px -1px hsl(28 13% 20% / 0.12); - --shadow-md: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 2px 4px -1px hsl(28 13% 20% / 0.12); - --shadow-lg: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 4px 6px -1px hsl(28 13% 20% / 0.12); - --shadow-xl: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 8px 10px -1px hsl(28 13% 20% / 0.12); - --shadow-2xl: 2px 3px 5px 0px hsl(28 13% 20% / 0.30); - } - - .vintage-dark { - --background: 25.0000 15.3846% 15.2941%; - --foreground: 39 34.4828% 88.6275%; - --card: 25.7143 13.7255% 20%; - --card-foreground: 39 34.4828% 88.6275%; - --popover: 25.7143 13.7255% 20%; - --popover-foreground: 39 34.4828% 88.6275%; - --primary: 30 33.6842% 62.7451%; - --primary-foreground: 25.0000 15.3846% 15.2941%; - --secondary: 24.7059 12.9771% 25.6863%; - --secondary-foreground: 39 34.4828% 88.6275%; - --muted: 25.7143 13.7255% 20%; - --muted-foreground: 38.4000 17.7305% 72.3529%; - --accent: 24.4444 17.8808% 29.6078%; - --accent-foreground: 39 34.4828% 88.6275%; - --destructive: 9.8438 54.7009% 45.8824%; - --destructive-foreground: 0 0% 100%; - --border: 24.7059 12.9771% 25.6863%; - --input: 24.7059 12.9771% 25.6863%; - --ring: 30 33.6842% 62.7451%; - --chart-1: 30 33.6842% 62.7451%; - --chart-2: 29.1176 30.9091% 56.8627%; - --chart-3: 30.0000 33.8710% 48.6275%; - --chart-4: 31.3846 29.9539% 42.5490%; - --chart-5: 33.6842 32.9480% 33.9216%; - --sidebar: 25.0000 15.3846% 15.2941%; - --sidebar-foreground: 39 34.4828% 88.6275%; - --sidebar-primary: 30 33.6842% 62.7451%; - --sidebar-primary-foreground: 25.0000 15.3846% 15.2941%; - --sidebar-accent: 24.4444 17.8808% 29.6078%; - --sidebar-accent-foreground: 39 34.4828% 88.6275%; - --sidebar-border: 24.7059 12.9771% 25.6863%; - --sidebar-ring: 30 33.6842% 62.7451%; - --font-sans: Libre Baskerville, serif; - --font-serif: Lora, serif; - --font-mono: IBM Plex Mono, monospace; - --radius: 0.25rem; - --shadow-2xs: 2px 3px 5px 0px hsl(28 13% 20% / 0.06); - --shadow-xs: 2px 3px 5px 0px hsl(28 13% 20% / 0.06); - --shadow-sm: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 1px 2px -1px hsl(28 13% 20% / 0.12); - --shadow: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 1px 2px -1px hsl(28 13% 20% / 0.12); - --shadow-md: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 2px 4px -1px hsl(28 13% 20% / 0.12); - --shadow-lg: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 4px 6px -1px hsl(28 13% 20% / 0.12); - --shadow-xl: 2px 3px 5px 0px hsl(28 13% 20% / 0.12), 2px 8px 10px -1px hsl(28 13% 20% / 0.12); - --shadow-2xl: 2px 3px 5px 0px hsl(28 13% 20% / 0.30); - } - - .neo-brutalism-light { - --background: 0 0% 100%; - --foreground: 0 0% 0%; - --card: 0 0% 100%; - --card-foreground: 0 0% 0%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 0%; - --primary: 0 100% 60%; - --primary-foreground: 0 0% 100%; - --secondary: 60 100% 50%; - --secondary-foreground: 0 0% 0%; - --muted: 0 0% 94.1176%; - --muted-foreground: 0 0% 20%; - --accent: 216 100% 50%; - --accent-foreground: 0 0% 100%; - --destructive: 0 0% 0%; - --destructive-foreground: 0 0% 100%; - --border: 0 0% 0%; - --input: 0 0% 0%; - --ring: 0 100% 60%; - --chart-1: 0 100% 60%; - --chart-2: 60 100% 50%; - --chart-3: 216 100% 50%; - --chart-4: 120 100% 40%; - --chart-5: 300 100% 40%; - --sidebar: 0 0% 94.1176%; - --sidebar-foreground: 0 0% 0%; - --sidebar-primary: 0 100% 60%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 216 100% 50%; - --sidebar-accent-foreground: 0 0% 100%; - --sidebar-border: 0 0% 0%; - --sidebar-ring: 0 100% 60%; - --font-sans: DM Sans, sans-serif; - --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: Space Mono, monospace; - --radius: 0px; - --shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50); - --shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50); - --shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00); - --shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00); - --shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00); - --shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00); - --shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00); - --shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50); - } - - .neo-brutalism-dark { - --background: 0 0% 0%; - --foreground: 0 0% 100%; - --card: 0 0% 20%; - --card-foreground: 0 0% 100%; - --popover: 0 0% 20%; - --popover-foreground: 0 0% 100%; - --primary: 0 100.0000% 70%; - --primary-foreground: 0 0% 0%; - --secondary: 60 100% 60%; - --secondary-foreground: 0 0% 0%; - --muted: 0 0% 20%; - --muted-foreground: 0 0% 80%; - --accent: 210 100% 60%; - --accent-foreground: 0 0% 0%; - --destructive: 0 0% 100%; - --destructive-foreground: 0 0% 0%; - --border: 0 0% 100%; - --input: 0 0% 100%; - --ring: 0 100.0000% 70%; - --chart-1: 0 100.0000% 70%; - --chart-2: 60 100% 60%; - --chart-3: 210 100% 60%; - --chart-4: 120 60.0000% 50%; - --chart-5: 300 60.0000% 50%; - --sidebar: 0 0% 0%; - --sidebar-foreground: 0 0% 100%; - --sidebar-primary: 0 100.0000% 70%; - --sidebar-primary-foreground: 0 0% 0%; - --sidebar-accent: 210 100% 60%; - --sidebar-accent-foreground: 0 0% 0%; - --sidebar-border: 0 0% 100%; - --sidebar-ring: 0 100.0000% 70%; - --font-sans: DM Sans, sans-serif; - --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: Space Mono, monospace; - --radius: 0px; - --shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50); - --shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50); - --shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00); - --shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00); - --shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00); - --shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00); - --shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00); - --shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50); - } - - .nature-light { - --background: 37.5000 36.3636% 95.6863%; - --foreground: 8.8889 27.8351% 19.0196%; - --card: 37.5000 36.3636% 95.6863%; - --card-foreground: 8.8889 27.8351% 19.0196%; - --popover: 37.5000 36.3636% 95.6863%; - --popover-foreground: 8.8889 27.8351% 19.0196%; - --primary: 123.0380 46.1988% 33.5294%; - --primary-foreground: 0 0% 100%; - --secondary: 124.6154 39.3939% 93.5294%; - --secondary-foreground: 124.4776 55.3719% 23.7255%; - --muted: 33.7500 34.7826% 90.9804%; - --muted-foreground: 15.0000 25.2874% 34.1176%; - --accent: 122 37.5000% 84.3137%; - --accent-foreground: 124.4776 55.3719% 23.7255%; - --destructive: 0 66.3866% 46.6667%; - --destructive-foreground: 0 0% 100%; - --border: 33.9130 27.0588% 83.3333%; - --input: 33.9130 27.0588% 83.3333%; - --ring: 123.0380 46.1988% 33.5294%; - --chart-1: 122.4242 39.4422% 49.2157%; - --chart-2: 122.7907 43.4343% 38.8235%; - --chart-3: 123.0380 46.1988% 33.5294%; - --chart-4: 124.4776 55.3719% 23.7255%; - --chart-5: 125.7143 51.2195% 8.0392%; - --sidebar: 33.7500 34.7826% 90.9804%; - --sidebar-foreground: 8.8889 27.8351% 19.0196%; - --sidebar-primary: 123.0380 46.1988% 33.5294%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 122 37.5000% 84.3137%; - --sidebar-accent-foreground: 124.4776 55.3719% 23.7255%; - --sidebar-border: 33.9130 27.0588% 83.3333%; - --sidebar-ring: 123.0380 46.1988% 33.5294%; - --font-sans: Montserrat, sans-serif; - --font-serif: Merriweather, serif; - --font-mono: Source Code Pro, monospace; - --radius: 0.5rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); - } - - .nature-dark { - --background: 132.8571 20% 13.7255%; - --foreground: 32.7273 26.8293% 91.9608%; - --card: 124.6154 12.6214% 20.1961%; - --card-foreground: 32.7273 26.8293% 91.9608%; - --popover: 124.6154 12.6214% 20.1961%; - --popover-foreground: 32.7273 26.8293% 91.9608%; - --primary: 122.4242 39.4422% 49.2157%; - --primary-foreground: 125.7143 51.2195% 8.0392%; - --secondary: 115.3846 9.6296% 26.4706%; - --secondary-foreground: 114.0000 13.8889% 85.8824%; - --muted: 124.6154 12.6214% 20.1961%; - --muted-foreground: 34.7368 19.1919% 80.5882%; - --accent: 122.7907 43.4343% 38.8235%; - --accent-foreground: 32.7273 26.8293% 91.9608%; - --destructive: 0 66.3866% 46.6667%; - --destructive-foreground: 32.7273 26.8293% 91.9608%; - --border: 115.3846 9.6296% 26.4706%; - --input: 115.3846 9.6296% 26.4706%; - --ring: 122.4242 39.4422% 49.2157%; - --chart-1: 122.5714 38.4615% 64.3137%; - --chart-2: 122.8235 38.4615% 56.6667%; - --chart-3: 122.4242 39.4422% 49.2157%; - --chart-4: 122.5806 40.9692% 44.5098%; - --chart-5: 122.7907 43.4343% 38.8235%; - --sidebar: 132.8571 20% 13.7255%; - --sidebar-foreground: 32.7273 26.8293% 91.9608%; - --sidebar-primary: 122.4242 39.4422% 49.2157%; - --sidebar-primary-foreground: 125.7143 51.2195% 8.0392%; - --sidebar-accent: 122.7907 43.4343% 38.8235%; - --sidebar-accent-foreground: 32.7273 26.8293% 91.9608%; - --sidebar-border: 115.3846 9.6296% 26.4706%; - --sidebar-ring: 122.4242 39.4422% 49.2157%; - --font-sans: Montserrat, sans-serif; - --font-serif: Merriweather, serif; - --font-mono: Source Code Pro, monospace; - --radius: 0.5rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); - } -} - -/* @layer base { - :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 142.1 76.2% 36.3%; - --primary-foreground: 355.7 100% 97.3%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 142.1 76.2% 36.3%; - --radius: 0.3rem; - } - - .dark { - --background: 20 14.3% 4.1%; - --foreground: 0 0% 95%; - --card: 24 9.8% 10%; - --card-foreground: 0 0% 95%; - --popover: 0 0% 9%; - --popover-foreground: 0 0% 95%; - --primary: 142.1 70.6% 45.3%; - --primary-foreground: 144.9 80.4% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 15%; - --muted-foreground: 240 5% 64.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 85.7% 97.3%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 142.4 71.8% 29.2%; - } -} */ - -@layer base { - * { - @apply border-border; - } - - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file diff --git a/app/icon.tsx b/app/icon.tsx deleted file mode 100644 index 6e49223..0000000 --- a/app/icon.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { ImageResponse } from 'next/og' - -// Route segment config -export const runtime = 'edge' - -// Image metadata -export const size = { - width: 32, - height: 32, -} -export const contentType = 'image/png' - -// Image generation -export default function Icon() { - return new ImageResponse( - ( - // ImageResponse JSX element - <div - style={{ - fontSize: 24, - background: 'black', - width: '100%', - height: '100%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: 'white', - }} - > - L - </div> - ), - // ImageResponse options - { - // For convenience, we can re-use the exported icons size metadata - // config to also set the ImageResponse's width and height. - ...size, - } - ) -} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index 510b483..0000000 --- a/app/layout.tsx +++ /dev/null @@ -1,151 +0,0 @@ -'use client'; - -import "./globals.css"; -import { NostrProvider } from "nostr-react"; -import { ThemeProvider } from "@/components/theme-provider"; -import { TopNavigation } from "@/components/headerComponents/TopNavigation"; -import BottomBar from "@/components/BottomBar"; -import { Toaster } from "@/components/ui/toaster" -import Umami from "@/components/Umami"; -import ErrorBoundary from "@/components/ErrorBoundary"; -import { useEffect, useState } from "react"; -import { getRelayConfig } from "@/utils/nip65Utils"; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const [relayUrls, setRelayUrls] = useState<string[]>([ - "wss://relay.nostr.band", - "wss://relay.damus.io", - "wss://profiles.nostr1.com" - ]); - - useEffect(() => { - // Load relay configuration from localStorage - try { - const config = getRelayConfig(); - - // Use inbox relays for the NostrProvider (for reading events) - setRelayUrls(config.inbox); - } catch (error) { - console.error("Error loading relay configuration:", error); - // Fallback to default relays - setRelayUrls([ - "wss://relay.nostr.band", - "wss://relay.damus.io", - ]); - } - - // Suppress unhandled promise rejection errors from nostr-tools - const handleUnhandledRejection = (event: PromiseRejectionEvent) => { - // Check if the error is related to nostr authentication or WebSocket handling - if (event.reason && typeof event.reason === 'object' && 'message' in event.reason) { - const message = event.reason.message; - if (message.includes('auth-required') || message.includes('auth required')) { - console.warn('Suppressed nostr authentication error:', message); - event.preventDefault(); - return; - } - } - - // Check if the error is from nostr-tools WebSocket handling - if (event.reason && typeof event.reason === 'object' && 'stack' in event.reason) { - const stack = event.reason.stack; - if (stack && typeof stack === 'string' && - (stack.includes('handleNext') || stack.includes('index.js:544') || - stack.includes('index.js:1078') || stack.includes('websocket'))) { - console.warn('Suppressed nostr WebSocket error:', event.reason); - event.preventDefault(); - return; - } - } - - // Check for WebSocket connection errors specifically - if (event.reason && typeof event.reason === 'string' && - event.reason.includes('websocket error')) { - console.warn('Suppressed WebSocket error:', event.reason); - event.preventDefault(); - return; - } - - // For other unhandled rejections, let them through - console.error('Unhandled promise rejection:', event.reason); - }; - - // Handle unhandled promise rejections - window.addEventListener('unhandledrejection', handleUnhandledRejection); - - // Handle uncaught errors - const handleUncaughtError = (event: ErrorEvent) => { - // Check if the error is from nostr-tools WebSocket handling - if (event.error && typeof event.error === 'object' && 'stack' in event.error) { - const stack = event.error.stack; - if (stack && typeof stack === 'string' && - (stack.includes('handleNext') || stack.includes('index.js:544') || - stack.includes('index.js:1078') || stack.includes('websocket'))) { - console.warn('Suppressed nostr WebSocket error:', event.error); - event.preventDefault(); - return; - } - } - - // Check for WebSocket connection errors specifically - if (event.message && event.message.includes('websocket error')) { - console.warn('Suppressed WebSocket error:', event.message); - event.preventDefault(); - return; - } - - // Check for localhost connection failures - if (event.message && event.message.includes('localhost:3334')) { - console.warn('Suppressed localhost relay connection error:', event.message); - event.preventDefault(); - return; - } - - // For other uncaught errors, let them through - console.error('Uncaught error:', event.error); - }; - - window.addEventListener('error', handleUncaughtError); - - return () => { - window.removeEventListener('unhandledrejection', handleUnhandledRejection); - window.removeEventListener('error', handleUncaughtError); - }; - }, []); - - return ( - <html lang="en"> - <head> - <link rel="icon" href="/favicon.ico" type="image/x-icon" /> - <link rel="manifest" href="/manifest.json" /> - <title>LUMINA</title> - <meta name="description" content="An effortless, enjoyable, and innovative way to capture, enhance, and share moments with everyone, decentralized and boundless." /> - </head> - <body className="font-sans"> - <ThemeProvider - attribute="class" - defaultTheme="dark" - enableSystem - disableTransitionOnChange - themes={["light", "dark", "purple-light", "purple-dark", "vintage-light", "vintage-dark", "neo-brutalism-light", "neo-brutalism-dark", "nature-light", "nature-dark", "system"]} - > - <Umami /> - <div className="main-content pb-14"> - <ErrorBoundary> - <NostrProvider relayUrls={relayUrls} debug={false}> - <TopNavigation /> - <Toaster /> - {children} - </NostrProvider> - </ErrorBoundary> - </div> - <BottomBar /> - </ThemeProvider> - </body> - </html> - ); -} \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx deleted file mode 100644 index eb2debe..0000000 --- a/app/login/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -'use client'; - -import Head from "next/head"; -import { LoginForm } from "@/components/LoginForm"; -import { useEffect } from "react"; - -export default function LoginPage() { - - useEffect(() => { - document.title = `Login | LUMINA`; - }, []); - - return ( - <> - <Head> - <title>LUMINA.rocks - Login</title> - <meta name="description" content="Yet another nostr web ui" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/favicon.ico" /> - </Head> - <div className="w-screen pt-10 flex items-center justify-center"> - <LoginForm /> - </div> - </> - ); -} diff --git a/app/note/[id]/page.tsx b/app/note/[id]/page.tsx deleted file mode 100644 index 47b397e..0000000 --- a/app/note/[id]/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import Head from "next/head"; -import { useParams } from 'next/navigation' -import NotePageComponent from "@/components/NotePageComponent"; -import { nip19 } from "nostr-tools"; -import { useEffect } from "react"; - -export default function NotePage() { - useEffect(() => { - document.title = `Note | LUMINA`; - }, []); - - const params = useParams() - let id = params.id - - if (id.includes("note")) { - id = nip19.decode(id.toString()).data.toString() - } else if (id.includes("nevent")) { - const decoded = nip19.decode(id.toString()) - if (decoded.type === 'nevent') { - id = decoded.data.id - } - } - - return ( - <> - <Head> - <title>LUMINA.rocks - {id}</title> - <meta name="description" content="Yet another nostr web ui" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/favicon.ico" /> - </Head> - <div className="px-6"> - <div className="pb-6"> - <NotePageComponent id={id.toString()} /> - </div> - </div> - </> - ); -} diff --git a/app/notifications/page.tsx b/app/notifications/page.tsx deleted file mode 100644 index 7ec8223..0000000 --- a/app/notifications/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { nip19 } from "nostr-tools"; -import Notifications from '@/components/Notifications'; -import { useEffect } from "react"; - -const NotificationsPage: React.FC = ({ }) => { - useEffect(() => { - document.title = `Notifications | LUMINA`; - }, []); - - let pubkey = ''; - - if (typeof window !== 'undefined') { - pubkey = window.localStorage.getItem("pubkey") ?? ''; - } - - // check if pubkey contains "npub" - // if so, then we need to convert it to a pubkey - if (pubkey.includes("npub")) { - // convert npub to pubkey - pubkey = nip19.decode(pubkey.toString()).data.toString() - } - - return ( - <> - <Notifications pubkey={pubkey.toString()} /> - </> - ); -} - -export default NotificationsPage; \ No newline at end of file diff --git a/app/onboarding/createProfile/page.tsx b/app/onboarding/createProfile/page.tsx deleted file mode 100644 index b7c27a0..0000000 --- a/app/onboarding/createProfile/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { CreateProfileForm } from "@/components/CreateProfileForm"; -import { useEffect } from "react"; -import { ArrowLeft } from "lucide-react"; -import Link from "next/link"; - -export default function OnboardingCreateProfile() { - useEffect(() => { - document.title = `Create Profile | LUMINA`; - }, []); - - return ( - <div className="max-w-3xl mx-auto py-12 px-6"> - <div className="space-y-6 text-center mb-8"> - <div className="flex items-center justify-center gap-2"> - <Link - href="/onboarding" - className="text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1 text-sm absolute left-6 md:left-10" - > - <ArrowLeft className="h-4 w-4" /> Back - </Link> - <h1 className="text-3xl font-bold tracking-tight">Create Your Profile</h1> - </div> - <p className="text-muted-foreground max-w-xl mx-auto"> - Set up your public profile information. This will be visible to everyone - on the Nostr network and help others find and connect with you. - </p> - </div> - - <div className="bg-card rounded-lg border p-6 shadow-sm"> - <div className="space-y-4 mb-6"> - <h2 className="text-xl font-semibold">Step 2: Personalize Your Profile</h2> - <p className="text-muted-foreground"> - Your profile information will be published to the Nostr network as a kind 0 event. - You can update this information anytime after creating your profile. - </p> - </div> - <CreateProfileForm /> - </div> - </div> - ); -} diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx deleted file mode 100644 index 0dbd5ce..0000000 --- a/app/onboarding/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { CreateSecretKeyForm } from "@/components/onboarding/createSecretKeyForm"; -import { useEffect } from "react"; - -export default function OnboardingHome() { - useEffect(() => { - document.title = `Onboarding | LUMINA`; - }, []); - - return ( - <div className="max-w-3xl mx-auto py-12 px-6"> - <div className="space-y-6 text-center mb-8"> - <h1 className="text-3xl font-bold tracking-tight">Welcome to LUMINA</h1> - <p className="text-muted-foreground max-w-xl mx-auto"> - Join the decentralized social network powered by Nostr. No central servers, just communication that you control. - </p> - </div> - - <div className="bg-card rounded-lg border p-6 shadow-sm"> - <div className="space-y-4 mb-6"> - <h2 className="text-xl font-semibold">Step 1: Create Your Keys</h2> - <p className="text-muted-foreground"> - In Nostr, your identity is based on a pair of cryptographic keys. The secret key (nsec) - is like your password - never share it with anyone. The public key (npub) is your public - identity that others use to find you. - </p> - </div> - <CreateSecretKeyForm /> - </div> - </div> - ); -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index f1464dc..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { Search } from "@/components/Search"; -import { WelcomeContent } from "@/components/WelcomeContent"; -import { GeyserFundDonation } from "@/components/GeyserFundDonation"; -import { useEffect } from "react"; - -export default function Home() { - useEffect(() => { - document.title = `LUMINA`; - }, []); - - // Check for environment variable - Next.js exposes public env vars with NEXT_PUBLIC_ prefix - const showGeyserFund = process.env.NEXT_PUBLIC_SHOW_GEYSER_FUND === 'true'; - - return ( - <> - {showGeyserFund && ( - <div className="flex flex-col items-center px-6"> - <GeyserFundDonation /> - </div> - )} - <div className="flex flex-col items-center py-4 px-6"> - <Search /> - </div> - <WelcomeContent /> - </> - ); -} \ No newline at end of file diff --git a/app/page.tsx.orig b/app/page.tsx.orig deleted file mode 100644 index dc191aa..0000000 --- a/app/page.tsx.orig +++ /dev/null @@ -1,113 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( - <main className="flex min-h-screen flex-col items-center justify-between p-24"> - <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex"> - <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30"> - Get started by editing&nbsp; - <code className="font-mono font-bold">app/page.tsx</code> - </p> - <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none"> - <a - className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0" - href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - By{" "} - <Image - src="/vercel.svg" - alt="Vercel Logo" - className="dark:invert" - width={100} - height={24} - priority - /> - </a> - </div> - </div> - - <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]"> - <Image - className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert" - src="/next.svg" - alt="Next.js Logo" - width={180} - height={37} - priority - /> - </div> - - <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left"> - <a - href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app" - className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30" - target="_blank" - rel="noopener noreferrer" - > - <h2 className={`mb-3 text-2xl font-semibold`}> - Docs{" "} - <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"> - -&gt; - </span> - </h2> - <p className={`m-0 max-w-[30ch] text-sm opacity-50`}> - Find in-depth information about Next.js features and API. - </p> - </a> - - <a - href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30" - target="_blank" - rel="noopener noreferrer" - > - <h2 className={`mb-3 text-2xl font-semibold`}> - Learn{" "} - <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"> - -&gt; - </span> - </h2> - <p className={`m-0 max-w-[30ch] text-sm opacity-50`}> - Learn about Next.js in an interactive course with&nbsp;quizzes! - </p> - </a> - - <a - href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app" - className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30" - target="_blank" - rel="noopener noreferrer" - > - <h2 className={`mb-3 text-2xl font-semibold`}> - Templates{" "} - <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"> - -&gt; - </span> - </h2> - <p className={`m-0 max-w-[30ch] text-sm opacity-50`}> - Explore starter templates for Next.js. - </p> - </a> - - <a - href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app" - className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30" - target="_blank" - rel="noopener noreferrer" - > - <h2 className={`mb-3 text-2xl font-semibold`}> - Deploy{" "} - <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"> - -&gt; - </span> - </h2> - <p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}> - Instantly deploy your Next.js site to a shareable URL with Vercel. - </p> - </a> - </div> - </main> - ); -} diff --git a/app/profile/[pubkey]/page.tsx b/app/profile/[pubkey]/page.tsx deleted file mode 100644 index 155181d..0000000 --- a/app/profile/[pubkey]/page.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client'; - -import ProfileInfoCard from "@/components/ProfileInfoCard"; -import ProfileFeed from "@/components/ProfileFeed"; -import { useParams } from 'next/navigation' -import { nip19 } from "nostr-tools"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { SectionIcon, GridIcon } from '@radix-ui/react-icons' -import ProfileQuickViewFeed from "@/components/ProfileQuickViewFeed"; -import ProfileTextFeed from "@/components/ProfileTextFeed"; -import ProfileGalleryViewFeed from "@/components/ProfileGalleryViewFeed"; -import { useProfile } from "nostr-react"; -import { useMemo, useEffect } from "react"; - -export default function ProfilePage() { - - const params = useParams() - let pubkey = Array.isArray(params.pubkey) ? params.pubkey[0] : params.pubkey; - // check if pubkey contains "npub" or "nprofile" - // if so, we need to convert it to a hex pubkey - if (pubkey.includes("npub")) { - // convert npub to pubkey - pubkey = nip19.decode(pubkey.toString()).data.toString() - } else if (pubkey.includes("nprofile")) { - // convert nprofile to pubkey - const decoded = nip19.decode(pubkey.toString()); - if (decoded.type === 'nprofile') { - const profileData = decoded.data as { pubkey: string; relays?: string[] }; - pubkey = profileData.pubkey; - } - } - - const npubShortened = useMemo(() => { - let encoded = nip19.npubEncode(pubkey); - let parts = encoded.split('npub'); - return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); - }, [pubkey]); - - const { data: userData, isLoading } = useProfile({ pubkey }); - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - - useEffect(() => { - if (title) { - document.title = `${title} | LUMINA`; - } - }, [title]); - - return ( - <> - <div className="md:px-6"> - <div> - <ProfileInfoCard pubkey={pubkey.toString()} /> - </div> - <Tabs className="w-full" defaultValue="QuickView"> - <TabsList className="w-full grid grid-cols-3"> - <TabsTrigger value="QuickView"><GridIcon /></TabsTrigger> - <TabsTrigger value="ProfileFeed"><SectionIcon /></TabsTrigger> - <TabsTrigger value="ProfileTextFeed">Notes</TabsTrigger> - {/* <TabsTrigger value="Gallery">Gallery</TabsTrigger> */} - </TabsList> - <TabsContent value="QuickView"> - <ProfileQuickViewFeed pubkey={pubkey.toString()} /> - </TabsContent> - <TabsContent value="ProfileFeed"> - <ProfileFeed pubkey={pubkey.toString()} /> - </TabsContent> - <TabsContent value="ProfileTextFeed"> - <ProfileTextFeed pubkey={pubkey.toString()} /> - </TabsContent> - {/* <TabsContent value="Gallery"> - <ProfileGalleryViewFeed pubkey={pubkey.toString()} /> - </TabsContent> */} - </Tabs> - </div> - </> - ); -} diff --git a/app/profile/settings/nwc/page.tsx b/app/profile/settings/nwc/page.tsx deleted file mode 100644 index 688fa4a..0000000 --- a/app/profile/settings/nwc/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { useParams } from 'next/navigation' -import { nip19 } from "nostr-tools"; -import { UpdateProfileForm } from "@/components/UpdateProfileForm"; -import { useEffect } from 'react'; -import { NostrWalletConnect } from '@/components/NostrWalletConnect'; - -export default function ProfileSettingsNWCPage() { - useEffect(() => { - document.title = `NWC Settings | LUMINA`; - }, []); - - let pubkey = null; - if (typeof window !== 'undefined') { - pubkey = window.localStorage.getItem('pubkey'); - } - // check if pubkey is not null and contains "npub" - // if so, then we need to convert it to a pubkey - if (pubkey && pubkey.includes("npub")) { - // convert npub to pubkey - pubkey = nip19.decode(pubkey.toString()).data.toString() - } - - return ( - <div className="container mx-auto py-8 px-4 sm:px-6"> - <div className="space-y-6"> - <div className="flex flex-col space-y-2"> - <h1 className="text-3xl font-bold tracking-tight">NWC Settings</h1> - {/* <p className="text-muted-foreground"> - Update your profile information that will be visible to others on the Nostr network - </p> */} - </div> - <div className="rounded-lg border bg-card shadow-sm"> - <div className="p-6"> - <NostrWalletConnect /> - </div> - </div> - </div> - </div> - ); -} \ No newline at end of file diff --git a/app/profile/settings/page.tsx b/app/profile/settings/page.tsx deleted file mode 100644 index d61e00d..0000000 --- a/app/profile/settings/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { useParams } from 'next/navigation' -import { nip19 } from "nostr-tools"; -import { UpdateProfileForm } from "@/components/UpdateProfileForm"; -import { useEffect } from 'react'; - -export default function ProfileSettingsPage() { - useEffect(() => { - document.title = `Settings | LUMINA`; - }, []); - - let pubkey = null; - if (typeof window !== 'undefined') { - pubkey = window.localStorage.getItem('pubkey'); - } - // check if pubkey is not null and contains "npub" - // if so, then we need to convert it to a pubkey - if (pubkey && pubkey.includes("npub")) { - // convert npub to pubkey - pubkey = nip19.decode(pubkey.toString()).data.toString() - } - - return ( - <div className="container mx-auto py-8 px-4 sm:px-6"> - <div className="space-y-6"> - <div className="flex flex-col space-y-2"> - <h1 className="text-3xl font-bold tracking-tight">Profile Settings</h1> - <p className="text-muted-foreground"> - Update your profile information that will be visible to others on the Nostr network - </p> - </div> - <div className="rounded-lg border bg-card shadow-sm"> - <div className="p-6"> - <UpdateProfileForm /> - </div> - </div> - </div> - </div> - ); -} \ No newline at end of file diff --git a/app/reel/page.tsx b/app/reel/page.tsx deleted file mode 100644 index dc422dd..0000000 --- a/app/reel/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import ReelFeed from "@/components/ReelFeed" -import { useEffect } from "react"; - -export default function ReelPage() { - useEffect(() => { - document.title = `Reels | LUMINA`; - // Prevent scrolling on this page for a full-screen experience - document.body.style.overflow = 'hidden'; - - // Hide the header and bottom bar when on the reel page - const topNav = document.querySelector('nav'); - const bottomBar = document.querySelector('.fixed.bottom-0'); - - if (topNav) { - (topNav as HTMLElement).style.display = 'none'; - } - - if (bottomBar) { - (bottomBar as HTMLElement).style.display = 'none'; - } - - return () => { - // Restore scrolling and show navigation elements when leaving the page - document.body.style.overflow = ''; - - if (topNav) { - (topNav as HTMLElement).style.display = ''; - } - - if (bottomBar) { - (bottomBar as HTMLElement).style.display = ''; - } - }; - }, []); - - return ( - <div className="fixed inset-0 h-screen w-screen overflow-hidden z-50"> - <ReelFeed /> - </div> - ); -} diff --git a/app/relays/page.tsx b/app/relays/page.tsx deleted file mode 100644 index 7dba7f8..0000000 --- a/app/relays/page.tsx +++ /dev/null @@ -1,290 +0,0 @@ -"use client"; - -import { useNostr } from "nostr-react"; -import { useEffect, useState } from "react"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { CheckCircle2, XCircle, AlertCircle, SignalHigh, Clock, RefreshCw } from "lucide-react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Button } from "@/components/ui/button"; -import { AddRelaySheet } from "@/components/AddRelaySheet"; -import { ManageCustomRelays } from "@/components/ManageCustomRelays"; -import { fetchNip65Relays, mergeAndStoreRelays } from "@/utils/nip65Utils"; -import { useToast } from "@/components/ui/use-toast"; - -export default function RelaysPage() { - const { connectedRelays } = useNostr(); - const [relayStatus, setRelayStatus] = useState<{ [url: string]: 'connected' | 'connecting' | 'disconnected' | 'error' }>({}); - const [loading, setLoading] = useState(true); - const [refreshKey, setRefreshKey] = useState(0); - const [refreshingNip65, setRefreshingNip65] = useState(false); - const { toast } = useToast(); - - useEffect(() => { - document.title = `Relays | LUMINA`; - - // Set a loading timeout - if relays don't connect within 10 seconds, show whatever we have - const loadingTimeout = setTimeout(() => { - if (loading) { - setLoading(false); - } - }, 10000); - - if (connectedRelays) { - const status: { [url: string]: 'connected' | 'connecting' | 'disconnected' | 'error' } = {}; - - // Get status of each relay - connectedRelays.forEach(relay => { - if (relay.status === 1) { - status[relay.url] = 'connected'; - } else if (relay.status === 0) { - status[relay.url] = 'connecting'; - } else { - status[relay.url] = 'disconnected'; - } - }); - - setRelayStatus(status); - - // Only stop loading when we have at least one connected relay or after a timeout - if (Object.values(status).some(s => s === 'connected') || connectedRelays.length === 0) { - setLoading(false); - } - } - - return () => clearTimeout(loadingTimeout); - }, [connectedRelays, refreshKey]); - - // Function to refresh NIP-65 relays for the current user - const refreshNip65Relays = async () => { - try { - setRefreshingNip65(true); - - // Get current user's public key from local storage - const pubkey = localStorage.getItem('pubkey'); - - if (!pubkey) { - toast({ - title: "Error refreshing relays", - description: "You need to be logged in to refresh NIP-65 relays", - variant: "destructive", - }); - return; - } - - // Default relays to query for NIP-65 data - const defaultRelays = [ - "wss://relay.nostr.band", - "wss://relay.damus.io", - "wss://nos.lol", - "wss://relay.nostr.ch", - "wss://profiles.nostr1.com" - ]; - - // Fetch NIP-65 relays - const nip65Relays = await fetchNip65Relays(pubkey, defaultRelays); - - if (nip65Relays.length > 0) { - // Merge with existing relays and store in localStorage - const mergedRelays = mergeAndStoreRelays(nip65Relays); - - toast({ - title: "NIP-65 relays updated", - description: `Found ${nip65Relays.length} relays in your NIP-65 list. Refresh the page to connect to them.`, - }); - - // Refresh page connection status - setRefreshKey(prev => prev + 1); - } else { - toast({ - title: "No NIP-65 relays found", - description: "We couldn't find any NIP-65 relay preferences for your account", - }); - } - } catch (error) { - console.error("Error refreshing NIP-65 relays:", error); - toast({ - title: "Error refreshing relays", - description: "There was an error fetching your relay preferences", - variant: "destructive", - }); - } finally { - setRefreshingNip65(false); - } - }; - - // Function to get the appropriate status icon - const getStatusIcon = (status: string) => { - switch (status) { - case 'connected': - return <CheckCircle2 className="h-5 w-5 text-green-500" />; - case 'connecting': - return <Clock className="h-5 w-5 text-amber-500 animate-pulse" />; - case 'disconnected': - return <XCircle className="h-5 w-5 text-red-500" />; - case 'error': - return <AlertCircle className="h-5 w-5 text-red-500" />; - default: - return null; - } - }; - - // Function to get the appropriate status badge - const getStatusBadge = (status: string) => { - switch (status) { - case 'connected': - return <Badge className="bg-green-500">Connected</Badge>; - case 'connecting': - return <Badge className="bg-amber-500">Connecting</Badge>; - case 'disconnected': - return <Badge className="bg-red-500">Disconnected</Badge>; - case 'error': - return <Badge className="bg-red-500">Error</Badge>; - default: - return null; - } - }; - - const handleRelayAdded = () => { - // Trigger a refresh of the component when a relay is added - setRefreshKey(prev => prev + 1); - }; - - return ( - <div className="py-4 px-2 md:py-6 md:px-6"> - <div className="flex items-center justify-between mb-4"> - <h2 className="text-2xl mr-2 font-bold">Relays</h2> - <div className="flex space-x-2"> - <Button - variant="outline" - className="gap-2" - onClick={refreshNip65Relays} - disabled={refreshingNip65} - > - <RefreshCw className={`h-4 w-4 ${refreshingNip65 ? 'animate-spin' : ''}`} /> - {refreshingNip65 ? 'Refreshing NIP-65...' : 'Refresh NIP-65 Relays'} - </Button> - <AddRelaySheet onRelayAdded={handleRelayAdded} /> - </div> - </div> - - <Tabs defaultValue="list"> - <TabsList className="mb-4"> - <TabsTrigger value="list">List View</TabsTrigger> - <TabsTrigger value="cards">Card View</TabsTrigger> - </TabsList> - - <TabsContent value="list"> - <Card> - <CardHeader> - <CardTitle>Connected Relays</CardTitle> - <CardDescription>Current active relay connections ({Object.keys(relayStatus).length})</CardDescription> - </CardHeader> - <CardContent> - <ScrollArea className=""> - {loading ? ( - <div className="space-y-4"> - <Skeleton className="h-10 w-full" /> - <Skeleton className="h-10 w-full" /> - <Skeleton className="h-10 w-full" /> - </div> - ) : ( - <div className="space-y-2"> - {Object.entries(relayStatus).map(([url, status]) => ( - <div - key={url} - className="flex items-center justify-between p-3 rounded-md border bg-card hover:bg-accent/50 transition-colors" - > - <div className="flex items-center space-x-3"> - {getStatusIcon(status)} - <div className="overflow-hidden"> - <p className="font-medium truncate">{url}</p> - </div> - </div> - <div> - {getStatusBadge(status)} - </div> - </div> - ))} - </div> - )} - </ScrollArea> - </CardContent> - <CardFooter className="flex justify-between"> - <Button variant="outline" onClick={() => window.location.reload()}> - Refresh Connection Status - </Button> - </CardFooter> - </Card> - </TabsContent> - - <TabsContent value="cards"> - {loading ? ( - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - <Skeleton className="h-32 w-full" /> - <Skeleton className="h-32 w-full" /> - <Skeleton className="h-32 w-full" /> - </div> - ) : ( - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - {Object.entries(relayStatus).map(([url, status]) => ( - <Card key={url} className={` - ${status === 'connected' ? 'border-green-500/50' : ''} - ${status === 'connecting' ? 'border-amber-500/50' : ''} - ${status === 'disconnected' || status === 'error' ? 'border-red-500/50' : ''} - `}> - <CardHeader className="pb-2"> - <CardTitle className="flex items-center gap-2 text-base"> - {getStatusIcon(status)} - <span className="truncate" title={url}> - {url.replace(/^wss:\/\//, '')} - </span> - </CardTitle> - </CardHeader> - <CardContent> - <div className="flex justify-between items-center"> - <div className="flex items-center gap-2"> - <SignalHigh className="h-4 w-4 text-muted-foreground" /> - <span className="text-xs text-muted-foreground"> - {status === 'connected' ? 'Ready for events' : 'Not receiving events'} - </span> - </div> - {getStatusBadge(status)} - </div> - </CardContent> - </Card> - ))} - </div> - )} - </TabsContent> - </Tabs> - - <ManageCustomRelays /> - - <div className="mt-6"> - <Card> - <CardHeader> - <CardTitle>About Nostr Relays</CardTitle> - </CardHeader> - <CardContent> - <p className="text-muted-foreground"> - Relays are servers that receive, store, and forward Nostr events. They act as the infrastructure - that makes the decentralized social network possible. You can connect to multiple relays to - increase the reach and resilience of your posts and profile. - </p> - <div className="mt-3"> - <h3 className="text-sm font-medium mb-1">NIP-65 Relay Lists</h3> - <p className="text-xs text-muted-foreground"> - NIP-65 is a Nostr standard that allows users to share their preferred relays. When you log in, - LUMINA automatically fetches your relay preferences from the Nostr network and adds them to your - connection list. Use the &quot;Refresh NIP-65 Relays&quot; button above to manually update your relay list. - </p> - </div> - </CardContent> - </Card> - </div> - </div> - ); -} \ No newline at end of file diff --git a/app/search/[searchTag]/page.tsx b/app/search/[searchTag]/page.tsx deleted file mode 100644 index bbc94b8..0000000 --- a/app/search/[searchTag]/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import Head from "next/head"; -import ProfileInfoCard from "@/components/ProfileInfoCard"; -import ProfileFeed from "@/components/ProfileFeed"; -import { useParams } from 'next/navigation' -import { nip19 } from "nostr-tools"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { SectionIcon, GridIcon } from '@radix-ui/react-icons' -import TagFeed from "@/components/TagFeed"; -import FollowerFeed from "@/components/FollowerFeed"; -import ProfileQuickViewFeed from "@/components/ProfileQuickViewFeed"; -import FollowerQuickViewFeed from "@/components/FollowerQuickViewFeed"; -import SearchProfilesBox from "@/components/searchComponents/SearchProfilesBox"; -import SearchNotesBox from "@/components/searchComponents/SearchNotesBox"; -import { useEffect } from "react"; - -export default function SearchPage() { - - let pubkey = null; - if (typeof window !== 'undefined') { - pubkey = window.localStorage.getItem('pubkey'); - } - - const params = useParams() - let searchTag = params.searchTag - - useEffect(() => { - document.title = `Search: ${searchTag} | LUMINA`; - }, [searchTag]); - - // check if pubkey contains "npub" - // if so, then we need to convert it to a pubkey - // if (pubkey.includes("npub")) { - // // convert npub to pubkey - // pubkey = nip19.decode(pubkey.toString()).data.toString() - // } - - return ( - <> - <Head> - <title>LUMINA.rocks</title> - <meta name="description" content="Yet another nostr web ui" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/favicon.ico" /> - </Head> - <div className="py-6 px-6"> - <div className='grid grid-cols-1 gap-6' > - <SearchProfilesBox searchTag={searchTag.toString()} /> - <SearchNotesBox searchTag={searchTag.toString()} /> - </div> - </div> - </> - ); -} diff --git a/app/search/page.tsx b/app/search/page.tsx deleted file mode 100644 index 9aff85b..0000000 --- a/app/search/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { Search } from "@/components/Search"; -import { useEffect } from "react"; - -export default function SearchMainPage() { - useEffect(() => { - document.title = `Search | LUMINA`; - }, []); - - return ( - <> - <div className="flex flex-col items-center py-6 px-6"> - <Search /> - </div> - </> - ); -} diff --git a/app/tag/[tag]/page.tsx b/app/tag/[tag]/page.tsx deleted file mode 100644 index 7e63245..0000000 --- a/app/tag/[tag]/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import Head from "next/head"; -import { useParams } from 'next/navigation' -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { SectionIcon, GridIcon } from '@radix-ui/react-icons' -import TagFeed from "@/components/TagFeed"; -import { useEffect } from "react"; -import TagQuickViewFeed from "@/components/TagQuickViewFeed"; - -export default function Home() { - - const params = useParams() - let tag = Array.isArray(params.tag) ? params.tag[0] : params.tag; - - useEffect(() => { - document.title = `#${tag} | LUMINA`; - }, [tag]); - - - // check if pubkey contains "npub" - // if so, then we need to convert it to a pubkey - // if (pubkey.includes("npub")) { - // // convert npub to pubkey - // pubkey = nip19.decode(pubkey.toString()).data.toString() - // } - - return ( - <> - <Head> - <title>LUMINA.rocks - {tag}</title> - <meta name="description" content="Yet another nostr web ui" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/favicon.ico" /> - </Head> - <div className="py-4 px-2 md:py-6 md:px-6"> - <Tabs defaultValue="QuickView"> - <TabsList className="mb-4 w-full grid grid-cols-2"> - <TabsTrigger value="QuickView"><GridIcon /></TabsTrigger> - <TabsTrigger value="ExtendedFeed"><SectionIcon /></TabsTrigger> - </TabsList> - <TabsContent value="QuickView"> - <TagQuickViewFeed tag={tag} /> - </TabsContent> - <TabsContent value="ExtendedFeed"> - <TagFeed tag={tag} /> - </TabsContent> - </Tabs> - </div> - </> - ); -} diff --git a/app/tag/page.tsx b/app/tag/page.tsx deleted file mode 100644 index db7ce47..0000000 --- a/app/tag/page.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import Head from "next/head"; -import { useNostrEvents } from "nostr-react"; -import { useState, useEffect } from "react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Skeleton } from "@/components/ui/skeleton"; -import TagCard from "@/components/TagCard"; - -export default function TagPage() { - const [trendingTags, setTrendingTags] = useState<string[]>([]); - - let pubkey = ''; - if (typeof window !== 'undefined') { - pubkey = window.localStorage.getItem("pubkey") ?? ''; - } - - const { events: followEvents, isLoading: isFollowLoading } = useNostrEvents({ - filter: { - kinds: [3], - limit: 1, - authors: [pubkey], - }, - }); - - const { events: globalEvents, isLoading: isGlobalLoading } = useNostrEvents({ - filter: { - kinds: [20], - limit: 100, - }, - }); - - // Extract tags from followed users - const followedTags = followEvents - .flatMap(event => event.tags) - .filter(tag => tag[0] === 't') - .map(tag => tag[1]); - - // Get unique followed tags - const uniqueFollowedTags = Array.from(new Set(followedTags)); - - // Extract tags from global feed for trending tags - useEffect(() => { - if (globalEvents.length > 0) { - const allTags = globalEvents - .flatMap(event => event.tags) - .filter(tag => tag[0] === 't') - .map(tag => tag[1]); - - // Count tag occurrences to find trending tags - const tagCounts = allTags.reduce((acc, tag) => { - acc[tag] = (acc[tag] || 0) + 1; - return acc; - }, {} as Record<string, number>); - - // Sort by frequency and get top 20 - const sortedTags = Object.entries(tagCounts) - .sort((a, b) => b[1] - a[1]) - .map(([tag]) => tag) - .slice(0, 20); - - // Only update state if the tags have actually changed - if (JSON.stringify(sortedTags) !== JSON.stringify(trendingTags)) { - setTrendingTags(sortedTags); - } - } - }, [globalEvents, trendingTags]); // Depend on the full trendingTags array to capture content and order changes - - return ( - <> - <Head> - <title>LUMINA.rocks - Tags</title> - <meta name="description" content="Explore tags on LUMINA" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/favicon.ico" /> - </Head> - <div className="px-2 md:px-6"> - <Tabs defaultValue="trending" className="mt-4"> - <TabsList className="mb-4 w-full grid grid-cols-1"> - <TabsTrigger value="trending">Trending Tags</TabsTrigger> - {/* {pubkey && <TabsTrigger value="followed">My Tags</TabsTrigger>} */} - </TabsList> - - <TabsContent value="trending"> - <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> - {isGlobalLoading ? ( - Array(8).fill(0).map((_, i) => ( - <div key={i}> - <Skeleton className="h-[110px] rounded-xl" /> - </div> - )) - ) : trendingTags.length > 0 ? ( - trendingTags.map(tag => ( - <TagCard key={tag} tag={tag} /> - )) - ) : ( - <div className="col-span-full text-center py-10"> - <p className="text-muted-foreground">No trending tags found</p> - </div> - )} - </div> - </TabsContent> - - {pubkey && ( - <TabsContent value="followed"> - <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> - {isFollowLoading ? ( - Array(4).fill(0).map((_, i) => ( - <div key={i}> - <Skeleton className="h-[110px] rounded-xl" /> - </div> - )) - ) : uniqueFollowedTags.length > 0 ? ( - uniqueFollowedTags.map(tag => ( - <TagCard key={tag} tag={tag} /> - )) - ) : ( - <div className="col-span-full text-center py-10"> - <p className="text-muted-foreground">No followed tags found. Follow tags to see them here.</p> - </div> - )} - </div> - </TabsContent> - )} - </Tabs> - </div> - </> - ); -} diff --git a/app/upload/page.tsx b/app/upload/page.tsx deleted file mode 100644 index ce324b5..0000000 --- a/app/upload/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import Head from "next/head"; -import { useEffect, Suspense } from "react"; -import UploadComponent from "@/components/UploadComponent"; - -export default function UploadPage() { - - useEffect(() => { - document.title = `Upload | LUMINA`; - }, []); - - // check if pubkey contains "npub" - // if so, then we need to convert it to a pubkey - // if (pubkey.includes("npub")) { - // // convert npub to pubkey - // pubkey = nip19.decode(pubkey.toString()).data.toString() - // } - - return ( - <> - <Head> - <title>LUMINA.rocks</title> - <meta name="description" content="Yet another nostr web ui" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" href="/favicon.ico" /> - </Head> - <div className="py-2 px-2"> - <Suspense fallback={<div>Loading...</div>}> - <UploadComponent /> - </Suspense> - </div> - </> - ); -} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 8163ac5..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/components.json b/components.json index 0b14023..f29e3f1 100644 --- a/components.json +++ b/components.json @@ -1,11 +1,11 @@ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", - "rsc": true, + "rsc": false, "tsx": true, "tailwind": { "config": "tailwind.config.ts", - "css": "app/globals.css", + "css": "src/index.css", "baseColor": "slate", "cssVariables": true, "prefix": "" @@ -16,6 +16,5 @@ "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" - }, - "iconLibrary": "lucide" + } } \ No newline at end of file diff --git a/components/AddRelaySheet.tsx b/components/AddRelaySheet.tsx deleted file mode 100644 index 7475625..0000000 --- a/components/AddRelaySheet.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useNostr } from "nostr-react"; -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { PlusCircle } from "lucide-react"; -import { useToast } from "@/components/ui/use-toast"; - -interface AddRelaySheetProps { - onRelayAdded: () => void; -} - -export function AddRelaySheet({ onRelayAdded }: AddRelaySheetProps) { - const [relayUrl, setRelayUrl] = useState(""); - const [isOpen, setIsOpen] = useState(false); - const { toast } = useToast(); - const { connectedRelays } = useNostr(); - - const handleAddRelay = () => { - // Basic validation - if (!relayUrl) { - toast({ - title: "Invalid relay URL", - description: "Please enter a relay URL", - variant: "destructive", - }); - return; - } - - // Format URL - let formattedUrl = relayUrl; - if (!formattedUrl.startsWith("wss://")) { - formattedUrl = `wss://${formattedUrl}`; - } - - // Remove trailing slash if present - if (formattedUrl.endsWith('/')) { - formattedUrl = formattedUrl.slice(0, -1); - } - - // Check if relay already exists in connected relays - const existingRelays = connectedRelays?.map(relay => relay.url) || []; - if (existingRelays.includes(formattedUrl)) { - toast({ - title: "Relay already exists", - description: "This relay is already in your list", - variant: "destructive", - }); - return; - } - - // Get existing custom relays from localStorage - const customRelays = JSON.parse(localStorage.getItem("customRelays") || "[]"); - - // Add new relay to the list if not already in custom relays - if (!customRelays.includes(formattedUrl)) { - customRelays.push(formattedUrl); - localStorage.setItem("customRelays", JSON.stringify(customRelays)); - - toast({ - title: "Relay added", - description: "The relay has been added to your list. Refresh to connect.", - variant: "default", - }); - - // Reset form and close sheet - setRelayUrl(""); - setIsOpen(false); - - // Call the callback to notify parent component - onRelayAdded(); - } else { - toast({ - title: "Relay already exists", - description: "This relay is already in your custom list", - variant: "destructive", - }); - } - }; - - return ( - <Sheet open={isOpen} onOpenChange={setIsOpen}> - <SheetTrigger asChild> - <Button variant="outline" className="gap-2"> - <PlusCircle className="h-4 w-4" /> - </Button> - </SheetTrigger> - <SheetContent> - <SheetHeader> - <SheetTitle>Add New Relay</SheetTitle> - <SheetDescription> - Enter the URL of a Nostr relay to add to your connection list. - </SheetDescription> - </SheetHeader> - <div className="grid gap-4 py-4"> - <div className="space-y-2"> - <Input - id="relay-url" - placeholder="wss://relay.example.com" - value={relayUrl} - onChange={(e) => setRelayUrl(e.target.value)} - className="w-full" - /> - <p className="text-xs text-muted-foreground"> - Relay URLs typically start with wss:// but you can omit it if needed. - </p> - </div> - <div className="flex justify-end space-x-2 pt-4"> - <Button variant="outline" onClick={() => setIsOpen(false)}> - Cancel - </Button> - <Button onClick={handleAddRelay}> - Add Relay - </Button> - </div> - </div> - </SheetContent> - </Sheet> - ); -} \ No newline at end of file diff --git a/components/BottomBar.tsx b/components/BottomBar.tsx deleted file mode 100644 index 45b44a4..0000000 --- a/components/BottomBar.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import { BellIcon, GlobeIcon, HomeIcon, RowsIcon, UploadIcon, PlayIcon } from "@radix-ui/react-icons" -import Link from "next/link" -import { FormEvent, JSX, SVGProps, useEffect, useState } from "react" -import { useRouter, usePathname } from 'next/navigation' -import { HashIcon, SearchIcon, TagIcon } from "lucide-react"; - -export default function BottomBar() { - const router = useRouter(); - const [pubkey, setPubkey] = useState<null | string>(null); - const [mounted, setMounted] = useState(false); - const pathname = usePathname(); - - useEffect(() => { - setMounted(true); - setPubkey(window.localStorage.getItem('pubkey')); - }, []); - - const isActive = (path: string, currentPath: string) => currentPath === path ? 'text-primary' : ''; - - // Render minimal navigation during SSR and hydration - if (!mounted) { - return ( - <nav className="fixed inset-x-0 bottom-0 h-14 flex flex-row shrink-0 items-center justify-between border-t bg-background/90 shadow-up-4 z-50 backdrop-blur"> - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/', pathname)}`} href="/"> - <HomeIcon className={`h-6 w-6`} /> - <span className="sr-only">Home</span> - </Link> - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/global', pathname)}`} href="/global"> - <GlobeIcon className={`h-6 w-6`} /> - <span className="sr-only">Global</span> - </Link> - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/reel', pathname)}`} href="/reel"> - <PlayIcon className={`h-6 w-6`} /> - <span className="sr-only">Reels</span> - </Link> - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/search', pathname)}`} href="/search"> - <SearchIcon className={`h-6 w-6`} /> - <span className="sr-only">Search</span> - </Link> - </nav> - ); - } - - return ( - <nav className="fixed inset-x-0 bottom-0 h-14 flex flex-row shrink-0 items-center justify-between border-t bg-background/90 shadow-up-4 z-50 backdrop-blur"> - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/', pathname)}`} href="/"> - <HomeIcon className={`h-6 w-6`} /> - <span className="sr-only">Home</span> - </Link> - {pubkey && ( - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/feed', pathname)}`} href="/feed"> - <RowsIcon className={`h-6 w-6`} /> - <span className="sr-only">Follower Feed</span> - </Link> - )} - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/global', pathname)}`} href="/global"> - <GlobeIcon className={`h-6 w-6`} /> - <span className="sr-only">Global</span> - </Link> - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/reel', pathname)}`} href="/reel"> - <PlayIcon className={`h-6 w-6`} /> - <span className="sr-only">Reels</span> - </Link> - {pubkey && window.localStorage.getItem('loginType') != 'readOnly_npub' && ( - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/upload', pathname)}`} href="/upload"> - <UploadIcon className={`h-6 w-6`} /> - <span className="sr-only">Upload</span> - </Link> - )} - {/* {pubkey && ( */} - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/tag', pathname)}`} href="/tag"> - {/* <TagIcon className={`h-6 w-6`} /> */} - <HashIcon className={`h-6 w-6`} /> - <span className="sr-only">Tags</span> - </Link> - {/* )} */} - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/search', pathname)}`} href="/search"> - <SearchIcon className={`h-6 w-6`} /> - <span className="sr-only">Search</span> - </Link> - {pubkey && ( - <Link className={`flex flex-col items-center justify-center w-full text-xs gap-1 px-4 ${isActive('/notifications', pathname)}`} href="/notifications"> - <BellIcon className={`h-6 w-6`} /> - <span className="sr-only">Notifications</span> - </Link> - )} - </nav> - ) -} \ No newline at end of file diff --git a/components/CardOptionsDropdown.tsx b/components/CardOptionsDropdown.tsx deleted file mode 100644 index 767dc3f..0000000 --- a/components/CardOptionsDropdown.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import React, { useMemo } from 'react'; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer"; -import { Textarea } from "./ui/textarea"; -import { DotsVerticalIcon, CodeIcon, Share1Icon } from "@radix-ui/react-icons"; -import { Input } from "./ui/input"; -import { useRef, useState } from 'react'; -import { useToast } from "./ui/use-toast"; -import { Event as NostrEvent, nip19 } from "nostr-tools"; -import { Trash2 } from "lucide-react"; -import { publishToOutbox } from "@/utils/publishUtils"; -import { useCurrentUserPubkey } from "@/utils/relayHooks"; -import { signEvent } from "@/utils/utils"; - -interface CardOptionsDropdownProps { - event: NostrEvent; -} - -export default function CardOptionsDropdown({ event }: CardOptionsDropdownProps) { - const jsonEvent = useMemo(() => JSON.stringify(event, null, 2), [event]); - const inputRef = useRef(null); - const inputRefID = useRef(null); - const { toast } = useToast(); - const currentUserPubkey = useCurrentUserPubkey(); - const [dropdownOpen, setDropdownOpen] = useState(false); - const [shareDrawerOpen, setShareDrawerOpen] = useState(false); - const [rawDrawerOpen, setRawDrawerOpen] = useState(false); - const [deleteDrawerOpen, setDeleteDrawerOpen] = useState(false); - const [deleteReason, setDeleteReason] = useState(''); - - const handleCopyLink = async () => { - try { - await navigator.clipboard.writeText(window.location.href); - toast({ - description: 'URL copied to clipboard', - title: 'Copied' - }); - } catch (err) { - toast({ - description: 'Error copying URL to clipboard', - title: 'Error', - variant: 'destructive' - }); - } - }; - - const handleCopyNoteId = async () => { - try { - // Create nevent with relay hints - const nevent = nip19.neventEncode({ - id: event.id, - relays: [] - }); - await navigator.clipboard.writeText(nevent); - toast({ - description: 'Event ID copied to clipboard', - title: 'Copied' - }); - } catch (err) { - toast({ - description: 'Error copying Event ID to clipboard', - title: 'Error', - variant: 'destructive' - }); - } - }; - - const handleRequestDeletion = async () => { - // Check if the user is the owner of the event - const userPubkey = window.localStorage.getItem('pubkey'); - if (!userPubkey) { - toast({ - description: 'You need to be logged in to request deletion', - title: 'Error', - variant: 'destructive' - }); - return; - } - - if (userPubkey !== event.pubkey) { - toast({ - description: 'You can only request deletion of your own posts', - title: 'Error', - variant: 'destructive' - }); - return; - } - - const loginType = window.localStorage.getItem('loginType'); - if (!loginType) { - toast({ - description: 'Login type is missing. Please log in again.', - title: 'Error', - variant: 'destructive' - }); - return; - } - - // Create a kind 5 event (deletion request) as per NIP-09 - const deletionEvent: NostrEvent = { - kind: 5, - created_at: Math.floor(Date.now() / 1000), - content: deleteReason, - tags: [ - ["e", event.id], - ["k", event.kind.toString()] - ], - pubkey: "", - id: "", - sig: "", - }; - - // Sign the event - const signedEvent = await signEvent(loginType, deletionEvent); - - if (signedEvent) { - // Publish the deletion request - await publishToOutbox(signedEvent, currentUserPubkey || undefined); - - toast({ - description: 'Deletion request has been published', - title: 'Success' - }); - - setDeleteDrawerOpen(false); - } else { - toast({ - description: 'Failed to sign deletion request', - title: 'Error', - variant: 'destructive' - }); - } - }; - - return ( - <> - <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> - <DropdownMenuTrigger asChild> - <Button variant="ghost" size="icon" className="ml-auto mt-0 flex-shrink-0" aria-label="Options"> - <DotsVerticalIcon className="h-4 w-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - {/* Share option */} - <DropdownMenuItem - onClick={() => { - setDropdownOpen(false); - setTimeout(() => setShareDrawerOpen(true), 100); - }} - > - <Share1Icon className="mr-2 h-4 w-4" /> - Share - </DropdownMenuItem> - - {/* View Raw option */} - <DropdownMenuItem - onClick={() => { - setDropdownOpen(false); - setTimeout(() => setRawDrawerOpen(true), 100); - }} - > - <CodeIcon className="mr-2 h-4 w-4" /> - View Raw - </DropdownMenuItem> - - {/* Delete option (only visible for the owner) */} - {window.localStorage.getItem('pubkey') === event.pubkey && ( - <DropdownMenuItem - onClick={() => { - setDropdownOpen(false); - setTimeout(() => setDeleteDrawerOpen(true), 100); - }} - > - <Trash2 className="mr-2 h-4 w-4" /> - Request Deletion - </DropdownMenuItem> - )} - </DropdownMenuContent> - </DropdownMenu> - - {/* Share Drawer */} - <Drawer open={shareDrawerOpen} onOpenChange={setShareDrawerOpen}> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Share this Note</DrawerTitle> - <DrawerDescription>Share this Note with others.</DrawerDescription> - </DrawerHeader> - <div className="px-4"> - <div className="flex items-center mb-4"> - <Input ref={inputRef} value={window.location.href} readOnly className="mr-2" /> - <Button variant="outline" onClick={handleCopyLink}>Copy Link</Button> - </div> - <div className="flex items-center mb-4"> - <Input ref={inputRefID} value={nip19.neventEncode({ - id: event.id, - relays: [] - })} readOnly className="mr-2" /> - <Button variant="outline" onClick={handleCopyNoteId}>Copy Event ID</Button> - </div> - </div> - <DrawerFooter> - <DrawerClose asChild> - <Button variant="outline">Close</Button> - </DrawerClose> - </DrawerFooter> - </DrawerContent> - </Drawer> - - {/* Raw Event Drawer */} - <Drawer open={rawDrawerOpen} onOpenChange={setRawDrawerOpen}> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Raw Event</DrawerTitle> - <DrawerDescription>This shows the raw event data.</DrawerDescription> - </DrawerHeader> - <div className="px-4 pb-4"> - <Textarea rows={20} readOnly value={jsonEvent} /> - </div> - <DrawerFooter> - <DrawerClose asChild> - <Button variant="outline">Close</Button> - </DrawerClose> - </DrawerFooter> - </DrawerContent> - </Drawer> - - {/* Deletion Request Drawer */} - <Drawer open={deleteDrawerOpen} onOpenChange={setDeleteDrawerOpen}> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Request Deletion</DrawerTitle> - <DrawerDescription> - This will publish a deletion request (NIP-09) for this event. Clients and relays may hide or delete this event when they see your request. - </DrawerDescription> - </DrawerHeader> - <div className="px-4 pb-4"> - <div className="space-y-2 mb-4"> - <label htmlFor="delete-reason" className="text-sm font-medium"> - Reason for deletion (optional) - </label> - <Textarea - id="delete-reason" - placeholder="Why do you want to delete this event?" - value={deleteReason} - onChange={(e) => setDeleteReason(e.target.value)} - rows={3} - /> - </div> - <div className="bg-muted p-3 rounded-md text-sm mb-4"> - <p className="font-medium mb-1">Note:</p> - <p className="text-muted-foreground"> - Deletion requests cannot be guaranteed to remove content from all relays and clients. - Some relays may choose to ignore deletion requests, and previously downloaded content may still be available in clients. - </p> - </div> - </div> - <DrawerFooter className="flex-row space-x-2"> - <DrawerClose asChild> - <Button variant="outline">Cancel</Button> - </DrawerClose> - <Button - variant="destructive" - onClick={handleRequestDeletion} - > - Request Deletion - </Button> - </DrawerFooter> - </DrawerContent> - </Drawer> - </> - ); -} diff --git a/components/CommentCard.tsx b/components/CommentCard.tsx deleted file mode 100644 index 6a35025..0000000 --- a/components/CommentCard.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, -} from "@/components/ui/carousel" -import ReactionButton from '@/components/ReactionButton'; -import { Avatar, AvatarImage } from '@/components/ui/avatar'; -import ViewRawButton from '@/components/ViewRawButton'; -import ViewNoteButton from './ViewNoteButton'; -import Link from 'next/link'; - -interface CommentCardProps { - pubkey: string; - text: string; - eventId: string; - tags: string[][]; - event: any; -} - -const NoteCard: React.FC<CommentCardProps> = ({ pubkey, text, eventId, tags, event }) => { - const { data: userData } = useProfile({ - pubkey, - }); - - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); - const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g); - const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g, ''); - const createdAt = new Date(event.created_at * 1000); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - - return ( - <> - <Card> - <CardHeader> - <CardTitle> - <Link href={hrefProfile} style={{ textDecoration: 'none'}}> - <TooltipProvider> - <Tooltip> - <TooltipTrigger> - <div style={{ display: 'flex', alignItems: 'center' }}> - <Avatar> - <AvatarImage src={profileImageSrc} /> - </Avatar> - <span className='break-all' style={{ marginLeft: '10px' }}>{title}</span> - </div> - </TooltipTrigger> - <TooltipContent> - <p>{title}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </Link> - </CardTitle> - </CardHeader> - <CardContent> - <div className='py-4'> - { - <div className='w-full h-full px-10'> - {imageSrc && imageSrc.length > 1 ? ( - <Carousel> - <CarouselContent> - {imageSrc.map((src, index) => ( - <CarouselItem key={index}> - <img - key={index} - src={src} - className='rounded lg:rounded-lg' - style={{ maxWidth: '100%', maxHeight: '100vh', objectFit: 'contain', margin: 'auto' }} - /> - </CarouselItem> - ))} - </CarouselContent> - <CarouselPrevious /> - <CarouselNext /> - </Carousel> - ) : ( - imageSrc ? <img src={imageSrc[0]} className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '100vh', objectFit: 'contain', margin: 'auto' }} /> : "" - )} - </div> - } - <br /> - <div className='break-word overflow-hidden'> - {textWithoutImage} - </div> - </div> - <hr /> - <div className='py-4 space-x-4 flex justify-between items-start'> - <div className='flex space-x-4'> - <ReactionButton event={event} /> - </div> - <ViewRawButton event={event} /> - </div> - </CardContent> - <CardFooter> - <small className="text-muted">{createdAt.toLocaleString()}</small> - </CardFooter> - </Card> - </> - ); -} - -export default NoteCard; \ No newline at end of file diff --git a/components/CommentsComponent.tsx b/components/CommentsComponent.tsx deleted file mode 100644 index 8f913a2..0000000 --- a/components/CommentsComponent.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { useNostrEvents } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import CommentCard from '@/components/CommentCard'; - -interface CommentsCompontentProps { - pubkey: string; - event: any; -} - -const CommentsCompontent: React.FC<CommentsCompontentProps> = ({ pubkey, event }) => { - - const { events } = useNostrEvents({ - filter: { - kinds: [1], - '#e': [event.id], - }, - }); - - return ( - <> - <h1 className='text-xl'>Comments</h1> - {events.map((event) => ( - <div key={event.id} className="py-6"> - <CommentCard key={event.id} pubkey={event.pubkey} text={event.content} eventId={event.id} tags={event.tags} event={event} /> - </div> - ))} - </> - ); -} - -export default CommentsCompontent; \ No newline at end of file diff --git a/components/CreateProfileForm.tsx b/components/CreateProfileForm.tsx deleted file mode 100644 index 3d3681b..0000000 --- a/components/CreateProfileForm.tsx +++ /dev/null @@ -1,405 +0,0 @@ -'use client'; - -import React, { useState, useEffect } from 'react'; -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { nip19 } from "nostr-tools" -import { Label } from "./ui/label" -import { Textarea } from "@/components/ui/textarea" -import { verifyEvent } from 'nostr-tools/pure' -import { hexToBytes } from '@noble/hashes/utils' -import { useNostr, useProfile } from 'nostr-react'; -import { signEvent } from '@/utils/utils'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { AlertCircle, User, AtSign, FileImage, Info, Loader2 } from 'lucide-react'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from "@/components/ui/tooltip"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { publishToOutbox } from "@/utils/publishUtils"; -import { useCurrentUserPubkey } from "@/utils/relayHooks"; - -export function CreateProfileForm() { - const currentUserPubkey = useCurrentUserPubkey(); - - // Local state for form inputs - const [username, setUsername] = useState(""); - const [displayName, setDisplayName] = useState(""); - const [bio, setBio] = useState(""); - const [picture, setPicture] = useState(""); - const [website, setWebsite] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); - - // Get user info from local storage - const [npub, setNpub] = useState(''); - const [pubkey, setPubkey] = useState(''); - const [loginType, setLoginType] = useState(''); - - useEffect(() => { - if (typeof window !== 'undefined') { - const storedPubkey = window.localStorage.getItem("pubkey") ?? ''; - const storedLoginType = window.localStorage.getItem("loginType") ?? ''; - - setPubkey(storedPubkey); - setLoginType(storedLoginType); - - if (storedPubkey && storedPubkey.length > 0) { - setNpub(nip19.npubEncode(storedPubkey)); - } - } - }, []); - - // Try to load existing profile data if available - const { data: userData, isLoading: profileLoading } = useProfile({ - pubkey, - }); - - useEffect(() => { - if (userData) { - setUsername(userData.name || ""); - setDisplayName(userData.display_name || ""); - setBio(userData.about || ""); - setPicture(userData.picture || ""); - setWebsite(userData.website || ""); - } - }, [userData]); - - // Input handlers - const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => { - const value = event.target.value; - setUsername(value); - - // Clear validation error if fixed - if (validationErrors.username && value.trim()) { - const newErrors = {...validationErrors}; - delete newErrors.username; - setValidationErrors(newErrors); - } - }; - - const handleDisplayNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setDisplayName(event.target.value); - }; - - const handleBioChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { - setBio(event.target.value); - }; - - const handlePictureChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setPicture(event.target.value); - }; - - const handleWebsiteChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setWebsite(event.target.value); - }; - - // Form validation - const validateForm = () => { - const errors: {[key: string]: string} = {}; - - if (!username.trim()) { - errors.username = "Username is required"; - } - - if (picture && !isValidUrl(picture)) { - errors.picture = "Please enter a valid URL for your profile picture"; - } - - if (website && !isValidUrl(website)) { - errors.website = "Please enter a valid URL for your website"; - } - - setValidationErrors(errors); - return Object.keys(errors).length === 0; - }; - - const isValidUrl = (url: string) => { - try { - new URL(url); - return true; - } catch { - return false; - } - }; - - // Form submission - async function handleProfileUpdate() { - if (!validateForm()) { - return; - } - - setIsSubmitting(true); - - try { - // Create the profile content object - const profileContent: Record<string, string> = { - name: username, - display_name: displayName || username, // Fallback to username if display name is empty - }; - - if (bio) profileContent.about = bio; - if (picture) profileContent.picture = picture; - if (website) profileContent.website = website; - - // Create the event - const event = { - kind: 0, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: JSON.stringify(profileContent), - pubkey: pubkey, - id: "", - sig: "", - }; - - // Sign the event - const signedEvent = await signEvent(loginType, event); - - if (signedEvent === null) { - throw new Error('Failed to sign the event'); - } - - // Verify the event - const isGood = verifyEvent(signedEvent); - - if (isGood) { - // Publish to outbox relays - await publishToOutbox(signedEvent, currentUserPubkey || undefined); - - // Redirect to profile page - window.location.href = `/profile/${npub}`; - } else { - throw new Error('Event verification failed'); - } - } catch (error) { - setValidationErrors({ - submit: error instanceof Error ? error.message : 'Failed to create profile. Please try again.' - }); - setIsSubmitting(false); - } - } - - // Extract initials for avatar fallback - const getInitials = () => { - if (displayName) { - return displayName.charAt(0).toUpperCase(); - } else if (username) { - return username.charAt(0).toUpperCase(); - } - return 'U'; - }; - - return ( - <div className="space-y-8"> - {/* Profile preview card */} - <Card className="bg-muted/30"> - <CardHeader className="pb-3"> - <CardTitle className="text-base">Profile Preview</CardTitle> - <CardDescription> - How your profile could appear to others - </CardDescription> - </CardHeader> - <CardContent> - <div className="flex flex-col sm:flex-row gap-4 items-center sm:items-start"> - <Avatar className="h-20 w-20 border-2 border-background"> - <AvatarImage src={picture} alt={displayName || username} /> - <AvatarFallback className="text-2xl">{getInitials()}</AvatarFallback> - </Avatar> - <div className="space-y-1.5 text-center sm:text-left"> - <h3 className="font-semibold text-lg"> - {displayName || username || 'Display Name'} - </h3> - {username && ( - <p className="text-muted-foreground text-sm flex items-center justify-center sm:justify-start gap-1"> - @{username} - </p> - )} - {bio && ( - <p className="text-sm pt-1 max-w-sm"> - {bio.length > 100 ? `${bio.substring(0, 100)}...` : bio} - </p> - )} - </div> - </div> - </CardContent> - </Card> - - {/* Profile form */} - <div className="space-y-5"> - <div className="space-y-2.5"> - <div className="flex items-center gap-2"> - <Label htmlFor="username" className="font-medium"> - Username - </Label> - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <Info className="h-3.5 w-3.5 text-muted-foreground" /> - </TooltipTrigger> - <TooltipContent> - <p className="w-60">This is your unique username on Nostr</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </div> - <Input - id="username" - placeholder="e.g., satoshi" - value={username} - onChange={handleUsernameChange} - className={validationErrors.username ? "border-destructive" : ""} - /> - {validationErrors.username && ( - <p className="text-destructive text-xs flex items-center gap-1 mt-1"> - <AlertCircle className="h-3 w-3" /> {validationErrors.username} - </p> - )} - </div> - - <div className="space-y-2.5"> - <div className="flex items-center gap-2"> - <Label htmlFor="displayname" className="font-medium"> - Display Name - </Label> - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <Info className="h-3.5 w-3.5 text-muted-foreground" /> - </TooltipTrigger> - <TooltipContent> - <p className="w-60">This is the name that will be displayed to others</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </div> - <Input - id="displayname" - placeholder="e.g., Satoshi Nakamoto" - value={displayName} - onChange={handleDisplayNameChange} - /> - </div> - - <div className="space-y-2.5"> - <div className="flex items-center gap-2"> - <Label htmlFor="picture" className="font-medium"> - Profile Picture URL - </Label> - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <Info className="h-3.5 w-3.5 text-muted-foreground" /> - </TooltipTrigger> - <TooltipContent> - <p className="w-80">Enter a direct link to your profile image (JPEG or PNG)</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </div> - <div className="flex"> - <div className="relative flex-grow"> - <Input - id="picture" - placeholder="https://example.com/your-image.jpg" - value={picture} - onChange={handlePictureChange} - className={`pl-9 ${validationErrors.picture ? "border-destructive" : ""}`} - /> - <FileImage className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> - </div> - </div> - {validationErrors.picture && ( - <p className="text-destructive text-xs flex items-center gap-1 mt-1"> - <AlertCircle className="h-3 w-3" /> {validationErrors.picture} - </p> - )} - </div> - - <div className="space-y-2.5"> - <div className="flex items-center gap-2"> - <Label htmlFor="website" className="font-medium"> - Website - </Label> - <span className="text-xs text-muted-foreground">(Optional)</span> - </div> - <Input - id="website" - placeholder="https://example.com" - value={website} - onChange={handleWebsiteChange} - className={validationErrors.website ? "border-destructive" : ""} - /> - {validationErrors.website && ( - <p className="text-destructive text-xs flex items-center gap-1 mt-1"> - <AlertCircle className="h-3 w-3" /> {validationErrors.website} - </p> - )} - </div> - - <div className="space-y-2.5"> - <Label htmlFor="bio" className="font-medium">Bio</Label> - <Textarea - id="bio" - placeholder="Tell the world about yourself..." - rows={5} - value={bio} - onChange={handleBioChange} - className="resize-none" - /> - </div> - - <Separator className="my-4" /> - - <div className="space-y-4"> - <div> - <Label className="text-xs text-muted-foreground mb-1 block"> - Your Public Key (npub) - </Label> - <Input - type="text" - value={npub} - readOnly - className="font-mono text-xs bg-muted/30" - /> - </div> - - {validationErrors.submit && ( - <Alert variant="destructive" className="py-2"> - <AlertDescription className="text-sm"> - {validationErrors.submit} - </AlertDescription> - </Alert> - )} - - <Button - type="submit" - className="w-full" - onClick={handleProfileUpdate} - disabled={isSubmitting} - > - {isSubmitting ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Creating profile... - </> - ) : ( - "Create My Profile" - )} - </Button> - </div> - </div> - </div> - ) -} \ No newline at end of file diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx deleted file mode 100644 index f88c4e7..0000000 --- a/components/ErrorBoundary.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; - -interface ErrorBoundaryState { - hasError: boolean; - error?: Error; -} - -interface ErrorBoundaryProps { - children: React.ReactNode; -} - -class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { - constructor(props: ErrorBoundaryProps) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - // Check if this is a nostr-related error that we want to suppress - if (ErrorBoundary.isNostrError(error)) { - console.warn('Suppressed nostr error in ErrorBoundary:', error); - return { hasError: false }; // Don't show error UI - } - - return { hasError: true, error }; - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - // Check if this is a nostr-related error that we want to suppress - if (ErrorBoundary.isNostrError(error)) { - console.warn('Suppressed nostr error in ErrorBoundary:', error); - return; // Don't log or handle the error - } - - console.error('Error caught by ErrorBoundary:', error, errorInfo); - } - - static isNostrError(error: Error): boolean { - const message = error.message || ''; - const stack = error.stack || ''; - - return ( - message.includes('auth-required') || - message.includes('auth required') || - stack.includes('handleNext') || - stack.includes('index.js:544') || - stack.includes('nostr-tools') - ); - } - - render() { - if (this.state.hasError) { - // Only show error UI for non-nostr errors - return ( - <div className="p-4 text-center"> - <h2 className="text-lg font-semibold text-red-600 mb-2">Something went wrong</h2> - <p className="text-gray-600 mb-4">An unexpected error occurred. Please try refreshing the page.</p> - <button - onClick={() => window.location.reload()} - className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" - > - Refresh Page - </button> - </div> - ); - } - - return this.props.children; - } -} - -export default ErrorBoundary; diff --git a/components/FollowButton.tsx b/components/FollowButton.tsx deleted file mode 100644 index a70001c..0000000 --- a/components/FollowButton.tsx +++ /dev/null @@ -1,97 +0,0 @@ - -import React, { useEffect, useState } from 'react'; -import { Button } from './ui/button'; -import { useNostr, useNostrEvents } from 'nostr-react'; -import { finalizeEvent } from 'nostr-tools'; -import { sign } from 'crypto'; -import { SignalMedium } from 'lucide-react'; - -interface FollowButtonProps { - pubkey: string; - userPubkey: string; -} - -const FollowButton: React.FC<FollowButtonProps> = ({ pubkey, userPubkey }) => { - // const { publish } = useNostr(); - const [isFollowing, setIsFollowing] = useState(false); - - let storedPubkey: string | null = null; - let storedNsec: string | null = null; - let isLoggedIn = false; - if (typeof window !== 'undefined') { - storedPubkey = window.localStorage.getItem('pubkey'); - storedNsec = window.localStorage.getItem('nsec'); - isLoggedIn = storedPubkey !== null; - } - - const { events } = useNostrEvents({ - filter: { - kinds: [3], - authors: [userPubkey], - limit: 1, - }, - }); - - let followingPubkeys = events.flatMap((event) => event.tags.map(tag => tag[1])); - // filter out all null or undefined - followingPubkeys = followingPubkeys.filter((tag) => tag); - - - useEffect(() => { - if (followingPubkeys.includes(pubkey)) { - setIsFollowing(true); - } - }, [followingPubkeys, isFollowing, setIsFollowing]); - - const handleFollow = async () => { - // if (isLoggedIn) { - - // let eventTemplate = { - // kind: 3, - // created_at: Math.floor(Date.now() / 1000), - // tags: [followingPubkeys], - // content: '', - // } - - // console.log(eventTemplate); - - // if (isFollowing) { - // eventTemplate.tags = eventTemplate.tags.filter(tag => tag[1] !== pubkey); - // } else { - // eventTemplate.tags[0].push(pubkey); - // } - - // console.log(eventTemplate); - - // let signedEvent = null; - // if (storedNsec != null) { - // // TODO: Sign Nostr Event with nsec - // const nsecArray = storedNsec ? new TextEncoder().encode(storedNsec) : new Uint8Array(); - // signedEvent = finalizeEvent(eventTemplate, nsecArray); - // console.log(signedEvent); - // } else if (storedPubkey != null) { - // // TODO: Request Extension to sign Nostr Event - // console.log('Requesting Extension to sign Nostr Event..'); - // try { - // signedEvent = await window.nostr.signEvent(eventTemplate); - // } catch (error) { - // console.error('Nostr Extension not found or aborted.'); - // } - // } - - // if (signedEvent !== null) { - // console.log(signedEvent); - // publish(signedEvent); - // setIsFollowing(!isFollowing); - // } - // } - }; - - return ( - <Button className='w-full' onClick={handleFollow} disabled> - {isFollowing ? 'Unfollow' : 'Follow'} - </Button> - ); -}; - -export default FollowButton; \ No newline at end of file diff --git a/components/FollowerFeed.tsx b/components/FollowerFeed.tsx deleted file mode 100644 index ae86d04..0000000 --- a/components/FollowerFeed.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents } from "nostr-react"; -import KIND20Card from "./KIND20Card"; -import NoteCard from "./NoteCard"; -import { getImageUrl } from "@/utils/utils"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -interface FollowerFeedProps { - pubkey: string; -} - -const FollowerFeed: React.FC<FollowerFeedProps> = ({ pubkey }) => { - const now = useRef(new Date()); - const [limit, setLimit] = useState(10); - - const { events: followingEvents } = useNostrEvents({ - filter: { - kinds: [3], - authors: [pubkey], - limit: 1, - }, - }); - - const followingPubkeys = followingEvents[0]?.tags - .filter((tag) => tag[0] === "p") - .map((tag) => tag[1]) || []; - - const { events, isLoading } = useNostrEvents({ - filter: { - limit: limit, - kinds: [20, 21, 22], - authors: followingPubkeys, - }, - }); - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 10); - }; - - return ( - <> - <div className="grid grid-cols-1 xl:grid-cols-3 gap-2"> - {events.length === 0 && isLoading ? ( - <div className="flex flex-col space-y-3"> - <Skeleton className="h-[125px] rounded-xl" /> - <div className="space-y-2"> - <Skeleton className="h-4 w-[250px]" /> - <Skeleton className="h-4 w-[200px]" /> - </div> - </div> - ) : events.some(event => getImageUrl(event.tags) || event.kind === 21 || event.kind === 22) ? ( - <> - {events.map((event) => { - const imageUrl = getImageUrl(event.tags); - const isVideo = event.kind === 21 || event.kind === 22; - - if (isVideo) { - // Use NoteCard for video content - const videoUrl = getVideoUrl(event.tags); - const contentWithVideo = videoUrl ? `${event.content}\n${videoUrl}` : event.content; - return ( - <NoteCard - key={event.id} - pubkey={event.pubkey} - text={contentWithVideo} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - /> - ); - } else if (imageUrl) { - // Use KIND20Card for image content - return ( - <KIND20Card - key={event.id} - pubkey={event.pubkey} - text={event.content} - image={imageUrl} - event={event} - tags={event.tags} - eventId={event.id} - showViewNoteCardButton={true} - /> - ); - } - return null; - })} - </> - ) : ( - <div className="flex flex-col items-center justify-center py-10 text-gray-500"> - <p className="text-lg">No posts found :(</p> - </div> - )} - </div> - {!isLoading && ( - <div className="flex justify-center p-4"> - <Button className="w-full" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default FollowerFeed; \ No newline at end of file diff --git a/components/FollowerQuickViewFeed.tsx b/components/FollowerQuickViewFeed.tsx deleted file mode 100644 index 0371947..0000000 --- a/components/FollowerQuickViewFeed.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents } from "nostr-react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard"; -import { getImageUrl } from "@/utils/utils"; - -interface FollowerQuickViewFeedProps { - pubkey: string; -} - -const FollowerQuickViewFeed: React.FC<FollowerQuickViewFeedProps> = ({ pubkey }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered - const [limit, setLimit] = useState(25); - - const { events: following, isLoading: followingLoading } = useNostrEvents({ - filter: { - kinds: [3], - authors: [pubkey], - limit: 1, - }, - }); - - let followingPubkeys = following.flatMap((event) => - event.tags - .filter(tag => tag[0] === 'p') - .map(tag => tag[1]) - ); - - const { events, isLoading } = useNostrEvents({ - filter: { - limit: limit, - kinds: [20, 21, 22], - authors: followingPubkeys, - }, - }); - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 25); - }; - - return ( - <> - <div className="grid grid-cols-3 gap-2"> - {events.length === 0 && isLoading ? ( - <> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - </> - ) : ( - events.map((event) => ( - <QuickViewKind20NoteCard - key={event.id} - pubkey={event.pubkey} - text={event.content} - image={getImageUrl(event.tags)} - event={event} - tags={event.tags} - eventId={event.id} - linkToNote={true} - /> - )) - )} - </div> - {!isLoading && ( - <div className="flex justify-center p-4"> - <Button className="w-full md:w-auto" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default FollowerQuickViewFeed; \ No newline at end of file diff --git a/components/GalleryCard.tsx b/components/GalleryCard.tsx deleted file mode 100644 index 8c9e659..0000000 --- a/components/GalleryCard.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - SmallCardContent, -} from "@/components/ui/card" -import Image from 'next/image'; -import Link from 'next/link'; -import { PlayIcon, StackIcon, VideoIcon } from '@radix-ui/react-icons'; - -interface GalleryCardProps { - pubkey: string; - eventId: string; - imageUrl: string; - linkToNote: boolean; - isVideo?: boolean; -} - -const GalleryCard: React.FC<GalleryCardProps> = ({ pubkey, eventId, imageUrl, linkToNote, isVideo = false }) => { - const { data: userData } = useProfile({ - pubkey, - }); - - // Check if the URL is a video file (for cases where we don't have a thumbnail image) - const isVideoFile = imageUrl.match(/\.(mp4|webm|mov|avi|mkv)$/i); - - // For gallery view, we want to show thumbnails for videos - // Use the isVideo prop or detect from file extension, or force for .mp4 files - const isVideoThumbnail = isVideo || isVideoFile || imageUrl.includes('.mp4'); - - // Debug logging - console.log('GalleryCard render:', { - imageUrl, - isVideo, - isVideoFile: !!isVideoFile, - isVideoThumbnail, - eventId - }); - - // Create neevent with relay hints - const nevent = nip19.neventEncode({ - id: eventId, - relays: [] // Add relay hints if available - }); - - const card = ( - <Card> - <SmallCardContent> - <div> - <div className='d-flex justify-content-center align-items-center'> - <div style={{ position: 'relative' }}> - {isVideoThumbnail ? ( - <> - {isVideoFile ? ( - // If it's a video file URL, show a placeholder with play icon - <div - className='rounded lg:rounded-lg w-full h-full object-cover bg-gray-800 flex items-center justify-center' - style={{ maxHeight: '75vh', margin: 'auto', aspectRatio: '16/9' }} - > - <div style={{ - background: 'rgba(0, 0, 0, 0.7)', - borderRadius: '50%', - width: '40px', - height: '40px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - }}> - <PlayIcon style={{ color: 'white', width: '20px', height: '20px' }} /> - </div> - </div> - ) : ( - // If it's an image URL, show the image with play icon overlay - <> - <img - src={imageUrl} - className='rounded lg:rounded-lg w-full h-full object-cover' - style={{ maxHeight: '75vh', margin: 'auto' }} - alt={eventId} - loading="lazy" - /> - <div style={{ - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - background: 'rgba(0, 0, 0, 0.7)', - borderRadius: '50%', - width: '40px', - height: '40px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - }}> - <PlayIcon style={{ color: 'white', width: '20px', height: '20px' }} /> - </div> - </> - )} - </> - ) : ( - <img - src={imageUrl} - className='rounded lg:rounded-lg w-full h-full object-cover' - style={{ maxHeight: '75vh', margin: 'auto' }} - alt={eventId} - loading="lazy" - /> - )} - </div> - </div> - </div> - </SmallCardContent> - </Card> - ); - - return ( - <> - {linkToNote ? ( - <Link href={`/note/${nevent}`}> - {card} - </Link> - ) : ( - card - )} - </> - ); -} - -export default GalleryCard; \ No newline at end of file diff --git a/components/GeyserFundAlertOverlay.tsx b/components/GeyserFundAlertOverlay.tsx deleted file mode 100644 index 484b136..0000000 --- a/components/GeyserFundAlertOverlay.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { X, ArrowRight } from 'lucide-react'; -import { Button } from "@/components/ui/button"; - -export function GeyserFundAlertOverlay() { - const [isVisible, setIsVisible] = useState(false); - - // Check if the feature is enabled via environment variable - const showGeyserFund = process.env.NEXT_PUBLIC_SHOW_GEYSER_FUND === 'true'; - - // Show the alert after a short delay for better UX, but only if enabled - useEffect(() => { - if (!showGeyserFund) return; - - const timer = setTimeout(() => { - setIsVisible(true); - }, 1000); - - return () => clearTimeout(timer); - }, [showGeyserFund]); - - // Store dismissal in localStorage to avoid showing again in the same session - const handleDismiss = () => { - setIsVisible(false); - localStorage.setItem("geyserAlertDismissed", "true"); - }; - - // Check if user has already dismissed - useEffect(() => { - const isDismissed = localStorage.getItem("geyserAlertDismissed") === "true"; - if (isDismissed) { - setIsVisible(false); - } - }, []); - - // Don't render anything if the feature is disabled or not visible - if (!showGeyserFund || !isVisible) return null; - - return ( - <div className="fixed bottom-4 right-4 left-4 md:left-auto md:w-96 z-50 animate-fade-in mb-14"> - <div className="bg-gradient-to-r from-purple-600 to-indigo-600 rounded-lg shadow-lg overflow-hidden"> - <div className="relative p-5"> - <button - onClick={handleDismiss} - className="absolute top-2 right-2 text-white/80 hover:text-white transition-colors" - aria-label="Dismiss alert" - > - <X size={18} /> - </button> - - <div className="flex flex-col space-y-3"> - <div className="bg-white/10 px-3 py-1 rounded-full w-fit text-xs font-medium text-white"> - Limited Time Offer - </div> - - <h3 className="text-xl font-bold text-white">Support the Geyser Fund</h3> - - <p className="text-white/90 text-sm"> - Join our community initiative to preserve natural geysers. Every donation helps protect these natural wonders for future generations. - </p> - - <div className="flex justify-between items-center pt-2"> - <Button - onClick={() => window.open("/donate", "_blank")} - className="bg-white text-purple-700 hover:bg-white/90 transition-colors group" - > - Donate Now - <ArrowRight size={16} className="ml-2 group-hover:translate-x-1 transition-transform" /> - </Button> - - <button - onClick={handleDismiss} - className="text-white/70 text-sm hover:text-white transition-colors" - > - Maybe later - </button> - </div> - </div> - </div> - - {/* Progress bar animation to create urgency */} - <div className="h-1 bg-white/20"> - <div className="h-full bg-white w-full animate-shrink origin-left"></div> - </div> - </div> - </div> - ); -} diff --git a/components/GeyserFundDonation.tsx b/components/GeyserFundDonation.tsx deleted file mode 100644 index 0775eb3..0000000 --- a/components/GeyserFundDonation.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import { Button } from '@/components/ui/button'; - -export function GeyserFundDonation() { - return ( - <div className="w-full bg-gradient-to-r from-purple-700/80 to-violet-800/80 rounded-lg p-4 my-4 shadow-md"> - <div className="flex flex-col sm:flex-row items-center justify-between gap-4"> - <div className="flex items-center gap-3"> - <div className="rounded-full p-2 flex items-center justify-center"> - {/* <svg - xmlns="http://www.w3.org/2000/svg" - width="24" - height="24" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - className="text-purple-800" - > - <path d="M5.7 15a7 7 0 1 1 13.6 0" /> - <path d="M5.7 15h13.6" /> - <path d="M5.7 15l-2.5 5" /> - <path d="M19.3 15l2.5 5" /> - <path d="M9.2 20H12m0 0h2.8" /> - <path d="M12 20v-5" /> - </svg> */} - <svg width='34' height='36' viewBox='0 0 349 365' fill='none' xmlns='http://www.w3.org/2000/svg'> - <path d='M346.396 125.679C346.193 124.459 345.986 123.24 345.755 122.028C345.017 118.123 345.017 118.123 345.017 118.123C343.98 113.895 339.304 110.436 334.628 110.435L221.265 110.425C216.587 110.424 211.581 114.065 210.14 118.514L186.054 192.873C185.343 195.069 184.408 197.774 185.441 199.236C186.502 200.735 189.57 200.963 191.939 200.963H238.093C238.093 200.963 240.249 200.945 240.536 202.442C240.843 204.031 239.19 205.743 239.19 205.743L157.833 291.25C154.987 294.242 150.017 299.46 146.792 302.845L125.923 324.742C125.923 324.742 125.497 325.221 124.999 325.68C124.712 325.946 124.436 326.178 124.173 326.383C121.278 328.632 119.909 327.374 121.11 323.26L131.729 286.87C133.04 282.381 135.182 275.033 136.493 270.544L142.575 249.702C143.595 246.206 144.753 242.242 145.481 239.743C145.688 239.033 146.17 236.844 145.442 235.737C144.751 234.686 143.587 234.487 142.28 234.487C140.864 234.487 139.196 234.487 137.442 234.487H93.4839C88.8059 234.487 87.2359 232.119 89.9939 229.225C92.7519 226.331 97.6479 221.193 100.875 217.807L135.051 181.949C138.278 178.563 143.246 173.345 146.094 170.352C148.942 167.359 153.91 162.139 157.133 158.75L162.904 152.684C166.129 149.295 171.404 143.75 174.627 140.362L186.471 127.916C189.694 124.527 194.758 119.205 197.721 116.089C200.686 112.974 203.889 109.608 204.838 108.609C205.787 107.611 209.201 104.022 212.426 100.633L278.317 31.3812C281.542 27.9922 280.911 23.2282 276.919 20.7932C276.919 20.7932 267.599 15.1122 258.755 11.4192C235.769 1.81919 210.816 -1.59681 185.61 0.678194C178.503 1.32019 171.393 2.41119 164.321 3.94219C145.237 8.07619 126.546 15.3542 108.954 25.4422C97.9699 31.7382 87.4599 39.1132 77.5829 47.4572C54.3189 67.1072 34.7959 91.9432 20.9249 120.702C14.0339 134.988 8.95193 149.42 5.51493 163.704C-1.69007 193.646 -1.63107 223.046 4.49993 249.499C8.57593 267.102 15.3589 283.516 24.5589 298.092C25.1199 298.982 25.6939 299.867 26.2739 300.743C31.2249 308.225 36.8479 315.217 43.0999 321.638C52.6369 331.432 63.6919 339.937 76.2069 346.811C82.4469 350.24 88.8519 353.149 95.3729 355.575C99.0779 356.951 102.824 358.176 106.605 359.24C113.087 361.068 119.679 362.438 126.337 363.378C184.333 371.547 251.78 346.032 298.382 290.078C307.3 279.37 315.068 268.043 321.671 256.283C330.341 240.845 336.993 224.657 341.544 208.154C345.339 194.392 347.665 180.431 348.483 166.541C349.298 152.728 348.617 139.014 346.396 125.679Z' fill='white' /> - </svg> - {/* <img src="geyser_logo.svg" alt="Geyser Fund Logo" className="w-8 h-8" /> */} - </div> - <div> - <h3 className="font-medium text-white">Support the Development</h3> - <p className="text-purple-100 text-sm">by donating to our Geyser Fund</p> - </div> - </div> - <Link href="https://geyser.fund/project/lumina" target="_blank" rel="noopener noreferrer"> - <Button className="bg-white text-purple-800 hover:bg-purple-100"> - Donate Now - </Button> - </Link> - </div> - </div> - ); -} \ No newline at end of file diff --git a/components/GlobalFeed.tsx b/components/GlobalFeed.tsx deleted file mode 100644 index b9ea874..0000000 --- a/components/GlobalFeed.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useNostrEvents } from "nostr-react"; -import KIND20Card from "./KIND20Card"; -import NoteCard from "./NoteCard"; -import { getImageUrl } from "@/utils/utils"; -import { useState, useRef } from "react"; -import { Button } from "@/components/ui/button"; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -const GlobalFeed: React.FC = () => { - const now = useRef(new Date()); - const [limit, setLimit] = useState(20); - - const { events, isLoading } = useNostrEvents({ - filter: { - limit: limit, - kinds: [20, 21, 22], - }, - }); - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 20); - }; - - return ( - <> - <div className="grid grid-cols-1 md:grid-cols-2 gap-4 px-2 md:px-4"> - {events.map((event) => { - const imageUrl = getImageUrl(event.tags); - const isVideo = event.kind === 21 || event.kind === 22; - - if (isVideo) { - // Use NoteCard for video content - const videoUrl = getVideoUrl(event.tags); - const contentWithVideo = videoUrl ? `${event.content}\n${videoUrl}` : event.content; - return ( - <div key={event.id} className="mb-4 md:mb-6"> - <NoteCard - key={event.id} - pubkey={event.pubkey} - text={contentWithVideo} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - /> - </div> - ); - } else { - // Use KIND20Card for image content - return ( - <div key={event.id} className="mb-4 md:mb-6"> - <KIND20Card - key={event.id} - pubkey={event.pubkey} - text={event.content} - image={imageUrl} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - /> - </div> - ); - } - })} - </div> - {!isLoading && ( - <div className="flex justify-center p-4"> - <Button className="w-full md:w-auto" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default GlobalFeed; \ No newline at end of file diff --git a/components/GlobalQuickViewFeed.tsx b/components/GlobalQuickViewFeed.tsx deleted file mode 100644 index 2843ffc..0000000 --- a/components/GlobalQuickViewFeed.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents } from "nostr-react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard"; -import { getImageUrl } from "@/utils/utils"; - -const GlobalQuickViewFeed: React.FC = () => { - const now = useRef(new Date()); - const [limit, setLimit] = useState(25); - - const { events, isLoading } = useNostrEvents({ - filter: { - limit: limit, - kinds: [20, 21, 22], - since: Math.floor((now.current.getTime() - 24 * 60 * 60 * 1000) / 1000), // Last 24 hours - }, - }); - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 25); - }; - - return ( - <> - <div className="grid grid-cols-3 gap-2"> - {events.length === 0 && isLoading ? ( - <> - <div> - <Skeleton className="h-[33vh] rounded-xl" /> - </div> - <div> - <Skeleton className="h-[33vh] rounded-xl" /> - </div> - <div> - <Skeleton className="h-[33vh] rounded-xl" /> - </div> - <div> - <Skeleton className="h-[33vh] rounded-xl" /> - </div> - <div> - <Skeleton className="h-[33vh] rounded-xl" /> - </div> - <div> - <Skeleton className="h-[33vh] rounded-xl" /> - </div> - </> - ) : ( - events.map((event) => ( - <QuickViewKind20NoteCard - key={event.id} - pubkey={event.pubkey} - text={event.content} - image={getImageUrl(event.tags)} - event={event} - tags={event.tags} - eventId={event.id} - linkToNote={true} - /> - )) - )} - </div> - {!isLoading && ( - <div className="flex justify-center p-4"> - <Button className="w-full md:w-auto" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default GlobalQuickViewFeed; \ No newline at end of file diff --git a/components/KIND20Card.tsx b/components/KIND20Card.tsx deleted file mode 100644 index 0cc971c..0000000 --- a/components/KIND20Card.tsx +++ /dev/null @@ -1,373 +0,0 @@ -import type React from "react" -import { useProfile } from "nostr-react" -import { nip19 } from "nostr-tools" -import { useState, useEffect, useRef } from "react" -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel" -import ReactionButton from "@/components/ReactionButton" -import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar" -import ViewNoteButton from "./ViewNoteButton" -import Link from "next/link" -import type { Event as NostrEvent } from "nostr-tools" -import ZapButton from "./ZapButton" -import Image from "next/image" -import CardOptionsDropdown from "./CardOptionsDropdown" -import { renderTextWithLinkedTags } from "@/utils/textUtils" -import { getProxiedImageUrl, hasNsfwContent } from "@/utils/utils" -import { Button } from "@/components/ui/button" -import { Eye, Play, Pause } from "lucide-react" - -// Function to extract all images from a kind 20 event's imeta tags -const extractImagesFromEvent = (tags: string[][]): string[] => { - return tags - .filter(tag => tag[0] === 'imeta') - .map(tag => { - const urlItem = tag.find(item => item.startsWith('url ')) - return urlItem ? urlItem.split(' ')[1] : null - }) - .filter(Boolean) as string[] -} - -// Function to check if this is a video event -const isVideoEvent = (event: NostrEvent): boolean => { - return event.kind === 21 || event.kind === 22; -} - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -} - -const useImgProxy = process.env.NEXT_PUBLIC_ENABLE_IMGPROXY === "true" - -interface KIND20CardProps { - pubkey: string - text: string - image: string // keeping for backward compatibility - eventId: string - tags: string[][] - event: NostrEvent - showViewNoteCardButton: boolean - videoUrl?: string // Optional video URL for video events -} - -const KIND20Card: React.FC<KIND20CardProps> = ({ - pubkey, - text, - image, - eventId, - tags, - event, - showViewNoteCardButton, - videoUrl: propVideoUrl, -}) => { - const { data: userData } = useProfile({ - pubkey, - }) - const [currentImage, setCurrentImage] = useState(0); - const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({}); - const [imagesWithoutProxy, setImagesWithoutProxy] = useState<Record<string, boolean>>({}); - const [showSensitiveContent, setShowSensitiveContent] = useState(false); - const [api, setApi] = useState<any>(null); - const [isVideoPlaying, setIsVideoPlaying] = useState(false); - const videoRef = useRef<HTMLVideoElement>(null); - - // Check if the event has nsfw content - const isNsfwContent = hasNsfwContent(tags); - - // Check if this is a video event - const isVideo = isVideoEvent(event); - - // Get video URL for video events - prefer prop over extracted - const videoUrl = propVideoUrl || (isVideo ? getVideoUrl(tags) : null); - - // Extract all images from imeta tags - const imetaImages = extractImagesFromEvent(tags); - - // For video events, if a thumbnail image is provided, use it directly - // For image events, use imeta images or fallback to provided image - const allImages = isVideo && image && image.startsWith("http") - ? [image] - : imetaImages.length > 0 - ? imetaImages - : (image && image.startsWith("http") ? [image] : []); - - // For video events, check if we have a thumbnail to show initially - const hasThumbnail = isVideo && image && image.startsWith("http"); - - // Filter out images with errors - const validImages = allImages.filter(img => !imageErrors[img]); - - // Handle image error by first trying without proxy, then marking as error if that fails too - const handleImageError = (errorImage: string) => { - if (imagesWithoutProxy[errorImage]) { - // Already tried without proxy, mark as error - setImageErrors(prev => ({ - ...prev, - [errorImage]: true - })); - } else { - // Try without proxy - setImagesWithoutProxy(prev => ({ - ...prev, - [errorImage]: true - })); - } - } - - // Toggle sensitive content visibility - const toggleSensitiveContent = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setShowSensitiveContent(true); - }; - - // Handle video play/pause - const handleVideoToggle = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (!videoRef.current || !videoUrl) return; - - if (isVideoPlaying) { - videoRef.current.pause(); - setIsVideoPlaying(false); - } else { - videoRef.current.play().catch(() => { - // Auto-play failed, keep paused - setIsVideoPlaying(false); - }); - setIsVideoPlaying(true); - } - }; - - // Handle video ended - const handleVideoEnded = () => { - setIsVideoPlaying(false); - }; - - // Handle video error - const handleVideoError = () => { - setIsVideoPlaying(false); - // Could show an error message here if needed - }; - - // Update current image index when carousel slides - useEffect(() => { - if (!api) return; - - const onSelect = () => { - setCurrentImage(api.selectedScrollSnap()); - }; - - api.on('select', onSelect); - - // Initial selection - onSelect(); - - return () => { - api.off('select', onSelect); - }; - }, [api]); - - // If no valid images are available, don't render the card - if (validImages.length === 0) return null; - - const title = - userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey) - - // Extract title from event tags - const eventTitle = tags.find(tag => tag[0] === 'title')?.[1] || '' - - text = text.replaceAll("\n", " ") - const createdAt = new Date(event.created_at * 1000) - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}` - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey - const uploadedVia = tags.find((tag) => tag[0] === "client")?.[1] - - return ( - <> - <div key={event.id}> - <Card className="my-4"> - <CardHeader className="flex flex-row items-center space-y-0"> - <CardTitle className="flex-1"> - <Link href={hrefProfile} style={{ textDecoration: "none" }}> - <TooltipProvider> - <Tooltip> - <TooltipTrigger> - <div style={{ display: "flex", alignItems: "center" }}> - <Avatar> - <AvatarImage src={profileImageSrc} /> - <AvatarFallback>{title.charAt(0).toUpperCase()}</AvatarFallback> - </Avatar> - <span className="break-all" style={{ marginLeft: "10px" }}> - {title} - </span> - </div> - </TooltipTrigger> - <TooltipContent> - <p>{title}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </Link> - </CardTitle> - <CardOptionsDropdown event={event} /> - </CardHeader> - <CardContent className="p-0"> - <div className="w-full"> - {validImages.length > 0 && ( - <div className="relative"> - <Carousel - className="w-full" - setApi={setApi} - > - <CarouselContent> - {validImages.map((imageUrl, index) => { - const shouldUseProxy = useImgProxy && !imagesWithoutProxy[imageUrl]; - const image = shouldUseProxy ? getProxiedImageUrl(imageUrl, 1200, 0) : imageUrl; - return ( - <CarouselItem key={`${imageUrl}-${index}`}> - <div className="w-full flex justify-center"> - <div className="relative w-full h-auto min-h-[300px] max-h-[80vh] flex justify-center"> - <div className="relative w-full h-full"> - {isVideo && videoUrl ? ( - <> - {/* Always render video element but hide it when showing thumbnail */} - <video - ref={videoRef} - src={videoUrl} - className={`rounded-lg w-full h-auto object-contain ${isNsfwContent && !showSensitiveContent ? 'blur-xl' : ''} ${hasThumbnail && !isVideoPlaying ? 'hidden' : ''}`} - onEnded={handleVideoEnded} - onError={handleVideoError} - style={{ - maxHeight: "80vh", - margin: "auto" - }} - muted - loop - playsInline - /> - - {/* Show thumbnail when available and video is not playing */} - {hasThumbnail && !isVideoPlaying && ( - <img - src={image} - alt={text} - className={`rounded-lg w-full h-auto object-contain ${isNsfwContent && !showSensitiveContent ? 'blur-xl' : ''} absolute inset-0`} - onError={() => handleImageError(imageUrl)} - loading="lazy" - style={{ - maxHeight: "80vh", - margin: "auto" - }} - /> - )} - - {/* Play/Pause button overlay */} - <div - className="absolute inset-0 flex items-center justify-center cursor-pointer" - onClick={handleVideoToggle} - > - <div className="bg-black bg-opacity-70 hover:bg-opacity-80 rounded-full p-4 transition-all duration-200"> - {isVideoPlaying ? ( - <Pause className="w-8 h-8 text-white" /> - ) : ( - <Play className="w-8 h-8 text-white" /> - )} - </div> - </div> - </> - ) : ( - <img - src={image} - alt={text} - className={`rounded-lg w-full h-auto object-contain ${isNsfwContent && !showSensitiveContent ? 'blur-xl' : ''}`} - onError={() => handleImageError(imageUrl)} - loading="lazy" - style={{ - maxHeight: "80vh", - margin: "auto" - }} - /> - )} - </div> - </div> - </div> - </CarouselItem> - ); - })} - </CarouselContent> - {validImages.length > 1 && !isNsfwContent && ( - <> - <CarouselPrevious className="left-2" /> - <CarouselNext className="right-2" /> - <div className="absolute bottom-4 left-0 right-0 flex justify-center"> - <div className="bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm"> - {`${currentImage + 1} / ${validImages.length}`} - </div> - </div> - </> - )} - </Carousel> - - {isNsfwContent && !showSensitiveContent && ( - <div - className="absolute inset-0 flex flex-col items-center justify-center z-10" - onClick={toggleSensitiveContent} - > - <Button - variant="secondary" - className="bg-black bg-opacity-50 hover:bg-opacity-70 text-white px-4 py-2 rounded-md" - onClick={toggleSensitiveContent} - > - <Eye className="h-4 w-4 mr-2" /> Show Sensitive Content - </Button> - <p className="mt-2 text-white text-sm bg-black bg-opacity-50 p-2 rounded"> - This image may contain sensitive content - </p> - </div> - )} - </div> - )} - </div> - <div className="p-4"> - {eventTitle && ( - <div className="mb-3"> - <h3 className="font-bold text-lg">{eventTitle}</h3> - </div> - )} - <div className="break-word overflow-hidden">{renderTextWithLinkedTags(text, tags)}</div> - <hr className="my-4" /> - <div className="space-x-4 flex justify-between items-start"> - <div className="flex space-x-4"> - <ReactionButton event={event} /> - <ZapButton event={event} /> - {showViewNoteCardButton && <ViewNoteButton event={event} />} - </div> - </div> - </div> - </CardContent> - <CardFooter> - <div className="grid grid-cols-1"> - <small className="text-secondary">{createdAt.toLocaleString()}</small> - {uploadedVia && <small className="text-secondary">Uploaded via {uploadedVia}</small>} - </div> - </CardFooter> - </Card> - </div> - </> - ) -} - -export default KIND20Card \ No newline at end of file diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx deleted file mode 100644 index a61e122..0000000 --- a/components/LoginForm.tsx +++ /dev/null @@ -1,503 +0,0 @@ -declare global { - interface Window { - nostr: any; - } -} - -import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" -import { useEffect, useRef, useState } from "react" -import { getPublicKey, generateSecretKey, nip19, SimplePool } from 'nostr-tools' -import { BunkerSigner, parseBunkerInput } from 'nostr-tools/nip46' -import { InfoIcon, Loader2, QrCode, X } from "lucide-react"; -import Link from "next/link"; -import { bytesToHex, hexToBytes } from '@noble/hashes/utils' -import { fetchNip65Relays, mergeAndStoreRelays } from "@/utils/nip65Utils" -import { Html5Qrcode } from "html5-qrcode"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogClose -} from "@/components/ui/dialog" - -export function LoginForm() { - - let publicKey = useRef(null); - let nsecInput = useRef<HTMLInputElement>(null); - let npubInput = useRef<HTMLInputElement>(null); - let bunkerUrlInput = useRef<HTMLInputElement>(null); - const [isLoading, setIsLoading] = useState(false); - const [isBunkerLoading, setIsBunkerLoading] = useState(false); - const [isExtensionLoading, setIsExtensionLoading] = useState(false); - const [isAmberLoading, setIsAmberLoading] = useState(false); - const [isNsecLoading, setIsNsecLoading] = useState(false); - const [isNpubLoading, setIsNpubLoading] = useState(false); - const [bunkerError, setBunkerError] = useState<string | null>(null); - const [isQRDialogOpen, setIsQRDialogOpen] = useState(false); - const [isScanning, setIsScanning] = useState(false); - const qrScannerRef = useRef<any>(null); - - // Default relays to query for NIP-65 data - const defaultRelays = [ - "wss://relay.nostr.band", - "wss://relay.damus.io", - "wss://nos.lol", - "wss://relay.nostr.ch", - "wss://profiles.nostr1.com" - ]; - - // Helper function to load NIP-65 relays for a user - const loadNip65Relays = async (pubkey: string) => { - try { - const nip65Relays = await fetchNip65Relays(pubkey, defaultRelays); - - if (nip65Relays.length > 0) { - mergeAndStoreRelays(nip65Relays); - console.log(`Loaded ${nip65Relays.length} relays from NIP-65 for user ${pubkey}`); - } else { - console.log(`No NIP-65 relays found for user ${pubkey}`); - } - } catch (error) { - console.error("Error loading NIP-65 relays:", error); - } - }; - - // Function to complete login process - const completeLogin = async (pubkey: string, loginType: string, redirect = true) => { - try { - localStorage.setItem("pubkey", pubkey); - localStorage.setItem("loginType", loginType); - - await loadNip65Relays(pubkey); - - if (redirect) { - window.location.href = `/profile/${nip19.npubEncode(pubkey)}`; - } - } catch (error) { - console.error("Error completing login:", error); - setIsLoading(false); - setIsBunkerLoading(false); - setIsExtensionLoading(false); - setIsAmberLoading(false); - setIsNsecLoading(false); - setIsNpubLoading(false); - } - }; - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const amberResponse = urlParams.get('amberResponse'); - if (amberResponse !== null) { - setIsAmberLoading(true); - completeLogin(amberResponse, "amber"); - } - - if (window.location.hash && window.location.hash.startsWith('#nostrconnect://')) { - handleNostrConnect(window.location.hash.substring(1)); - } - - return () => { - if (qrScannerRef.current) { - qrScannerRef.current.stop().catch(console.error); - } - }; - }, []); - - // Handle QR Scanner dialog - const startQRScanner = () => { - setIsQRDialogOpen(true); - setTimeout(() => { - setIsScanning(true); - const qrContainer = document.getElementById('qr-reader'); - - if (qrContainer) { - const html5QrCode = new Html5Qrcode("qr-reader"); - qrScannerRef.current = html5QrCode; - - html5QrCode.start( - { facingMode: "environment" }, - { - fps: 10, - qrbox: { width: 250, height: 250 }, - }, - (decodedText) => { - if (decodedText) { - html5QrCode.stop().catch(console.error); - setIsQRDialogOpen(false); - setIsScanning(false); - - if (bunkerUrlInput.current) { - bunkerUrlInput.current.value = decodedText; - } - } - }, - (errorMessage) => { - console.error("QR Scan error:", errorMessage); - } - ).catch(error => { - console.error("Starting QR Scanner failed:", error); - setIsScanning(false); - }); - } - }, 300); - }; - - const stopQRScanner = () => { - if (qrScannerRef.current) { - qrScannerRef.current.stop().catch(console.error); - } - setIsScanning(false); - setIsQRDialogOpen(false); - }; - - // Handle NIP-46 connection initiated by bunker - const handleNostrConnect = async (url: string) => { - try { - setIsLoading(true); - setIsBunkerLoading(true); - setBunkerError(null); - - const localSecretKey = generateSecretKey(); - const localSecretKeyHex = bytesToHex(localSecretKey); - - const bunkerUrl = url.includes('://') ? url : `nostrconnect://${url}`; - const bunkerPointer = await parseBunkerInput(bunkerUrl); - - if (!bunkerPointer) { - throw new Error('Invalid bunker URL'); - } - - const pool = new SimplePool(); - const bunker = new BunkerSigner(localSecretKey, bunkerPointer, { pool }); - - try { - await bunker.connect(); - - const userPubkey = await bunker.getPublicKey(); - - localStorage.setItem("bunkerLocalKey", localSecretKeyHex); - localStorage.setItem("bunkerUrl", bunkerUrl); - - await bunker.close(); - pool.close([]); - - await completeLogin(userPubkey, "bunker", true); - } catch (err) { - console.error("Bunker connection error:", err); - setBunkerError("Failed to connect to bunker. Please check the URL and try again."); - await bunker.close().catch(console.error); - pool.close([]); - setIsBunkerLoading(false); - } - } catch (err) { - console.error("Bunker parsing error:", err); - setBunkerError("Invalid bunker URL format."); - setIsBunkerLoading(false); - } finally { - setIsLoading(false); - } - }; - - const handleBunkerLogin = async () => { - if (bunkerUrlInput.current && bunkerUrlInput.current.value) { - const bunkerUrl = bunkerUrlInput.current.value.trim(); - await handleNostrConnect(bunkerUrl); - } else { - setBunkerError("Please enter a bunker URL"); - } - }; - - const handleAmber = async () => { - try { - setIsAmberLoading(true); - setIsLoading(true); - const hostname = window.location.host; - console.log(hostname); - if (!hostname) { - throw new Error("Hostname is null or undefined"); - } - const intent = `intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;S.callbackUrl=http://${hostname}/login?amberResponse=;end`; - window.location.href = intent; - } catch (error) { - console.error("Error launching Amber:", error); - setIsAmberLoading(false); - setIsLoading(false); - } - } - - const handleExtensionLogin = async () => { - try { - setIsExtensionLoading(true); - setIsLoading(true); - if (window.nostr !== undefined) { - publicKey.current = await window.nostr.getPublicKey() - console.log("Logged in with pubkey: ", publicKey.current); - if (publicKey.current !== null) { - await completeLogin(publicKey.current, "extension"); - } else { - throw new Error("Failed to get public key from extension"); - } - } else { - throw new Error("Nostr extension not detected"); - } - } catch (error) { - console.error("Extension login error:", error); - setIsExtensionLoading(false); - setIsLoading(false); - } - }; - - const handleNsecLogin = async () => { - if (nsecInput.current !== null) { - try { - setIsNsecLoading(true); - setIsLoading(true); - let input = nsecInput.current.value; - if(input.includes("nsec")) { - input = bytesToHex(nip19.decode(input).data as Uint8Array); - console.log('decoded nsec: ' + input); - } - let nsecBytes = hexToBytes(input); - let nsecHex = bytesToHex(nsecBytes); - let pubkey = getPublicKey(nsecBytes); - - localStorage.setItem("nsec", nsecHex); - await completeLogin(pubkey, "raw_nsec"); - } catch (e) { - console.error(e); - setIsNsecLoading(false); - setIsLoading(false); - } - } - }; - - const handleNpubLogin = async () => { - if (npubInput.current !== null) { - try { - setIsNpubLoading(true); - setIsLoading(true); - let input = npubInput.current.value; - let npub = null; - let pubkey = null; - if(input.startsWith("npub")) { - npub = input; - pubkey = nip19.decode(input).data.toString(); - } else { - pubkey = input; - npub = nip19.npubEncode(input); - } - - await completeLogin(pubkey, "readOnly_npub"); - } catch (e) { - console.error(e); - setIsNpubLoading(false); - setIsLoading(false); - } - } - }; - - return ( - <> - <Card className="w-full max-w-xl"> - <CardHeader> - <CardTitle className="text-2xl">Login to Lumina</CardTitle> - <CardDescription> - Login to your account with nostr extension, bunker, or with your nsec. - </CardDescription> - </CardHeader> - <CardContent className="grid gap-4"> - <div className="grid grid-cols-8 gap-2"> - <Button - className="w-full col-span-7" - onClick={handleExtensionLogin} - disabled={isLoading || isExtensionLoading} - > - {isExtensionLoading ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Connecting... - </> - ) : "Sign in with Extension (NIP-07)"} - </Button> - <Link target="_blank" href="https://www.getflamingo.org/"> - <Button variant={"outline"} disabled={isLoading}><InfoIcon /></Button> - </Link> - </div> - <div className="grid grid-cols-8 gap-2"> - <Button - className="w-full col-span-7" - onClick={handleAmber} - disabled={isLoading || isAmberLoading} - > - {isAmberLoading ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Connecting... - </> - ) : "Sign in with Amber"} - </Button> - <Link target="_blank" href="https://github.com/greenart7c3/Amber"> - <Button variant={"outline"} disabled={isLoading}><InfoIcon /></Button> - </Link> - </div> - <hr /> - or - <Accordion type="single" collapsible> - <AccordionItem value="item-1"> - <AccordionTrigger>Login with Bunker (NIP-46)</AccordionTrigger> - <AccordionContent> - <div className="grid gap-2"> - <Label htmlFor="bunkerUrl">Bunker URL</Label> - <div className="flex gap-2"> - <Input - placeholder="bunker://... or nostrconnect://..." - id="bunkerUrl" - ref={bunkerUrlInput} - type="text" - disabled={isLoading || isBunkerLoading} - className="flex-1" - /> - <Button - variant="outline" - onClick={startQRScanner} - disabled={isLoading || isBunkerLoading} - title="Scan QR code" - > - <QrCode className="h-4 w-4" /> - </Button> - </div> - {bunkerError && <p className="text-red-500 text-sm">{bunkerError}</p>} - <Button - className="w-full" - onClick={handleBunkerLogin} - disabled={isLoading || isBunkerLoading} - > - {isBunkerLoading ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Connecting... - </> - ) : "Sign in with Bunker"} - </Button> - <p className="text-sm text-muted-foreground"> - Use a NIP-46 compatible bunker URL that starts with bunker:// or nostrconnect:// - </p> - </div> - </AccordionContent> - </AccordionItem> - </Accordion> - or - <Accordion type="single" collapsible> - <AccordionItem value="item-1"> - <AccordionTrigger>Login with npub (read-only)</AccordionTrigger> - <AccordionContent> - <div className="grid gap-2"> - <Label htmlFor="npub">npub</Label> - <Input - placeholder="npub..." - id="npub" - ref={npubInput} - type="text" - disabled={isLoading || isNpubLoading} - /> - <Button - className="w-full" - onClick={handleNpubLogin} - disabled={isLoading || isNpubLoading} - > - {isNpubLoading ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Signing in... - </> - ) : "Sign in"} - </Button> - </div> - </AccordionContent> - </AccordionItem> - </Accordion> - or - <Accordion type="single" collapsible> - <AccordionItem value="item-1"> - <AccordionTrigger>Login with nsec (not recommended)</AccordionTrigger> - <AccordionContent> - <div className="grid gap-2"> - <Label htmlFor="nsec">nsec</Label> - <Input - placeholder="nsecabcdefghijklmnopqrstuvwxyz" - id="nsec" - ref={nsecInput} - type="password" - disabled={isLoading || isNsecLoading} - /> - <Button - className="w-full" - onClick={handleNsecLogin} - disabled={isLoading || isNsecLoading} - > - {isNsecLoading ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Signing in... - </> - ) : "Sign in"} - </Button> - </div> - </AccordionContent> - </AccordionItem> - </Accordion> - </CardContent> - <CardFooter> - </CardFooter> - </Card> - - {/* QR Code Scanner Dialog */} - <Dialog open={isQRDialogOpen} onOpenChange={(open) => { - if (!open) stopQRScanner(); - setIsQRDialogOpen(open); - }}> - <DialogContent className="sm:max-w-md"> - <DialogHeader> - <DialogTitle>Scan Bunker QR Code</DialogTitle> - <DialogDescription> - Position the QR code in the camera view to scan your bunker URL. - </DialogDescription> - </DialogHeader> - <div className="flex flex-col items-center justify-center"> - <div id="qr-reader" style={{ width: '100%', maxWidth: '500px' }}></div> - {isScanning ? ( - <p className="text-sm text-center mt-2">Scanning...</p> - ) : ( - <p className="text-sm text-center mt-2">Starting camera...</p> - )} - </div> - <div className="flex justify-center"> - <DialogClose asChild> - <Button variant="secondary" onClick={stopQRScanner}> - <X className="h-4 w-4 mr-2" /> Cancel - </Button> - </DialogClose> - </div> - </DialogContent> - </Dialog> - </> - ) -} \ No newline at end of file diff --git a/components/ManageCustomRelays.tsx b/components/ManageCustomRelays.tsx deleted file mode 100644 index aad9a32..0000000 --- a/components/ManageCustomRelays.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useNostr } from "nostr-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Trash2 } from "lucide-react"; -import { useToast } from "@/components/ui/use-toast"; -import { Badge } from "@/components/ui/badge"; - -export function ManageCustomRelays() { - const [customRelays, setCustomRelays] = useState<string[]>([]); - const { toast } = useToast(); - const { connectedRelays } = useNostr(); - - useEffect(() => { - // Load custom relays from localStorage - const storedRelays = JSON.parse(localStorage.getItem("customRelays") || "[]"); - setCustomRelays(storedRelays); - }, []); - - const handleRemoveRelay = (relayUrl: string) => { - // Filter out the relay to remove - const updatedRelays = customRelays.filter(url => url !== relayUrl); - - // Update state and localStorage - setCustomRelays(updatedRelays); - localStorage.setItem("customRelays", JSON.stringify(updatedRelays)); - - toast({ - title: "Relay removed", - description: "The relay has been removed from your list. Refresh to apply changes.", - variant: "default", - }); - }; - - // Check if a relay is currently connected - const isConnected = (relayUrl: string) => { - // Normalize the URL by removing trailing slash - const normalizedRelayUrl = relayUrl.endsWith('/') ? relayUrl.slice(0, -1) : relayUrl; - - return connectedRelays?.some(relay => { - // Normalize connected relay URL too - const normalizedConnectedUrl = relay.url.endsWith('/') ? relay.url.slice(0, -1) : relay.url; - return normalizedConnectedUrl === normalizedRelayUrl && relay.status === 1; - }) || false; - }; - - if (customRelays.length === 0) { - return null; - } - - return ( - <Card className="mt-6"> - <CardHeader> - <CardTitle>Custom Relays</CardTitle> - </CardHeader> - <CardContent> - <div className="space-y-3"> - {customRelays.map((relayUrl) => ( - <div key={relayUrl} className="flex items-center justify-between p-3 rounded-md border bg-card hover:bg-accent/50 transition-colors"> - <div className="flex items-center space-x-3 overflow-hidden"> - <div> - <p className="font-medium truncate">{relayUrl}</p> - <div className="mt-1"> - {isConnected(relayUrl) ? ( - <Badge className="bg-green-500">Connected</Badge> - ) : ( - <Badge variant="outline">Not Connected</Badge> - )} - </div> - </div> - </div> - <Button - variant="ghost" - size="icon" - onClick={() => handleRemoveRelay(relayUrl)} - className="text-destructive hover:text-destructive hover:bg-destructive/10" - > - <Trash2 className="h-4 w-4" /> - <span className="sr-only">Remove</span> - </Button> - </div> - ))} - <p className="text-xs text-muted-foreground mt-2"> - Note: Refresh the page after adding or removing relays to apply changes to connections. - </p> - </div> - </CardContent> - </Card> - ); -} \ No newline at end of file diff --git a/components/NostrWalletConnect.tsx b/components/NostrWalletConnect.tsx deleted file mode 100644 index e3da3a6..0000000 --- a/components/NostrWalletConnect.tsx +++ /dev/null @@ -1,259 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { nwc } from "@getalby/sdk"; -import { Loader2, CheckCircle, XCircle, ExternalLink, Copy, Trash2 } from "lucide-react"; -import QRCode from "react-qr-code"; - -const NWC_STORAGE_KEY = "lumina-nwc-connection"; - -export function NostrWalletConnect() { - const [connectionUrl, setConnectionUrl] = useState<string>(""); - const [nwcClient, setNwcClient] = useState<nwc.NWCClient | null>(null); - const [isConnected, setIsConnected] = useState<boolean>(false); - const [isConnecting, setIsConnecting] = useState<boolean>(false); - const [walletInfo, setWalletInfo] = useState<any>(null); - const [error, setError] = useState<string | null>(null); - const [balance, setBalance] = useState<number | null>(null); - - // Load saved connection from localStorage on component mount - useEffect(() => { - const savedConnection = localStorage.getItem(NWC_STORAGE_KEY); - if (savedConnection) { - setConnectionUrl(savedConnection); - connectWithSavedUrl(savedConnection); - } - }, []); - - // Connect with a saved connection URL - const connectWithSavedUrl = async (url: string) => { - if (!url) return; - - try { - setIsConnecting(true); - setError(null); - - const client = new nwc.NWCClient({ - nostrWalletConnectUrl: url, - }); - - // Test the connection by getting wallet info - const info = await client.getInfo(); - - setNwcClient(client); - setWalletInfo(info); - setIsConnected(true); - - // Get balance - try { - const balanceInfo = await client.getBalance(); - setBalance(balanceInfo.balance / 1000); // Convert to sats - } catch (e) { - console.error("Error fetching balance:", e); - } - - } catch (e) { - console.error("NWC connection error:", e); - setError("Failed to connect to wallet. Please check your connection URL."); - } finally { - setIsConnecting(false); - } - }; - - // Connect with a new NWC URL - const handleConnect = async () => { - if (!connectionUrl) { - setError("Please enter a valid NWC connection URL"); - return; - } - - try { - await connectWithSavedUrl(connectionUrl); - - // If we get here without an error being thrown, connection was successful - // Save to localStorage - localStorage.setItem(NWC_STORAGE_KEY, connectionUrl); - } catch (error) { - // Error will be handled in connectWithSavedUrl - console.error("Connection failed:", error); - } - }; - - // Handle authorizing with a new wallet - const handleAuthorize = async () => { - try { - setIsConnecting(true); - setError(null); - - const client = await nwc.NWCClient.fromAuthorizationUrl( - "https://my.albyhub.com/apps/new", - { - name: "Lumina", - } - ); - - // Get the connection URL for storage - const newConnectionUrl = client.getNostrWalletConnectUrl(); - setConnectionUrl(newConnectionUrl); - - // Test the connection by getting wallet info - const info = await client.getInfo(); - - setNwcClient(client); - setWalletInfo(info); - setIsConnected(true); - - // Save to localStorage - localStorage.setItem(NWC_STORAGE_KEY, newConnectionUrl); - - // Get balance - try { - const balanceInfo = await client.getBalance(); - setBalance(balanceInfo.balance / 1000); - } catch (e) { - console.error("Error fetching balance:", e); - } - - } catch (e) { - console.error("NWC authorization error:", e); - setError("Failed to authorize with wallet."); - } finally { - setIsConnecting(false); - } - }; - - // Disconnect wallet - const handleDisconnect = () => { - localStorage.removeItem(NWC_STORAGE_KEY); - setConnectionUrl(""); - setNwcClient(null); - setWalletInfo(null); - setIsConnected(false); - setBalance(null); - }; - - // Copy connection URL to clipboard - const copyConnectionUrl = () => { - navigator.clipboard.writeText(connectionUrl); - // You could add a toast notification here - }; - - return ( - <div className="space-y-6"> - {!isConnected ? ( - <Card> - <CardHeader> - <CardTitle>Connect Lightning Wallet</CardTitle> - <CardDescription> - Connect your Lightning wallet using Nostr Wallet Connect (NWC) - </CardDescription> - </CardHeader> - <CardContent className="space-y-4"> - <div className="space-y-2"> - <Label htmlFor="nwc-url">NWC Connection URL</Label> - <Input - id="nwc-url" - placeholder="nostr+walletconnect://..." - value={connectionUrl} - onChange={(e) => setConnectionUrl(e.target.value)} - className="font-mono text-sm" - /> - {error && <p className="text-destructive text-sm">{error}</p>} - </div> - </CardContent> - <CardFooter className="flex flex-col md:flex-row gap-3"> - <Button onClick={handleConnect} disabled={isConnecting} className="w-full md:w-auto"> - {isConnecting ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Connecting... - </> - ) : ( - "Connect with URL" - )} - </Button> - <Button onClick={handleAuthorize} disabled={isConnecting} variant="outline" className="w-full md:w-auto"> - {isConnecting ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Authorizing... - </> - ) : ( - "Connect with Alby Hub" - )} - </Button> - </CardFooter> - </Card> - ) : ( - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - Connected Wallet - <CheckCircle className="h-5 w-5 text-green-500" /> - </CardTitle> - <CardDescription> - Your Lightning wallet is connected via Nostr Wallet Connect - </CardDescription> - </CardHeader> - <CardContent className="space-y-4"> - {walletInfo && ( - <div className="space-y-4"> - <div> - <h3 className="text-sm font-medium">Wallet Info</h3> - <p className="text-sm text-muted-foreground truncate"> - {walletInfo.alias || "Unknown Wallet"} - </p> - </div> - - <div> - <h3 className="text-sm font-medium">Balance</h3> - <p className="text-sm text-muted-foreground"> - {balance !== null ? `${balance} sats` : "Not available"} - </p> - </div> - - <div> - <h3 className="text-sm font-medium">Connection URL</h3> - <div className="flex items-center gap-2 mt-1"> - <p className="text-xs font-mono bg-muted p-2 rounded overflow-hidden overflow-ellipsis whitespace-nowrap max-w-[300px]"> - {connectionUrl.substring(0, 20)}... - </p> - <Button variant="ghost" size="icon" onClick={copyConnectionUrl}> - <Copy className="h-4 w-4" /> - </Button> - </div> - </div> - </div> - )} - </CardContent> - <CardFooter className="flex justify-between"> - <Button onClick={handleDisconnect} variant="destructive"> - <Trash2 className="mr-2 h-4 w-4" /> - Disconnect - </Button> - </CardFooter> - </Card> - )} - - {isConnected && ( - <Card> - <CardHeader> - <CardTitle>Connection QR Code</CardTitle> - <CardDescription> - Scan this QR code with a compatible wallet to connect - </CardDescription> - </CardHeader> - <CardContent className="flex justify-center p-6"> - <div className="p-4 bg-white rounded-lg"> - <QRCode value={connectionUrl} size={200} /> - </div> - </CardContent> - </Card> - )} - </div> - ); -} \ No newline at end of file diff --git a/components/NoteCard.tsx b/components/NoteCard.tsx deleted file mode 100644 index 92d2bf9..0000000 --- a/components/NoteCard.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import React from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, -} from "@/components/ui/carousel" -import ReactionButton from '@/components/ReactionButton'; -import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; -import ViewNoteButton from './ViewNoteButton'; -import Link from 'next/link'; -import { Event as NostrEvent } from "nostr-tools"; -import ZapButton from './ZapButton'; -import CardOptionsDropdown from './CardOptionsDropdown'; -import { renderTextWithLinkedTags } from '@/utils/textUtils'; -import { PinIcon } from "lucide-react"; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -// Function to check if an event has reference tags (e, a, u) -const hasReferenceTags = (tags: string[][]): boolean => { - return tags.some(tag => ['e', 'a', 'u'].includes(tag[0])); -}; - -// Function to get the first reference tag for opening source -const getFirstReferenceTag = (tags: string[][]): { type: string; value: string; relays?: string[] } | null => { - for (const tag of tags) { - if (tag[0] === 'e') { - return { type: 'e', value: tag[1], relays: tag.slice(2) }; - } - if (tag[0] === 'a') { - return { type: 'a', value: tag[1], relays: tag.slice(2) }; - } - if (tag[0] === 'u') { - return { type: 'u', value: tag[1] }; - } - } - return null; -}; - - - -// Component for the purple pin icon -const PinButton: React.FC<{ - referenceTag: { type: string; value: string; relays?: string[] }; - onPinClick?: (referenceTag: { type: string; value: string; relays?: string[] }) => void; -}> = ({ referenceTag, onPinClick }) => { - return ( - <button - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - if (onPinClick) { - onPinClick(referenceTag); - } - }} - className="absolute top-3 right-3 z-10 bg-purple-600 hover:bg-purple-700 text-white rounded-full p-1.5 shadow-lg transition-colors duration-200" - title="Open source" - > - <PinIcon className="w-3 h-3" /> - </button> - ); -}; - -interface NoteCardProps { - pubkey: string; - text: string; - eventId: string; - tags: string[][]; - event: NostrEvent; - showViewNoteCardButton: boolean; - onPinClick?: (referenceTag: { type: string; value: string; relays?: string[] }) => void; -} - -const NoteCard: React.FC<NoteCardProps> = ({ pubkey, text, eventId, tags, event, showViewNoteCardButton, onPinClick }) => { - const { data: userData } = useProfile({ - pubkey, - }); - - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); - // text = text.replaceAll('\n', '<br />'); - text = text.replaceAll('\n', ' '); - - // Extract video URL from imeta tags for video events (kind 21 or 22) - const imetaVideoUrl = (event.kind === 21 || event.kind === 22) ? getVideoUrl(tags) : null; - - // Combine text-based video detection with imeta-based detection - const textVideoSrc = text.match(/https?:\/\/[^ ]*\.(mp4|webm|mov)/g); - const videoSrc = imetaVideoUrl ? [imetaVideoUrl] : textVideoSrc; - - const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g); - const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov|jpeg)/g, ''); - const createdAt = new Date(event.created_at * 1000); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - - // Check for reference tags and gallery tags - const hasReferences = hasReferenceTags(tags); - const referenceTag = getFirstReferenceTag(tags); - const isGalleryTagged = text.includes('#gallery') || tags.some((tag: string[]) => tag[0] === 't' && tag[1] === 'gallery'); - - return ( - <> - <Card className="relative"> - {(hasReferences && referenceTag) || isGalleryTagged ? ( - <PinButton - referenceTag={referenceTag || { type: 'gallery', value: 'gallery' }} - onPinClick={onPinClick} - /> - ) : null} - <CardHeader className="flex flex-row items-center space-y-0"> - <CardTitle className="flex-1"> - <Link href={hrefProfile} style={{ textDecoration: 'none' }}> - <TooltipProvider> - <Tooltip> - <TooltipTrigger> - <div style={{ display: 'flex', alignItems: 'center' }}> - <Avatar> - <AvatarImage src={profileImageSrc} /> - <AvatarFallback>{title.charAt(0).toUpperCase()}</AvatarFallback> - </Avatar> - <span className='break-all' style={{ marginLeft: '10px' }}>{title}</span> - </div> - </TooltipTrigger> - <TooltipContent> - <p>{title}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </Link> - </CardTitle> - <CardOptionsDropdown event={event} /> - </CardHeader> - <CardContent> - <div className='py-4'> - { - <div> - <div className='w-full h-full px-10'> - {imageSrc && imageSrc.length > 1 ? ( - <Carousel> - <CarouselContent> - {imageSrc.map((src, index) => ( - <CarouselItem key={index}> - <img - key={index} - src={src} - className='rounded lg:rounded-lg w-full h-auto object-contain' - style={{ maxHeight: '66vh', margin: 'auto' }} - alt={textWithoutImage || "Post image"} - loading="lazy" - /> - </CarouselItem> - ))} - </CarouselContent> - <CarouselPrevious /> - <CarouselNext /> - </Carousel> - ) : ( - imageSrc ? - <img - src={imageSrc[0]} - className='rounded lg:rounded-lg w-full h-auto object-contain' - style={{ maxHeight: '66vh', margin: 'auto' }} - alt={textWithoutImage || "Post image"} - loading="lazy" - /> : "" - )} - </div> - <div className='w-full h-full px-10'> - {videoSrc && videoSrc.length > 1 ? ( - <Carousel> - <CarouselContent> - {videoSrc.map((src, index) => ( - <CarouselItem key={index}> - <video - key={index} - src={src} - controls - className='rounded lg:rounded-lg' - style={{ maxWidth: '100%', maxHeight: '66vh', objectFit: 'contain', margin: 'auto' }} - /> - </CarouselItem> - ))} - </CarouselContent> - <CarouselPrevious /> - <CarouselNext /> - </Carousel> - ) : ( - videoSrc ? <video src={videoSrc[0]} controls className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '66vh', objectFit: 'contain', margin: 'auto' }} /> : "" - )} - </div> - </div> - } - <br /> - <div className='break-word overflow-hidden'> - {renderTextWithLinkedTags(textWithoutImage, tags)} - </div> - </div> - <hr /> - <div className='py-4 space-x-4 flex'> - <div className='flex space-x-4'> - <ReactionButton event={event} /> - <ZapButton event={event} /> - {showViewNoteCardButton && <ViewNoteButton event={event} />} - </div> - </div> - </CardContent> - <CardFooter> - <small className="text-secondary">{createdAt.toLocaleString()}</small> - </CardFooter> - </Card> - </> - ); -} - -export default NoteCard; \ No newline at end of file diff --git a/components/NotePageComponent.tsx b/components/NotePageComponent.tsx deleted file mode 100644 index 908f12f..0000000 --- a/components/NotePageComponent.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useRef } from "react"; -import { useNostrEvents } from "nostr-react"; -import NoteCard from '@/components/NoteCard'; -import CommentsCompontent from "@/components/CommentsComponent"; -import KIND20Card from "./KIND20Card"; -import { getImageUrl } from "@/utils/utils"; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -interface NotePageComponentProps { - id: string; -} - -const NotePageComponent: React.FC<NotePageComponentProps> = ({ id }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered - - const { events } = useNostrEvents({ - filter: { - ids: [id], - limit: 1, - }, - }); - - // filter out all events that also have another e tag with another id - const filteredEvents = events.filter((event) => { - return event.tags.filter((tag) => { - return tag[0] === '#e' && tag[1] !== id; - }).length === 0; - }); - - return ( - <> - {filteredEvents.map((event) => ( - <div key={event.id} className="py-6"> - {event.kind === 1 && ( - <NoteCard - key={event.id} - pubkey={event.pubkey} - text={event.content} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={false} - /> - )} - {event.kind === 20 && ( - <KIND20Card - key={event.id} - pubkey={event.pubkey} - text={event.content} - image={getImageUrl(event.tags)} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={false} - /> - )} - {(event.kind === 21 || event.kind === 22) && ( - <NoteCard - key={event.id} - pubkey={event.pubkey} - text={(() => { - const videoUrl = getVideoUrl(event.tags); - return videoUrl ? `${event.content}\n${videoUrl}` : event.content; - })()} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={false} - /> - )} - <div className="py-6 px-6"> - <CommentsCompontent pubkey={event.pubkey} event={event} /> - </div> - </div> - ))} - </> - ); -} - -export default NotePageComponent; \ No newline at end of file diff --git a/components/Notification.tsx b/components/Notification.tsx deleted file mode 100644 index e3eca58..0000000 --- a/components/Notification.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import { useNostrEvents, useProfile } from "nostr-react"; -import { - NostrEvent, - Event, - nip19, -} from "nostr-tools"; -import { Avatar, AvatarImage } from './ui/avatar'; -import Link from 'next/link'; -import { format } from 'date-fns'; - -interface NotificationProps { - event: NostrEvent; -} - -const Notification: React.FC<NotificationProps> = ({ event }) => { - let sender = event.pubkey; - let sats = 0; - let reactedToId = ''; - - const { data: userData, isLoading: userDataLoading } = useProfile({ - pubkey: sender, - }); - - if (!event) { - return null; - } - - if (event.kind === 9735) { - for (let tag of event.tags) { - if (tag[0] === 'P') { - sender = tag[1]; - } - if (tag[0] === 'bolt11') { - let bolt11decoded = require('light-bolt11-decoder').decode(tag[1]); - for (let field of bolt11decoded.sections) { - if (field.name === 'amount') { - sats = field.value / 1000; - } - } - } - } - } - - if (event.kind === 7) { - for (let tag of event.tags) { - if (tag[0] === 'e') { - reactedToId = tag[1]; - } - } - } - - let name = userData?.name ?? nip19.npubEncode(event.pubkey).slice(0, 8) + ':' + nip19.npubEncode(event.pubkey).slice(-3); - let createdAt = new Date(event.created_at * 1000); - - const formatTime = (date: Date) => { - return format(date, 'h:mm a'); - }; - - const getNotificationContent = () => { - switch (event.kind) { - case 9735: // ZAP - return ( - <div className='flex items-center space-x-3 p-3 hover:bg-muted/50 rounded-md transition-colors'> - <div className='flex-shrink-0 w-10 text-center font-medium text-amber-500'> - {sats} ⚡️ - </div> - <Avatar className='flex-shrink-0'> - <AvatarImage src={userData?.picture} alt={name} /> - </Avatar> - <div className='flex-1 min-w-0'> - <p className='text-sm font-medium'>{name} <span className='text-muted-foreground font-normal'>zapped you</span></p> - <p className='text-xs text-muted-foreground'>{formatTime(createdAt)}</p> - </div> - </div> - ); - case 3: // FOLLOW - return ( - <div className='flex items-center space-x-3 p-3 hover:bg-muted/50 rounded-md transition-colors'> - <div className='flex-shrink-0 w-10 text-center font-medium text-blue-500'> - 👋 - </div> - <Avatar className='flex-shrink-0'> - <AvatarImage src={userData?.picture} alt={name} /> - </Avatar> - <div className='flex-1 min-w-0'> - <p className='text-sm font-medium'>{name} <span className='text-muted-foreground font-normal'>started following you</span></p> - <p className='text-xs text-muted-foreground'>{formatTime(createdAt)}</p> - </div> - </div> - ); - case 7: // REACTION - return ( - <Link href={"/note/" + reactedToId} className='block'> - <div className='flex items-center space-x-3 p-3 hover:bg-muted/50 rounded-md transition-colors'> - <div className='flex-shrink-0 w-10 text-center text-lg'> - {event.content} - </div> - <Avatar className='flex-shrink-0'> - <AvatarImage src={userData?.picture} alt={name} /> - </Avatar> - <div className='flex-1 min-w-0'> - <p className='text-sm font-medium'>{name} <span className='text-muted-foreground font-normal'>reacted to your post</span></p> - <p className='text-xs text-muted-foreground'>{formatTime(createdAt)}</p> - </div> - </div> - </Link> - ); - default: - return null; - } - }; - - return ( - <div className='notification-item'> - {getNotificationContent()} - </div> - ); -} - -export default Notification; \ No newline at end of file diff --git a/components/Notifications.tsx b/components/Notifications.tsx deleted file mode 100644 index 506ca36..0000000 --- a/components/Notifications.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import { useNostrEvents, useProfile } from "nostr-react"; -import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card'; -import { Skeleton } from '@/components/ui/skeleton'; -import { AvatarImage } from '@radix-ui/react-avatar'; -import { Avatar } from '@/components/ui/avatar'; -import NIP05 from '@/components/nip05'; -import { - nip19, -} from "nostr-tools"; -import Notification from './Notification'; -import { format, isSameDay, parseISO } from 'date-fns'; - -interface NotificationsProps { - pubkey: string; -} - -const Notifications: React.FC<NotificationsProps> = ({ pubkey }) => { - const { data: userData, isLoading: userDataLoading } = useProfile({ - pubkey, - }); - - // const { events: followers, isLoading: followersLoading } = useNostrEvents({ - // filter: { - // kinds: [3], - // '#p': [pubkey], - // limit: 50, - // }, - // }); - - const { events, isLoading: isLoading } = useNostrEvents({ - filter: { - kinds: [7, 9735], - '#p': [pubkey], - limit: 50, - }, - }); - - // const { events: following, isLoading: followingLoading } = useNostrEvents({ - // filter: { - // kinds: [3], - // authors: [pubkey], - // limit: 1, - // }, - // }); - - // filter for only new followings (latest in a users followers list) - // const filteredFollowers = followers.filter(follower => { - // const lastPTag = follower.tags[follower.tags.length - 1]; - // if (lastPTag[0] === "p" && lastPTag[1] === pubkey.toString()) { - // // console.log(follower.tags[follower.tags.length - 1]); - // return true; - // } - // }); - - // Sort all notifications by date (newest first) - const sortedEvents = [...events].sort( - (a, b) => (b.created_at || 0) - (a.created_at || 0) - ); - - // Group notifications by date - const groupedNotifications = () => { - const groups: { [key: string]: typeof events } = {}; - - sortedEvents.forEach(event => { - const date = new Date(event.created_at * 1000); - const dateKey = format(date, 'yyyy-MM-dd'); - - if (!groups[dateKey]) { - groups[dateKey] = []; - } - - groups[dateKey].push(event); - }); - - return groups; - }; - - // Get formatted date heading based on date - const getDateHeading = (dateStr: string) => { - const date = parseISO(dateStr); - const today = new Date(); - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - - if (isSameDay(date, today)) { - return "Today"; - } else if (isSameDay(date, yesterday)) { - return "Yesterday"; - } else { - return format(date, 'EEEE, MMMM d, yyyy'); - } - }; - - const notificationGroups = groupedNotifications(); - - return ( - <> - <div className='pt-6 px-6'> - {/* <ProfileInfoCard pubkey={pubkey.toString()} /> */} - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-base font-normal">Notifications</CardTitle> - </CardHeader> - <CardContent> - {events.length > 0 ? ( - Object.keys(notificationGroups).map(dateKey => ( - <div key={dateKey} className="mb-6"> - <div className="sticky top-0 backdrop-blur-sm py-2 mb-2 border-b"> - <h3 className="text-sm font-medium text-muted-foreground"> - {getDateHeading(dateKey)} - </h3> - </div> - <div className="space-y-1"> - {notificationGroups[dateKey].map((notification) => ( - <Notification key={notification.id} event={notification} /> - ))} - </div> - </div> - )) - ) : ( - <div className="text-center py-4 text-muted-foreground">No notifications yet</div> - )} - {(!events) && ( - <div className="text-center py-2"> - <Skeleton className="h-12 w-full mb-2" /> - <Skeleton className="h-12 w-full" /> - </div> - )} - </CardContent> - </Card> - </div> - </> - ); -} - -export default Notifications; \ No newline at end of file diff --git a/components/ProfileFeed.tsx b/components/ProfileFeed.tsx deleted file mode 100644 index 50f41b1..0000000 --- a/components/ProfileFeed.tsx +++ /dev/null @@ -1,530 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents, dateToUnix, useProfile } from "nostr-react"; -import NoteCard from '@/components/NoteCard'; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import KIND20Card from "./KIND20Card"; -import { getImageUrl, getThumbnailUrl } from "@/utils/utils"; -import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; -import Link from "next/link"; -import { nip19 } from "nostr-tools"; -import { PinIcon } from "lucide-react"; - -// Component to display profile picture for pinned events -const ProfilePictureCard: React.FC<{ - pubkey: string; - eventId: string; - content: string; -}> = ({ pubkey, eventId, content }) => { - const { data: userData } = useProfile({ - pubkey, - }); - - const profileImageSrc = userData?.picture || `https://robohash.org/${pubkey}`; - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); - - return ( - <div className="relative bg-white rounded-xl shadow-sm border overflow-hidden"> - <div className="p-4"> - <div className="flex items-center space-x-3 mb-3"> - <Avatar className="w-10 h-10"> - <AvatarImage src={profileImageSrc} /> - <AvatarFallback>{title.charAt(0).toUpperCase()}</AvatarFallback> - </Avatar> - <div> - <p className="font-medium text-sm">{title}</p> - <p className="text-xs text-gray-500">Pinned by you</p> - </div> - </div> - <div className="flex space-x-3"> - <div className="flex-shrink-0"> - <Avatar className="w-16 h-16"> - <AvatarImage src={profileImageSrc} /> - <AvatarFallback>{title.charAt(0).toUpperCase()}</AvatarFallback> - </Avatar> - </div> - <div className="flex-1 min-w-0"> - <p className="text-sm text-gray-700 line-clamp-3">{content}</p> - </div> - </div> - </div> - <Link - href={`/note/${nip19.neventEncode({ - id: eventId, - relays: [] - })}`} - className="absolute inset-0" - /> - </div> - ); -}; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - const videoUrl = tag[i].substring(4); - console.log("Found video URL in imeta tag:", videoUrl); - return videoUrl; - } - } - } - } - console.log("No video URL found in tags:", tags); - return null; -}; - -// Function to extract audio URL from imeta tags -const getAudioUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - const mimeItem = tag.find(item => item.startsWith('m ')); - const urlItem = tag.find(item => item.startsWith('url ')); - - if (mimeItem && mimeItem.startsWith('m audio/') && urlItem) { - return urlItem.substring(4); - } - } - } - return null; -}; - - - -// Function to get the first reference tag for opening source -const getFirstReferenceTag = (tags: string[][]): { type: string; value: string; relays?: string[] } | null => { - for (const tag of tags) { - if (tag[0] === 'e') { - const result = { type: 'e', value: tag[1], relays: tag.slice(2) }; - console.log("Found 'e' tag:", result); - return result; - } - if (tag[0] === 'a') { - const result = { type: 'a', value: tag[1], relays: tag.slice(2) }; - console.log("Found 'a' tag:", result); - return result; - } - if (tag[0] === 'u') { - const result = { type: 'u', value: tag[1] }; - console.log("Found 'u' tag:", result); - return result; - } - } - console.log("No reference tag found in tags:", tags); - return null; -}; - -// Function to determine if an event should show a pin and what it should do -const getPinInfo = (event: any, pubkey: string, pinnedEventIds: string[]): { showPin: boolean; referenceTag: { type: string; value: string; relays?: string[] } | null } => { - const isOwnEvent = event.pubkey === pubkey; - const isMediaEvent = event.kind === 20 || event.kind === 21 || event.kind === 22; - const isPinned = pinnedEventIds.includes(event.id); - const hasReferenceTag = getFirstReferenceTag(event.tags); - const isGalleryEvent = event.content.includes('#gallery') || event.tags.some((tag: string[]) => tag[0] === 't' && tag[1] === 'gallery'); - - - - // Case 1: If it's my kind 20, 21, or 22 and has a reference tag, show pin to that reference - if (isOwnEvent && isMediaEvent && hasReferenceTag) { - return { showPin: true, referenceTag: hasReferenceTag }; - } - - // Case 2: If it's pinned (came onto board because it or an event that references it contained "gallery") - if (isPinned) { - return { showPin: true, referenceTag: { type: 'e', value: event.id } }; - } - - // Case 3: If it's my own event with gallery tag - if (isOwnEvent && isGalleryEvent) { - return { showPin: true, referenceTag: { type: 'gallery', value: 'gallery' } }; - } - - // Case 4: If it's any kind 21 event with a reference tag, show pin (for debugging) - if (event.kind === 21 && hasReferenceTag) { - return { showPin: true, referenceTag: hasReferenceTag }; - } - - return { showPin: false, referenceTag: null }; -}; - -// Function to handle pin click -const handlePinClick = (referenceTag: { type: string; value: string; relays?: string[] }) => { - console.log("Pin clicked with referenceTag:", referenceTag); - - if (referenceTag.type === 'u') { - // Open URL in new tab - console.log("Opening URL:", referenceTag.value); - window.open(referenceTag.value, '_blank'); - } else if (referenceTag.type === 'e') { - // For event references, use nevent encoding to go to note page - console.log("Opening event:", referenceTag.value); - const nevent = nip19.neventEncode({ - id: referenceTag.value, - relays: referenceTag.relays || [] - }); - console.log("Navigating to:", `/note/${nevent}`); - window.location.href = `/note/${nevent}`; - } else if (referenceTag.type === 'a') { - // For address references, check the kind and route appropriately - console.log("Opening address:", referenceTag.value); - const parts = referenceTag.value.split(':'); - if (parts.length >= 3) { - const kind = parseInt(parts[2]); - const pubkey = parts[1]; - - if (kind === 0) { - // Kind 0 (profile) - go to profile page using npub - const npub = nip19.npubEncode(pubkey); - console.log("Navigating to profile:", `/profile/${npub}`); - window.location.href = `/profile/${npub}`; - } else { - // All other kinds - go to njump (since we don't have the event ID for nevent) - console.log("Opening in njump:", `https://njump.me/${referenceTag.value}`); - window.open(`https://njump.me/${referenceTag.value}`, '_blank'); - } - } else { - // Fallback to njump for malformed addresses - console.log("Fallback to njump:", `https://njump.me/${referenceTag.value}`); - window.open(`https://njump.me/${referenceTag.value}`, '_blank'); - } - } else { - console.log("Unknown reference tag type:", referenceTag.type); - } -}; - -// Component for the purple pin icon -const PinButton: React.FC<{ - referenceTag: { type: string; value: string; relays?: string[] }; - onPinClick?: (referenceTag: { type: string; value: string; relays?: string[] }) => void; -}> = ({ referenceTag, onPinClick }) => { - return ( - <button - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - if (onPinClick) { - onPinClick(referenceTag); - } else { - handlePinClick(referenceTag); - } - }} - className="absolute top-3 right-3 z-10 bg-purple-600 hover:bg-purple-700 text-white rounded-full p-1.5 shadow-lg transition-colors duration-200" - title="Open source" - > - <PinIcon className="w-3 h-3" /> - </button> - ); -}; - -interface ProfileFeedProps { - pubkey: string; -} - -const ProfileFeed: React.FC<ProfileFeedProps> = ({ pubkey }) => { - const now = useRef(new Date()); - const [limit, setLimit] = useState(10); - - // Get user's own posts (kinds 20, 21, 22) - const { events: userEvents, isLoading: userEventsLoading } = useNostrEvents({ - filter: { - authors: [pubkey], - kinds: [1, 20, 21, 22, 1111], - limit: limit, - }, - }); - - - - // Get events that the user has "pinned" by responding with #gallery - const { events: galleryReplies } = useNostrEvents({ - filter: { - authors: [pubkey], - "#t": ["gallery"], - kinds: [1], // replies are typically kind 1 - limit: 100, - }, - }); - - // Extract the event IDs that the user has pinned with #gallery - const pinnedEventIds = galleryReplies - .map(event => event.tags.find(tag => tag[0] === 'e')?.[1]) - .filter(id => id) as string[]; - - // Get the actual events that the user has pinned - const { events: pinnedEvents, isLoading: pinnedEventsLoading } = useNostrEvents({ - filter: { - ids: pinnedEventIds, - limit: 100, - }, - }); - - // Filter user's own events that contain "#gallery" in content or have "gallery" t-tag - const userGalleryEvents = userEvents.filter(event => { - // Check for "#gallery" in content - if (event.content.includes('#gallery')) { - return true; - } - - // Check for "gallery" t-tag - const hasGalleryTag = event.tags.some((tag: string[]) => - tag[0] === 't' && tag[1] === 'gallery' - ); - - return hasGalleryTag; - }); - - // Combine all events: user's media posts + pinned events (the actual media events) + user's gallery events - const allEvents = [...userEvents, ...pinnedEvents, ...userGalleryEvents]; - const uniqueEvents = allEvents.filter((event, index, self) => - index === self.findIndex(e => e.id === event.id) - ); - - - - const isLoading = userEventsLoading || pinnedEventsLoading; - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 10); - }; - - // Helper function to check if an event contains video content - const hasVideoContent = (event: any): boolean => { - // Check for video URLs in content - const videoMatch = event.content.match(/https?:\/\/[^ ]*\.(mp4|webm|mov|avi|mkv)/g); - if (videoMatch && videoMatch.length > 0) return true; - - // Check for video URLs in imeta tags - const videoUrl = getVideoUrl(event.tags); - if (videoUrl) return true; - - // Debug logging for the specific event - if (event.id === "aefcd52baa6f63684d7304c94228dbff886378ef6ba1d26febd5110b041e6996") { - console.log("hasVideoContent check:", { - eventId: event.id, - videoMatch, - videoUrl, - result: videoMatch && videoMatch.length > 0 || !!videoUrl - }); - } - - return false; - }; - - // Helper function to check if an event contains audio content - const hasAudioContent = (event: any): boolean => { - // Check for audio URLs in content - const audioMatch = event.content.match(/https?:\/\/[^ ]*\.(mp3|wav|ogg|flac|aac|m4a)/g); - if (audioMatch && audioMatch.length > 0) return true; - - // Check for audio URLs in imeta tags - const audioUrl = getAudioUrl(event.tags); - if (audioUrl) return true; - - return false; - }; - - // Helper function to check if an event contains image content - const hasImageContent = (event: any): boolean => { - const imageUrl = getImageUrl(event.tags); - if (imageUrl) return true; - - // Check for image URLs in content - const imageMatch = event.content.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg|webp|bmp|svg)/g); - if (imageMatch && imageMatch.length > 0) return true; - - return false; - }; - - // Helper function to check if an event has any media content - const hasMediaContent = (event: any): boolean => { - return hasVideoContent(event) || hasAudioContent(event) || hasImageContent(event); - }; - - - - // Filter events to only include those with media content or are pinned - const mediaEvents = uniqueEvents.filter(event => { - const isPinned = pinnedEventIds.includes(event.id); - const hasMedia = hasMediaContent(event); - - - - return isPinned || hasMedia; - }); - - - - return ( - <> - <div className="grid grid-cols-1 xl:grid-cols-3 gap-2"> - {mediaEvents.length === 0 && isLoading ? ( - <div className="flex flex-col space-y-3"> - <Skeleton className="h-[125px] rounded-xl" /> - <div className="space-y-2"> - <Skeleton className="h-4 w-[250px]" /> - <Skeleton className="h-4 w-[200px]" /> - </div> - </div> - ) : mediaEvents.length > 0 ? ( - <> - {mediaEvents.map((event) => { - const imageUrl = getImageUrl(event.tags); - const isVideo = event.kind === 21 || event.kind === 22; - const hasVideo = hasVideoContent(event); - const hasAudio = hasAudioContent(event); - const hasImage = hasImageContent(event); - const { showPin, referenceTag } = getPinInfo(event, pubkey, pinnedEventIds); - - - - // Priority: Video > Audio > Image > Pinned (no media) - if (isVideo || hasVideo) { - const videoUrl = getVideoUrl(event.tags); - const thumbnailUrl = getThumbnailUrl(event.tags); - - - - // If video has a thumbnail, use KIND20Card to display the thumbnail - if (thumbnailUrl) { - return ( - <div key={event.id} className="relative"> - {showPin && referenceTag && ( - <PinButton referenceTag={referenceTag} /> - )} - <KIND20Card - pubkey={event.pubkey} - text={event.content} - image={thumbnailUrl} - event={event} - tags={event.tags} - eventId={event.id} - showViewNoteCardButton={true} - videoUrl={videoUrl || undefined} - /> - </div> - ); - } else if (videoUrl) { - // If no thumbnail but video URL exists, use NoteCard to display the video - const contentWithVideo = `${event.content}\n${videoUrl}`; - return ( - <div key={event.id} className="relative"> - {showPin && referenceTag && ( - <PinButton referenceTag={referenceTag} /> - )} - <NoteCard - pubkey={event.pubkey} - text={contentWithVideo} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - onPinClick={handlePinClick} - /> - </div> - ); - } else { - // If no video URL found, try to extract from content - const videoMatch = event.content.match(/https?:\/\/[^ ]*\.(mp4|webm|mov|avi|mkv)/g); - if (videoMatch && videoMatch.length > 0) { - return ( - <div key={event.id} className="relative"> - {showPin && referenceTag && ( - <PinButton - referenceTag={referenceTag} - onPinClick={handlePinClick} - /> - )} - <NoteCard - pubkey={event.pubkey} - text={event.content} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - onPinClick={handlePinClick} - /> - </div> - ); - } - } - } else if (hasAudio) { - // For audio content, use NoteCard - return ( - <div key={event.id} className="relative"> - {showPin && referenceTag && ( - <PinButton - referenceTag={referenceTag} - onPinClick={handlePinClick} - /> - )} - <NoteCard - pubkey={event.pubkey} - text={event.content} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - onPinClick={handlePinClick} - /> - </div> - ); - } else if (hasImage && imageUrl) { - // Use KIND20Card for image content - return ( - <div key={event.id} className="relative"> - {showPin && referenceTag && ( - <PinButton - referenceTag={referenceTag} - onPinClick={handlePinClick} - /> - )} - <KIND20Card - pubkey={event.pubkey} - text={event.content} - image={imageUrl} - event={event} - tags={event.tags} - eventId={event.id} - showViewNoteCardButton={true} - /> - </div> - ); - } else if (pinnedEventIds.includes(event.id)) { - // For pinned events without images/videos, show profile picture - return ( - <div key={event.id} className="relative"> - {showPin && referenceTag && ( - <PinButton - referenceTag={referenceTag} - onPinClick={handlePinClick} - /> - )} - <ProfilePictureCard - pubkey={event.pubkey} - eventId={event.id} - content={event.content} - /> - </div> - ); - } - return null; - })} - </> - ) : ( - <div className="flex flex-col items-center justify-center py-10 text-gray-500"> - <p className="text-lg">No posts found :(</p> - </div> - )} - </div> - {!isLoading && mediaEvents.length > 0 && ( - <div className="flex justify-center p-4"> - <Button className="w-full" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default ProfileFeed; \ No newline at end of file diff --git a/components/ProfileGalleryViewFeed.tsx b/components/ProfileGalleryViewFeed.tsx deleted file mode 100644 index bcc6bd7..0000000 --- a/components/ProfileGalleryViewFeed.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useRef } from "react"; -import { useNostrEvents } from "nostr-react"; -import { Skeleton } from "@/components/ui/skeleton"; -import GalleryCard from "./GalleryCard"; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -// Function to extract image URLs from imeta tags (for video thumbnails) -const getVideoImageUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('image ')) { - return tag[i].substring(6); - } - } - } - } - return null; -}; - -interface ProfileGalleryViewFeedProps { - pubkey: string; -} - -const ProfileGalleryViewFeed: React.FC<ProfileGalleryViewFeedProps> = ({ pubkey }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered - - const { isLoading, events } = useNostrEvents({ - filter: { - authors: [pubkey], - limit: 50, - kinds: [10011, 21, 22], - }, - }); - - console.log('ProfileGalleryViewFeed events:', events.map(e => ({ id: e.id, kind: e.kind, tags: e.tags }))); - - const imagesAndIds = events.map((event) => { - if (event.kind === 10011) { - // Handle gallery events (kind 10011) - return { - id: event.tags.filter((tag) => tag[0] === 'G').map((tag) => tag[1]), - images: event.tags.filter((tag) => tag[0] === 'G').map((tag) => tag[2]), - isVideo: false - }; - } else if (event.kind === 21 || event.kind === 22) { - // Handle video events (kind 21/22) - const videoUrl = getVideoUrl(event.tags); - const imageUrl = getVideoImageUrl(event.tags); - if (videoUrl) { - // If no image URL is provided in imeta, we need to handle this differently - // For now, we'll pass the video URL but mark it as a video - const isVideo = !imageUrl; // Mark as video if no image URL provided - console.log('Processing kind 21/22 event:', { - eventId: event.id, - videoUrl, - imageUrl, - isVideo, - tags: event.tags - }); - return { - id: [event.id], - images: [videoUrl], - isVideo: isVideo - }; - } - } - return null; - }).filter((item): item is { id: string[]; images: string[]; isVideo: boolean } => item !== null); - - return ( - <> - <div className="grid grid-cols-3 gap-2"> - {imagesAndIds.length === 0 && isLoading ? ( - <> - <div> - <Skeleton className="h-[125px] rounded-xl" /> - </div> - <div> - <Skeleton className="h-[125px] rounded-xl" /> - </div> - <div> - <Skeleton className="h-[125px] rounded-xl" /> - </div> - </> - ) : ( - imagesAndIds.map((galleryEntry) => ( - galleryEntry.images.map((imageUrl, index) => ( - <GalleryCard - pubkey={pubkey} - key={`${galleryEntry.id[index]}-${index}`} - eventId={galleryEntry.id[index]} - imageUrl={imageUrl} - linkToNote={true} - isVideo={galleryEntry.isVideo} - /> - )) - )) - )} - </div> - </> - ); -} - -export default ProfileGalleryViewFeed; \ No newline at end of file diff --git a/components/ProfileInfoCard.tsx b/components/ProfileInfoCard.tsx deleted file mode 100644 index 4177b35..0000000 --- a/components/ProfileInfoCard.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import React, { useMemo } from 'react'; -import { useProfile, useNostrEvents } from "nostr-react"; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; -import { AvatarImage } from '@radix-ui/react-avatar'; -import { Avatar } from '@/components/ui/avatar'; -import NIP05 from '@/components/nip05'; -import { nip19, type Event as NostrEvent } from "nostr-tools"; -import Link from 'next/link'; -import { Button } from './ui/button'; -import { ImStatsDots } from "react-icons/im"; -import FollowButton from './FollowButton'; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { Input } from './ui/input'; -import { Share1Icon, LightningBoltIcon, GlobeIcon } from '@radix-ui/react-icons'; -import { toast } from './ui/use-toast'; -import { Globe, UserCheck } from 'lucide-react'; -import { Badge } from './ui/badge'; -import { MusicIcon, ActivityIcon } from 'lucide-react'; - -// NIP-38 Status types -const STATUS_TYPES = { - GENERAL: 'general', - MUSIC: 'music' -}; - -interface ProfileInfoCardProps { - pubkey: string; -} - -interface StatusMap { - [key: string]: NostrEvent; -} - -const ProfileInfoCard: React.FC<ProfileInfoCardProps> = React.memo(({ pubkey }) => { - - let userPubkey = ''; - let host = ''; - if (typeof window !== 'undefined') { - userPubkey = window.localStorage.getItem('pubkey') ?? ''; - host = window.location.host; - } - - const { data: userData, isLoading } = useProfile({ pubkey }); - - // Fetch user status events (NIP-38) - const { events: statusEvents } = useNostrEvents({ - filter: { - authors: [pubkey], - kinds: [30315], // NIP-38 user status event kind - limit: 1, - }, - }); - - // Fetch follow list (NIP-02) to check if profile owner follows the logged-in user - const { events: followListEvents } = useNostrEvents({ - filter: { - authors: [pubkey], - kinds: [3], // NIP-02 follow list event kind - limit: 1, - }, - enabled: !!userPubkey, // Only fetch if a user is logged in - }); - - // Check if the profile owner follows the logged-in user - const isFollowingUser = useMemo(() => { - if (!userPubkey || followListEvents.length === 0) return false; - - const followList = followListEvents[0]; - if (!followList) return false; - - // Look for a 'p' tag with the logged-in user's pubkey - return followList.tags.some(tag => tag[0] === 'p' && tag[1] === userPubkey); - }, [followListEvents, userPubkey]); - - // Get the latest status events by type - const userStatuses = useMemo(() => { - const statuses: StatusMap = {}; - - // Process status events - for (const event of statusEvents) { - const dTag = event.tags.find(tag => tag[0] === 'd'); - if (!dTag || !dTag[1]) continue; - - const statusType = dTag[1]; - - // Check if event has expiration - const expirationTag = event.tags.find(tag => tag[0] === 'expiration'); - if (expirationTag && expirationTag[1]) { - const expirationTime = parseInt(expirationTag[1]); - const now = Math.floor(Date.now() / 1000); - - // Skip expired statuses - if (expirationTime < now) continue; - } - - // Set/update status (most recent one for the type) - if (!statuses[statusType] || statuses[statusType].created_at < event.created_at) { - statuses[statusType] = event; - } - } - - return statuses; - }, [statusEvents]); - - const npubShortened = useMemo(() => { - let encoded = nip19.npubEncode(pubkey); - let parts = encoded.split('npub'); - return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); - }, [pubkey]); - - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - const description = userData?.about?.replace(/(?:\r\n|\r|\n)/g, '<br>'); - const nip05 = userData?.nip05; - const lightningAddress = userData?.lud16; - const website = userData?.website; - - const handleCopyLink = async () => { - try { - await navigator.clipboard.writeText(host+"/profile/"+nip19.npubEncode(pubkey)); - toast({ - description: 'URL copied to clipboard', - title: 'Copied' - }); - } catch (err) { - toast({ - description: 'Error copying URL to clipboard', - title: 'Error', - variant: 'destructive' - }); - } - }; - - const handleCopyPublicKey = async () => { - try { - await navigator.clipboard.writeText(nip19.npubEncode(pubkey)); - toast({ - description: 'PublicKey copied to clipboard', - title: 'Copied' - }); - } catch (err) { - toast({ - description: 'Error copying PublicKey to clipboard', - title: 'Error', - variant: 'destructive' - }); - } - }; - - const handleCopyLightningAddress = async () => { - if (!lightningAddress) return; - - try { - await navigator.clipboard.writeText(lightningAddress); - toast({ - description: 'Lightning Address copied to clipboard', - title: 'Copied' - }); - } catch (err) { - toast({ - description: 'Error copying Lightning Address to clipboard', - title: 'Error', - variant: 'destructive' - }); - } - }; - - const handleOpenWebsite = () => { - if (!website) return; - - // Add https:// prefix if not present - let url = website; - if (!/^https?:\/\//i.test(url)) { - url = 'https://' + url; - } - - window.open(url, '_blank'); - }; - - // Get reference URL from status event - const getStatusReference = (event: NostrEvent): string | null => { - const refTag = event.tags.find(tag => tag[0] === 'r'); - return refTag ? refTag[1] : null; - }; - - // Render user status component - const renderUserStatus = () => { - const generalStatus = userStatuses[STATUS_TYPES.GENERAL]; - const musicStatus = userStatuses[STATUS_TYPES.MUSIC]; - - if (!generalStatus && !musicStatus) return null; - - return ( - <div className="flex flex-col gap-2 my-3"> - {generalStatus && ( - <Badge - variant="outline" - className="flex items-center gap-2 px-3 text-sm font-normal bg-primary/5 hover:bg-primary/10 transition-colors" - > - <ActivityIcon size={16} className="text-primary" /> - <span>{generalStatus.content}</span> - {getStatusReference(generalStatus) && ( - <Link href={getStatusReference(generalStatus) as string} target="_blank" className="text-primary hover:underline ml-1 text-xs"> - Link - </Link> - )} - </Badge> - )} - - {musicStatus && ( - <Badge - variant="outline" - className="flex items-center gap-2 px-3 text-sm font-normal bg-primary/5 hover:bg-primary/10 transition-colors" - > - <MusicIcon size={16} className="text-primary" /> - <span className="italic">{musicStatus.content}</span> - {getStatusReference(musicStatus) && ( - <Link href={getStatusReference(musicStatus) as string} target="_blank" className="text-primary hover:underline ml-1 text-xs"> - Listen - </Link> - )} - </Badge> - )} - </div> - ); - }; - - return ( - <div className='py-2'> - <Card> - <CardHeader> - <div className="flex items-center gap-6"> - <div className="relative"> - <Avatar className={`h-24 w-24 ${userPubkey && userPubkey !== pubkey && isFollowingUser ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' : ''}`}> - <AvatarImage className="object-cover w-full h-full" src={userData?.picture} alt={title} /> - </Avatar> - {userPubkey && userPubkey !== pubkey && isFollowingUser && ( - <div className="absolute -top-1 -right-1 bg-primary text-primary-foreground rounded-full p-1" title="Follows you"> - <UserCheck className="h-4 w-4" aria-label='Follows you' /> - </div> - )} - </div> - <div className="flex flex-col gap-1.5"> - <Link href={`/profile/${nip19.npubEncode(pubkey)}`}> - <div className="text-2xl">{title}</div> - </Link> - <div className="text-sm text-muted-foreground"> - <NIP05 nip05={nip05?.toString() ?? ''} pubkey={pubkey} /> - </div> - {lightningAddress && ( - <div className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-purple-400 transition-colors" onClick={handleCopyLightningAddress}> - <LightningBoltIcon className="h-4 w-4 text-yellow-500" /> - <span>{lightningAddress}</span> - </div> - )} - {website && ( - <div className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-purple-400 transition-colors" onClick={handleOpenWebsite}> - <Globe className="h-4 w-4 text-purple-500" /> - <span>{website}</span> - </div> - )} - {renderUserStatus()} - </div> - </div> - <div> - <div className='py-6 grid grid-cols-5 gap-4'> - <div className='col-span-2'> - <FollowButton pubkey={pubkey} userPubkey={userPubkey}></FollowButton> - </div> - <Link className='col-span-2' href={`/dashboard/${nip19.npubEncode(pubkey)}`}> - <Button className='w-full' variant="outline">View Statistics</Button> - </Link> - <Drawer> - <DrawerTrigger asChild> - <Button className='w-full' variant="outline"><Share1Icon /></Button> - </DrawerTrigger> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Share this Profile</DrawerTitle> - <DrawerDescription>Share this Profile with others.</DrawerDescription> - </DrawerHeader> - <div className="px-2"> - <div className="flex items-center mb-4"> - <Input value={host+"/profile/"+nip19.npubEncode(pubkey)} disabled className="mr-2" /> - <Button variant="outline" onClick={handleCopyLink}>Copy Link</Button> - </div> - <div className="flex items-center mb-4"> - <Input value={nip19.npubEncode(pubkey)} disabled className="mr-2" /> - <Button variant="outline" onClick={handleCopyPublicKey}>Copy PublicKey</Button> - </div> - </div> - <DrawerFooter> - <DrawerClose asChild> - <div> - <Button variant="outline">Close</Button> - </div> - </DrawerClose> - </DrawerFooter> - </DrawerContent> - </Drawer> - </div> - <hr /> - </div> - </CardHeader> - <CardContent> - <div className='break-words' dangerouslySetInnerHTML={{ __html: description ?? '' }} /> - </CardContent> - </Card> - </div> - ); -}); - -ProfileInfoCard.displayName = 'ProfileInfoCard'; - -export default ProfileInfoCard; \ No newline at end of file diff --git a/components/ProfileQuickViewFeed.tsx b/components/ProfileQuickViewFeed.tsx deleted file mode 100644 index 72ef6a7..0000000 --- a/components/ProfileQuickViewFeed.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents, useProfile } from "nostr-react"; -import { nip19 } from "nostr-tools"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; -import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard"; -import { getImageUrl, getThumbnailUrl } from "@/utils/utils"; -import Link from "next/link"; -import { Play } from "lucide-react"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; - -// Component to display profile picture with optional play button -const ProfilePictureCard: React.FC<{ - pubkey: string; - eventId: string; - showPlayButton: boolean; -}> = ({ pubkey, eventId, showPlayButton }) => { - const { data: userData } = useProfile({ - pubkey, - }); - - const profileImageSrc = userData?.picture || `https://robohash.org/${pubkey}`; - - return ( - <div className="relative aspect-square w-full bg-gray-800 rounded-xl flex items-center justify-center overflow-hidden"> - <Avatar className="w-full h-full rounded-xl"> - <AvatarImage - src={profileImageSrc} - className="w-full h-full object-cover" - /> - </Avatar> - {showPlayButton && ( - <div className="absolute inset-0 flex items-center justify-center"> - <div className="bg-white bg-opacity-80 rounded-full p-3"> - <Play className="w-8 h-8 text-black" /> - </div> - </div> - )} - <Link - href={`/note/${nip19.neventEncode({ - id: eventId, - relays: [] - })}`} - className="absolute inset-0" - /> - </div> - ); -}; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -interface ProfileQuickViewFeedProps { - pubkey: string; -} - -const ProfileQuickViewFeed: React.FC<ProfileQuickViewFeedProps> = ({ pubkey }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered - const [limit, setLimit] = useState(20); - - const { isLoading, events } = useNostrEvents({ - filter: { - authors: [pubkey], - limit: limit, - kinds: [20, 21, 22], - }, - }); - - const loadMore = () => { - setLimit(limit => limit + 50); - } - - return ( - <> - <div className="grid grid-cols-3 gap-2"> - {events.length === 0 && isLoading ? ( - <> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - </> - ) : events.length > 0 ? ( - <> - {events.map((event) => { - const imageUrl = getImageUrl(event.tags); - const isVideo = event.kind === 21 || event.kind === 22; - const videoUrl = isVideo ? getVideoUrl(event.tags) : null; - const thumbnailUrl = isVideo ? getThumbnailUrl(event.tags) : null; - - // Use QuickViewKind20NoteCard for images and videos with thumbnails - if (imageUrl || (isVideo && thumbnailUrl)) { - return ( - <QuickViewKind20NoteCard - key={event.id} - pubkey={event.pubkey} - text={event.content} - image={imageUrl || thumbnailUrl || ""} // Prefer thumbnail for videos - event={event} - tags={event.tags} - eventId={event.id} - linkToNote={true} - /> - ); - } - - // For videos without thumbnails, show profile picture with play button - if (isVideo && videoUrl) { - return ( - <ProfilePictureCard - key={event.id} - pubkey={event.pubkey} - eventId={event.id} - showPlayButton={true} - /> - ); - } - - // Fallback for text-only content - show profile picture - return ( - <ProfilePictureCard - key={event.id} - pubkey={event.pubkey} - eventId={event.id} - showPlayButton={false} - /> - ); - })} - </> - ) : ( - <div className="col-span-3 flex flex-col items-center justify-center py-10 text-gray-500"> - <p className="text-lg">No posts found :(</p> - </div> - )} - </div> - {!isLoading && events.length > 0 ? ( - <div className="flex justify-center p-4"> - <Button className="w-full" onClick={loadMore}>Load More</Button> - </div> - ) : null} - </> - ); -} - -export default ProfileQuickViewFeed; \ No newline at end of file diff --git a/components/ProfileTextFeed.tsx b/components/ProfileTextFeed.tsx deleted file mode 100644 index 826ed60..0000000 --- a/components/ProfileTextFeed.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; -import NoteCard from '@/components/NoteCard'; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; - -interface ProfileTextFeedProps { - pubkey: string; -} - -const ProfileTextFeed: React.FC<ProfileTextFeedProps> = ({ pubkey }) => { - const now = useRef(new Date()); - const [limit, setLimit] = useState(100); - - const { events, isLoading } = useNostrEvents({ - filter: { - authors: [pubkey], - kinds: [1], - limit: limit, - }, - }); - - // filter out all images since we only want text messages - // let filteredEvents = events.filter((event) => !event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g)?.[0]); - // filter out all replies (tag[0] == e) - let filteredEvents = events.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 10); - }; - - return ( - <> - <div className="grid grid-cols-1 xl:grid-cols-3 gap-2"> - {filteredEvents.length === 0 && isLoading ? ( - <div className="flex flex-col space-y-3"> - <Skeleton className="h-[125px] rounded-xl" /> - <div className="space-y-2"> - <Skeleton className="h-4 w-[250px]" /> - <Skeleton className="h-4 w-[200px]" /> - </div> - </div> - ) : ( - <> - {filteredEvents.map((event) => ( - <div key={event.id} className="py-6"> - <NoteCard - key={event.id} - pubkey={event.pubkey} - text={event.content} - event={event} - tags={event.tags} - eventId={event.id} - showViewNoteCardButton={true} - /> - </div> - ))} - </> - )} - </div> - {!isLoading && filteredEvents.length > 0 && ( - <div className="flex justify-center p-4"> - <Button className="w-full" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default ProfileTextFeed; \ No newline at end of file diff --git a/components/QuickViewKind20NoteCard.tsx b/components/QuickViewKind20NoteCard.tsx deleted file mode 100644 index 84b2480..0000000 --- a/components/QuickViewKind20NoteCard.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import React, { useState } from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - SmallCardContent, -} from "@/components/ui/card" -import Link from 'next/link'; -import Image from 'next/image'; -import { Button } from '@/components/ui/button'; -import { Eye, Play, PlayCircle } from 'lucide-react'; -import { extractDimensions, getProxiedImageUrl, hasNsfwContent, getThumbnailUrl } from '@/utils/utils'; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4).trim(); - } - } - } - } - return null; -}; - -interface QuickViewKind20NoteCardProps { - pubkey: string; - text: string; - image: string; - eventId: string; - tags: string[][]; - event: any; - linkToNote: boolean; -} - -const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubkey, text, image, eventId, tags, event, linkToNote }) => { - const {data, isLoading} = useProfile({ - pubkey, - }); - const [imageError, setImageError] = useState(false); - const [tryWithoutProxy, setTryWithoutProxy] = useState(false); - const [showSensitiveContent, setShowSensitiveContent] = useState(false); - - // Check if the event has nsfw content - const isNsfwContent = hasNsfwContent(tags); - - // Check if this is a video - const isVideo = event.kind === 21 || event.kind === 22; - const videoUrl = isVideo ? getVideoUrl(tags) : null; - const thumbnailUrl = isVideo ? getThumbnailUrl(tags) : null; - - // If no image is provided but we have a video URL, use the video URL as the image - // For videos, prefer thumbnail URL if available, otherwise use video URL - const displayImage = image || (isVideo && thumbnailUrl ? thumbnailUrl : videoUrl); - - // For video events, we don't need to check if the image starts with http - // since video URLs are valid for display purposes - if (!displayImage) return null; - if (imageError && tryWithoutProxy && !isVideo) return null; - - const useImgProxy = process.env.NEXT_PUBLIC_ENABLE_IMGPROXY === "true" && !tryWithoutProxy; - - const processedImage = useImgProxy ? getProxiedImageUrl(displayImage, 500, 0) : displayImage; - - // For video events, don't process the text content - const displayText = isVideo ? "" : text.replaceAll('\n', ' '); - - // Create nevent with relay hints - const nevent = nip19.neventEncode({ - id: event.id, - relays: event.relays || [] - }) - - const { width, height } = extractDimensions(event); - - // Toggle sensitive content visibility - const toggleSensitiveContent = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setShowSensitiveContent(true); - }; - - const card = ( - <Card className="aspect-square overflow-hidden"> - <SmallCardContent className="h-full p-0"> - <div className="h-full w-full"> - <div className='relative w-full h-full'> - {imageError && tryWithoutProxy && !isVideo ? ( - // Fallback for failed images - show a placeholder with play button for videos - <div className="w-full h-full bg-gray-800 rounded-lg flex items-center justify-center"> - {isVideo ? ( - <div className="bg-white bg-opacity-80 rounded-full p-3"> - <Play className="h-6 w-6 text-black" /> - </div> - ) : ( - <div className="text-center text-gray-400 p-4"> - <p className="text-sm">Image unavailable</p> - </div> - )} - </div> - ) : ( - <> - {isVideo ? ( - // For videos, use the video element to show the first frame - <video - src={videoUrl || ''} - className={`w-full h-full rounded-lg object-cover ${isNsfwContent && !showSensitiveContent ? 'blur-xl' : ''}`} - preload="metadata" - playsInline - muted - // If we have a thumbnail, use it as poster - poster={thumbnailUrl ? processedImage : undefined} - onError={() => { - if (tryWithoutProxy) { - setImageError(true); - } else { - setTryWithoutProxy(true); - } - }} - style={{ objectPosition: 'center' }} - /> - ) : ( - // For images, show the actual image - <img - src={processedImage} - alt={displayText} - className={`w-full h-full rounded-lg object-cover ${isNsfwContent && !showSensitiveContent ? 'blur-xl' : ''}`} - loading="lazy" - onError={() => { - if (tryWithoutProxy) { - setImageError(true); - } else { - setTryWithoutProxy(true); - } - }} - style={{ objectPosition: 'center' }} - /> - )} - {isVideo && ( - // Play button overlay for videos - <div className="absolute inset-0 flex items-center justify-center"> - <div className="bg-black bg-opacity-70 rounded-full p-3"> - <Play className="h-6 w-6 text-white" /> - </div> - </div> - )} - {isNsfwContent && !showSensitiveContent && !isVideo && ( - <div - className="absolute inset-0 flex flex-col items-center justify-center" - onClick={toggleSensitiveContent} - > - <Button - variant="secondary" - className="bg-black bg-opacity-50 hover:bg-opacity-70 text-white px-2 py-1 sm:px-4 sm:py-2 rounded-md text-xs sm:text-sm" - onClick={toggleSensitiveContent} - > - <Eye className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" /> Show - </Button> - <p className="mt-1 sm:mt-2 text-white text-xs sm:text-sm bg-black bg-opacity-50 p-1 sm:p-2 rounded max-w-[80%] text-center"> - Sensitive Content - </p> - </div> - )} - </> - )} - </div> - </div> - </SmallCardContent> - </Card> - ); - - return ( - <> - {linkToNote ? ( - <Link - href={`/note/${nevent}`} - className="block w-full aspect-square" - onClick={isNsfwContent && !showSensitiveContent ? (e) => e.preventDefault() : undefined} - > - {card} - </Link> - ) : ( - card - )} - </> - ); -} - -export default QuickViewKind20NoteCard; \ No newline at end of file diff --git a/components/QuickViewNoteCard.tsx b/components/QuickViewNoteCard.tsx deleted file mode 100644 index af72ea2..0000000 --- a/components/QuickViewNoteCard.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useState } from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - SmallCardContent, -} from "@/components/ui/card" -import Image from 'next/image'; -import Link from 'next/link'; -import { PlayIcon, StackIcon, VideoIcon } from '@radix-ui/react-icons'; - -interface NoteCardProps { - pubkey: string; - text: string; - eventId: string; - tags: string[][]; - event: any; - linkToNote: boolean; -} - -const QuickViewNoteCard: React.FC<NoteCardProps> = ({ pubkey, text, eventId, tags, event, linkToNote }) => { - const { data: userData } = useProfile({ - pubkey, - }); - const [imageError, setImageError] = useState(false); - - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); - text = text.replaceAll('\n', ' '); - const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g); - const videoSrc = text.match(/https?:\/\/[^ ]*\.(mp4|webm|mov)/g); - const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov|jpeg)/g, ''); - const createdAt = new Date(event.created_at * 1000); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - // Create nevent with relay hints - const nevent = nip19.neventEncode({ - id: event.id, - relays: event.relays || [] - }) - - const card = ( - <Card> - <SmallCardContent> - <div> - <div className='d-flex justify-content-center align-items-center'> - {imageSrc && imageSrc.length > 1 && !videoSrc ? ( - <div style={{ position: 'relative' }}> - <div className="absolute top-2 right-2 w-7 h-7 lg:w-12 lg:h-12 bg-black bg-opacity-40 rounded-lg flex items-center justify-center"> - <StackIcon className='absolute w-7 h-7 lg:w-12 lg:h-12'/> - </div> - <img src={imageSrc[0]} - className='rounded lg:rounded-lg w-full h-auto object-cover' - style={{ maxHeight: '75vh', margin: 'auto' }} - alt={text} - loading="lazy" - onError={() => setImageError(true)} - /> - </div> - ) : imageSrc && imageSrc.length > 0 ? ( - <div style={{ position: 'relative' }}> - {videoSrc && videoSrc.length > 0 && - <div className="absolute top-2 right-2 w-7 h-7 lg:w-12 lg:h-12 bg-black bg-opacity-40 rounded-lg flex items-center justify-center"> - <PlayIcon className='absolute w-7 h-7 lg:w-12 lg:h-12' /> - </div> - } - <img src={imageSrc[0]} - className='rounded lg:rounded-lg w-full h-auto object-cover' - style={{ maxHeight: '75vh', margin: 'auto' }} - alt={text} - loading="lazy" - onError={() => setImageError(true)} - /> - </div> - ) : videoSrc && videoSrc.length > 0 ? ( - <div style={{ position: 'relative' }}> - <div className="absolute top-2 right-2 w-7 h-7 lg:w-12 lg:h-12 bg-black bg-opacity-40 rounded-lg flex items-center justify-center"> - <PlayIcon className='absolute w-7 h-7 lg:w-12 lg:h-12' /> - </div> - <video src={videoSrc[0] + "#t=0.5"} className='rounded lg:rounded-lg' style={{ maxWidth: '100%', maxHeight: '75vh', objectFit: 'contain', margin: 'auto' }} /> - </div> - ) : imageError ? ( - // Fallback for failed images - <div className="w-full h-32 bg-gray-800 rounded-lg flex items-center justify-center"> - <div className="text-center text-gray-400"> - <p className="text-sm">Image unavailable</p> - </div> - </div> - ) : null} - </div> - </div> - </SmallCardContent> - </Card> - ); - - return ( - <> - {linkToNote ? ( - <Link href={`/note/${nevent}`}> - {card} - </Link> - ) : ( - card - )} - </> - ); -} - -export default QuickViewNoteCard; \ No newline at end of file diff --git a/components/ReactionButton.tsx b/components/ReactionButton.tsx deleted file mode 100644 index a4e46b1..0000000 --- a/components/ReactionButton.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState, useEffect, useMemo } from "react"; -import { useNostrEvents } from "nostr-react"; -import { Event as NostrEvent } from "nostr-tools"; -import { Button } from "@/components/ui/button"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { ReloadIcon } from "@radix-ui/react-icons" -import ReactionButtonReactionList from "./ReactionButtonReactionList" -import { signEvent } from "@/utils/utils" -import { publishToOutbox } from "@/utils/publishUtils"; -import { useCurrentUserPubkey } from "@/utils/relayHooks"; - -export default function ReactionButton({ event }: { event: any }) { - const loginType = typeof window !== "undefined" ? window.localStorage.getItem("loginType") : null - const loggedInUserPublicKey = typeof window !== "undefined" ? window.localStorage.getItem("pubkey") : null - const currentUserPubkey = useCurrentUserPubkey() - - const [liked, setLiked] = useState(false) - const [likeIcon, setLikeIcon] = useState("") - - const { events, isLoading } = useNostrEvents({ - filter: { - "#e": [event.id], - kinds: [7], - }, - }) - - const filteredEvents = useMemo(() => { - return events.filter((e) => { - return e.tags.filter((tag) => tag[0] === "e" && tag[1] !== event.id).length === 0 - }) - }, [events, event.id]) - - const reactionCount = filteredEvents.length - - useEffect(() => { - const userReaction = filteredEvents.find((e) => e.pubkey === loggedInUserPublicKey) - if (userReaction) { - setLiked(true) - setLikeIcon(userReaction.content) - } else { - setLiked(false) - setLikeIcon("") - } - }, [filteredEvents, loggedInUserPublicKey]) - - const onPost = async (icon: string) => { - const message = icon || "+" - - const likeEvent: NostrEvent = { - content: message, - kind: 7, - tags: [], - created_at: Math.floor(Date.now() / 1000), - pubkey: "", - id: "", - sig: "", - } - - likeEvent.tags.push(["e", event.id]) - likeEvent.tags.push(["p", event.pubkey]) - likeEvent.tags.push(["k", event.kind.toString()]) - - const signedEvent = await signEvent(loginType, likeEvent) - - if (signedEvent) { - await publishToOutbox(signedEvent, currentUserPubkey || undefined) - setLiked(true) - setLikeIcon(message) - filteredEvents.push(signedEvent) - } else { - console.error("Failed to sign event") - alert("Failed to sign event") - } - } - - return ( - <Drawer> - <DrawerTrigger asChild> - <Button variant={liked ? "default" : "outline"}> - {isLoading ? ( - <> - <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> 💜 - </> - ) : ( - <> - {reactionCount} {liked ? likeIcon : "💜"} - </> - )} - </Button> - </DrawerTrigger> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Reactions</DrawerTitle> - </DrawerHeader> - <div className="px-4 grid grid-cols-3"> - <Button - variant={liked && likeIcon === "💜" ? "secondary" : "outline"} - className={`mx-1`} - onClick={() => onPost("💜")} - > - 💜 - </Button> - <Button - variant={liked && likeIcon === "👍" ? "secondary" : "outline"} - className={`mx-1`} - onClick={() => onPost("👍")} - > - 👍 - </Button> - <Button - variant={liked && likeIcon === "👎" ? "secondary" : "outline"} - className={`mx-1`} - onClick={() => onPost("👎")} - > - 👎 - </Button> - </div> - <hr className="my-4" /> - <ReactionButtonReactionList filteredEvents={filteredEvents} /> - <DrawerFooter> - <DrawerClose asChild> - <div> - <Button variant={"secondary"}>Close</Button> - </div> - </DrawerClose> - </DrawerFooter> - </DrawerContent> - </Drawer> - ) -} \ No newline at end of file diff --git a/components/ReactionButtonReactionList.tsx b/components/ReactionButtonReactionList.tsx deleted file mode 100644 index 13d0a7f..0000000 --- a/components/ReactionButtonReactionList.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ScrollArea } from "@/components/ui/scroll-area" -import ReactionButtonReactionListItem from "./ReactionButtonReactionListItem"; - -export default function ReactionButtonReactionList({ filteredEvents }: { filteredEvents: any }) { - return ( - <ScrollArea className="px-4 h-[50vh]"> - {filteredEvents.map((event: any) => ( - <ReactionButtonReactionListItem key={event.id} event={event} /> - ))} - </ScrollArea> - ); -} \ No newline at end of file diff --git a/components/ReactionButtonReactionListItem.tsx b/components/ReactionButtonReactionListItem.tsx deleted file mode 100644 index 6dff4cc..0000000 --- a/components/ReactionButtonReactionListItem.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import Link from "next/link"; -import { useNostr, dateToUnix, useNostrEvents, useProfile } from "nostr-react"; - -import { - type Event as NostrEvent, - getEventHash, - getPublicKey, - finalizeEvent, - nip19, -} from "nostr-tools"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; - -export default function ReactionButtonReactionListItem({ event }: { event: NostrEvent }) { - - let pubkey = event.pubkey; - - const { data: userData } = useProfile({ - pubkey, - }); - - const title = userData?.username || userData?.display_name || userData?.name || nip19.npubEncode(pubkey).slice(0, 8) + ':' + nip19.npubEncode(pubkey).slice(-3);; - const createdAt = new Date(event.created_at * 1000); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - const content = event.content; - - console.log("event", event.content); - - return ( - <Link href={hrefProfile}> - <div key={event.id} className="flex items-center space-x-2"> - <div className="flex items-center space-x-2 p-1"> - {/* <img src={profileImageSrc} className="w-8 h-8 rounded-full" /> */} - <Avatar> - <AvatarImage src={profileImageSrc} alt={title} /> - </Avatar> - <span>{title}</span> - <span className="pl-2">{content}</span> - </div> - </div> - </Link> - ); -} \ No newline at end of file diff --git a/components/ReelFeed.tsx b/components/ReelFeed.tsx deleted file mode 100644 index 8dcec14..0000000 --- a/components/ReelFeed.tsx +++ /dev/null @@ -1,769 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useNostrEvents, useNostr, dateToUnix } from "nostr-react"; -import { ChevronUp, ChevronDown, Heart, MessageCircle, Share2, User, X, Copy, Check, Volume2, VolumeX } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { nip19, Event as NostrEvent } from "nostr-tools"; -import { useProfile } from "nostr-react"; -import Link from "next/link"; -import { blacklistPubkeys, signEvent } from "@/utils/utils"; -import { toast } from "@/components/ui/use-toast"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; - -// Simple video event interface -interface VideoEvent { - id: string; - pubkey: string; - created_at: number; - title: string; - description: string; - videoUrl: string; - imageUrl: string; -} - -const ReelFeed: React.FC = () => { - const [currentVideoIndex, setCurrentVideoIndex] = useState(0); - const [isLiked, setIsLiked] = useState<Record<string, boolean>>({}); - const videoRefs = useRef<Record<string, HTMLVideoElement | null>>({}); - const [touchStart, setTouchStart] = useState<number | null>(null); - const [touchEnd, setTouchEnd] = useState<number | null>(null); - const [videoEvents, setVideoEvents] = useState<VideoEvent[]>([]); - const [commentModalOpen, setCommentModalOpen] = useState(false); - const [shareModalOpen, setShareModalOpen] = useState(false); - const [selectedVideo, setSelectedVideo] = useState<VideoEvent | null>(null); - const [commentText, setCommentText] = useState(""); - const [copiedText, setCopiedText] = useState<string | null>(null); - const [isAudioMuted, setIsAudioMuted] = useState(true); - const [volume, setVolume] = useState(0.5); - - const { publish, connectedRelays } = useNostr(); - - // Get current user pubkey - const [currentUserPubkey, setCurrentUserPubkey] = useState<string | null>(null); - const [isClient, setIsClient] = useState(false); - - useEffect(() => { - setIsClient(true); - const pubkey = localStorage.getItem('pubkey'); - setCurrentUserPubkey(pubkey); - }, []); - - // Convert npub to hex if needed - const currentUserHexPubkey = currentUserPubkey?.startsWith('npub') - ? nip19.decode(currentUserPubkey).data as string - : currentUserPubkey; - - // Fetch user's follow list - const { events: followListEvents } = useNostrEvents({ - filter: { - kinds: [3], - authors: currentUserHexPubkey ? [currentUserHexPubkey] : [], - limit: 1, - }, - enabled: !!currentUserHexPubkey, - }); - - // Extract followed pubkeys - const followedPubkeys = followListEvents[0]?.tags - .filter(tag => tag[0] === 'p') - .map(tag => tag[1]) || []; - - // Fetch videos from follows - const { events: followVideos } = useNostrEvents({ - filter: { - kinds: [21, 22], - authors: followedPubkeys, - limit: 5, - }, - enabled: followedPubkeys.length > 0, - }); - - // Fetch global videos - const { events: globalVideos } = useNostrEvents({ - filter: { - kinds: [21, 22], - limit: 5, - }, - }); - - // Fetch tagged events with reel-related hashtags - const reelTags = ['reels', 'reel', 'vlogs', 'vlog']; - const { events: taggedEvents } = useNostrEvents({ - filter: { - kinds: [1], - "#t": reelTags, - limit: 5, - }, - }); - - // Fetch replies that contain reel tags - const { events: replyEvents } = useNostrEvents({ - filter: { - kinds: [1], - "#t": reelTags, - limit: 5, - }, - }); - - // Extract event IDs that are being replied to - const repliedToEventIds = replyEvents - .filter(event => event.tags.some(tag => tag[0] === 'e')) - .map(event => event.tags.find(tag => tag[0] === 'e')?.[1]) - .filter(Boolean) as string[]; - - // Fetch the original events that are being replied to - const { events: originalRepliedEvents } = useNostrEvents({ - filter: { - kinds: [1], - ids: repliedToEventIds, - limit: 5, - }, - enabled: repliedToEventIds.length > 0, - }); - - // Helper function to extract video URL from imeta tags - const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; - }; - - // Helper function to extract image URL from imeta tags - const getImageUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('image ')) { - return tag[i].substring(6); - } - } - } - } - return null; - }; - - // Helper function to check if content contains reel hashtags - const hasReelHashtags = (content: string): boolean => { - const lowerContent = content.toLowerCase(); - return reelTags.some(tag => lowerContent.includes(`#${tag}`)); - }; - - // Helper function to check if tags contain reel hashtags - const hasReelTagTags = (tags: string[][]): boolean => { - return tags.some(tag => tag[0] === 't' && reelTags.includes(tag[1].toLowerCase())); - }; - - // Parse video events - useEffect(() => { - const allEvents = [...(followVideos || []), ...(globalVideos || [])]; - const parsedEvents: VideoEvent[] = []; - const seenIds = new Set<string>(); - - allEvents.forEach(event => { - if (blacklistPubkeys.has(event.pubkey)) return; - if (seenIds.has(event.id)) return; // Skip if we've already seen this event - - const videoUrl = getVideoUrl(event.tags); - if (!videoUrl) return; - - const imageUrl = getImageUrl(event.tags) || ''; - const title = event.tags.find(tag => tag[0] === 'title')?.[1] || 'Untitled Video'; - - parsedEvents.push({ - id: event.id, - pubkey: event.pubkey, - created_at: event.created_at, - title, - description: event.content, - videoUrl, - imageUrl, - }); - - seenIds.add(event.id); // Mark this event as seen - }); - - // Add tagged events (kind 1 with reel hashtags) - only if they have actual video content - const taggedVideoEvents: VideoEvent[] = []; - taggedEvents.forEach(event => { - if (blacklistPubkeys.has(event.pubkey)) return; - if (seenIds.has(event.id)) return; - - // Check if event has reel hashtags in content or tags - if (!hasReelHashtags(event.content) && !hasReelTagTags(event.tags)) return; - - // Only include events that have actual video URLs - const videoUrl = getVideoUrl(event.tags); - if (!videoUrl) return; // Skip text-only posts - - const imageUrl = getImageUrl(event.tags) || ''; - const title = event.tags.find(tag => tag[0] === 'title')?.[1] || 'Reel Post'; - - taggedVideoEvents.push({ - id: event.id, - pubkey: event.pubkey, - created_at: event.created_at, - title, - description: event.content, - videoUrl, - imageUrl, - }); - - seenIds.add(event.id); - }); - - // Add original events that are being replied to with reel tags - only if they have actual video content - const repliedVideoEvents: VideoEvent[] = []; - originalRepliedEvents.forEach(event => { - if (blacklistPubkeys.has(event.pubkey)) return; - if (seenIds.has(event.id)) return; - - // Only include events that have actual video URLs - const videoUrl = getVideoUrl(event.tags); - if (!videoUrl) return; // Skip text-only posts - - const imageUrl = getImageUrl(event.tags) || ''; - const title = event.tags.find(tag => tag[0] === 'title')?.[1] || 'Replied Reel'; - - repliedVideoEvents.push({ - id: event.id, - pubkey: event.pubkey, - created_at: event.created_at, - title, - description: event.content, - videoUrl, - imageUrl, - }); - - seenIds.add(event.id); - }); - - // Combine all events and sort by creation time (newest first) - const allVideoEvents = [...parsedEvents, ...taggedVideoEvents, ...repliedVideoEvents]; - allVideoEvents.sort((a, b) => b.created_at - a.created_at); - setVideoEvents(allVideoEvents); - }, [followVideos, globalVideos, taggedEvents, originalRepliedEvents]); - - // Touch handlers for swiping - const handleTouchStart = (e: React.TouchEvent) => { - setTouchStart(e.targetTouches[0].clientY); - }; - - const handleTouchMove = (e: React.TouchEvent) => { - setTouchEnd(e.targetTouches[0].clientY); - }; - - const handleTouchEnd = () => { - if (!touchStart || !touchEnd) return; - - const distance = touchStart - touchEnd; - const isUpSwipe = distance > 50; - const isDownSwipe = distance < -50; - - if (isUpSwipe && currentVideoIndex < videoEvents.length - 1) { - setCurrentVideoIndex(prev => prev + 1); - } else if (isDownSwipe && currentVideoIndex > 0) { - setCurrentVideoIndex(prev => prev - 1); - } - - setTouchStart(null); - setTouchEnd(null); - }; - - // Keyboard handlers - const handleKeyDown = (e: React.KeyboardEvent) => { - switch (e.key) { - case 'ArrowUp': - e.preventDefault(); - if (currentVideoIndex > 0) { - setCurrentVideoIndex(prev => prev - 1); - } - break; - case 'ArrowDown': - e.preventDefault(); - if (currentVideoIndex < videoEvents.length - 1) { - setCurrentVideoIndex(prev => prev + 1); - } - break; - case ' ': - e.preventDefault(); - const currentVideo = videoRefs.current[videoEvents[currentVideoIndex]?.id]; - if (currentVideo) { - if (currentVideo.paused) { - currentVideo.play().catch(err => console.error("Error playing video:", err)); - } else { - currentVideo.pause(); - } - } - break; - case 'm': - case 'M': - e.preventDefault(); - setIsAudioMuted(prev => !prev); - break; - } - }; - - // Play current video and pause others - useEffect(() => { - if (videoEvents.length === 0) return; - - Object.entries(videoRefs.current).forEach(([id, videoElement]) => { - if (videoElement) { - if (id === videoEvents[currentVideoIndex]?.id) { - videoElement.play().catch(err => console.error("Error playing video:", err)); - } else { - videoElement.pause(); - } - videoElement.muted = isAudioMuted; - videoElement.volume = volume; - } - }); - }, [currentVideoIndex, videoEvents, isAudioMuted, volume]); - - // Toggle like - const toggleLike = async (id: string) => { - const loginType = typeof window !== 'undefined' ? localStorage.getItem('loginType') : null; - - if (!loginType || !currentUserHexPubkey) { - toast({ - title: "Login required", - description: "Please login to like videos", - variant: "destructive" - }); - return; - } - - const eventToSend: Partial<NostrEvent> = { - kind: 7, - content: isLiked[id] ? '' : '+', - tags: [['e', id]], - created_at: dateToUnix(), - }; - - try { - const signedEvent = await signEvent(loginType, eventToSend as NostrEvent); - if (signedEvent) { - publish(signedEvent); - toast({ - title: isLiked[id] ? "Unliked" : "Liked", - description: `Successfully ${isLiked[id] ? 'removed like from' : 'liked'} the video`, - }); - } - } catch (error) { - console.error("Error sending reaction:", error); - toast({ - title: "Error", - description: "Failed to send reaction", - variant: "destructive" - }); - } - }; - - // Open comment modal - const openCommentModal = (video: VideoEvent) => { - const loginType = typeof window !== 'undefined' ? localStorage.getItem('loginType') : null; - - if (!loginType) { - toast({ - title: "Login required", - description: "Please login to comment on videos", - variant: "destructive" - }); - return; - } - - setSelectedVideo(video); - setCommentModalOpen(true); - }; - - // Submit comment - const submitComment = async () => { - if (!selectedVideo || !commentText.trim()) return; - - const loginType = typeof window !== 'undefined' ? localStorage.getItem('loginType') : null; - if (!loginType) return; - - const eventToSend: Partial<NostrEvent> = { - kind: 1, - content: commentText, - tags: [['e', selectedVideo.id]], - created_at: dateToUnix(), - }; - - try { - const signedEvent = await signEvent(loginType, eventToSend as NostrEvent); - if (signedEvent) { - publish(signedEvent); - toast({ - title: "Comment posted", - description: "Your comment has been posted successfully", - }); - setCommentText(""); - setCommentModalOpen(false); - setSelectedVideo(null); - } - } catch (error) { - console.error("Error sending comment:", error); - toast({ - title: "Error", - description: "Failed to post comment", - variant: "destructive" - }); - } - }; - - // Open share modal - const openShareModal = (video: VideoEvent) => { - setSelectedVideo(video); - setShareModalOpen(true); - }; - - // Copy text to clipboard - const copyToClipboard = async (text: string, label: string) => { - try { - await navigator.clipboard.writeText(text); - setCopiedText(label); - toast({ - title: "Copied!", - description: `${label} copied to clipboard`, - }); - setTimeout(() => setCopiedText(null), 2000); - } catch (error) { - console.error("Failed to copy:", error); - toast({ - title: "Error", - description: "Failed to copy to clipboard", - variant: "destructive" - }); - } - }; - - if (videoEvents.length === 0) { - return ( - <div className="fixed inset-0 bg-black flex items-center justify-center text-white"> - <div className="text-center"> - <p>Loading videos...</p> - {isClient && currentUserPubkey && ( - <div className="mt-4 text-sm text-gray-400"> - <p>Debug Info:</p> - <p>User: {currentUserPubkey.slice(0, 8)}...</p> - <p>Follows: {followedPubkeys.length}</p> - <p>Follow Videos: {followVideos?.length || 0}</p> - <p>Global Videos: {globalVideos?.length || 0}</p> - </div> - )} - </div> - </div> - ); - } - - return ( - <div - className="fixed inset-0 bg-black overflow-hidden" - onTouchStart={handleTouchStart} - onTouchMove={handleTouchMove} - onTouchEnd={handleTouchEnd} - onKeyDown={handleKeyDown} - tabIndex={0} - > - {/* Navigation indicators */} - <div className="absolute top-1/2 left-6 z-30 transform -translate-y-1/2"> - {currentVideoIndex > 0 && ( - <button - className="p-2 rounded-full bg-black/20 text-white" - onClick={() => setCurrentVideoIndex(prev => Math.max(0, prev - 1))} - > - <ChevronUp className="h-8 w-8" /> - </button> - )} - </div> - <div className="absolute top-1/2 left-6 z-30 transform translate-y-1/2"> - {currentVideoIndex < videoEvents.length - 1 && ( - <button - className="p-2 rounded-full bg-black/20 text-white" - onClick={() => setCurrentVideoIndex(prev => Math.min(videoEvents.length - 1, prev + 1))} - > - <ChevronDown className="h-8 w-8" /> - </button> - )} - </div> - - {/* Videos */} - {videoEvents.map((video, index) => ( - <VideoEventDisplay - key={video.id} - video={video} - index={index} - currentIndex={currentVideoIndex} - videoRef={el => videoRefs.current[video.id] = el} - isLiked={!!isLiked[video.id]} - toggleLike={() => toggleLike(video.id)} - onComment={() => openCommentModal(video)} - onShare={() => openShareModal(video)} - isAudioMuted={isAudioMuted} - volume={volume} - currentUserPubkey={currentUserPubkey} - isClient={isClient} - setIsAudioMuted={setIsAudioMuted} - /> - ))} - - {/* Progress indicators */} - <div className="absolute top-4 left-0 right-0 flex justify-center gap-1 px-4 z-40"> - {videoEvents.map((_, index) => ( - <button - key={index} - className={cn( - "h-1 rounded-full transition-all cursor-pointer", - index === currentVideoIndex - ? "bg-white w-6" - : "bg-white/40 w-4" - )} - onClick={() => setCurrentVideoIndex(index)} - /> - ))} - </div> - - {/* Comment Modal */} - <Dialog open={commentModalOpen} onOpenChange={setCommentModalOpen}> - <DialogContent className="sm:max-w-md"> - <DialogHeader> - <DialogTitle>Add Comment</DialogTitle> - </DialogHeader> - <div className="space-y-4"> - <div> - <Label htmlFor="comment">Comment</Label> - <Textarea - id="comment" - placeholder="Write your comment..." - value={commentText} - onChange={(e) => setCommentText(e.target.value)} - className="min-h-[100px]" - /> - </div> - <div className="flex justify-end gap-2"> - <Button variant="outline" onClick={() => { - setCommentModalOpen(false); - setCommentText(""); - }}> - Cancel - </Button> - <Button onClick={submitComment} disabled={!commentText.trim()}> - Post Comment - </Button> - </div> - </div> - </DialogContent> - </Dialog> - - {/* Share Modal */} - <Dialog open={shareModalOpen} onOpenChange={setShareModalOpen}> - <DialogContent className="sm:max-w-md"> - <DialogHeader> - <DialogTitle>Share Video</DialogTitle> - </DialogHeader> - <div className="space-y-4"> - {selectedVideo && ( - <> - <div> - <Label>Video Event (nevent)</Label> - <div className="flex items-center gap-2 p-2 bg-gray-100 rounded"> - <code className="text-sm flex-1 break-all"> - {nip19.neventEncode({ - id: selectedVideo.id, - relays: [] - })} - </code> - <Button - size="sm" - variant="outline" - onClick={() => copyToClipboard( - nip19.neventEncode({ - id: selectedVideo.id, - relays: [] - }), - "nevent" - )} - > - {copiedText === "nevent" ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} - </Button> - </div> - </div> - <div> - <Label>Author Profile (npub)</Label> - <div className="flex items-center gap-2 p-2 bg-gray-100 rounded"> - <code className="text-sm flex-1 break-all"> - {nip19.npubEncode(selectedVideo.pubkey)} - </code> - <Button - size="sm" - variant="outline" - onClick={() => copyToClipboard( - nip19.npubEncode(selectedVideo.pubkey), - "npub" - )} - > - {copiedText === "npub" ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} - </Button> - </div> - </div> - </> - )} - <div className="flex justify-end"> - <Button variant="outline" onClick={() => setShareModalOpen(false)}> - Close - </Button> - </div> - </div> - </DialogContent> - </Dialog> - </div> - ); -} - -interface VideoEventDisplayProps { - video: VideoEvent; - index: number; - currentIndex: number; - videoRef: (el: HTMLVideoElement | null) => void; - isLiked: boolean; - toggleLike: () => void; - onComment: () => void; - onShare: () => void; - isAudioMuted: boolean; - volume: number; - currentUserPubkey: string | null; - isClient: boolean; - setIsAudioMuted: (value: boolean | ((prev: boolean) => boolean)) => void; -} - -const VideoEventDisplay: React.FC<VideoEventDisplayProps> = ({ - video, - index, - currentIndex, - videoRef, - isLiked, - toggleLike, - onComment, - onShare, - isAudioMuted, - volume, - currentUserPubkey, - isClient, - setIsAudioMuted -}) => { - const { data: userData } = useProfile({ - pubkey: video.pubkey, - }); - - const username = userData?.name || userData?.display_name || - `${nip19.npubEncode(video.pubkey).slice(0, 8)}...`; - - const profileImageSrc = userData?.picture || `https://robohash.org/${video.pubkey}`; - const npub = nip19.npubEncode(video.pubkey); - const profileUrl = `/profile/${npub}`; - - return ( - <div - className={cn( - "absolute inset-0 transition-transform duration-300", - index === currentIndex ? "translate-y-0" : - index < currentIndex ? "-translate-y-full" : "translate-y-full" - )} - > - <video - ref={videoRef} - src={video.videoUrl} - poster={video.imageUrl} - className="w-full h-full object-contain bg-black" - loop - muted={isAudioMuted} - playsInline - autoPlay={index === currentIndex} - /> - - {/* Video info overlay */} - <div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent"> - <div className="flex items-end justify-between"> - <div className="text-white max-w-[80%]"> - <div className="flex items-center gap-2 mb-2"> - <Link href={profileUrl}> - <div className="w-10 h-10 rounded-full bg-gray-600 flex items-center justify-center overflow-hidden"> - {profileImageSrc ? ( - <img src={profileImageSrc} alt={username} className="w-full h-full object-cover" /> - ) : ( - <User className="h-6 w-6 text-white" /> - )} - </div> - </Link> - <div> - <Link href={profileUrl}> - <p className="font-bold">{username}</p> - </Link> - {video.title && <p className="text-sm font-semibold">{video.title}</p>} - </div> - </div> - <p className="text-sm">{video.description}</p> - </div> - - {/* Interaction buttons */} - <div className="flex flex-col items-center gap-4"> - <button - className="flex flex-col items-center" - onClick={toggleLike} - > - <Heart - className={cn( - "h-8 w-8", - isLiked ? "fill-red-500 text-red-500" : "text-white" - )} - /> - <span className="text-white text-xs mt-1">0</span> - </button> - <button - className="flex flex-col items-center" - onClick={onComment} - > - <MessageCircle className="h-8 w-8 text-white" /> - <span className="text-white text-xs mt-1">0</span> - </button> - <button - className="flex flex-col items-center" - onClick={onShare} - > - <Share2 className="h-8 w-8 text-white" /> - <span className="text-white text-xs mt-1">0</span> - </button> - - {/* Audio Controls */} - <button - className="flex flex-col items-center" - onClick={() => setIsAudioMuted((prev: boolean) => !prev)} - title={isAudioMuted ? "Unmute" : "Mute"} - > - {isAudioMuted ? ( - <VolumeX className="h-8 w-8 text-white" /> - ) : ( - <Volume2 className="h-8 w-8 text-white" /> - )} - <span className="text-white text-xs mt-1"> - {isAudioMuted ? "Muted" : `${Math.round(volume * 100)}%`} - </span> - </button> - </div> - </div> - </div> - </div> - ); -}; - -export default ReelFeed; \ No newline at end of file diff --git a/components/Search.tsx b/components/Search.tsx deleted file mode 100644 index 58f374f..0000000 --- a/components/Search.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { queryProfile } from "nostr-tools/nip05" -import { nip19 } from "nostr-tools" -import { useState } from 'react'; -import { ReloadIcon } from "@radix-ui/react-icons"; -import { useRouter } from 'next/navigation'; - -export function Search() { - const router = useRouter(); - - const [inputValue, setInputValue] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const calculateAndRedirect = async () => { - setIsLoading(true); - - let value = inputValue.trim(); - value = value.replaceAll('nostr:', ''); - - if (value.startsWith('npub')) { // npub Search - // window.location.href = `/profile/${inputValue}`; - router.push(`/profile/${value}`); - } else if (value.startsWith('#')) { // Hashtag Search - // window.location.href = `/tag/${inputValue.replaceAll('#', '')}`; - router.push(`/tag/${value.replaceAll('#', '')}`); - } else if(value.includes('@')) { // NIP-05 Search - // if inputValue starts with @, then add a "_" at the beginning - if(value.startsWith('@')) { - setInputValue('_' + value); - } - - let profile = await queryProfile(value); - if(profile?.pubkey !== undefined) { // Only redirect if profile is found - router.push(`/profile/${nip19.npubEncode(profile?.pubkey)}`); - } - } else { - router.push(`/search/${value}`); - } - setIsLoading(false); - } - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - calculateAndRedirect(); - } - } - - return ( - <div className="flex w-full max-w-sm items-center space-x-2"> - <Input - type="text" - placeholder="npub, NIP-05, #tag or anything else" - value={inputValue} - onChange={(e) => setInputValue(e.target.value)} - onKeyDown={handleKeyDown} - /> - {/* <Button type="submit" onClick={calculateAndRedirect}>Search</Button> */} - <Button type="submit" onClick={calculateAndRedirect}> - {isLoading ? <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> : 'Search'} {/* Spinner-Komponente anzeigen, wenn geladen wird */} - </Button> - </div> - ) -} \ No newline at end of file diff --git a/components/TagCard.tsx b/components/TagCard.tsx deleted file mode 100644 index 81454e0..0000000 --- a/components/TagCard.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Hash } from "lucide-react"; - -interface TagCardProps { - tag: string; -} - -const TagCard: React.FC<TagCardProps> = ({ tag }) => { - return ( - <Link href={`/tag/${tag}`}> - <Card className="hover:bg-accent transition-colors h-full"> - <CardHeader className="pb-2"> - <CardTitle className="flex items-center text-xl"> - <Hash className="h-5 w-5 mr-2" /> - {tag} - </CardTitle> - </CardHeader> - <CardContent> - <p className="text-sm text-muted-foreground"> - View content tagged with #{tag} - </p> - </CardContent> - </Card> - </Link> - ); -}; - -export default TagCard; \ No newline at end of file diff --git a/components/TagFeed.tsx b/components/TagFeed.tsx deleted file mode 100644 index 4e48a77..0000000 --- a/components/TagFeed.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents } from "nostr-react"; -import KIND20Card from "./KIND20Card"; -import NoteCard from "./NoteCard"; -import { getImageUrl } from "@/utils/utils"; -import { Button } from "@/components/ui/button"; -import { Skeleton } from "@/components/ui/skeleton"; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -interface TagFeedProps { - tag: string; -} - -const TagFeed: React.FC<TagFeedProps> = ({ tag }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered - const [limit, setLimit] = useState(25); - - const { events, isLoading } = useNostrEvents({ - filter: { - // since: dateToUnix(now.current), // all new events from now - // since: 0, - limit: limit, - kinds: [20, 21, 22], - "#t": [tag], - }, - }); - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 25); - }; - - return ( - <> - <div className="grid lg:grid-cols-3 gap-2"> - {events.length === 0 && isLoading ? ( - <> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - </> - ) : ( - events.map((event) => { - const imageUrl = getImageUrl(event.tags); - const isVideo = event.kind === 21 || event.kind === 22; - - if (isVideo) { - // Use NoteCard for video content - const videoUrl = getVideoUrl(event.tags); - const contentWithVideo = videoUrl ? `${event.content}\n${videoUrl}` : event.content; - return ( - <div key={event.id}> - <NoteCard - key={event.id} - pubkey={event.pubkey} - text={contentWithVideo} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - /> - </div> - ); - } else { - // Use KIND20Card for image content - return ( - <div key={event.id}> - <KIND20Card key={event.id} pubkey={event.pubkey} text={event.content} image={imageUrl} eventId={event.id} tags={event.tags} event={event} showViewNoteCardButton={true} /> - </div> - ); - } - }) - )} - </div> - {!isLoading && ( - <div className="flex justify-center p-4"> - <Button className="w-full md:w-auto" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default TagFeed; \ No newline at end of file diff --git a/components/TagQuickViewFeed.tsx b/components/TagQuickViewFeed.tsx deleted file mode 100644 index 13cffbb..0000000 --- a/components/TagQuickViewFeed.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useRef, useState } from "react"; -import { useNostrEvents } from "nostr-react"; -import { getImageUrl } from "@/utils/utils"; -import QuickViewKind20NoteCard from "./QuickViewKind20NoteCard"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Button } from "@/components/ui/button"; - -interface TagQuickViewFeedProps { - tag: string; -} - -const TagQuickViewFeed: React.FC<TagQuickViewFeedProps> = ({ tag }) => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered - const [limit, setLimit] = useState(25); - - const { events, isLoading } = useNostrEvents({ - filter: { - // since: dateToUnix(now.current), // all new events from now - // since: 0, - limit: limit, - kinds: [20, 21, 22], - "#t": [tag], - }, - }); - - const loadMore = () => { - setLimit(prevLimit => prevLimit + 25); - }; - - return ( - <> - <div className="grid grid-cols-3 gap-2"> - {events.length === 0 && isLoading ? ( - <> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - <div className="aspect-square w-full"> - <Skeleton className="h-full w-full rounded-xl" /> - </div> - </> - ) : ( - events.map((event) => ( - <div key={event.id}> - <QuickViewKind20NoteCard pubkey={event.pubkey} text={event.content} image={getImageUrl(event.tags)} eventId={event.id} tags={event.tags} event={event} linkToNote={true} /> - </div> - )) - )} - </div> - {!isLoading && ( - <div className="flex justify-center p-4"> - <Button className="w-full md:w-auto" onClick={loadMore}>Load More</Button> - </div> - )} - </> - ); -} - -export default TagQuickViewFeed; \ No newline at end of file diff --git a/components/TrendingAccount.tsx b/components/TrendingAccount.tsx deleted file mode 100644 index 9d844a8..0000000 --- a/components/TrendingAccount.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - CardHeader, - CardTitle, - SmallCardContent, -} from "@/components/ui/card" -import { Avatar, AvatarImage } from '@/components/ui/avatar'; -import Link from 'next/link'; - -interface TrendingAccountProps { - pubkey: string; -} - -const TrendingAccount: React.FC<TrendingAccountProps> = ({ pubkey }) => { - const { data: userData } = useProfile({ - pubkey, - }); - - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - - return ( - <> - <Card> - <CardHeader> - <CardTitle> - <Link href={hrefProfile} style={{ textDecoration: 'none' }}> - <div style={{ display: 'flex', alignItems: 'center' }}> - <Avatar> - <AvatarImage src={profileImageSrc} /> - </Avatar> - {/* <span style={{ marginLeft: '10px' }}>{title.substring(0, 12)}</span> */} - <span className='break-all' style={{ marginLeft: '10px' }}>{title}</span> - </div> - </Link> - </CardTitle> - </CardHeader> - <SmallCardContent> - <div> - <div className='d-flex justify-content-center align-items-center'> - </div> - </div> - </SmallCardContent> - </Card> - </> - ); -} - -export default TrendingAccount; \ No newline at end of file diff --git a/components/TrendingAccounts.tsx b/components/TrendingAccounts.tsx deleted file mode 100644 index bcc48a2..0000000 --- a/components/TrendingAccounts.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import TrendingAccount from '@/components/TrendingAccount'; - -export function TrendingAccounts() { - const [profiles, setProfiles] = useState<any[]>([]); - - useEffect(() => { - fetch('https://api.nostr.band/v0/trending/profiles') - .then(res => res.json()) - .then(data => setProfiles(data.profiles)) - .catch(error => { - console.error('Error calling trending profiles:', error); - }); - }, []); - - return ( - <div className="flex flex-col items-center py-6 px-6"> - <h1 className="text-3xl font-bold">Trending Accounts</h1> - <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-6"> - {profiles && profiles.length > 0 && profiles.slice(0,4).map((profile, index) => ( - <TrendingAccount key={index} pubkey={profile.pubkey} /> - ))} - </div> - </div> - ); -} \ No newline at end of file diff --git a/components/TrendingImage.tsx b/components/TrendingImage.tsx deleted file mode 100644 index e6b6d13..0000000 --- a/components/TrendingImage.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useMemo } from 'react'; -import { useNostr, useNostrEvents, useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - CardHeader, - CardTitle, - SmallCardContent, -} from "@/components/ui/card" -import Image from 'next/image'; -import Link from 'next/link'; -import { Avatar } from './ui/avatar'; -import { AvatarImage } from '@radix-ui/react-avatar'; - -interface TrendingImageProps { - eventId: string; - pubkey: string; -} - -const TrendingImage: React.FC<TrendingImageProps> = ({ eventId, pubkey }) => { - const { data: userData } = useProfile({ - pubkey, - }); - - const { events } = useNostrEvents({ - filter: { - kinds: [1], - ids: [eventId] - }, - }); - - const npubShortened = useMemo(() => { - let encoded = nip19.npubEncode(pubkey); - let parts = encoded.split('npub'); - return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); - }, [pubkey]); - - let text = events && events.length > 0 ? events[0].content : ''; - const createdAt = events && events.length > 0 ? new Date(events[0].created_at * 1000) : new Date(); - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - text = text.replaceAll('\n', ' '); - const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif|jpeg)/g); - const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|jpeg)/g, ''); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - // Create nevent with relay hints - const nevent = nip19.neventEncode({ - id: eventId, - relays: [] // Add relay hints if available - }); - const hrefNote = `/note/${nevent}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - - return ( - <> - <Card> - <CardHeader> - <CardTitle> - <div style={{ display: 'flex', alignItems: 'center' }}> - <Avatar> - <AvatarImage src={profileImageSrc} /> - </Avatar> - <span className='break-all' style={{ marginLeft: '10px' }}>{title}</span> - </div> - </CardTitle> - </CardHeader> - <SmallCardContent> - <div className='p-2'> - <div className='d-flex justify-content-center align-items-center'> - {imageSrc && imageSrc.length > 0 && ( - <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}> - <Link href={hrefNote}> - <img src={imageSrc[0]} className='rounded lg:rounded-lg' style={{ width: '100%', height: '100%', objectFit: 'cover' }} alt={text} /> - </Link> - </div> - // <img src={imageSrc[0]} style={{ maxWidth: '100%', maxHeight: '100vh', objectFit: 'cover', margin: 'auto' }} alt={text} /> - // <div style={{ position: 'relative', width: '100%', maxHeight: '100vh' }}> - // <Image src={imageSrc[0]} alt={text} layout='fill' objectFit='contain' /> - // </div> - )} - </div> - </div> - </SmallCardContent> - </Card> - </> - ); -} - -export default TrendingImage; \ No newline at end of file diff --git a/components/TrendingImageNew.tsx b/components/TrendingImageNew.tsx deleted file mode 100644 index d7fbcb6..0000000 --- a/components/TrendingImageNew.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState } from 'react'; -import { useProfile } from "nostr-react"; -import { nip19 } from "nostr-tools"; -import { - Card, - CardHeader, - CardTitle, - SmallCardContent, -} from "@/components/ui/card" -import Link from 'next/link'; -import { Avatar } from './ui/avatar'; -import { AvatarImage } from '@radix-ui/react-avatar'; -import { Button } from '@/components/ui/button'; -import { Eye } from 'lucide-react'; -import { hasNsfwContent } from '@/utils/utils'; - -interface TrendingImageNewProps { - event: { - id: string; - pubkey: string; - content: string; - created_at: string; - tags: Array<string[]>; - } -} - -const TrendingImageNew: React.FC<TrendingImageNewProps> = ({ event }) => { - const { data: userData } = useProfile({ - pubkey: event.pubkey, - }); - - // Check if the event has nsfw or sexy tags - const hasNsfwTag = hasNsfwContent(event.tags); - - // State to control image blur - const [showSensitiveContent, setShowSensitiveContent] = useState(false); - - const npubShortened = (() => { - let encoded = nip19.npubEncode(event.pubkey); - let parts = encoded.split('npub'); - return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); - })(); - - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - const text = event.content.replaceAll('\n', ' '); - - // Get image URL from imeta tags - const imageUrl = event.tags.find(tag => tag[0] === 'imeta' && tag[1]?.startsWith('url ')) - ?.slice(1)[0]?.replace('url ', ''); - - const hrefProfile = `/profile/${nip19.npubEncode(event.pubkey)}`; - // Create nevent with relay hints - const nevent = nip19.neventEncode({ - id: event.id, - relays: [] - }); - const hrefNote = `/note/${nevent}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + event.pubkey; - - // Toggle sensitive content visibility - const toggleSensitiveContent = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setShowSensitiveContent(!showSensitiveContent); - }; - - return ( - <Card> - <CardHeader> - <CardTitle> - <Link href={hrefProfile} style={{ textDecoration: 'none' }}> - <div style={{ display: 'flex', alignItems: 'center' }}> - <Avatar> - <AvatarImage src={profileImageSrc} /> - </Avatar> - <span className='break-all' style={{ marginLeft: '10px' }}>{title}</span> - </div> - </Link> - </CardTitle> - </CardHeader> - <SmallCardContent> - <div className='p-2'> - <div className='d-flex justify-content-center align-items-center'> - {imageUrl && ( - <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}> - <Link href={hrefNote} onClick={hasNsfwTag && !showSensitiveContent ? (e) => e.preventDefault() : undefined}> - <img - src={imageUrl} - className={`rounded lg:rounded-lg w-full h-full object-cover ${hasNsfwTag && !showSensitiveContent ? 'blur-xl' : ''}`} - style={{ margin: 'auto' }} - alt={text} - loading="lazy" - /> - {hasNsfwTag && !showSensitiveContent && ( - <div - className="absolute inset-0 flex flex-col items-center justify-center" - onClick={toggleSensitiveContent} - > - <Button - variant="secondary" - className="bg-black bg-opacity-50 hover:bg-opacity-70 text-white px-4 py-2 rounded-md" - onClick={toggleSensitiveContent} - > - <Eye className="h-4 w-4 mr-2" /> Show Sensitive Content - </Button> - <p className="mt-2 text-white text-sm bg-black bg-opacity-50 p-2 rounded"> - This image may contain sensitive content - </p> - </div> - )} - </Link> - </div> - )} - </div> - </div> - </SmallCardContent> - </Card> - ); -} - -export default TrendingImageNew; \ No newline at end of file diff --git a/components/TrendingImages.tsx b/components/TrendingImages.tsx deleted file mode 100644 index 10c09c7..0000000 --- a/components/TrendingImages.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import TrendingImage from './TrendingImage'; - -export function TrendingImages() { - const [profiles, setProfiles] = useState<any[]>([]); - - useEffect(() => { - fetch('https://api.nostr.band/v0/trending/images') - .then(res => res.json()) - .then(data => setProfiles(data.images)) - .catch(error => { - console.error('Error calling trending profiles:', error); - }); - }, []); - - return ( - <div className="flex flex-col items-center py-6 px-6"> - <h1 className="text-3xl font-bold">Currently Trending</h1> - <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-6"> - {profiles && profiles.length > 0 && profiles.map((profile, index) => ( - // <h1 key={index}>{profile.id}</h1> - <TrendingImage key={index} eventId={profile.id} pubkey={profile.pubkey} /> - ))} - </div> - </div> - ); -} \ No newline at end of file diff --git a/components/TrendingImagesNew.tsx b/components/TrendingImagesNew.tsx deleted file mode 100644 index e0dec93..0000000 --- a/components/TrendingImagesNew.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import TrendingImage from '@/components/TrendingImageNew'; -import { Spinner } from '@/components/spinner'; - -export function TrendingImagesNew() { - const [events, setEvents] = useState<any[]>([]); - - useEffect(() => { - // TODO: Fetch trending images from luminas own relay via http call - fetch('https://relay.lumina.rocks/api/trending/kind20') - .then(res => res.json()) - .then(data => setEvents(data.trending)) - .catch(error => { - console.error('Error calling trending images:', error); - }); - }, []); - - return ( - <div className="flex flex-col items-center py-6 px-6"> - <h1 className="text-3xl font-bold">Currently Trending</h1> - <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-6"> - {events && events.length > 0 ? ( - events.map((event, index) => ( - <TrendingImage key={event.id} event={event} /> - )) - ) : ( - <div className="col-span-full text-center py-8 text-lg flex flex-col items-center gap-4"> - <Spinner /> - Curating Trending Images for you.. 💜 - </div> - )} - </div> - </div> - ); -} \ No newline at end of file diff --git a/components/Umami.tsx b/components/Umami.tsx deleted file mode 100644 index 6548196..0000000 --- a/components/Umami.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Script from "next/script"; -import React from "react"; - -const Umami = () => { - if(process.env.NEXT_PUBLIC_ENABLE_UMAMI == "true") { - return ( - <Script - src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/script.js`} - strategy="afterInteractive" - data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID} - defer - /> - ); - } else { - return null; - } -}; - -export default Umami; diff --git a/components/UpdateProfileForm.tsx b/components/UpdateProfileForm.tsx deleted file mode 100644 index 7c2be89..0000000 --- a/components/UpdateProfileForm.tsx +++ /dev/null @@ -1,368 +0,0 @@ -'use client'; - -import React, { useEffect, useState } from 'react'; -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { nip19 } from "nostr-tools" -import { Label } from "./ui/label" -import { Textarea } from "@/components/ui/textarea" -import { verifyEvent } from 'nostr-tools/pure' -import { hexToBytes } from '@noble/hashes/utils' -import { useNostr, useProfile } from 'nostr-react'; -import { signEvent } from '@/utils/utils'; -import { Card, CardContent } from "@/components/ui/card"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Loader2, Globe, Image, ImageIcon, BadgeCheck, Zap } from "lucide-react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { publishToOutbox } from "@/utils/publishUtils"; -import { useCurrentUserPubkey } from "@/utils/relayHooks"; - -export function UpdateProfileForm() { - const currentUserPubkey = useCurrentUserPubkey(); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isSaved, setIsSaved] = useState(false); - const [isDataLoaded, setIsDataLoaded] = useState(false); - - let npub = ''; - let pubkey = ''; - let loginType = ''; - let nsec: Uint8Array; - - if (typeof window !== 'undefined') { - pubkey = window.localStorage.getItem("pubkey") ?? ''; - const nsecHex = window.localStorage.getItem("nsec"); - loginType = window.localStorage.getItem("loginType") ?? ''; - - if (pubkey && pubkey.length > 0) { - npub = nip19.npubEncode(pubkey); - } - - if (nsecHex && nsecHex.length > 0) { - nsec = hexToBytes(nsecHex); - } - } - - const { data: userData, isLoading: isUserDataLoading } = useProfile({ - pubkey, - }); - - const [username, setUsername] = useState<string | undefined>(''); - const [displayName, setDisplayName] = useState<string | undefined>(''); - const [bio, setBio] = useState<string | undefined>(''); - const [picture, setPicture] = useState<string | undefined>(''); - const [banner, setBanner] = useState<string | undefined>(''); - const [nip05, setNip05] = useState<string | undefined>(''); - const [lud16, setLud16] = useState<string | undefined>(''); - const [website, setWebsite] = useState<string | undefined>(''); - - // Update form data when userData changes - useEffect(() => { - if (userData && !isDataLoaded) { - setUsername(userData.name); - setDisplayName(userData.display_name); - setBio(userData.about); - setPicture(userData.picture); - setBanner(userData.banner); - setNip05(userData.nip05); - setLud16(userData.lud16); - setWebsite(userData.website); - setIsDataLoaded(true); - } - }, [userData, isDataLoaded]); - - // Field change handlers - const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setUsername(event.target.value); - setIsSaved(false); - }; - - const handleDisplayNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setDisplayName(event.target.value); - setIsSaved(false); - }; - - const handleBioChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { - setBio(event.target.value); - setIsSaved(false); - }; - - const handlePictureChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setPicture(event.target.value); - setIsSaved(false); - }; - - const handleBannerChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setBanner(event.target.value); - setIsSaved(false); - }; - - const handleNip05Change = (event: React.ChangeEvent<HTMLInputElement>) => { - setNip05(event.target.value); - setIsSaved(false); - }; - - const handleLud16Change = (event: React.ChangeEvent<HTMLInputElement>) => { - setLud16(event.target.value); - setIsSaved(false); - }; - - const handleWebsiteChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setWebsite(event.target.value); - setIsSaved(false); - }; - - async function handleProfileUpdate() { - setIsSubmitting(true); - setIsSaved(false); - - if (loginType) { - try { - let event = { - kind: 0, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: JSON.stringify({ - name: username, - display_name: displayName, - about: bio, - picture: picture, - banner: banner, - nip05: nip05, - lud16: lud16, - website: website, - }), - pubkey: pubkey, - id: "", - sig: "", - }; - - let signedEvent = await signEvent(loginType, event); - - if (signedEvent === null) { - throw new Error('Failed to sign the event'); - } - - let isGood = verifyEvent(signedEvent); - - if (isGood) { - await publishToOutbox(signedEvent, currentUserPubkey || undefined); - setIsSaved(true); - setTimeout(() => { - window.location.href = `/profile/${npub}`; - }, 1000); - } - } catch (error) { - console.error("Error updating profile:", error); - alert('Failed to update profile. Please check your connection and try again.'); - } finally { - setIsSubmitting(false); - } - } - } - - if (isUserDataLoading && !isDataLoaded) { - return ( - <div className="w-full space-y-6"> - <div className="flex items-center gap-4"> - <Skeleton className="h-20 w-20 rounded-full" /> - <div> - <Skeleton className="h-8 w-48 mb-2" /> - <Skeleton className="h-4 w-64" /> - </div> - </div> - <Card className="border rounded-lg"> - <CardContent className="p-6 space-y-5"> - <Skeleton className="h-10 w-full mb-4" /> - <div className="grid grid-cols-1 md:grid-cols-2 gap-5"> - <Skeleton className="h-28 w-full" /> - <Skeleton className="h-28 w-full" /> - </div> - <Skeleton className="h-32 w-full" /> - <div className="grid grid-cols-1 md:grid-cols-2 gap-5"> - <Skeleton className="h-28 w-full" /> - <Skeleton className="h-28 w-full" /> - </div> - </CardContent> - </Card> - </div> - ); - } - - return ( - <div className="w-full space-y-6"> - <div className="flex items-center gap-4"> - <Avatar className="h-20 w-20 border"> - <AvatarImage src={picture} alt={username || "Profile"} /> - <AvatarFallback className="text-lg"> - {username?.charAt(0) || "U"} - </AvatarFallback> - </Avatar> - <div> - <h2 className="text-2xl font-semibold">{displayName || username || "Your Profile"}</h2> - <p className="text-sm text-muted-foreground break-all">{nip05 || npub}</p> - </div> - </div> - - <Card className="border rounded-lg"> - <CardContent className="p-6 space-y-5"> - <div> - <Label htmlFor="npub" className="text-sm font-medium">Your npub (Public Key)</Label> - <Input - id="npub" - type="text" - value={npub} - readOnly - className="font-mono text-sm mt-1 bg-muted/50" - /> - <p className="text-xs text-muted-foreground mt-1">Your public identity on the Nostr network</p> - </div> - - <div className="grid grid-cols-1 md:grid-cols-2 gap-5"> - <div> - <Label htmlFor="username" className="text-sm font-medium">Username</Label> - <Input - id="username" - type="text" - placeholder="e.g., satoshi" - value={username || ""} - onChange={handleUsernameChange} - className="mt-1" - /> - <p className="text-xs text-muted-foreground mt-1">Your unique username on the network</p> - </div> - - <div> - <Label htmlFor="displayname" className="text-sm font-medium">Display Name</Label> - <Input - id="displayname" - type="text" - placeholder="e.g., Satoshi Nakamoto" - value={displayName || ""} - onChange={handleDisplayNameChange} - className="mt-1" - /> - <p className="text-xs text-muted-foreground mt-1">How your name appears to others</p> - </div> - </div> - - <div> - <Label htmlFor="bio" className="text-sm font-medium">Bio</Label> - <Textarea - id="bio" - placeholder="Tell the world about yourself..." - rows={5} - value={bio || ""} - onChange={handleBioChange} - className="mt-1 resize-none" - /> - <p className="text-xs text-muted-foreground mt-1">A short description about yourself</p> - </div> - - <div className="grid grid-cols-1 md:grid-cols-2 gap-5"> - <div> - <Label htmlFor="picture" className="text-sm font-medium flex items-center gap-1"> - <ImageIcon className="h-4 w-4" /> Profile Picture URL - </Label> - <Input - id="picture" - type="text" - placeholder="https://example.com/your-picture.jpg" - value={picture || ""} - onChange={handlePictureChange} - className="mt-1" - /> - <p className="text-xs text-muted-foreground mt-1">URL to your profile image</p> - </div> - - <div> - <Label htmlFor="banner" className="text-sm font-medium flex items-center gap-1"> - <Image className="h-4 w-4" /> Banner Image URL - </Label> - <Input - id="banner" - type="text" - placeholder="https://example.com/your-banner.jpg" - value={banner || ""} - onChange={handleBannerChange} - className="mt-1" - /> - <p className="text-xs text-muted-foreground mt-1">URL to your profile banner image</p> - </div> - </div> - - <div className="grid grid-cols-1 md:grid-cols-2 gap-5"> - <div> - <Label htmlFor="nip05" className="text-sm font-medium flex items-center gap-1"> - <BadgeCheck className="h-4 w-4" /> NIP-05 Identifier - </Label> - <Input - id="nip05" - type="text" - placeholder="_@example.com" - value={nip05 || ""} - onChange={handleNip05Change} - className="mt-1" - /> - <p className="text-xs text-muted-foreground mt-1">Your verified Nostr identifier</p> - </div> - - <div> - <Label htmlFor="lud16" className="text-sm font-medium flex items-center gap-1"> - <Zap className="h-4 w-4" /> Lightning Address - </Label> - <Input - id="lud16" - type="text" - placeholder="you@wallet.com" - value={lud16 || ""} - onChange={handleLud16Change} - className="mt-1" - /> - <p className="text-xs text-muted-foreground mt-1">Your Lightning address for receiving payments</p> - </div> - </div> - - <div> - <Label htmlFor="website" className="text-sm font-medium flex items-center gap-1"> - <Globe className="h-4 w-4" /> Website - </Label> - <Input - id="website" - type="text" - placeholder="https://example.com" - value={website || ""} - onChange={handleWebsiteChange} - className="mt-1" - /> - <p className="text-xs text-muted-foreground mt-1">Your personal website or social media link</p> - </div> - </CardContent> - </Card> - - <div className="flex justify-end gap-3"> - <Button - variant="outline" - onClick={() => window.location.href = `/profile/${npub}`} - > - Cancel - </Button> - <Button - onClick={handleProfileUpdate} - disabled={isSubmitting} - className="min-w-[120px]" - > - {isSubmitting ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Saving... - </> - ) : isSaved ? ( - "Saved!" - ) : ( - "Save Changes" - )} - </Button> - </div> - </div> - ) -} \ No newline at end of file diff --git a/components/UploadComponent.tsx b/components/UploadComponent.tsx deleted file mode 100644 index 2e0971b..0000000 --- a/components/UploadComponent.tsx +++ /dev/null @@ -1,1348 +0,0 @@ -"use client" - -import { useNostr, useNostrEvents } from "nostr-react" -import { nip19, type NostrEvent } from "nostr-tools" -import type React from "react" -import { type ChangeEvent, type FormEvent, useState, useEffect, useCallback } from "react" -import { useSearchParams } from "next/navigation" -import { Button } from "./ui/button" -import { Textarea } from "./ui/textarea" -import { ReloadIcon, UploadIcon, ImageIcon } from "@radix-ui/react-icons" -import { Input } from "./ui/input" -import { encode } from "blurhash" -import { - Drawer, - DrawerContent, - DrawerHeader, - DrawerTitle, - DrawerDescription, - DrawerFooter, -} from "@/components/ui/drawer" -import { Spinner } from "@/components/spinner" -import { signEvent } from "@/utils/utils" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Label } from "@/components/ui/label" -import { Switch } from "@/components/ui/switch" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Separator } from "@/components/ui/separator" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { useCurrentUserPubkey } from "@/utils/relayHooks"; -import { getWriteRelays, getRelayConfig } from "@/utils/nip65Utils"; -import { SimplePool } from "nostr-tools"; -import { createHash } from "crypto"; - -// File type detection functions -const getFileTypeFromUrl = (url: string): string | null => { - try { - const urlObj = new URL(url) - const pathname = urlObj.pathname.toLowerCase() - const extension = pathname.split('.').pop() - - if (!extension) return null - - // Image extensions - const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'apng', 'avif'] - if (imageExtensions.includes(extension)) { - return 'image' - } - - // Video extensions - const videoExtensions = ['mp4', 'webm', 'mov', 'avi', 'm4v', 'mkv', 'm4a'] - if (videoExtensions.includes(extension)) { - return 'video' - } - - return null - } catch { - return null - } -} - -const getFileTypeFromFile = (file: File): string => { - if (file.type.startsWith('image/')) { - return 'image' - } else if (file.type.startsWith('video/') || file.type.startsWith('audio/')) { - return 'video' - } - return 'unknown' -} - -const getKindFromFileType = (fileType: string): string => { - switch (fileType) { - case 'image': - return '20' - case 'video': - return '21' // Default to normal video, user can change to 22 if needed - default: - return '20' // Default fallback - } -} - -const isValidKindForFileType = (kind: string, fileType: string): boolean => { - if (fileType === 'image') { - return kind === '20' - } else if (fileType === 'video') { - return kind === '21' || kind === '22' - } - return false -} - -// Reference validation functions -const isValidHexId = (value: string): boolean => { - return /^[a-fA-F0-9]{64}$/.test(value) -} - -const isValidNoteId = (value: string): boolean => { - return value.startsWith('note') && value.length > 5 -} - -const isValidNEvent = (value: string): boolean => { - return value.startsWith('nevent') && value.length > 7 -} - -const isValidNAddr = (value: string): boolean => { - return value.startsWith('naddr') && value.length > 6 -} - -const isValidUrl = (value: string): boolean => { - try { - new URL(value) - return true - } catch { - return false - } -} - -const validateReference = (type: "e" | "a" | "u", value: string): { isValid: boolean; error?: string } => { - if (!value.trim()) { - return { isValid: true } // Empty is valid (optional field) - } - - // Remove "nostr:" prefix for validation - let valueToValidate = value.trim() - if (valueToValidate.startsWith('nostr:')) { - valueToValidate = valueToValidate.substring(6) - } - - switch (type) { - case "e": - // Accept hex IDs, note..., nevent..., or URLs containing them - if (isValidHexId(valueToValidate) || isValidNoteId(valueToValidate) || isValidNEvent(valueToValidate) || - valueToValidate.includes('note') || valueToValidate.includes('nevent') || /^[a-fA-F0-9]{64}$/.test(valueToValidate)) { - return { isValid: true } - } - return { - isValid: false, - error: "Invalid event reference. Must be a 64-character hex ID, note..., nevent..., or URL containing them" - } - - case "a": - // Accept naddr... or URLs containing them - if (isValidNAddr(valueToValidate) || valueToValidate.includes('naddr')) { - return { isValid: true } - } - return { - isValid: false, - error: "Invalid address reference. Must be an naddr... or URL containing it" - } - - case "u": - if (isValidUrl(value)) { - return { isValid: true } - } - return { - isValid: false, - error: "Invalid URL. Must be a valid URL starting with http:// or https://" - } - - default: - return { isValid: false, error: "Unknown reference type" } - } -} - -// Normalization functions -const normalizeEventReference = (value: string): string => { - // Remove "nostr:" prefix if present - let trimmed = value.trim() - if (trimmed.startsWith('nostr:')) { - trimmed = trimmed.substring(6) - } - - // If it's already a hex ID, return as is - if (isValidHexId(trimmed)) { - return trimmed.toLowerCase() - } - - // If it's a note..., extract the hex ID - if (isValidNoteId(trimmed)) { - try { - const decoded = nip19.decode(trimmed) - if (decoded.type === 'note' && typeof decoded.data === 'object' && decoded.data !== null && 'id' in decoded.data) { - return (decoded.data as { id: string }).id - } - } catch { - // If decoding fails, return as is - return trimmed - } - } - - // If it's a nevent..., extract the hex ID - if (isValidNEvent(trimmed)) { - try { - const decoded = nip19.decode(trimmed) - if (decoded.type === 'nevent' && typeof decoded.data === 'object' && decoded.data !== null && 'id' in decoded.data) { - return (decoded.data as { id: string }).id - } - } catch { - // If decoding fails, return as is - return trimmed - } - } - - // If it's a URL that might contain a note ID, try to extract it - if (trimmed.includes('note') || trimmed.includes('nevent')) { - const noteMatch = trimmed.match(/(note[a-zA-Z0-9]+)/) - const neventMatch = trimmed.match(/(nevent[a-zA-Z0-9]+)/) - - if (noteMatch) { - return normalizeEventReference(noteMatch[1]) - } - if (neventMatch) { - return normalizeEventReference(neventMatch[1]) - } - } - - // If it's a hex ID but with different casing, normalize to lowercase - if (/^[a-fA-F0-9]{64}$/.test(trimmed)) { - return trimmed.toLowerCase() - } - - return trimmed -} - -const normalizeAddressReference = (value: string): string => { - // Remove "nostr:" prefix if present - let trimmed = value.trim() - if (trimmed.startsWith('nostr:')) { - trimmed = trimmed.substring(6) - } - - // If it's already an naddr..., return as is - if (isValidNAddr(trimmed)) { - return trimmed - } - - // If it's a URL that might contain an naddr, try to extract it - if (trimmed.includes('naddr')) { - const naddrMatch = trimmed.match(/(naddr[a-zA-Z0-9]+)/) - if (naddrMatch) { - return naddrMatch[1] - } - } - - return trimmed -} - -const normalizeUrl = (value: string): string => { - const trimmed = value.trim() - - try { - const url = new URL(trimmed) - // Normalize to lowercase protocol and hostname - url.protocol = url.protocol.toLowerCase() - url.hostname = url.hostname.toLowerCase() - // Remove trailing slash from pathname if it's just a slash - if (url.pathname === '/') { - url.pathname = '' - } - return url.toString() - } catch { - return trimmed - } -} - -const normalizeReference = (type: "e" | "a" | "u", value: string): string => { - switch (type) { - case "e": - return normalizeEventReference(value) - case "a": - return normalizeAddressReference(value) - case "u": - return normalizeUrl(value) - default: - return value - } -} - -// Function to strip metadata from image files -async function stripImageMetadata(file: File): Promise<File> { - return new Promise((resolve, reject) => { - const img = new Image() - const objectUrl = URL.createObjectURL(file) - - img.onload = () => { - // Create a canvas to draw the image without metadata - const canvas = document.createElement("canvas") - canvas.width = img.width - canvas.height = img.height - - // Draw the image onto the canvas (this strips the metadata) - const ctx = canvas.getContext("2d") - if (!ctx) { - URL.revokeObjectURL(objectUrl) - reject(new Error("Failed to get canvas context")) - return - } - - ctx.drawImage(img, 0, 0) - - // Convert canvas back to a file - canvas.toBlob((blob) => { - // Clean up the object URL - URL.revokeObjectURL(objectUrl) - - if (!blob) { - reject(new Error("Failed to create blob from canvas")) - return - } - - // Create a new file with the same name but stripped metadata - const strippedFile = new File([blob], file.name, { - type: file.type, - lastModified: file.lastModified, - }) - - resolve(strippedFile) - }, file.type) - } - - img.onerror = () => { - URL.revokeObjectURL(objectUrl) - reject(new Error("Failed to load image")) - } - - img.src = objectUrl - }) -} - -async function calculateBlurhash(file: File): Promise<string> { - return new Promise((resolve, reject) => { - const canvas = document.createElement("canvas") - const ctx = canvas.getContext("2d") - const img = new Image() - img.onload = () => { - canvas.width = 32 - canvas.height = 32 - ctx?.drawImage(img, 0, 0, 32, 32) - const imageData = ctx?.getImageData(0, 0, 32, 32) - if (imageData) { - const blurhash = encode(imageData.data, imageData.width, imageData.height, 4, 4) - resolve(blurhash) - } else { - reject(new Error("Failed to get image data")) - } - } - img.onerror = reject - img.src = URL.createObjectURL(file) - }) -} - -const UploadComponent: React.FC = () => { - const searchParams = useSearchParams() - const currentUserPubkey = useCurrentUserPubkey() - const { publish } = useNostr() - const [previewUrl, setPreviewUrl] = useState("") - const [imageUrl, setImageUrl] = useState("") - const [title, setTitle] = useState("") - const [selectedKind, setSelectedKind] = useState("20") - const [serverChoice, setServerChoice] = useState("blossom.band") - const [enableNip89, setEnableNip89] = useState(false) - const [referenceType, setReferenceType] = useState<"e" | "a" | "u">("e") - const [referenceValue, setReferenceValue] = useState("") - const [detectedFileType, setDetectedFileType] = useState<string | null>(null) - const [uploadMethod, setUploadMethod] = useState<"file" | "url">("file") - const [thumbnailUrl, setThumbnailUrl] = useState("") // New state for video thumbnails - const [isLoading, setIsLoading] = useState(false) - const [isDrawerOpen, setIsDrawerOpen] = useState(false) - const [uploadedNoteId, setUploadedNoteId] = useState("") - const [retryCount, setRetryCount] = useState(0) - const [shouldFetch, setShouldFetch] = useState(false) - - // Add state for client-side authentication info - const [loginType, setLoginType] = useState<string | null>(null) - const [isClient, setIsClient] = useState(false) - - // Use useEffect to handle client-side localStorage access - useEffect(() => { - setIsClient(true) - const storedLoginType = window.localStorage.getItem("loginType") - setLoginType(storedLoginType) - }, []) - - const { events, isLoading: isNoteLoading } = useNostrEvents({ - filter: shouldFetch - ? { - ids: uploadedNoteId ? [uploadedNoteId] : [], - kinds: [parseInt(selectedKind)], - limit: 1, - } - : { ids: [], kinds: [parseInt(selectedKind)], limit: 1 }, - enabled: shouldFetch, - }) - - useEffect(() => { - if (uploadedNoteId) { - setShouldFetch(true) - } - }, [uploadedNoteId]) - - useEffect(() => { - let timeoutId: NodeJS.Timeout - - if (shouldFetch && events.length === 0 && !isNoteLoading) { - timeoutId = setTimeout(() => { - setRetryCount((prevCount) => prevCount + 1) - setShouldFetch(false) - setShouldFetch(true) - }, 5000) // Retry every 5 seconds - } - - return () => { - if (timeoutId) { - clearTimeout(timeoutId) - } - } - }, [shouldFetch, events, isNoteLoading]) - - const handleRetry = useCallback(() => { - setRetryCount((prevCount) => prevCount + 1) - setShouldFetch(false) - setShouldFetch(true) - }, []) - - const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => { - const file = event.target.files?.[0] - if (file) { - const url = URL.createObjectURL(file) - if (url.startsWith("blob:")) { - setPreviewUrl(url) - } - - // Detect file type and auto-select kind - const fileType = getFileTypeFromFile(file) - setDetectedFileType(fileType) - - if (fileType !== 'unknown') { - const suggestedKind = getKindFromFileType(fileType) - setSelectedKind(suggestedKind) - } - - // Optional: Bereinigung alter URLs - return () => URL.revokeObjectURL(url) - } - } - - const handleUrlChange = (event: ChangeEvent<HTMLInputElement>) => { - const url = event.target.value - setImageUrl(url) - setPreviewUrl(url) - - // Detect file type from URL and auto-select kind - if (url) { - const fileType = getFileTypeFromUrl(url) - setDetectedFileType(fileType) - - if (fileType) { - const suggestedKind = getKindFromFileType(fileType) - setSelectedKind(suggestedKind) - } - } else { - setDetectedFileType(null) - } - } - - const handleTextChange = (event: ChangeEvent<HTMLTextAreaElement>) => { - const { value } = event.target - - // Replace links only if they contain https://lumina.rocks - let updatedValue = value; - - // Replace https://lumina.rocks/profile/npub... with "nostr:npub..." - updatedValue = updatedValue.replace(/https:\/\/lumina\.rocks\/profile\/(npub[1-9a-zA-Z]{0,64})/g, "nostr:$1"); - - // Replace https://lumina.rocks/note/note... with "nostr:note..." - updatedValue = updatedValue.replace(/https:\/\/lumina\.rocks\/note\/(note[1-9a-zA-Z]{0,64})/g, "nostr:$1"); - - // Update the textarea with the modified value - event.target.value = updatedValue; - - return updatedValue - } - - const handleServerChange = (value: string) => { - setServerChoice(value) - } - - const handleKindChange = (value: string) => { - // Validate that the selected kind is compatible with the detected file type - if (detectedFileType && !isValidKindForFileType(value, detectedFileType)) { - alert(`Invalid kind selection: Kind ${value} is not compatible with ${detectedFileType} files.`) - return - } - setSelectedKind(value) - } - - const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => { - setTitle(event.target.value) - } - - const handleReferenceTypeChange = (value: string) => { - setReferenceType(value as "e" | "a" | "u") - setReferenceValue("") // Clear the value when type changes - } - - const handleReferenceValueChange = (event: ChangeEvent<HTMLInputElement>) => { - setReferenceValue(event.target.value) - } - - const handleThumbnailUrlChange = (event: ChangeEvent<HTMLInputElement>) => { - setThumbnailUrl(event.target.value) - } - - async function onSubmit(event: FormEvent<HTMLFormElement>) { - event.preventDefault() - - // Ensure client is ready - if (!isClient) { - alert("Please wait for the page to load completely before uploading.") - return - } - - setIsLoading(true) - - // Check if user is authenticated first - const pubkey = window.localStorage.getItem("pubkey") - - console.log("Authentication check:", { pubkey, loginType }) - - if (!loginType || !pubkey) { - alert("You must be logged in to upload files. Please log in and try again.") - setIsLoading(false) - return - } - - const formData = new FormData(event.currentTarget) - const desc = formData.get("description") as string - const title = formData.get("title") as string - let file = formData.get("file") as File - let sha256 = "" - let finalNoteContent = desc - let finalFileUrl = "" - console.log("File:", file) - console.log("File type:", typeof file) - console.log("File is null:", file === null) - console.log("File is undefined:", file === undefined) - - const hasFile = file && file.size && file.size > 0 - if (!desc && !hasFile && !imageUrl) { - alert("Please enter a description and/or upload a file or provide an image URL") - setIsLoading(false) - return - } - - // Validate kind and file type compatibility - if (detectedFileType && !isValidKindForFileType(selectedKind, detectedFileType)) { - alert(`Invalid combination: Kind ${selectedKind} cannot be used with ${detectedFileType} files.`) - setIsLoading(false) - return - } - - // Validate reference if provided - if (referenceValue.trim()) { - const validation = validateReference(referenceType, referenceValue) - if (!validation.isValid) { - alert(validation.error) - setIsLoading(false) - return - } - } - - // get every hashtag in desc and cut off the # symbol - let hashtags: string[] = desc.match(/#[a-zA-Z0-9]+/g) || [] - if (hashtags) { - hashtags = hashtags.map((hashtag) => hashtag.slice(1)) - } - - // If file is present, upload it to the media server - if (file) { - const readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> => { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = () => resolve(reader.result as ArrayBuffer) - reader.onerror = () => reject(reader.error) - reader.readAsArrayBuffer(file) - }) - } - - try { - // Strip metadata from the file - file = await stripImageMetadata(file) - - const arrayBuffer = await readFileAsArrayBuffer(file) - const hashBuffer = createHash("sha256").update(new Uint8Array(arrayBuffer)).digest() - sha256 = hashBuffer.toString("hex") - - const unixNow = () => Math.floor(Date.now() / 1000) - const newExpirationValue = () => (unixNow() + 60 * 5).toString() - - const pubkey = window.localStorage.getItem("pubkey") - const createdAt = Math.floor(Date.now() / 1000) - - // alert("SHA256: " + sha256) - - // Create auth event for blossom auth via nostr - const authEvent: NostrEvent = { - kind: 24242, - // content: desc, - content: "File upload", - created_at: createdAt, - tags: [ - // ["t", "media"], - ["t", "upload"], - ["x", sha256], - ["expiration", newExpirationValue()], - ], - pubkey: "", // Add a placeholder for pubkey - id: "", // Add a placeholder for id - sig: "", // Add a placeholder for sig - } - - console.log(authEvent) - console.log("Login type:", loginType) - console.log("Pubkey from localStorage:", pubkey) - - // Sign auth event - let authEventSigned: NostrEvent - try { - const signedEvent = await signEvent(loginType, authEvent) - if (!signedEvent) { - throw new Error("Failed to sign event - no signed event returned") - } - authEventSigned = signedEvent - } catch (error) { - console.error("Error signing event:", error) - alert(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}. Please check your login and try again.`) - setIsLoading(false) - return - } - - // authEventSigned as base64 encoded string - const authString = btoa(JSON.stringify(authEventSigned)) - - const blossomServer = "https://" + serverChoice - - // await fetch(blossomServer + "/media", { - await fetch(blossomServer + "/upload", { - method: "PUT", - body: file, - headers: { authorization: "Nostr " + authString }, - }).then(async (res) => { - if (res.ok) { - const responseText = await res.text() - const responseJson = JSON.parse(responseText) - finalFileUrl = responseJson.url - sha256 = responseJson.sha256 - - const noteTags = [ - ...(title ? [["title", title]] : []), - ...hashtags.map((tag) => ["t", tag]), - ...(referenceValue.trim() ? [[referenceType, normalizeReference(referenceType, referenceValue)]] : []) - ] - - let blurhash = "" - if (selectedKind === "20" && file && file.type.startsWith("image/")) { - try { - blurhash = await calculateBlurhash(file) - } catch (error) { - console.error("Error calculating blurhash:", error) - } - } - - if (finalFileUrl) { - const image = new Image() - image.src = URL.createObjectURL(file) - await new Promise((resolve) => { - image.onload = resolve - }) - - finalNoteContent = desc - - // Add imeta tag based on kind - if (selectedKind === "20") { - // Picture event - use imeta with image-specific properties - noteTags.push([ - "imeta", - "url " + finalFileUrl, - "m " + file.type, - "x " + sha256, - "blurhash " + blurhash, - `dim ${image.width}x${image.height}`, - ]) - noteTags.push(["x", sha256]) - noteTags.push(["m", file.type]) - } else if (selectedKind === "21" || selectedKind === "22") { - // Video events - use imeta with video-specific properties - const videoImetaTags = [ - "imeta", - `dim ${image.width}x${image.height}`, - "url " + finalFileUrl, - "x " + sha256, - "m " + file.type, - ] - - // Add thumbnail URL as image field if provided - if (thumbnailUrl) { - videoImetaTags.push("image " + thumbnailUrl) - } - - noteTags.push(videoImetaTags) - noteTags.push(["x", sha256]) - } - } - - const createdAt = Math.floor(Date.now() / 1000) - - // NIP-89 client tagging (optional) - if (enableNip89) { - noteTags.push([ - "client", - "lumina", - "31990:" + "ff363e4afc398b7dd8ceb0b2e73e96fe9621ababc22ab150ffbb1aa0f34df8b2" + ":" + createdAt, - ]) - } - - // Create the actual note - const noteEvent: NostrEvent = { - kind: parseInt(selectedKind), - content: finalNoteContent, - created_at: createdAt, - tags: noteTags, - pubkey: "", // Add a placeholder for pubkey - id: "", // Add a placeholder for id - sig: "", // Add a placeholder for sig - } - - console.log("Created note event:", { - kind: noteEvent.kind, - content: noteEvent.content, - tags: noteEvent.tags, - created_at: noteEvent.created_at - }) - - let signedEvent: NostrEvent | null = null - - // Sign the actual note - try { - const signedNoteEvent = await signEvent(loginType, noteEvent) - if (!signedNoteEvent) { - throw new Error("Failed to sign note event - no signed event returned") - } - signedEvent = signedNoteEvent - } catch (error) { - console.error("Error signing note event:", error) - // Don't show alert for this error since the auth event already succeeded - // Just log it and continue if possible - console.warn("Note signing failed, but continuing...") - } - - // If we got a signed event, publish it to nostr - if (signedEvent) { - console.log("final Event: ") - console.log(signedEvent) - - try { - // Publish using NostrProvider - console.log("Publishing using NostrProvider...") - console.log("Current user pubkey:", currentUserPubkey) - publish(signedEvent); - console.log("Successfully published using NostrProvider") - } catch (publishError) { - console.error("Error publishing:", publishError) - alert(`Failed to publish to relays: ${publishError instanceof Error ? publishError.message : 'Unknown error'}`) - setIsLoading(false) - return - } - // alert(JSON.stringify(signedEvent)) - } else { - console.error("No signed event available for publishing") - alert("Failed to sign the event. Please check your login and try again.") - setIsLoading(false) - return - } - - setIsLoading(false) - if (signedEvent != null) { - setUploadedNoteId(signedEvent.id) - setIsDrawerOpen(true) - setShouldFetch(true) - setRetryCount(0) - } - } else { - // alert(await res.text()) - throw new Error("Failed to upload file: " + (await res.text())) - } - }) - } catch (error) { - alert(error) - console.error("Error reading file:", error) - setIsLoading(false) - } - } else if (imageUrl) { - // Handle image URL upload - try { - const createdAt = Math.floor(Date.now() / 1000) - const noteTags = [ - ...(title ? [["title", title]] : []), - ...hashtags.map((tag) => ["t", tag]), - ...(referenceValue.trim() ? [[referenceType, normalizeReference(referenceType, referenceValue)]] : []) - ] - - // Add the image URL directly to the note - finalNoteContent = desc - - // Add imeta tag based on kind - if (selectedKind === "20") { - // Picture event - use imeta with image-specific properties - noteTags.push(["imeta", "url " + imageUrl]) - } else if (selectedKind === "21" || selectedKind === "22") { - // Video events - use imeta with video-specific properties - const videoImetaTags = ["imeta", "url " + imageUrl] - - // Add thumbnail URL as image field if provided - if (thumbnailUrl) { - videoImetaTags.push("image " + thumbnailUrl) - } - - noteTags.push(videoImetaTags) - } - - // NIP-89 client tagging (optional) - if (enableNip89) { - noteTags.push([ - "client", - "lumina", - "31990:" + "ff363e4afc398b7dd8ceb0b2e73e96fe9621ababc22ab150ffbb1aa0f34df8b2" + ":" + createdAt, - ]) - } - - // Create the actual note - const noteEvent: NostrEvent = { - kind: parseInt(selectedKind), - content: finalNoteContent, - created_at: createdAt, - tags: noteTags, - pubkey: "", // Add a placeholder for pubkey - id: "", // Add a placeholder for id - sig: "", // Add a placeholder for sig - } - - console.log("Created note event (image URL):", { - kind: noteEvent.kind, - content: noteEvent.content, - tags: noteEvent.tags, - created_at: noteEvent.created_at - }) - - let signedEvent: NostrEvent | null = null - - // Sign the actual note - try { - const signedNoteEvent = await signEvent(loginType, noteEvent) - if (!signedNoteEvent) { - throw new Error("Failed to sign note event - no signed event returned") - } - signedEvent = signedNoteEvent - } catch (error) { - console.error("Error signing note event:", error) - // Don't show alert for this error since the auth event already succeeded - // Just log it and continue if possible - console.warn("Note signing failed, but continuing...") - } - - // If we got a signed event, publish it to nostr - if (signedEvent) { - console.log("final Event: ") - console.log(signedEvent) - - try { - // Publish using NostrProvider - console.log("Publishing using NostrProvider...") - console.log("Current user pubkey:", currentUserPubkey) - publish(signedEvent); - console.log("Successfully published using NostrProvider") - } catch (publishError) { - console.error("Error publishing:", publishError) - alert(`Failed to publish to relays: ${publishError instanceof Error ? publishError.message : 'Unknown error'}`) - setIsLoading(false) - return - } - } else { - console.error("No signed event available for publishing") - alert("Failed to sign the event. Please check your login and try again.") - setIsLoading(false) - return - } - - setIsLoading(false) - if (signedEvent != null) { - setUploadedNoteId(signedEvent.id) - setIsDrawerOpen(true) - setShouldFetch(true) - setRetryCount(0) - } - } catch (error) { - alert(error) - console.error("Error processing image URL:", error) - setIsLoading(false) - } - } - } - - return ( - <> - <Card className="w-full max-w-2xl mx-auto shadow-md"> - <CardHeader> - <CardTitle>Share Content</CardTitle> - <CardDescription> - {detectedFileType - ? `${detectedFileType === 'image' - ? 'Upload an image' - : 'Upload a video'} with your description to the Nostr network (Kind ${selectedKind})` - : selectedKind === "20" - ? "Upload an image with your description to the Nostr network" - : selectedKind === "21" - ? "Upload a normal video with your description to the Nostr network" - : "Upload a short video with your description to the Nostr network" - } - </CardDescription> - </CardHeader> - <CardContent> - {/* Debug section - only show in development */} - {process.env.NODE_ENV === 'development' && isClient && ( - <div className="mb-4 p-3 bg-gray-100 rounded text-xs"> - <div className="font-bold mb-2">Debug Info:</div> - <div>Login Type: {loginType || 'Not logged in'}</div> - <div>Pubkey: {currentUserPubkey ? `${currentUserPubkey.slice(0, 10)}...` : 'None'}</div> - <div>Write Relays: {isClient ? getWriteRelays(currentUserPubkey || undefined).length : 0}</div> - <button - onClick={() => { - console.log("Current relay config:", getRelayConfig()) - console.log("Write relays:", getWriteRelays(currentUserPubkey || undefined)) - }} - className="text-blue-600 underline" - > - Log relay config to console - </button> - <button - onClick={async () => { - const relays = getWriteRelays(currentUserPubkey || undefined) - console.log("Testing relay connections...") - for (const relay of relays) { - try { - const pool = new SimplePool() - const testEvent = { - kind: 1, - content: "Test message", - created_at: Math.floor(Date.now() / 1000), - tags: [], - pubkey: "test", - id: "test", - sig: "test" - } - await pool.publish([relay], testEvent) - console.log(`✅ ${relay} - Connected`) - pool.close([relay]) - } catch (error) { - console.log(`❌ ${relay} - Failed:`, error) - } - } - }} - className="text-blue-600 underline ml-2" - > - Test relay connections - </button> - </div> - )} - - {!isClient ? ( - <div className="flex items-center justify-center py-8"> - <div className="flex items-center space-x-2"> - <ReloadIcon className="h-4 w-4 animate-spin" /> - <span>Loading...</span> - </div> - </div> - ) : ( - <form className="space-y-6" onSubmit={onSubmit}> - <div className="space-y-2"> - <Label htmlFor="title">Title</Label> - <Input - name="title" - placeholder="Enter a title for your post" - id="title" - className="w-full" - value={title} - onChange={handleTitleChange} - - /> - </div> - - <div className="space-y-2"> - <Label htmlFor="description">Description</Label> - <Textarea - name="description" - rows={4} - placeholder="What's on your mind? Add #hashtags to categorize your post." - id="description" - className="w-full resize-none" - onChange={handleTextChange} - /> - </div> - - <div className="space-y-2"> - <Label>{selectedKind === "20" ? "Image" : "Video"}</Label> - <Tabs defaultValue="file" searchParam="upload-method"> - <TabsList className="grid w-full grid-cols-2"> - <TabsTrigger value="file">Upload File</TabsTrigger> - <TabsTrigger value="url">Media URL</TabsTrigger> - </TabsList> - - <TabsContent value="file" className="space-y-4"> - <div className="border-2 border-dashed rounded-lg p-6 transition-colors hover:border-primary/50 hover:bg-muted/50"> - <div className="flex flex-col items-center space-y-4 text-center"> - {previewUrl ? ( - <div className="w-full rounded-md"> - {selectedKind === "20" ? ( - <img - src={previewUrl} - alt="Preview" - /> - ) : ( - <video - src={previewUrl} - controls - className="w-full rounded-md" - /> - )} - </div> - ) : ( - <ImageIcon className="h-10 w-10 text-muted-foreground" /> - )} - - <div className="space-y-2"> - <div className="text-sm font-medium"> - {previewUrl - ? `Replace ${selectedKind === "20" ? "image" : "video"}` - : `Add ${selectedKind === "20" ? "image" : "video"}` - } - </div> - <div className="text-xs text-muted-foreground"> - {selectedKind === "20" - ? "Supported formats: JPEG, PNG, WebP, GIF, APNG, AVIF" - : "Supported formats: MP4, WebM, MOV, AVI, M4V, MKV, M4A" - } - </div> - </div> - - <label - htmlFor="file" - className={`relative cursor-pointer rounded-md px-4 py-2 text-sm font-medium ring-offset-background transition-colors - ${previewUrl ? 'bg-muted hover:bg-muted/80' : 'bg-primary text-primary-foreground hover:bg-primary/90'}`} - > - {previewUrl ? "Change file" : `Select ${selectedKind === "20" ? "image" : "video"}`} - <Input - id="file" - name="file" - type="file" - accept={selectedKind === "20" - ? "image/jpeg,image/png,image/webp,image/gif,image/apng,image/avif" - : "video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-m4v,video/x-matroska,audio/mp4" - } - onChange={handleFileChange} - className="sr-only" - /> - </label> - </div> - </div> - </TabsContent> - - <TabsContent value="url" className="space-y-4"> - <div className="space-y-2"> - <Label htmlFor="image-url">{selectedKind === "20" ? "Image URL" : "Video URL"}</Label> - <Input - id="image-url" - name="image-url" - type="url" - placeholder={selectedKind === "20" - ? "https://example.com/image.jpg" - : "https://example.com/video.mp4" - } - value={imageUrl} - onChange={handleUrlChange} - className="w-full" - /> - </div> - - {previewUrl && ( - <div className="border rounded-lg p-4"> - <div className="text-sm font-medium mb-2">Preview:</div> - {selectedKind === "20" ? ( - <img - src={previewUrl} - alt="Preview" - className="w-full rounded-md" - onError={() => setPreviewUrl("")} - /> - ) : ( - <video - src={previewUrl} - controls - className="w-full rounded-md" - onError={() => setPreviewUrl("")} - /> - )} - </div> - )} - </TabsContent> - </Tabs> - </div> - - <Separator className="my-4" /> - - {/* Thumbnail URL field for video events */} - {(selectedKind === "21" || selectedKind === "22") && ( - <div className="space-y-2"> - <Label htmlFor="thumbnail-url">Video Thumbnail Image URL (Optional)</Label> - <Input - id="thumbnail-url" - name="thumbnail-url" - type="url" - placeholder="https://example.com/thumbnail.jpg" - value={thumbnailUrl} - onChange={handleThumbnailUrlChange} - className="w-full" - /> - <p className="text-xs text-muted-foreground"> - Provide an image URL to use as a thumbnail/preview for your video. This will be displayed in galleries and feeds. - </p> - </div> - )} - - <div className="space-y-4"> - <div className="flex flex-row items-center justify-between"> - <div className="flex flex-col space-y-1"> - <Label htmlFor="kind-choice">Note Kind</Label> - <p className="text-xs text-muted-foreground"> - {detectedFileType - ? `Detected: ${detectedFileType} file - ${detectedFileType === 'image' ? 'Use Kind 20 for images' : 'Use Kind 21/22 for videos'}` - : "Choose the type of note to publish" - } - {detectedFileType && ( - <span className="ml-1 text-xs text-green-600 font-medium"> - (Auto-selected) - </span> - )} - </p> - </div> - <Select onValueChange={handleKindChange} value={selectedKind}> - <SelectTrigger className="w-[180px]"> - <SelectValue placeholder={selectedKind} /> - </SelectTrigger> - <SelectContent> - <SelectItem value="20" disabled={detectedFileType === 'video'}> - Kind 20 - Picture Event - </SelectItem> - <SelectItem value="21" disabled={detectedFileType === 'image'}> - Kind 21 - Normal Video - </SelectItem> - <SelectItem value="22" disabled={detectedFileType === 'image'}> - Kind 22 - Short Video - </SelectItem> - </SelectContent> - </Select> - </div> - - <div className="flex flex-row items-center justify-between"> - <div className="flex flex-col space-y-1"> - <Label htmlFor="server-choice">Upload destination</Label> - <p className="text-xs text-muted-foreground">Choose where to store your image</p> - </div> - <Select onValueChange={handleServerChange} value={serverChoice}> - <SelectTrigger className="w-[180px]"> - <SelectValue placeholder={serverChoice} /> - </SelectTrigger> - <SelectContent> - <SelectItem value="blossom.band">blossom.band</SelectItem> - <SelectItem value="blossom.primal.net">blossom.primal.net</SelectItem> - </SelectContent> - </Select> - </div> - - <div className="flex items-center justify-between"> - <div className="flex flex-col space-y-1"> - <Label htmlFor="nip89-toggle">Client tagging</Label> - <p className="text-xs text-muted-foreground">Enable NIP-89 client identification</p> - </div> - <Switch id="nip89-toggle" checked={enableNip89} onCheckedChange={setEnableNip89} /> - </div> - - <div className="space-y-3"> - <div className="flex flex-row items-center justify-between"> - <div className="flex flex-col space-y-1"> - <Label htmlFor="reference-type">Reference Type</Label> - <p className="text-xs text-muted-foreground">Add a reference to another event, address, or URL</p> - </div> - <Select onValueChange={handleReferenceTypeChange} value={referenceType}> - <SelectTrigger className="w-[180px]"> - <SelectValue placeholder={referenceType} /> - </SelectTrigger> - <SelectContent> - <SelectItem value="e">Event (e)</SelectItem> - <SelectItem value="a">Address (a)</SelectItem> - <SelectItem value="u">URL (u)</SelectItem> - </SelectContent> - </Select> - </div> - - <div className="space-y-2"> - <Label htmlFor="reference-value"> - Reference Value - {referenceType === "e" && " (nostr:note..., note..., nevent..., hex ID, or URL)"} - {referenceType === "a" && " (nostr:naddr..., naddr..., or URL)"} - {referenceType === "u" && " (URL)"} - </Label> - <Input - id="reference-value" - name="reference-value" - type="text" - placeholder={ - referenceType === "e" - ? "nostr:note... or note... or nevent... or hex ID or URL" - : referenceType === "a" - ? "nostr:naddr... or naddr... or URL containing naddr" - : "https://example.com" - } - value={referenceValue} - onChange={handleReferenceValueChange} - className="w-full" - /> - {referenceValue.trim() && ( - <div className="space-y-1"> - <p className={`text-xs ${validateReference(referenceType, referenceValue).isValid ? 'text-green-600' : 'text-red-600'}`}> - {validateReference(referenceType, referenceValue).isValid - ? "✓ Valid reference" - : validateReference(referenceType, referenceValue).error - } - </p> - {validateReference(referenceType, referenceValue).isValid && ( - <p className="text-xs text-blue-600"> - Will be stored as: {normalizeReference(referenceType, referenceValue)} - </p> - )} - </div> - )} - </div> - </div> - </div> - - <div className="pt-4 space-y-2"> - {isLoading ? ( - <Button className="w-full" disabled> - <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> - {uploadMethod === "file" ? "Uploading..." : "Publishing..."} - </Button> - ) : !isClient ? ( - <Button className="w-full" disabled> - <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> - Loading... - </Button> - ) : ( - <> - <Button type="submit" className="w-full"> - <UploadIcon className="mr-2 h-4 w-4" /> - Share to Nostr - </Button> - <Button - type="button" - variant="outline" - className="w-full" - onClick={() => { - // Reset form and close modal - setTitle("") - setImageUrl("") - setPreviewUrl("") - setDetectedFileType(null) - setSelectedKind("20") - setReferenceType("e") - setReferenceValue("") - setIsLoading(false) - }} - > - Cancel - </Button> - </> - )} - </div> - </form> - )} - </CardContent> - </Card> - - <Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Upload Status</DrawerTitle> - <DrawerDescription> - {isNoteLoading ? ( - <span className="flex items-center space-x-2"> - <Spinner /> - <span>Checking note status...</span> - </span> - ) : events.length > 0 ? ( - <span - className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative block" - role="alert" - > - <strong className="font-bold">Success!</strong> - <span className="block sm:inline"> Note found with ID: </span> - <span className="block sm:inline font-mono"> - {`${events[0].id.slice(0, 5)}...${events[0].id.slice(-3)}`} - </span> - </span> - ) : ( - <span>Note not found. It may take a moment to propagate.</span> - )} - </DrawerDescription> - </DrawerHeader> - <DrawerFooter className="flex flex-col space-y-2"> - {events.length === 0 && ( - <Button onClick={handleRetry} variant="outline" className="w-full"> - Retry Now - </Button> - )} - <Button asChild className="w-full"> - <a href={`/note/${nip19.neventEncode({ - id: uploadedNoteId, - relays: [] - })}`}>View Note</a> - </Button> - <Button variant="outline" onClick={() => setIsDrawerOpen(false)} className="w-full"> - Close - </Button> - </DrawerFooter> - </DrawerContent> - </Drawer> - </> - ) -} - -export default UploadComponent \ No newline at end of file diff --git a/components/ViewCopyButton.tsx b/components/ViewCopyButton.tsx deleted file mode 100644 index 6d0b27e..0000000 --- a/components/ViewCopyButton.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { Textarea } from "./ui/textarea"; -import { Share1Icon } from "@radix-ui/react-icons"; -import { Input } from "./ui/input"; -import React, { useRef } from 'react'; -import { useToast } from "./ui/use-toast"; -import { Event as NostrEvent, nip19 } from "nostr-tools"; - -interface ViewCopyButtonProps { - event: NostrEvent; -} - -export default function ViewCopyButton({ event }: ViewCopyButtonProps) { - const jsonEvent = JSON.stringify(event, null, 2); - const inputRef = useRef(null); - const inputRefID = useRef(null); - const { toast } = useToast() - - const handleCopyLink = async () => { - try { - await navigator.clipboard.writeText(window.location.href); - toast({ - description: 'URL copied to clipboard', - title: 'Copied' - }); - } catch (err) { - toast({ - description: 'Error copying URL to clipboard', - title: 'Error', - variant: 'destructive' - }); - } - }; - - const handleCopyNoteId = async () => { - try { - // Create nevent with relay hints - const nevent = nip19.neventEncode({ - id: event.id, - relays: [] - }); - await navigator.clipboard.writeText(nevent); - toast({ - description: 'Event ID copied to clipboard', - title: 'Copied' - }); - } catch (err) { - toast({ - description: 'Error copying Event ID to clipboard', - title: 'Error', - variant: 'destructive' - }); - } - }; - - return ( - <Drawer> - <DrawerTrigger asChild> - <Button variant="outline"><Share1Icon /></Button> - </DrawerTrigger> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Share this Note</DrawerTitle> - <DrawerDescription>Share this Note with others.</DrawerDescription> - </DrawerHeader> - <div className="px-2"> - {/* <h1>URL</h1> */} - <div className="flex items-center mb-4"> - <Input ref={inputRef} value={window.location.href} disabled className="mr-2" /> - <Button variant="outline" onClick={handleCopyLink}>Copy Link</Button> - </div> - <div className="flex items-center mb-4"> - <Input ref={inputRefID} value={nip19.neventEncode({ - id: event.id, - relays: [] - })} disabled className="mr-2" /> - <Button variant="outline" onClick={handleCopyNoteId}>Copy Event ID</Button> - </div> - </div> - <DrawerFooter> - <DrawerClose asChild> - <div> - <Button variant="outline">Close</Button> - </div> - </DrawerClose> - </DrawerFooter> - </DrawerContent> - </Drawer> - ); -} \ No newline at end of file diff --git a/components/ViewNoteButton.tsx b/components/ViewNoteButton.tsx deleted file mode 100644 index 0e18616..0000000 --- a/components/ViewNoteButton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { SizeIcon } from "@radix-ui/react-icons"; -import Link from "next/link"; -import { nip19 } from "nostr-tools"; - -export default function ViewNoteButton({ event }: { event: any }) { - // Create nevent with relay hints - const nevent = nip19.neventEncode({ - id: event.id, - relays: event.relays || [] - }); - return ( - <Link href={'/note/' + nevent} passHref> - <Button variant="secondary"><SizeIcon /></Button> - </Link> - ); -} \ No newline at end of file diff --git a/components/ViewRawButton.tsx b/components/ViewRawButton.tsx deleted file mode 100644 index 430dd0e..0000000 --- a/components/ViewRawButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { Textarea } from "./ui/textarea"; -import { CodeIcon } from "@radix-ui/react-icons"; -import { Event as NostrEvent } from "nostr-tools"; - -interface ViewRawButtonProps { - event: NostrEvent; -} - -export default function ViewRawButton({ event }: ViewRawButtonProps) { - return ( - <Drawer> - <DrawerTrigger asChild> - <Button variant="outline"><CodeIcon /></Button> - </DrawerTrigger> - <DrawerContent> - <DrawerHeader> - <DrawerTitle>Raw Event</DrawerTitle> - <DrawerDescription>This shows the raw event data.</DrawerDescription> - </DrawerHeader> - <Textarea rows={20} disabled>{JSON.stringify(event, null, 2)}</Textarea> - <DrawerFooter> - <DrawerClose asChild> - <div> - <Button variant="outline">Close</Button> - </div> - </DrawerClose> - </DrawerFooter> - </DrawerContent> - </Drawer> - - ); -} \ No newline at end of file diff --git a/components/WelcomeContent.tsx b/components/WelcomeContent.tsx deleted file mode 100644 index e8e5866..0000000 --- a/components/WelcomeContent.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card'; -import { ImageIcon, Users, Zap, Shield, Globe } from 'lucide-react'; - -export function WelcomeContent() { - return ( - <div className="max-w-4xl mx-auto py-6 px-6 space-y-6"> - {/* Main Welcome Section */} - <div className="text-center space-y-4"> - <h1 className="text-4xl font-bold tracking-tight">Welcome to LUMINA</h1> - <p className="text-xl text-muted-foreground max-w-2xl mx-auto"> - A decentralized social media platform for sharing images, built on the Nostr protocol - </p> - </div> - - {/* What is LUMINA Section */} - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <ImageIcon className="h-6 w-6 text-primary" /> - What is LUMINA? - </CardTitle> - <CardDescription> - Your creative freedom, decentralized - </CardDescription> - </CardHeader> - <CardContent className="space-y-4"> - <p className="text-muted-foreground"> - LUMINA is a social media image platform that puts you in complete control of your content and connections. - Share your photos, discover amazing content, and connect with creators from around the world. All without - relying on centralized servers or corporations. - </p> - <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6"> - <div className="flex flex-col items-center text-center p-4 rounded-lg bg-muted"> - <Users className="h-8 w-8 text-primary mb-2" /> - <h3 className="font-semibold">Social Connection</h3> - <p className="text-sm text-muted-foreground">Follow creators and build communities</p> - </div> - <div className="flex flex-col items-center text-center p-4 rounded-lg bg-muted"> - <Zap className="h-8 w-8 text-primary mb-2" /> - <h3 className="font-semibold">Bitcoin Integration</h3> - <p className="text-sm text-muted-foreground">Support creators with Lightning Network</p> - </div> - <div className="flex flex-col items-center text-center p-4 rounded-lg bg-muted"> - <Shield className="h-8 w-8 text-primary mb-2" /> - <h3 className="font-semibold">True Ownership</h3> - <p className="text-sm text-muted-foreground">Your content, your data, your keys</p> - </div> - </div> - </CardContent> - </Card> - - {/* What is Nostr Section */} - <Card> - <CardHeader> - <CardTitle className="flex items-center gap-2"> - <Globe className="h-6 w-6 text-primary" /> - What is Nostr? - </CardTitle> - <CardDescription> - The protocol powering LUMINA&apos;s decentralization - </CardDescription> - </CardHeader> - <CardContent className="space-y-4"> - <p className="text-muted-foreground"> - Nostr (Notes and Other Stuff Transmitted by Relays) is a simple, open protocol for creating - decentralized social networks. Instead of relying on a single company&apos;s servers, Nostr uses - a network of relays to distribute your content across the internet. - </p> - <div className="bg-muted rounded-lg p-4 space-y-2"> - <h4 className="font-semibold">Key Benefits:</h4> - <ul className="space-y-1 text-sm text-muted-foreground"> - <li>• <strong>Censorship Resistant:</strong> No single entity can control or delete your content</li> - <li>• <strong>Portable Identity:</strong> Your profile and followers work across all Nostr apps</li> - <li>• <strong>Open Source:</strong> Transparent, community-driven development</li> - <li>• <strong>Privacy Focused:</strong> You control what data you share and with whom</li> - </ul> - </div> - </CardContent> - </Card> - </div> - ); -} \ No newline at end of file diff --git a/components/ZapButton.tsx b/components/ZapButton.tsx deleted file mode 100644 index 2825a48..0000000 --- a/components/ZapButton.tsx +++ /dev/null @@ -1,785 +0,0 @@ -import { - finalizeEvent, - nip19, -} from "nostr-tools"; -import { Button } from "@/components/ui/button"; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, -} from "@/components/ui/drawer" -import { ReloadIcon, CheckCircledIcon } from "@radix-ui/react-icons"; -import ZapButtonList from "./ZapButtonList"; -import { Input } from "./ui/input"; -import { useNostr, useNostrEvents, useProfile } from "nostr-react"; -import { useEffect, useState, useRef } from "react"; -import QRCode from "react-qr-code"; -import Link from "next/link"; -import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; -import { signEvent } from "@/utils/utils"; -import { nwc } from "@getalby/sdk"; -import { Sparkles, Zap } from "lucide-react"; -import { publishToOutbox } from "@/utils/publishUtils"; -import { useCurrentUserPubkey } from "@/utils/relayHooks"; - -// NWC connection storage key (same as used in NostrWalletConnect.tsx) -const NWC_STORAGE_KEY = "lumina-nwc-connection"; - -export default function ZapButton({ event }: { event: any }) { - - let loginType = ''; - if (typeof window !== 'undefined') { - loginType = window.localStorage.getItem("loginType") ?? ''; - } - - const { connectedRelays } = useNostr(); - const currentUserPubkey = useCurrentUserPubkey(); - - const { events, isLoading } = useNostrEvents({ - filter: { - '#e': [event.id], - kinds: [9735], - }, - }); - - const [lnurlPayInfo, setLnurlPayInfo] = useState<any>(null); - const [invoice, setInvoice] = useState<string>(""); - const [customAmount, setCustomAmount] = useState<string>("21"); - const [isProcessing, setIsProcessing] = useState<boolean>(false); - const [errorMessage, setErrorMessage] = useState<string>(""); - const [paymentComplete, setPaymentComplete] = useState<boolean>(false); - - - // NWC state - const [useNwc, setUseNwc] = useState<boolean>(false); - const [nwcClient, setNwcClient] = useState<nwc.NWCClient | null>(null); - const [nwcPaymentStatus, setNwcPaymentStatus] = useState<string>(""); - const [paymentPreimage, setPaymentPreimage] = useState<string>(""); - - // Store the initial count of zap receipts when an invoice is generated - const invoiceEventsCountRef = useRef<number>(0); - - // Effect to check for new zap receipts when showing an invoice - useEffect(() => { - if (invoice) { - // Store the current count of zap receipts when invoice is generated - invoiceEventsCountRef.current = events.length; - setPaymentComplete(false); - } - }, [invoice]); - - // Effect to detect new zap receipts after invoice is shown - useEffect(() => { - if (invoice && events.length > invoiceEventsCountRef.current) { - // Filter events to find new zap receipts related to current invoice - const newEvents = events.slice(invoiceEventsCountRef.current); - - // Check if any new events contain the current invoice - const relevantEvents = newEvents.filter(zapEvent => { - // Look for bolt11 tag containing the invoice - return zapEvent.tags.some(tag => - tag[0] === 'bolt11' && invoice.includes(tag[1].substring(0, 50)) - ); - }); - - if (relevantEvents.length > 0) { - setPaymentComplete(true); - } - } - }, [events, invoice]); - - // Initialize NWC client on first render - useEffect(() => { - initializeNwc(); - }, []); - - // Initialize NWC client from localStorage - const initializeNwc = async () => { - if (typeof window === 'undefined') return; - - const connectionUrl = localStorage.getItem(NWC_STORAGE_KEY); - if (!connectionUrl) return; - - try { - const client = new nwc.NWCClient({ - nostrWalletConnectUrl: connectionUrl, - }); - - // Test the connection by getting wallet info - await client.getInfo(); - - setNwcClient(client); - setUseNwc(true); - } catch (error) { - console.error("Failed to initialize NWC client:", error); - // Don't show error to user here as this is just an initialization check - } - }; - - const { data: userData } = useProfile({ - pubkey: event.pubkey, - }); - - let sats = 0; - var lightningPayReq = require('bolt11'); - events.forEach((event) => { - event.tags.forEach((tag) => { - if (tag[0] === 'bolt11') { - try { - lightningPayReq.decode(tag[1]); - let decoded = lightningPayReq.decode(tag[1]); - sats = sats + decoded.satoshis; - } catch (e) { - console.error("Error decoding bolt11 tag:", e); - console.log(tag[1]); - return null; - } - } - }); - }); - - const fetchLnurlInfo = async () => { - setErrorMessage(""); - if (!userData?.lud06 && !userData?.lud16) { - setErrorMessage("This user doesn't have a Lightning address configured"); - return; - } - - try { - setIsProcessing(true); - - let lnurl; - - if (userData.lud06) { - lnurl = userData.lud06; - } else if (userData.lud16) { - const [name, domain] = userData.lud16.split('@'); - if (!name || !domain) { - throw new Error("Invalid lightning address format"); - } - const url = `https://${domain}/.well-known/lnurlp/${name}`; - lnurl = url; - } else { - throw new Error("No lightning address found in profile"); - } - - if (lnurl.toLowerCase().startsWith('lnurl')) { - try { - lnurl = bech32ToUrl(lnurl); - } catch (e) { - console.error("Error decoding LNURL:", e); - throw new Error("Invalid LNURL format"); - } - } - - try { - // Add a timeout to the fetch request - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout - - const response = await fetch(lnurl, { - signal: controller.signal, - headers: { - 'Accept': 'application/json', - } - }); - - clearTimeout(timeoutId); - - // Check content type to ensure it's JSON - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) { - console.error("Invalid content type:", contentType); - throw new Error("Lightning service returned invalid content type (expected JSON)"); - } - - // Parse response as JSON - const data = await response.json(); - - if (!response.ok) { - throw new Error(`Error fetching LNURL info: ${data.reason || response.statusText || 'Unknown error'}`); - } - - if (!data.callback) { - throw new Error("Invalid LNURL response: missing callback URL"); - } - - if (!data.allowsNostr) { - setErrorMessage("This Lightning address doesn't support Nostr zaps"); - setIsProcessing(false); - return; - } - - setLnurlPayInfo(data); - } catch (error) { - if (typeof error === "object" && error !== null && "name" in error && typeof (error as any).name === "string") { - if ((error as any).name === 'AbortError') { - throw new Error("Request timed out - Lightning service not responding"); - } else if ((error as any).name === 'SyntaxError') { - throw new Error("Lightning service returned invalid data format"); - } - } - if (typeof error === "object" && error !== null && "message" in error && typeof (error as any).message === "string" && (error as any).message.includes('NetworkError')) { - throw new Error("Network error: CORS issue or service unavailable"); - } else { - throw error; - } - } - } catch (error) { - console.error("Error fetching LNURL info:", error); - setErrorMessage(error instanceof Error ? error.message : "Failed to fetch Lightning payment information"); - } finally { - setIsProcessing(false); - } - }; - - const createZapRequest = async (amount: number) => { - if (!lnurlPayInfo || !lnurlPayInfo.callback || !lnurlPayInfo.allowsNostr || !lnurlPayInfo.nostrPubkey) { - setErrorMessage("Invalid Lightning payment information"); - return; - } - - if (amount < lnurlPayInfo.minSendable || amount > lnurlPayInfo.maxSendable) { - setErrorMessage(`Amount must be between ${lnurlPayInfo.minSendable / 1000} and ${lnurlPayInfo.maxSendable / 1000} sats`); - return; - } - - try { - setIsProcessing(true); - setErrorMessage(""); - - let senderPubkey = ''; - if (typeof window !== 'undefined') { - senderPubkey = window.localStorage.getItem('pubkey') ?? ''; - } - - if (!senderPubkey) { - setErrorMessage("You need to be logged in to send zaps"); - setIsProcessing(false); - return; - } - - let zapRequestEvent = { - kind: 9734, - content: "", - tags: [ - ["relays", ...connectedRelays.map((relay) => relay.url)], - ["amount", amount.toString()], - ["p", event.pubkey], - ["e", event.id], - ], - created_at: Math.floor(Date.now() / 1000), - pubkey: senderPubkey, - id: "", // Add placeholder for id - sig: "", // Add placeholder for sig - }; - - let params = new URLSearchParams(); - params.append('amount', amount.toString()); - - if (userData?.lud06) { - zapRequestEvent.tags.push(["lnurl", userData.lud06]); - } - - const signedZapRequest = await signEvent(loginType, zapRequestEvent); - params.append('nostr', JSON.stringify(signedZapRequest)); - - if (userData?.lud06) { - params.append('lnurl', userData.lud06); - } - - let callbackUrl = `${lnurlPayInfo.callback}?${params.toString()}`; - - const invoiceResponse = await fetch(callbackUrl); - const invoiceData = await invoiceResponse.json(); - - if (!invoiceResponse.ok || !invoiceData.pr) { - throw new Error(`Failed to get invoice: ${invoiceData.reason || 'Unknown error'}`); - } - - setInvoice(invoiceData.pr); - } catch (error) { - console.error("Error creating zap request:", error); - setErrorMessage(error instanceof Error ? error.message : "Failed to create zap request"); - } finally { - setIsProcessing(false); - } - }; - - // Handle sending a zap using NWC - const handleNwcPayment = async (amountSats: number) => { - if (!nwcClient) { - setErrorMessage("NWC client not initialized"); - return; - } - - if (!userData?.lud16 && !userData?.lud06) { - setErrorMessage("This user doesn't have a Lightning address configured"); - return; - } - - setIsProcessing(true); - setNwcPaymentStatus("Preparing payment..."); - setPaymentComplete(false); - setErrorMessage(""); - - try { - let senderPubkey = ''; - if (typeof window !== 'undefined') { - senderPubkey = window.localStorage.getItem('pubkey') ?? ''; - } - - if (!senderPubkey) { - setErrorMessage("You need to be logged in to send zaps"); - return; - } - - // Create zap request event - let zapRequestEvent = { - kind: 9734, - content: "", - tags: [ - ["relays", ...connectedRelays.map((relay) => relay.url)], - ["amount", (amountSats * 1000).toString()], - ["p", event.pubkey], - ["e", event.id], - ], - created_at: Math.floor(Date.now() / 1000), - pubkey: senderPubkey, - id: "", - sig: "", - }; - - // Sign the zap request - setNwcPaymentStatus("Signing zap request..."); - const signedZapRequest = await signEvent(loginType, zapRequestEvent); - - // Get LNURL payment info for the Lightning address - let recipient = ''; - if (userData?.lud16) { - recipient = userData.lud16; - } else if (userData?.lud06) { - recipient = userData.lud06; - } - - if (!recipient) { - throw new Error("Could not determine recipient lightning address"); - } - - setNwcPaymentStatus("Getting invoice from recipient..."); - - // Get LNURL payment info and invoice - let lnurl; - let invoice; - - if (recipient.includes('@')) { - // Handle Lightning Address (lud16) - const [name, domain] = recipient.split('@'); - const url = `https://${domain}/.well-known/lnurlp/${name}`; - - // Get LNURL callback info - const lnurlResponse = await fetch(url); - const lnurlData = await lnurlResponse.json(); - - if (!lnurlData.callback || !lnurlData.allowsNostr) { - throw new Error("Lightning address doesn't support Nostr zaps"); - } - - // Get invoice with zap request - const params = new URLSearchParams(); - params.append('amount', (amountSats * 1000).toString()); - params.append('nostr', JSON.stringify(signedZapRequest)); - - const callbackUrl = `${lnurlData.callback}?${params.toString()}`; - const invoiceResponse = await fetch(callbackUrl); - const invoiceData = await invoiceResponse.json(); - - if (!invoiceResponse.ok || !invoiceData.pr) { - throw new Error(`Failed to get invoice: ${invoiceData.reason || 'Unknown error'}`); - } - - invoice = invoiceData.pr; - } else { - // Handle LNURL directly (lud06) - // Decode if needed - if (recipient.toLowerCase().startsWith('lnurl')) { - try { - recipient = bech32ToUrl(recipient); - } catch (e) { - throw new Error("Invalid LNURL format"); - } - } - - // Get LNURL callback info - const lnurlResponse = await fetch(recipient); - const lnurlData = await lnurlResponse.json(); - - if (!lnurlData.callback || !lnurlData.allowsNostr) { - throw new Error("LNURL doesn't support Nostr zaps"); - } - - // Get invoice with zap request - const params = new URLSearchParams(); - params.append('amount', (amountSats * 1000).toString()); - params.append('nostr', JSON.stringify(signedZapRequest)); - - const callbackUrl = `${lnurlData.callback}?${params.toString()}`; - const invoiceResponse = await fetch(callbackUrl); - const invoiceData = await invoiceResponse.json(); - - if (!invoiceResponse.ok || !invoiceData.pr) { - throw new Error(`Failed to get invoice: ${invoiceData.reason || 'Unknown error'}`); - } - - invoice = invoiceData.pr; - } - - // Now pay the invoice using NWC - setNwcPaymentStatus("Sending payment via NWC..."); - const paymentResponse = await nwcClient.payInvoice({ - invoice: invoice - }); - - if (paymentResponse.preimage) { - setPaymentPreimage(paymentResponse.preimage); - setPaymentComplete(true); - setNwcPaymentStatus("Payment sent successfully!"); - } else { - throw new Error("Payment failed - no preimage returned"); - } - } catch (error) { - console.error("NWC payment error:", error); - setNwcPaymentStatus(""); - setErrorMessage(error instanceof Error ? error.message : "Payment failed"); - } finally { - setIsProcessing(false); - } - }; - - const handleZap = async (amountSats: number) => { - if (useNwc && nwcClient) { - await handleNwcPayment(amountSats); - } else { - if (!lnurlPayInfo) { - await fetchLnurlInfo(); - } - createZapRequest(amountSats * 1000); - } - }; - - const handleCustomZap = async () => { - const amount = parseInt(customAmount, 10); - if (isNaN(amount) || amount <= 0) { - setErrorMessage("Please enter a valid amount"); - return; - } - - if (useNwc && nwcClient) { - await handleNwcPayment(amount); - } else { - if (!lnurlPayInfo) { - await fetchLnurlInfo(); - } - createZapRequest(amount * 1000); - } - }; - - const bech32ToUrl = (lnurl: string): string => { - try { - const decoded = nip19.decode(lnurl); - return decoded.data as string; - } catch (e) { - return lnurl; - } - }; - - const handleOpenDrawer = () => { - if (!useNwc) { - fetchLnurlInfo(); - } - }; - - const handleCloseDrawer = () => { - setInvoice(""); - setErrorMessage(""); - setIsProcessing(false); - setPaymentComplete(false); - setNwcPaymentStatus(""); - }; - - const checkPaymentStatus = async () => { - setIsProcessing(true); - try { - // Force a re-fetch of zap receipt events - const eventFilter = { - '#e': [event.id], - kinds: [9735], - }; - - // Manually check relays for new zap events - const zapPromises = connectedRelays.map(async (relay) => { - return new Promise(async (resolve) => { - const timeout = setTimeout(() => resolve([]), 3000); // 3 second timeout - try { - const sub = relay.sub([eventFilter]); - const events: any[] = []; - - sub.on('event', (event) => { - // Check if this event contains the current invoice - const hasBolt11 = event.tags.some(tag => - tag[0] === 'bolt11' && invoice.includes(tag[1].substring(0, 50)) - ); - if (hasBolt11) { - events.push(event); - } - }); - - sub.on('eose', () => { - clearTimeout(timeout); - resolve(events); - sub.unsub(); - }); - } catch (error) { - clearTimeout(timeout); - resolve([]); - } - }); - }); - - const zapEventsArrays = await Promise.all(zapPromises); - const newZapEvents = zapEventsArrays.flat(); - - if (newZapEvents.length > 0) { - setPaymentComplete(true); - } - } catch (error) { - console.error("Error checking payment status:", error); - } finally { - setIsProcessing(false); - } - }; - - return ( - <Drawer onOpenChange={(open) => open ? handleOpenDrawer() : handleCloseDrawer()}> - <DrawerTrigger asChild> - {isLoading ? ( - <Button variant="outline"><ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> ⚡️</Button> - ) : ( - <Button variant="outline">{sats} sats ⚡️</Button> - )} - </DrawerTrigger> - <DrawerContent> - {errorMessage && ( - <Alert variant={"destructive"} className="mx-4 mt-4"> - {/* <AlertCircleIcon className="h-4 w-4" /> */} - <AlertTitle>Error</AlertTitle> - <AlertDescription>{errorMessage}</AlertDescription> - </Alert> - )} - - {/* NWC Payment Mode */} - {useNwc && !invoice && ( - <div className="px-4 pt-4"> - {!paymentComplete && ( - <Alert variant="default" className="mb-4 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300"> - <Sparkles className="h-4 w-4" /> - <AlertTitle>NWC Enabled</AlertTitle> - <AlertDescription>Payments will be sent directly through your connected NWC wallet</AlertDescription> - </Alert> - )} - - {nwcPaymentStatus && !paymentComplete && ( - <Alert variant="default" className="mb-4 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300"> - {isProcessing ? ( - <ReloadIcon className="h-4 w-4 animate-spin" /> - ) : ( - <Zap className="h-4 w-4" /> - )} - <AlertDescription>{nwcPaymentStatus}</AlertDescription> - </Alert> - )} - - {paymentComplete && ( - <div className="flex flex-col items-center mb-6"> - <div className="flex flex-col items-center justify-center mb-4 p-4 rounded-lg bg-green-50 dark:bg-green-900/20 w-[200px] h-[200px]"> - <CheckCircledIcon className="h-24 w-24 text-green-500" /> - <p className="text-lg font-semibold text-green-500 mt-4 text-center"> - Payment Complete! - </p> - </div> - - {/* {paymentPreimage && ( - <div className="w-full overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs mb-4"> - <p className="text-xs text-center mb-1">Payment Preimage:</p> - <p className="break-all text-center">{paymentPreimage}</p> - </div> - )} */} - - <div className="flex items-center text-green-500 mb-4"> - <CheckCircledIcon className="mr-2 h-4 w-4" /> - Zap sent successfully! - </div> - - <Button variant="outline" onClick={() => { - setPaymentComplete(false); - setNwcPaymentStatus(""); - setPaymentPreimage(""); - }}> - Send Another Zap - </Button> - </div> - )} - - {(!paymentComplete || !isProcessing) && !paymentComplete && ( - <div className="grid grid-cols-3 gap-2 mb-6"> - <Button - variant={"outline"} - className="mx-1" - onClick={() => handleZap(1)} - disabled={isProcessing} - > - 1 sat - </Button> - <Button - variant={"outline"} - className="mx-1" - onClick={() => handleZap(21)} - disabled={isProcessing} - > - 21 sats - </Button> - <div className="flex"> - <Input - className="mx-1" - placeholder="21" - value={customAmount} - onChange={(e) => setCustomAmount(e.target.value)} - disabled={isProcessing} - /> - <Button - variant={"outline"} - className="mx-1" - onClick={handleCustomZap} - disabled={isProcessing} - > - {isProcessing ? <ReloadIcon className="h-4 w-4 animate-spin" /> : "send"} - </Button> - </div> - </div> - )} - - {!paymentComplete && <hr className="my-4" />} - {!paymentComplete && <ZapButtonList events={events} />} - </div> - )} - - {/* Original LNURL Payment Flow */} - {(!useNwc || invoice) && ( - <> - {invoice ? ( - <div className="flex flex-col items-center px-4 py-4"> - {paymentComplete ? ( - <div className="flex flex-col items-center justify-center mb-4 p-2 rounded-lg bg-green-50 dark:bg-green-900/20 w-[200px] h-[200px]"> - <CheckCircledIcon className="h-24 w-24 text-green-500" /> - <p className="text-lg font-semibold text-green-500 mt-4"> - Payment Complete! - </p> - </div> - ) : ( - <div className="mb-4 p-2 bg-white rounded-lg"> - <Link href={`lightning:${invoice}`} target="_blank" rel="noopener noreferrer"> - <QRCode value={invoice} size={200} /> - </Link> - </div> - )} - - {!paymentComplete && ( - <> - <p className="text-sm text-center mb-4"> - Scan this QR code with a Lightning wallet to pay the invoice - </p> - <div className="w-full overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs mb-4"> - <code className="break-all">{invoice}</code> - </div> - </> - )} - - - {paymentComplete ? ( - <div className="flex items-center text-green-500 mb-4"> - <CheckCircledIcon className="mr-2 h-4 w-4" /> - Zap sent successfully! - </div> - ) : ( - <Button - variant="outline" - className="mb-4" - onClick={() => checkPaymentStatus()} - disabled={isProcessing} - > - {isProcessing ? ( - <> - <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> Checking... - </> - ) : ( - "Check if paid" - )} - </Button> - )} - - <Button variant="outline" onClick={() => setInvoice("")}> - {paymentComplete ? "Send Another Zap" : "Back to Zap Options"} - </Button> - </div> - ) : ( - <> - <div className="px-4 pt-4 grid grid-cols-3 gap-2"> - <Button - variant={"outline"} - className="mx-1" - onClick={() => handleZap(1)} - disabled={isProcessing || !lnurlPayInfo} - > - 1 sat - </Button> - <Button - variant={"outline"} - className="mx-1" - onClick={() => handleZap(21)} - disabled={isProcessing || !lnurlPayInfo} - > - 21 sats - </Button> - <div className="flex"> - <Input - className="mx-1" - placeholder="1000 sats" - value={customAmount} - onChange={(e) => setCustomAmount(e.target.value)} - disabled={isProcessing} - /> - <Button - variant={"outline"} - className="mx-1" - onClick={handleCustomZap} - disabled={isProcessing || !lnurlPayInfo} - > - {isProcessing ? <ReloadIcon className="h-4 w-4 animate-spin" /> : "send"} - </Button> - </div> - </div> - - <hr className="my-4" /> - <ZapButtonList events={events} /> - </> - )} - </> - )} - </DrawerContent> - </Drawer> - ); -} \ No newline at end of file diff --git a/components/ZapButtonList.tsx b/components/ZapButtonList.tsx deleted file mode 100644 index cf6e0d8..0000000 --- a/components/ZapButtonList.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ScrollArea } from "@/components/ui/scroll-area" -import ZapButtonListItem from "./ZapButtonListItem"; - -export default function ZapButtonList({ events }: { events: any }) { - return ( - <ScrollArea className="px-4 h-[50vh]"> - {events.map((event: any) => ( - <ZapButtonListItem key={event.id} event={event} /> - ))} - </ScrollArea> - ); -} \ No newline at end of file diff --git a/components/ZapButtonListItem.tsx b/components/ZapButtonListItem.tsx deleted file mode 100644 index 95f5d54..0000000 --- a/components/ZapButtonListItem.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import Link from "next/link"; -import { useNostr, dateToUnix, useNostrEvents, useProfile } from "nostr-react"; - -import { - type Event as NostrEvent, - getEventHash, - getPublicKey, - finalizeEvent, - nip19, -} from "nostr-tools"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; - -export default function ZapButtonListItem({ event }: { event: NostrEvent }) { - - let pubkey = event.pubkey; - - // Try to extract pubkey from description tag if available - const descriptionTag = event.tags.find(tag => tag[0] === 'description'); - if (descriptionTag && descriptionTag[1]) { - try { - const descriptionEvent = JSON.parse(descriptionTag[1]); - if (descriptionEvent.pubkey) { - pubkey = descriptionEvent.pubkey; - } - } catch (e) { - console.error('Failed to parse description tag:', e); - } - } - - // Fallback to 'p' tag if description doesn't have a pubkey - if (pubkey === event.pubkey) { - for(let i = 0; i < event.tags.length; i++) { - if(event.tags[i][0] === 'P' || event.tags[i][0] === 'p') { - pubkey = event.tags[i][1]; - break; - } - } - } - - const { data: userData } = useProfile({ - pubkey, - }); - - const title = userData?.username || userData?.display_name || userData?.name || nip19.npubEncode(pubkey).slice(0, 8) + ':' + nip19.npubEncode(pubkey).slice(-3);; - const createdAt = new Date(event.created_at * 1000); - const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - let sats = 0; - var lightningPayReq = require('bolt11'); - event.tags.forEach((tag) => { - if (tag[0] === 'bolt11') { - try { - let decoded = lightningPayReq.decode(tag[1]); - // console.log(decoded.satoshis); - sats = decoded.satoshis; - } catch (e) { - console.error("Error decoding bolt11 tag:", e); - return null; - } - } - }); - - return ( - <Link href={hrefProfile}> - <div key={event.id} className="flex items-center space-x-2"> - <div className="flex items-center space-x-2 p-1"> - {/* <img src={profileImageSrc} className="w-8 h-8 rounded-full" /> */} - <Avatar> - <AvatarImage src={profileImageSrc} alt={title} /> - </Avatar> - <span>{title}</span> - <span className="pl-2">{sats} sats ⚡️</span> - </div> - </div> - </Link> - ); -} \ No newline at end of file diff --git a/components/dashboard/RecentFollower.tsx b/components/dashboard/RecentFollower.tsx deleted file mode 100644 index c421e7a..0000000 --- a/components/dashboard/RecentFollower.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import Link from "next/link"; - -export function RecentFollower({ follower }: { follower: any }) { - - const { data: userData, isLoading: userDataLoading } = useProfile({ - pubkey: follower.pubkey, - }); - - let encoded = nip19.npubEncode(follower.pubkey); - let parts = encoded.split('npub'); - let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); - let title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - const profileImageSrc = userData?.picture || "https://robohash.org/" + follower.pubkey; - return ( - <div className="flex items-center" key={follower.id}> - <Link href={`/profile/${encoded}`}> - <Avatar className="h-9 w-9"> - <AvatarImage src={profileImageSrc} alt="Avatar" /> - <AvatarFallback>n/a</AvatarFallback> - </Avatar> - </Link> - <div className="ml-4 space-y-1"> - <p className="text-sm font-medium leading-none">{title}</p> - <p className="text-sm text-muted-foreground"> - {new Date(follower.created_at * 1000).toLocaleDateString()} {new Date(follower.created_at * 1000).toLocaleTimeString()} </p> - </div> - {/* <div className="ml-auto font-medium">{follower.amount}</div> */} - </div> - ) -} \ No newline at end of file diff --git a/components/dashboard/RecentFollowerCard.tsx b/components/dashboard/RecentFollowerCard.tsx deleted file mode 100644 index ff72bb4..0000000 --- a/components/dashboard/RecentFollowerCard.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; -import { RecentFollower } from "./RecentFollower"; - -export function RecentFollowerCard({ followers }: { followers: Array<any> }) { - const lastFiveFollowers = followers.slice(-5).reverse(); - return ( - <Card className="col-span-2"> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-base font-normal">Recent Follower 🫂</CardTitle> - </CardHeader> - <CardContent> - <div className='pt-4'> - <div className="space-y-8"> - {lastFiveFollowers.map((follower) => ( - <RecentFollower follower={follower} key={follower.id}/> - ))} - </div> - </div> - </CardContent> - </Card> - ) -} \ No newline at end of file diff --git a/components/dashboard/RecentZap.tsx b/components/dashboard/RecentZap.tsx deleted file mode 100644 index c4d38ce..0000000 --- a/components/dashboard/RecentZap.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; -import { useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import Link from "next/link"; - -export function RecentZap({ zap }: { zap: any }) { - - let zapperPubkey = zap.pubkey; - for(let tag of zap.tags){ - if(tag[0] === 'P') { - zapperPubkey = tag[1]; - } - } - - const { data: userData, isLoading: userDataLoading } = useProfile({ - pubkey: zapperPubkey, - }); - - console.log('zap', zap) - - let encoded = nip19.npubEncode(zapperPubkey); - let parts = encoded.split('npub'); - let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); - let title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - const profileImageSrc = userData?.picture || "https://robohash.org/" + zap.pubkey; - return ( - <div className="flex items-center" key={zap.id}> - <Link href={`/profile/${encoded}`}> - <Avatar className="h-9 w-9"> - <AvatarImage src={profileImageSrc} alt="Avatar" /> - <AvatarFallback>n/a</AvatarFallback> - </Avatar> - </Link> - <div className="ml-4 space-y-1"> - <p className="text-sm font-medium leading-none">{title}</p> - <p className="text-sm text-muted-foreground"> - {new Date(zap.created_at * 1000).toLocaleDateString()} {new Date(zap.created_at * 1000).toLocaleTimeString()} </p> - </div> - {/* <div className="ml-auto font-medium">{zap.amount}</div> */} - </div> - ) -} \ No newline at end of file diff --git a/components/dashboard/RecentZapsCard.tsx b/components/dashboard/RecentZapsCard.tsx deleted file mode 100644 index 425f10f..0000000 --- a/components/dashboard/RecentZapsCard.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; -import { RecentZap } from "./RecentZap"; - -export function RecentZapsCard({ zaps }: { zaps: Array<any> }) { - const lastFiveZaps = zaps.slice(-5).reverse(); - return ( - <Card className="col-span-2"> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-base font-normal">Recent Zaps ⚡️</CardTitle> - </CardHeader> - <CardContent> - <div className='pt-4'> - <div className="space-y-8"> - {lastFiveZaps.map((zap) => ( - <RecentZap zap={zap} key={zap.id}/> - ))} - </div> - </div> - </CardContent> - </Card> - ) -} \ No newline at end of file diff --git a/components/dashboard/Statistics.tsx b/components/dashboard/Statistics.tsx deleted file mode 100644 index 132986f..0000000 --- a/components/dashboard/Statistics.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import { useNostrEvents, useProfile } from "nostr-react"; -import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card'; -import { Skeleton } from '@/components/ui/skeleton'; -import { AvatarImage } from '@radix-ui/react-avatar'; -import { Avatar } from '@/components/ui/avatar'; -import NIP05 from '@/components/nip05'; -import { RecentFollowerCard } from './RecentFollowerCard'; -import { - nip19, -} from "nostr-tools"; -import { RecentZapsCard } from './RecentZapsCard'; - -interface ProfileInfoCardProps { - pubkey: string; -} - -const ProfileInfoCard: React.FC<ProfileInfoCardProps> = ({ pubkey }) => { - const { data: userData, isLoading: userDataLoading } = useProfile({ - pubkey, - }); - - const { events: followers, isLoading: followersLoading } = useNostrEvents({ - filter: { - kinds: [3], - '#p': [pubkey], - }, - }); - - const { events: zaps, isLoading: zapsLoading } = useNostrEvents({ - filter: { - kinds: [9735], - '#p': [pubkey], - limit: 50, - }, - }); - - const { events: following, isLoading: followingLoading } = useNostrEvents({ - filter: { - kinds: [3], - authors: [pubkey], - limit: 1, - }, - }); - - // filter for only new followings (latest in a users followers list) - const filteredFollowers = followers.filter(follower => { - const lastPTag = follower.tags[follower.tags.length - 1]; - if (lastPTag[0] === "p" && lastPTag[1] === pubkey.toString()) { - // console.log(follower.tags[follower.tags.length - 1]); - return true; - } - }); - - let encoded = nip19.npubEncode(pubkey); - let parts = encoded.split('npub'); - let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; - const description = userData?.about?.replace(/(?:\r\n|\r|\n)/g, '<br>'); - const nip05 = userData?.nip05 - let profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; - return ( - <> - <div className='pt-6 px-6'> - {/* <ProfileInfoCard pubkey={pubkey.toString()} /> */} - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - {/* <CardTitle className="text-base font-normal">Profile</CardTitle> */} - </CardHeader> - <CardContent> - <div className="flex flex-row items-center space-x-4"> - <Avatar> - <AvatarImage - src={profileImageSrc} - alt="Avatar" - className="rounded-full" - /> - </Avatar> - <div> - <h1 className="text-2xl font-bold">{title}</h1> - <NIP05 nip05={nip05?.toString() ?? ''} pubkey={pubkey} /> - - </div> - </div> - </CardContent> - </Card> - </div> - <div className='grid gap-4 grid-cols-2 p-6'> - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-base font-normal">Total Followers</CardTitle> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold">{followers.length}</div> - {/* <p className="text-xs text-muted-foreground"> - +20.1% from last month - </p> */} - </CardContent> - </Card> - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-base font-normal">Total Following</CardTitle> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold"> - {followingLoading ? "Loading.." : (following.length > 0 ? following[0]?.tags.length : "-")} - </div> - {/* <p className="text-xs text-muted-foreground"> - +20.1% from last month - </p> */} - </CardContent> - </Card> - <RecentFollowerCard followers={filteredFollowers.reverse()} /> - <RecentZapsCard zaps={zaps.reverse() ?? []} /> - </div> - </> - ); -} - -export default ProfileInfoCard; \ No newline at end of file diff --git a/components/headerComponents/AuthButton.tsx b/components/headerComponents/AuthButton.tsx deleted file mode 100644 index 0589dd9..0000000 --- a/components/headerComponents/AuthButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import Link from "next/link"; -import { UserIcon } from "lucide-react"; - -export default function AuthButton() { - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="outline" className="flex items-center gap-2"> - <UserIcon className="h-[1.2rem] w-[1.2rem]" /> - <span className="hidden sm:inline">Account</span> - <span className="sr-only">Account options</span> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem asChild> - <Link href="/login" className="w-full cursor-pointer"> - Sign In - </Link> - </DropdownMenuItem> - <DropdownMenuItem asChild> - <Link href="/onboarding" className="w-full cursor-pointer"> - Register - </Link> - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ); -} diff --git a/components/headerComponents/AvatarDropdown.tsx b/components/headerComponents/AvatarDropdown.tsx deleted file mode 100644 index c3edb9f..0000000 --- a/components/headerComponents/AvatarDropdown.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client" - -import * as React from "react" -import { MoonIcon, SunIcon } from "@radix-ui/react-icons" -import { useTheme } from "next-themes" - -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar" -import { useProfile } from "nostr-react" -import Link from "next/link" -import { nip19 } from "nostr-tools" - -export function AvatarDropdown() { - let pubkey = window.localStorage.getItem('pubkey'); - let pubkeyEncoded = pubkey ? nip19.npubEncode(pubkey) : pubkey; - let src = "https://robohash.org/" + (pubkey as string); - const { data: userData } = useProfile({ - pubkey: pubkey as string, - }); - if (pubkey !== null) { - src = userData?.picture || "https://robohash.org/" + pubkey; - } - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Avatar> - <AvatarImage src={src} /> - </Avatar> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem asChild> - <Link href={`/profile/${pubkeyEncoded}`}> - Profile - </Link> - </DropdownMenuItem> - <DropdownMenuItem asChild> - <Link href="/relays"> - Relays - </Link> - </DropdownMenuItem> - <DropdownMenuItem asChild> - <Link href="/profile/settings"> - Settings - </Link> - </DropdownMenuItem> - <DropdownMenuItem asChild> - <Link href="/profile/settings/nwc"> - NWC - </Link> - </DropdownMenuItem> - <DropdownMenuItem onSelect={() => { - window.localStorage.clear(); - window.location.href = "/"; - }}> - Logout - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ) -} \ No newline at end of file diff --git a/components/headerComponents/ConnectedRelaysButton.tsx b/components/headerComponents/ConnectedRelaysButton.tsx deleted file mode 100644 index eff3043..0000000 --- a/components/headerComponents/ConnectedRelaysButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import React, { useEffect, useState } from 'react'; -import Link from "next/link"; -import { useNostr } from "nostr-react"; -import { Wifi } from "lucide-react"; - -export default function ConnectedRelaysButton() { - const { connectedRelays } = useNostr(); - const [relayCount, setRelayCount] = useState<number>(0); - const [isConnecting, setIsConnecting] = useState<boolean>(true); - - useEffect(() => { - // Update relay count when connectedRelays changes - if (connectedRelays && connectedRelays.length > 0) { - // Count only connected relays (status === 1) - const activeRelays = connectedRelays.filter(relay => relay.status === 1).length; - setRelayCount(activeRelays); - - // If at least one relay is connected, we're no longer in connecting state - if (activeRelays > 0) { - setIsConnecting(false); - } - } - - // Set a timeout to stop showing "Connecting..." after a reasonable time - const timer = setTimeout(() => { - setIsConnecting(false); - }, 5000); - - return () => clearTimeout(timer); - }, [connectedRelays]); - - return ( - <Link href={"/relays"}> - <Button variant={"outline"} className="gap-2"> - <Wifi className={`h-4 w-4 ${isConnecting ? 'animate-pulse' : ''}`} /> - {isConnecting && relayCount === 0 ? 'Connecting...' : `${relayCount}`} - </Button> - </Link> - ); -} \ No newline at end of file diff --git a/components/headerComponents/DropdownThemeMode.tsx b/components/headerComponents/DropdownThemeMode.tsx deleted file mode 100644 index e952707..0000000 --- a/components/headerComponents/DropdownThemeMode.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client" - -import * as React from "react" -import { MoonIcon, SunIcon } from "@radix-ui/react-icons" -import { useTheme } from "next-themes" - -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" - -export function DropdownThemeMode() { - const { setTheme } = useTheme() - - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="outline" size="icon"> - <SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> - <MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> - <span className="sr-only">Toggle theme</span> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem onClick={() => setTheme("light")}> - Light - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("dark")}> - Dark - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("purple-light")}> - Purple Light - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("purple-dark")}> - Purple Dark - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("vintage-light")}> - Vintage Light - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("vintage-dark")}> - Vintage Dark - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("neo-brutalism-light")}> - Neo Brutalism Light - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("neo-brutalism-dark")}> - Neo Brutalism Dark - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("nature-light")}> - Nature Light - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("nature-dark")}> - Nature Dark - </DropdownMenuItem> - <DropdownMenuItem onClick={() => setTheme("system")}> - System - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ) -} diff --git a/components/headerComponents/GitHubButton.tsx b/components/headerComponents/GitHubButton.tsx deleted file mode 100644 index 23785ac..0000000 --- a/components/headerComponents/GitHubButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client" - -import { Button } from "@/components/ui/button" -import { Github } from "lucide-react" -import Link from "next/link" - -interface GitHubButtonProps { - repoUrl?: string -} - -export default function GitHubButton({ repoUrl = "https://github.com/lumina-rocks/lumina" }: GitHubButtonProps) { - return ( - <Link href={repoUrl} target="_blank" rel="noopener noreferrer"> - <Button variant="outline" size="icon" aria-label="View GitHub Repository"> - <Github className="h-[1.2rem] w-[1.2rem]" /> - </Button> - </Link> - ) -} - diff --git a/components/headerComponents/LoginButton.tsx b/components/headerComponents/LoginButton.tsx deleted file mode 100644 index 8312d38..0000000 --- a/components/headerComponents/LoginButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import React, { useRef } from 'react'; -import Link from "next/link"; - -export default function LoginButton() { - return ( - <Link href={"/login"}> - <Button variant={"secondary"}>Sign In</Button> - </Link> - ); -} \ No newline at end of file diff --git a/components/headerComponents/RegisterButton.tsx b/components/headerComponents/RegisterButton.tsx deleted file mode 100644 index 5ebd704..0000000 --- a/components/headerComponents/RegisterButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import React, { useRef } from 'react'; -import Link from "next/link"; - -export default function RegisterButton() { - return ( - <Link href={"/onboarding"}> - <Button>Register</Button> - </Link> - ); -} \ No newline at end of file diff --git a/components/headerComponents/TopNavigation.tsx b/components/headerComponents/TopNavigation.tsx deleted file mode 100644 index f3a70bc..0000000 --- a/components/headerComponents/TopNavigation.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client" - -import { siteConfig } from "@/config/site" -import { useEffect, useState } from "react" -import { TopNavigationItems } from "./TopNavigationItems" -import { DropdownThemeMode } from "./DropdownThemeMode" -import { AvatarDropdown } from "./AvatarDropdown" -import ConnectedRelaysButton from "@/components/headerComponents/ConnectedRelaysButton" -import AuthButton from "./AuthButton" -import { Button } from "@/components/ui/button" -import { UserIcon } from "lucide-react" - -export function TopNavigation() { - const [pubkey, setPubkey] = useState<string | null>(null) - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - setPubkey(window.localStorage.getItem("pubkey")) - }, []) - - // Prevent hydration mismatch by not rendering auth-dependent content until mounted - if (!mounted) { - return ( - <header className="bg-background/80 sticky top-0 z-40 w-full border-b backdrop-blur"> - <div className="container flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0"> - <TopNavigationItems items={siteConfig.mainNav} /> - <div className="flex flex-1 items-center justify-end space-x-4"> - <nav className="flex items-center space-x-2"> - <ConnectedRelaysButton /> - <DropdownThemeMode /> - {/* Placeholder for auth button to prevent layout shift */} - <Button variant="outline" className="flex items-center gap-2" disabled> - <UserIcon className="h-[1.2rem] w-[1.2rem]" /> - <span className="hidden sm:inline">Account</span> - </Button> - </nav> - </div> - </div> - </header> - ) - } - - return ( - <header className="bg-background/80 sticky top-0 z-40 w-full border-b backdrop-blur"> - <div className="container flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0"> - <TopNavigationItems items={siteConfig.mainNav} /> - <div className="flex flex-1 items-center justify-end space-x-4"> - <nav className="flex items-center space-x-2"> - <ConnectedRelaysButton /> - <DropdownThemeMode /> - {pubkey !== null ? <AvatarDropdown /> : <AuthButton />} - </nav> - </div> - </div> - </header> - ) -} - diff --git a/components/headerComponents/TopNavigationItems.tsx b/components/headerComponents/TopNavigationItems.tsx deleted file mode 100644 index 4931604..0000000 --- a/components/headerComponents/TopNavigationItems.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from "react" -import Link from "next/link" - -import { NavItem } from "@/types/nav" -import { siteConfig } from "@/config/site" -import { cn } from "@/lib/utils" -// import { Icons } from "@/components/icons" - -interface TopNavigationItemsProps { - items?: NavItem[] -} - -export function TopNavigationItems({ items }: TopNavigationItemsProps) { - return ( - <div className="flex gap-6 md:gap-10"> - <Link href="/" className="flex items-center space-x-2"> - {/* <Icons.logo className="h-6 w-6" /> */} - <span className="inline-block font-bold">{siteConfig.name}</span> - {/* <span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground"> - v{siteConfig.version} - </span> */} - </Link> - {/* TOP NAVIGATION ITEMS */} - {/* {items?.length ? ( - <nav className="flex gap-6"> - {items?.map( - (item, index) => - item.href && ( - <Link - key={index} - href={item.href} - className={cn( - "flex items-center text-sm font-medium text-muted-foreground", - item.disabled && "cursor-not-allowed opacity-80" - )} - > - {item.title} - </Link> - ) - )} - </nav> - ) : null} */} - </div> - ) -} \ No newline at end of file diff --git a/components/icons.tsx b/components/icons.tsx deleted file mode 100644 index a620b33..0000000 --- a/components/icons.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// import { -// LucideProps, -// Moon, -// SunMedium, -// Twitter, -// type Icon as LucideIcon, -// } from "lucide-react" - -// export type Icon = LucideIcon - -// export const Icons = { -// sun: SunMedium, -// moon: Moon, -// twitter: Twitter, -// logo: (props: LucideProps) => ( -// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}> -// <path -// fill="currentColor" -// d="M11.572 0c-.176 0-.31.001-.358.007a19.76 19.76 0 0 1-.364.033C7.443.346 4.25 2.185 2.228 5.012a11.875 11.875 0 0 0-2.119 5.243c-.096.659-.108.854-.108 1.747s.012 1.089.108 1.748c.652 4.506 3.86 8.292 8.209 9.695.779.25 1.6.422 2.534.525.363.04 1.935.04 2.299 0 1.611-.178 2.977-.577 4.323-1.264.207-.106.247-.134.219-.158-.02-.013-.9-1.193-1.955-2.62l-1.919-2.592-2.404-3.558a338.739 338.739 0 0 0-2.422-3.556c-.009-.002-.018 1.579-.023 3.51-.007 3.38-.01 3.515-.052 3.595a.426.426 0 0 1-.206.214c-.075.037-.14.044-.495.044H7.81l-.108-.068a.438.438 0 0 1-.157-.171l-.05-.106.006-4.703.007-4.705.072-.092a.645.645 0 0 1 .174-.143c.096-.047.134-.051.54-.051.478 0 .558.018.682.154.035.038 1.337 1.999 2.895 4.361a10760.433 10760.433 0 0 0 4.735 7.17l1.9 2.879.096-.063a12.317 12.317 0 0 0 2.466-2.163 11.944 11.944 0 0 0 2.824-6.134c.096-.66.108-.854.108-1.748 0-.893-.012-1.088-.108-1.747-.652-4.506-3.859-8.292-8.208-9.695a12.597 12.597 0 0 0-2.499-.523A33.119 33.119 0 0 0 11.573 0zm4.069 7.217c.347 0 .408.005.486.047a.473.473 0 0 1 .237.277c.018.06.023 1.365.018 4.304l-.006 4.218-.744-1.14-.746-1.14v-3.066c0-1.982.01-3.097.023-3.15a.478.478 0 0 1 .233-.296c.096-.05.13-.054.5-.054z" -// /> -// </svg> -// ), -// gitHub: (props: LucideProps) => ( -// <svg viewBox="0 0 438.549 438.549" {...props}> -// <path -// fill="currentColor" -// d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" -// ></path> -// </svg> -// ), -// } \ No newline at end of file diff --git a/components/nip05.tsx b/components/nip05.tsx deleted file mode 100644 index 4a0fac4..0000000 --- a/components/nip05.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { CheckIcon, ReloadIcon } from "@radix-ui/react-icons"; -import { BadgeCheck, Check } from "lucide-react"; -import React, { useState, useEffect } from 'react'; - -interface NIP05Props { - nip05: string; - pubkey: string; -} - -const NIP05: React.FC<NIP05Props> = ({ nip05, pubkey }) => { - const [isLoading, setIsLoading] = useState(true); - const [isValid, setIsValid] = useState(false); - - let name = nip05.split('@')[0] - let domain = nip05.split('@')[1] - - useEffect(() => { - if(nip05.length > 0) { - fetch(`https://${domain}/.well-known/nostr.json?name=${name}`) - .then(response => response.json()) - .then(data => { - if (data.names[name] === pubkey) { - setIsValid(true); - } else { - setIsValid(false); - } - setIsLoading(false); - }) - } - }, [nip05, pubkey]); - - return ( - <> - <div style={{ display: 'flex', alignItems: 'center' }}> - {nip05.length > 0 && - <> - {isLoading ? ( - <ReloadIcon className="mr-1 h-4 w-4 animate-spin" /> - ) : isValid ? ( - <BadgeCheck className="mr-1 h-4 w-4 text-blue-500" /> - ) : ( - <span className="mr-1 text-red-500">❌</span> - )} - { name === "_" ? domain : nip05 } - </> - } - </div> - </> - ); -} - -export default NIP05; \ No newline at end of file diff --git a/components/onboarding/createSecretKeyForm.tsx b/components/onboarding/createSecretKeyForm.tsx deleted file mode 100644 index 83f7e52..0000000 --- a/components/onboarding/createSecretKeyForm.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { generateSecretKey, getPublicKey } from 'nostr-tools/pure' -import { nip19 } from "nostr-tools" -import { Label } from "../ui/label" -import { bytesToHex, hexToBytes } from '@noble/hashes/utils' -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Shield, Key, Copy, RefreshCw, ArrowRight } from 'lucide-react' -import { Card, CardContent } from "@/components/ui/card" - -export function CreateSecretKeyForm() { - const [nsec, setNsec] = useState(''); - const [npub, setNpub] = useState(''); - const [nsecCopied, setNsecCopied] = useState(false); - const [npubCopied, setNpubCopied] = useState(false); - const [isGenerating, setIsGenerating] = useState(true); - - const regenerateKey = () => { - setIsGenerating(true); - - // Add a small delay to show the loading state - setTimeout(() => { - let sk = generateSecretKey(); // `sk` is a Uint8Array - let pk = getPublicKey(sk); // `pk` is a hex string - let newNpub = nip19.npubEncode(pk); // `npub` is a string - let newNsec = nip19.nsecEncode(sk); // `nsec` is a string - - setNsec(newNsec); - setNpub(newNpub); - setIsGenerating(false); - }, 500); - } - - const copyToClipboard = (text: string, setStateFunc: React.Dispatch<React.SetStateAction<boolean>>) => { - navigator.clipboard.writeText(text); - setStateFunc(true); - setTimeout(() => setStateFunc(false), 2000); - }; - - // Generate keys when the component is first rendered - useEffect(() => { - if(localStorage.getItem("nsec")) { - const nsecString = localStorage.getItem('nsec') || ''; - const nsecBytes = hexToBytes(nsecString); - setNsec(nip19.nsecEncode(nsecBytes)); - setNpub(nip19.npubEncode(getPublicKey(nsecBytes))); - } else { - regenerateKey(); - } - }, []); - - return ( - <div className="w-full space-y-6"> - <Alert className="bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800"> - <Shield className="h-4 w-4 text-amber-600 dark:text-amber-400" /> - <AlertTitle className="text-amber-800 dark:text-amber-400">Important security notice</AlertTitle> - <AlertDescription className="text-amber-700 dark:text-amber-500"> - Your <b>secret key</b> gives full control of your account. It&apos;s stored locally in your browser - and never sent to any server. Make sure to back it up securely. - </AlertDescription> - </Alert> - - <Card className="border-muted"> - <CardContent className="pt-6 space-y-4"> - <div className="space-y-2"> - <div className="flex items-center justify-between"> - <Label className="text-sm font-medium flex items-center gap-2"> - <Key className="h-4 w-4" /> Your Secret Key (nsec) - </Label> - <Button - variant="ghost" - size="sm" - className="h-8 px-2 text-xs" - onClick={() => copyToClipboard(nsec, setNsecCopied)} - > - {nsecCopied ? "Copied!" : <Copy className="h-3.5 w-3.5" />} - </Button> - </div> - <div className="relative"> - <Input - type="text" - className="font-mono text-sm bg-muted/30" - value={nsec} - readOnly - /> - </div> - </div> - - <div className="space-y-2"> - <div className="flex items-center justify-between"> - <Label className="text-sm font-medium flex items-center gap-2"> - <Key className="h-4 w-4" /> Your Public Key (npub) - </Label> - <Button - variant="ghost" - size="sm" - className="h-8 px-2 text-xs" - onClick={() => copyToClipboard(npub, setNpubCopied)} - > - {npubCopied ? "Copied!" : <Copy className="h-3.5 w-3.5" />} - </Button> - </div> - <Input - type="text" - className="font-mono text-sm bg-muted/30" - value={npub} - readOnly - /> - </div> - </CardContent> - </Card> - - <div className="flex flex-col gap-3 pt-2"> - <Button - variant="outline" - onClick={regenerateKey} - disabled={isGenerating} - className="flex items-center gap-2" - > - <RefreshCw className={`h-4 w-4 ${isGenerating ? 'animate-spin' : ''}`} /> - {isGenerating ? 'Generating...' : 'Regenerate New Keys'} - </Button> - - <Button - onClick={() => { - localStorage.setItem('nsec', bytesToHex(nip19.decode(nsec).data as Uint8Array)); - localStorage.setItem("loginType", "raw_nsec"); - localStorage.setItem("pubkey", nip19.decode(npub).data.toString()); - window.location.href = '/onboarding/createProfile'; - }} - className="flex items-center gap-2" - > - Continue to Profile Setup - <ArrowRight className="h-4 w-4" /> - </Button> - </div> - </div> - ) -} \ No newline at end of file diff --git a/components/searchComponents/SearchNotesBox.tsx b/components/searchComponents/SearchNotesBox.tsx deleted file mode 100644 index 15e6336..0000000 --- a/components/searchComponents/SearchNotesBox.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { useNostrEvents, useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, -} from "@/components/ui/carousel" -import ReactionButton from '@/components/ReactionButton'; -import { Avatar, AvatarImage } from '@/components/ui/avatar'; -import ViewRawButton from '@/components/ViewRawButton'; -import Link from 'next/link'; -import { Event as NostrEvent } from "nostr-tools"; -import ProfileInfoCard from '../ProfileInfoCard'; -import NoteCard from '../NoteCard'; -import KIND20Card from '../KIND20Card'; -import { getImageUrl } from '@/utils/utils'; - -// Function to extract video URL from imeta tags -const getVideoUrl = (tags: string[][]): string | null => { - for (const tag of tags) { - if (tag[0] === 'imeta') { - for (let i = 1; i < tag.length; i++) { - if (tag[i].startsWith('url ')) { - return tag[i].substring(4); - } - } - } - } - return null; -}; - -interface SearchNotesBoxProps { - searchTag: string; -} - -const SearchNotesBox: React.FC<SearchNotesBoxProps> = ({ searchTag }) => { - const { events: notes } = useNostrEvents({ - filter: { - kinds: [1, 20, 21, 22], - search: searchTag, - limit: 10, - }, - }); - - return ( - <> - <Card> - <CardHeader> - Notes - </CardHeader> - <CardContent> - <div className="grid grid-cols-1 gap-6"> - {notes.map((event: NostrEvent) => { - const imageUrl = getImageUrl(event.tags); - const isVideo = event.kind === 21 || event.kind === 22; - - if (event.kind === 1) { - return ( - <NoteCard event={event} eventId={event.id} pubkey={event.pubkey} showViewNoteCardButton={true} tags={event.tags} text={event.content} key={event.id} /> - ); - } else if (isVideo) { - // Use NoteCard for video content - const videoUrl = getVideoUrl(event.tags); - const contentWithVideo = videoUrl ? `${event.content}\n${videoUrl}` : event.content; - return ( - <NoteCard - key={event.id} - pubkey={event.pubkey} - text={contentWithVideo} - eventId={event.id} - tags={event.tags} - event={event} - showViewNoteCardButton={true} - /> - ); - } else if (event.kind === 20) { - return ( - <KIND20Card key={event.id} pubkey={event.pubkey} text={event.content} image={imageUrl} event={event} tags={event.tags} eventId={event.id} showViewNoteCardButton={true}/> - ); - } - return null; - })} - </div> - </CardContent> - </Card> - </> - ); -} - -export default SearchNotesBox; \ No newline at end of file diff --git a/components/searchComponents/SearchProfilesBox.tsx b/components/searchComponents/SearchProfilesBox.tsx deleted file mode 100644 index b61b8bc..0000000 --- a/components/searchComponents/SearchProfilesBox.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import { useNostrEvents, useProfile } from "nostr-react"; -import { - nip19, -} from "nostr-tools"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, -} from "@/components/ui/carousel" -import ReactionButton from '@/components/ReactionButton'; -import { Avatar, AvatarImage } from '@/components/ui/avatar'; -import ViewRawButton from '@/components/ViewRawButton'; -import Link from 'next/link'; -import { Event as NostrEvent } from "nostr-tools"; -import ProfileInfoCard from '../ProfileInfoCard'; -import NoteCard from '../NoteCard'; - -interface SearchProfilesBoxProps { - searchTag: string; -} - -const SearchProfilesBox: React.FC<SearchProfilesBoxProps> = ({ searchTag }) => { - const { events: profiles } = useNostrEvents({ - filter: { - kinds: [0], - search: searchTag, - limit: 10, - }, - }); - - return ( - <> - <Card> - <CardHeader> - Profiles - </CardHeader> - <CardContent> - <div className="grid grid-cols-1 gap-6"> - {profiles.map((event: NostrEvent) => ( - <ProfileInfoCard key={event.id} pubkey={event.pubkey} /> - ))} - </div> - </CardContent> - </Card> - </> - ); -} - -export default SearchProfilesBox; \ No newline at end of file diff --git a/components/spinner.tsx b/components/spinner.tsx deleted file mode 100644 index 942e152..0000000 --- a/components/spinner.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { cn } from "@/lib/utils" - -export function Spinner({ className }: { className?: string }) { - return ( - <svg - xmlns="http://www.w3.org/2000/svg" - width="24" - height="24" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - className={cn("animate-spin", className)} - > - <path d="M21 12a9 9 0 1 1-6.219-8.56" /> - </svg> - ) -} \ No newline at end of file diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx deleted file mode 100644 index 8c90fbc..0000000 --- a/components/theme-provider.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client" - -import * as React from "react" -import { ThemeProvider as NextThemesProvider } from "next-themes" -import { type ThemeProviderProps } from "next-themes/dist/types" - -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return <NextThemesProvider {...props}>{children}</NextThemesProvider> -} diff --git a/components/ui/button.tsx b/components/ui/button.tsx deleted file mode 100644 index 0ba4277..0000000 --- a/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - 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 } diff --git a/components/ui/input.tsx b/components/ui/input.tsx deleted file mode 100644 index 677d05f..0000000 --- a/components/ui/input.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -export interface InputProps - extends React.InputHTMLAttributes<HTMLInputElement> {} - -const Input = React.forwardRef<HTMLInputElement, InputProps>( - ({ className, type, ...props }, ref) => { - return ( - <input - type={type} - className={cn( - "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - className - )} - ref={ref} - {...props} - /> - ) - } -) -Input.displayName = "Input" - -export { Input } diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx deleted file mode 100644 index 47da24e..0000000 --- a/components/ui/tabs.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import Link, { LinkProps } from "next/link"; -import { usePathname, useSearchParams } from "next/navigation"; -import * as React from "react"; -import { Suspense } from "react"; - -interface Context { - defaultValue: string; - hrefFor: (value: string) => LinkProps["href"]; - searchParam: string; - selected: string; -} -const TabsContext = React.createContext<Context>(null as any); - -export function Tabs(props: { - children: React.ReactNode; - className?: string; - defaultValue: string; - searchParam?: string; -}) { - const { children, className, searchParam = "tab", ...other } = props; - - return ( - <Suspense fallback={<div>Loading...</div>}> - <TabsContentWrapper {...props} /> - </Suspense> - ); -} - -function TabsContentWrapper(props: { - children: React.ReactNode; - className?: string; - defaultValue: string; - searchParam?: string; -}) { - const { children, className, searchParam = "tab", ...other } = props; - const searchParams = useSearchParams()!; - - const selected = searchParams.get(searchParam) || props.defaultValue; - - const pathname = usePathname(); - const hrefFor: Context["hrefFor"] = React.useCallback( - (value) => { - const params = new URLSearchParams(searchParams); - if (value === props.defaultValue) { - params.delete(searchParam); - } else { - params.set(searchParam, value); - } - - const asString = params.toString(); - - return pathname + (asString ? "?" + asString : ""); - }, - [searchParams, props.searchParam], - ); - - return ( - <TabsContext.Provider value={{ ...other, hrefFor, searchParam, selected }}> - <div className={className}>{children}</div> - </TabsContext.Provider> - ); -} - -const useContext = () => { - const context = React.useContext(TabsContext); - if (!context) { - throw new Error( - "Tabs compound components cannot be rendered outside the Tabs component", - ); - } - - return context; -}; - -export function TabsList(props: { - children: React.ReactNode; - className?: string; -}) { - return ( - <div - {...props} - className={cn( - "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", - props.className, - )} - /> - ); -} - -export const TabsTrigger = (props: { - children: React.ReactNode; - className?: string; - value: string; -}) => { - const context = useContext(); - - return ( - <Link - {...props} - className={cn( - "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", - props.className, - )} - data-state={context.selected === props.value ? "active" : "inactive"} - href={context.hrefFor(props.value)} - scroll={false} - shallow={true} - /> - ); -}; - -export function TabsContent(props: { - children: React.ReactNode; - className?: string; - value: string; -}) { - const context = useContext(); - - if (context.selected !== props.value) { - return null; - } - - return ( - <div - {...props} - className={cn( - "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - props.className, - )} - /> - ); -} diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 2cf66fa..0000000 --- a/compose.yaml +++ /dev/null @@ -1,7 +0,0 @@ -version: '3' -services: - lumina: - build: . - # image: ghcr.io/lumina-rocks/lumina:latest - ports: - - "8080:3000" \ No newline at end of file diff --git a/config/site.ts b/config/site.ts deleted file mode 100644 index 6def428..0000000 --- a/config/site.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Import package information dynamically -import packageInfo from "../package.json" - -export type SiteConfig = typeof siteConfig - -export const siteConfig = { - name: "LUMINA", - version: packageInfo.version, // Use the version from package.json - description: - "A beautiful Nostr client for images.", - mainNav: [ - { - title: "Home", - href: "/", - }, - ], - links: { - twitter: "https://lumina.rocks/profile/npub1fq8vrf63vsrqjrwqgtwlvauqauc0yme6se8g8dqhcpf6tfs3equqntmzut", - github: "https://github.com/mroxso/lumina-rocks-website", - docs: "https://heynostr.com", - }, -} \ No newline at end of file diff --git a/docs/AI_CHAT.md b/docs/AI_CHAT.md new file mode 100644 index 0000000..b7019e1 --- /dev/null +++ b/docs/AI_CHAT.md @@ -0,0 +1,332 @@ +### AI Integration with Shakespeare API + +Use the `useShakespeare` hook for AI chat completions with Nostr authentication. The API dynamically provides available models, so you should query them at runtime rather than hardcoding model names. + +```tsx +import { useShakespeare, type ChatMessage, type Model } from '@/hooks/useShakespeare'; + +const { + sendChatMessage, + sendStreamingMessage, + getAvailableModels, + isLoading, + error, + isAuthenticated +} = useShakespeare(); +``` + +#### Model Selector Component + +```tsx +function ModelSelector({ onModelSelect }: { onModelSelect: (modelId: string) => void }) { + const { getAvailableModels, isLoading } = useShakespeare(); + const [models, setModels] = useState<Model[]>([]); + const [selectedModel, setSelectedModel] = useState<string>(''); + + useEffect(() => { + const fetchModels = async () => { + try { + const response = await getAvailableModels(); + // Sort models by total cost (cheapest first) + const sortedModels = response.data.sort((a, b) => { + const costA = parseFloat(a.pricing.prompt) + parseFloat(a.pricing.completion); + const costB = parseFloat(b.pricing.prompt) + parseFloat(b.pricing.completion); + return costA - costB; + }); + setModels(sortedModels); + + // Select the cheapest model by default + if (sortedModels.length > 0) { + const cheapestModel = sortedModels[0]; + setSelectedModel(cheapestModel.id); + onModelSelect(cheapestModel.id); + } + } catch (err) { + console.error('Failed to fetch models:', err); + } + }; + + fetchModels(); + }, [getAvailableModels, onModelSelect]); + + const handleModelChange = (modelId: string) => { + setSelectedModel(modelId); + onModelSelect(modelId); + }; + + return ( + <div> + <label htmlFor="model-select">Choose Model:</label> + <select + id="model-select" + value={selectedModel} + onChange={(e) => handleModelChange(e.target.value)} + disabled={isLoading} + > + <option value="">Select a model...</option> + {models.map((model, index) => { + const totalCost = parseFloat(model.pricing.prompt) + parseFloat(model.pricing.completion); + const isCheapest = index === 0; + return ( + <option key={model.id} value={model.id}> + {model.name} - {isCheapest ? "Cheapest" : `$${totalCost.toFixed(6)}/token`} + </option> + ); + })} + </select> + </div> + ); +} +``` + +#### Basic Chat Example + +```tsx +function AIChat() { + const { sendChatMessage, isLoading, error, isAuthenticated } = useShakespeare(); + const [messages, setMessages] = useState<ChatMessage[]>([]); + const [input, setInput] = useState(''); + const [selectedModel, setSelectedModel] = useState<string>(''); + + const handleSend = async () => { + if (!input.trim() || !selectedModel) return; + + const newMessages = [...messages, { role: 'user', content: input }]; + setMessages(newMessages); + setInput(''); + + try { + const response = await sendChatMessage(newMessages, selectedModel); + setMessages(prev => [...prev, { + role: 'assistant', + content: response.choices[0].message.content as string + }]); + } catch (err) { + console.error('Chat error:', err); + } + }; + + if (!isAuthenticated) return <div>Please log in to use AI</div>; + + return ( + <div className="max-w-2xl mx-auto p-4"> + {error && <div className="text-red-500 mb-4">{error}</div>} + + {/* Model Selection */} + <div className="mb-4"> + <ModelSelector onModelSelect={setSelectedModel} /> + </div> + + <div className="space-y-2 mb-4"> + {messages.map((msg, i) => ( + <div key={i} className={`p-2 rounded ${msg.role === 'user' ? 'bg-blue-100' : 'bg-gray-100'}`}> + <strong>{msg.role}:</strong> {msg.content} + </div> + ))} + </div> + + <div className="flex gap-2"> + <input + value={input} + onChange={(e) => setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSend()} + className="flex-1 p-2 border rounded" + disabled={isLoading || !selectedModel} + placeholder={!selectedModel ? "Select a model first..." : "Type your message..."} + /> + <button + onClick={handleSend} + disabled={isLoading || !selectedModel} + className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50" + > + Send + </button> + </div> + </div> + ); +} +``` + +#### Streaming Chat Example + +```tsx +function StreamingChat() { + const { sendStreamingMessage } = useShakespeare(); + const [messages, setMessages] = useState<ChatMessage[]>([]); + const [currentResponse, setCurrentResponse] = useState(''); + const [selectedModel, setSelectedModel] = useState<string>(''); + + const handleStreaming = async (content: string) => { + if (!selectedModel) return; + + setCurrentResponse(''); + const newMessages = [...messages, { role: 'user', content }]; + setMessages(newMessages); + + try { + await sendStreamingMessage(newMessages, selectedModel, (chunk) => { + setCurrentResponse(prev => prev + chunk); + }); + + // Add the complete response to messages + if (currentResponse.trim()) { + setMessages(prev => [...prev, { + role: 'assistant', + content: currentResponse + }]); + } + } catch (err) { + console.error('Streaming error:', err); + } finally { + setCurrentResponse(''); + } + }; + + return ( + <div> + {/* Model selection UI */} + <div className="mb-4"> + <ModelSelector onModelSelect={setSelectedModel} /> + </div> + + {/* Chat interface */} + {/* ... rest of your chat UI */} + </div> + ); +} +``` + +#### Model Information + +Models are dynamically fetched from the Shakespeare API and include: + +- **Model ID**: Unique identifier for the model +- **Name**: Human-readable model name +- **Description**: Model capabilities and use cases +- **Context Window**: Maximum token limit for conversations +- **Pricing**: Cost per token for prompt and completion +- **Free Models**: Models with `pricing.prompt === "0"` and `pricing.completion === "0"` + +#### Key Points + +- **Dynamic Model Discovery**: Always fetch available models using `getAvailableModels()` +- **Authentication Required**: User must be logged in with Nostr account +- **Free vs Premium**: Check pricing to determine if model requires credits +- **Error Handling**: Handle `isLoading` and `error` states appropriately +- **Model Selection**: Provide UI for users to choose between available models + +## Implementation Patterns and Best Practices + +### Dialog Component Patterns + +When using Dialog components, always ensure accessibility compliance by including required elements: + +```tsx +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; + +// ✅ Correct - Always include DialogHeader with DialogTitle +<Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent> + <DialogHeader> + <DialogTitle>Dialog Title</DialogTitle> + <DialogDescription> + Optional description for screen readers + </DialogDescription> + </DialogHeader> + {/* Dialog content */} + </DialogContent> +</Dialog> +``` + +**Important**: Even if you want to hide the title visually, use the `VisuallyHidden` component to maintain accessibility: + +```tsx +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; + +<DialogHeader> + <VisuallyHidden> + <DialogTitle>Hidden Title for Screen Readers</DialogTitle> + </VisuallyHidden> +</DialogHeader> +``` + +### Streaming Response Handling + +When implementing streaming chat interfaces, always accumulate streamed content in a local variable before clearing the streaming state to prevent content loss: + +```tsx +const handleStreamingResponse = async () => { + let streamedContent = ''; // ✅ Use local variable to accumulate content + + try { + await sendStreamingMessage(messages, model, (chunk) => { + streamedContent += chunk; // ✅ Accumulate in local variable + setCurrentStreamingMessage(streamedContent); // Update UI + }); + + // ✅ Save accumulated content to persistent state + if (streamedContent.trim()) { + const assistantMessage: MessageDisplay = { + id: Date.now().toString(), + role: 'assistant', + content: streamedContent, // ✅ Use accumulated content + timestamp: new Date() + }; + setMessages(prev => [...prev, assistantMessage]); + } + } finally { + setCurrentStreamingMessage(''); // ✅ Clear streaming state after saving + } +}; +``` + +### Error Boundary Patterns + +Always wrap AI components with error boundaries and provide user-friendly error messages for common failure scenarios: + +```tsx +import { ErrorBoundary } from '@/components/ErrorBoundary'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +function AIChatWithErrorBoundary() { + return ( + <ErrorBoundary + fallback={ + <div className="p-4"> + <Alert variant="destructive"> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + Something went wrong with the AI chat. Please refresh the page and try again. + </AlertDescription> + </Alert> + </div> + } + > + <AIChat /> + </ErrorBoundary> + ); +} + +// In your AI component, handle specific error types gracefully: +function useAIWithErrorHandling() { + const { sendChatMessage, error, clearError } = useShakespeare(); + + const sendMessage = async (messages: ChatMessage[], modelId: string) => { + try { + await sendChatMessage(messages, modelId); + } catch (err) { + // Handle specific error types with user-friendly messages + if (err.message.includes('401')) { + throw new Error('Authentication failed. Please log in again.'); + } else if (err.message.includes('402')) { + throw new Error('Insufficient credits. Please add credits to use premium features.'); + } else if (err.message.includes('network')) { + throw new Error('Network error. Please check your internet connection.'); + } + throw err; // Re-throw for error boundary + } + }; + + return { sendMessage, error, clearError }; +} +``` diff --git a/docs/NOSTR_COMMENTS.md b/docs/NOSTR_COMMENTS.md new file mode 100644 index 0000000..0e99cec --- /dev/null +++ b/docs/NOSTR_COMMENTS.md @@ -0,0 +1,54 @@ +# Adding Nostr Comments Sections + +The project includes a complete commenting system using NIP-22 (kind 1111) comments that can be added to any Nostr event or URL. The `CommentsSection` component provides a full-featured commenting interface with threaded replies, user authentication, and real-time updates. + +## Basic Usage + +```tsx +import { CommentsSection } from "@/components/comments/CommentsSection"; + +function ArticlePage({ article }: { article: NostrEvent }) { + return ( + <div className="space-y-6"> + {/* Your article content */} + <div>{/* article content */}</div> + + {/* Comments section */} + <CommentsSection root={article} /> + </div> + ); +} +``` + +## Props and Customization + +The `CommentsSection` component accepts the following props: + +- **`root`** (required): The root event or URL to comment on. Can be a `NostrEvent` or `URL` object. +- **`title`**: Custom title for the comments section (default: "Comments") +- **`emptyStateMessage`**: Message shown when no comments exist (default: "No comments yet") +- **`emptyStateSubtitle`**: Subtitle for empty state (default: "Be the first to share your thoughts!") +- **`className`**: Additional CSS classes for styling +- **`limit`**: Maximum number of comments to load (default: 500) + +```tsx +<CommentsSection + root={event} + title="Discussion" + emptyStateMessage="Start the conversation" + emptyStateSubtitle="Share your thoughts about this post" + className="mt-8" + limit={100} +/> +``` + +## Commenting on URLs + +The comments system also supports commenting on external URLs, making it useful for web pages, articles, or any online content: + +```tsx +<CommentsSection + root={new URL("https://example.com/article")} + title="Comments on this article" +/> +``` diff --git a/docs/NOSTR_DIRECT_MESSAGES.md b/docs/NOSTR_DIRECT_MESSAGES.md new file mode 100644 index 0000000..9513603 --- /dev/null +++ b/docs/NOSTR_DIRECT_MESSAGES.md @@ -0,0 +1,473 @@ +### Direct Messaging on Nostr + +This project includes a complete direct messaging system supporting both NIP-04 (legacy) and NIP-17 (modern, more private) encrypted messages with real-time subscriptions, optimistic updates, and a persistent cache-first local storage. + +**The DM system is not enabled by default** - follow the setup instructions below to add messaging functionality to your application. + +## Setup Instructions + +### 1. Add DMProvider to Your App + +First, add the `DMProvider` to your app's provider tree in `src/App.tsx`: + +```tsx +// Add these imports at the top of src/App.tsx +import { DMProvider, type DMConfig } from '@/components/DMProvider'; +import { PROTOCOL_MODE } from '@/lib/dmConstants'; + +// Add this configuration before your App component +const dmConfig: DMConfig = { + // Enable or disable DMs entirely + enabled: true, // Set to true to enable messaging functionality + + // Choose one protocol mode: + // PROTOCOL_MODE.NIP04_ONLY - Force NIP-04 (legacy) only + // PROTOCOL_MODE.NIP17_ONLY - Force NIP-17 (private) only + // PROTOCOL_MODE.NIP04_OR_NIP17 - Allow users to choose between NIP-04 and NIP-17 (defaults to NIP-17) + protocolMode: PROTOCOL_MODE.NIP17_ONLY, // Recommended for new apps +}; + +// Then wrap your app components with DMProvider: +export function App() { + return ( + <UnheadProvider head={head}> + <AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig}> + <QueryClientProvider client={queryClient}> + <NostrLoginProvider storageKey='nostr:login'> + <NostrProvider> + <NostrSync /> + <DMProvider config={dmConfig}> + <TooltipProvider> + <Toaster /> + <Suspense> + <AppRouter /> + </Suspense> + </TooltipProvider> + </DMProvider> + </NostrProvider> + </NostrLoginProvider> + </QueryClientProvider> + </AppProvider> + </UnheadProvider> + ); +} +``` + +### 2. Configure DM Settings + +The `DMConfig` object supports the following options: + +- `enabled` (boolean, default: `false`) - Enable/disable entire DM system. When false, no messages are loaded, stored, or processed. +- `protocolMode` (ProtocolMode, default: `PROTOCOL_MODE.NIP17_ONLY`) - Which protocols to support: + - `PROTOCOL_MODE.NIP04_ONLY` - Legacy encryption only + - `PROTOCOL_MODE.NIP17_ONLY` - Modern private messages (recommended) + - `PROTOCOL_MODE.NIP04_OR_NIP17` - Support both protocols (for backwards compatibility) + +**Note**: The DM system uses domain-based IndexedDB naming (`nostr-dm-store-${hostname}`) to prevent conflicts between multiple apps on the same domain. + +## Quick Start + +### 1. Send Messages + +```tsx +import { useDMContext } from '@/hooks/useDMContext'; +import { MESSAGE_PROTOCOL } from '@/lib/dmConstants'; + +function ComposeMessage({ recipientPubkey }: { recipientPubkey: string }) { + const { sendMessage } = useDMContext(); + const [content, setContent] = useState(''); + + const handleSend = async () => { + await sendMessage({ + recipientPubkey, + content, + protocol: MESSAGE_PROTOCOL.NIP17, // Uses NIP-44 encryption + gift wrapping + }); + setContent(''); + }; + + return ( + <form onSubmit={(e) => { e.preventDefault(); handleSend(); }}> + <textarea + value={content} + onChange={(e) => setContent(e.target.value)} + placeholder="Type a message..." + /> + <button type="submit">Send</button> + </form> + ); +} +``` + +### 2. Display Conversations + +```tsx +import { useDMContext } from '@/hooks/useDMContext'; +import { useAuthor } from '@/hooks/useAuthor'; +import { genUserName } from '@/lib/genUserName'; + +function ConversationList({ onSelectConversation }: { onSelectConversation: (pubkey: string) => void }) { + const { conversations, isLoading } = useDMContext(); + + if (isLoading) { + return <div>Loading conversations...</div>; + } + + return ( + <div className="space-y-2"> + {conversations.map((conversation) => ( + <ConversationItem + key={conversation.pubkey} + conversation={conversation} + onClick={() => onSelectConversation(conversation.pubkey)} + /> + ))} + </div> + ); +} + +function ConversationItem({ conversation, onClick }: { + conversation: ConversationSummary; + onClick: () => void; +}) { + const author = useAuthor(conversation.pubkey); + const displayName = author.data?.metadata?.name || genUserName(conversation.pubkey); + const avatarUrl = author.data?.metadata?.picture; + + return ( + <button onClick={onClick} className="w-full p-3 hover:bg-accent rounded-lg"> + <div className="flex items-center gap-3"> + <Avatar> + <AvatarImage src={avatarUrl} /> + <AvatarFallback>{displayName.slice(0, 2).toUpperCase()}</AvatarFallback> + </Avatar> + <div className="flex-1 text-left"> + <div className="font-medium">{displayName}</div> + <div className="text-sm text-muted-foreground truncate"> + {conversation.lastMessage?.decryptedContent || 'No messages yet'} + </div> + </div> + </div> + </button> + ); +} +``` + +### 3. Display Messages in a Conversation + +```tsx +import { useConversationMessages } from '@/hooks/useConversationMessages'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; + +function MessageThread({ conversationPubkey }: { conversationPubkey: string }) { + const { user } = useCurrentUser(); + const { messages, hasMoreMessages, loadEarlierMessages } = useConversationMessages(conversationPubkey); + + return ( + <div className="flex flex-col space-y-2"> + {hasMoreMessages && ( + <button onClick={loadEarlierMessages} className="text-sm text-muted-foreground"> + Load earlier messages + </button> + )} + + {messages.map((message) => { + const isFromMe = message.pubkey === user?.pubkey; + + return ( + <div + key={message.id} + className={cn( + "flex", + isFromMe ? "justify-end" : "justify-start" + )} + > + <div className={cn( + "max-w-[70%] rounded-lg px-4 py-2", + isFromMe ? "bg-primary text-primary-foreground" : "bg-muted" + )}> + {message.error ? ( + <span className="text-red-500">🔒 {message.error}</span> + ) : ( + <p className="whitespace-pre-wrap break-words"> + {message.decryptedContent} + </p> + )} + {message.isSending && ( + <span className="text-xs opacity-50">Sending...</span> + )} + </div> + </div> + ); + })} + </div> + ); +} +``` + +## Using the Complete Messaging Interface + +For a fully-featured messaging UI out of the box, use the `DMMessagingInterface` component: + +```tsx +import { DMMessagingInterface } from "@/components/dm/DMMessagingInterface"; + +function MessagesPage() { + return ( + <div className="container mx-auto p-4 h-screen"> + <DMMessagingInterface /> + </div> + ); +} +``` + +The `DMMessagingInterface` component provides a complete messaging UI with: +- Conversation list with Active/Requests tabs +- Message thread view with pagination +- Compose area with file upload support +- Real-time message updates +- Mobile-responsive layout (shows one panel at a time on mobile) + +It requires no props and works automatically when wrapped in `DMProvider`. + +**For custom layouts**, see the "Building Custom Messaging UIs" section below for individual components (`DMConversationList`, `DMChatArea`, `DMStatusInfo`). + +## Sending Files with Messages + +```tsx +import { useDMContext } from '@/hooks/useDMContext'; +import { useUploadFile } from '@/hooks/useUploadFile'; +import { MESSAGE_PROTOCOL } from '@/lib/dmConstants'; +import type { FileAttachment } from '@/contexts/DMContext'; + +function ComposeWithFiles({ recipientPubkey }: { recipientPubkey: string }) { + const { sendMessage } = useDMContext(); + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); + const [content, setContent] = useState(''); + const [selectedFile, setSelectedFile] = useState<File | null>(null); + + const handleSend = async () => { + let attachments: FileAttachment[] | undefined; + + // Upload file if one is selected + if (selectedFile) { + const tags = await uploadFile(selectedFile); + + attachments = [{ + url: tags[0][1], // URL from first tag + mimeType: selectedFile.type, + size: selectedFile.size, + name: selectedFile.name, + tags: tags + }]; + } + + await sendMessage({ + recipientPubkey, + content, + protocol: MESSAGE_PROTOCOL.NIP17, + attachments, + }); + + setContent(''); + setSelectedFile(null); + }; + + return ( + <form onSubmit={(e) => { e.preventDefault(); handleSend(); }}> + <textarea + value={content} + onChange={(e) => setContent(e.target.value)} + placeholder="Type a message..." + /> + + <input + type="file" + onChange={(e) => setSelectedFile(e.target.files?.[0] || null)} + /> + + {selectedFile && <div>Selected: {selectedFile.name}</div>} + + <button type="submit" disabled={isUploading}> + {isUploading ? 'Uploading...' : 'Send'} + </button> + </form> + ); +} +``` + +## Protocol Comparison + +### NIP-04 (Legacy) +- **Encryption**: NIP-04 (simpler, older) +- **Metadata**: Sender and recipient visible to relays +- **Event Kind**: Kind 4 +- **Use When**: Compatibility with older clients + +### NIP-17 (Modern & Private) +- **Encryption**: NIP-44 (stronger) +- **Metadata**: Hidden via gift wrapping (NIP-59) +- **Event Kinds**: Kind 14 (text), Kind 15 (files) +- **Wrapped In**: Kind 1059 (Gift Wrap) with ephemeral keys +- **Use When**: Maximum privacy (recommended) + +**Key Privacy Features of NIP-17:** +- Sender identity hidden (uses random ephemeral keys) +- Timestamps randomized (±2 days) to hide send time +- Dual gift wraps (recipient + sender) for message history + +## Advanced Features + +### Conversation Categorization + +The system automatically categorizes conversations: + +```tsx +const { conversations } = useDMContext(); + +// Filter by category +const knownConversations = conversations.filter(c => c.isKnown); +const requestConversations = conversations.filter(c => c.isRequest); + +// isKnown = true if user has sent at least one message +// isRequest = true if only received messages, never replied +``` + +### Loading States + +```tsx +const { isLoading, loadingPhase, scanProgress } = useDMContext(); + +// Check overall loading state +if (isLoading) { + console.log('Current phase:', loadingPhase); + // LOADING_PHASES.CACHE - Loading from local cache + // LOADING_PHASES.RELAYS - Querying relays + // LOADING_PHASES.SUBSCRIPTIONS - Setting up real-time updates + // LOADING_PHASES.READY - Fully loaded +} + +// Display scan progress for large message histories +if (scanProgress.nip17) { + console.log(`NIP-17: ${scanProgress.nip17.current} messages - ${scanProgress.nip17.status}`); +} +``` + +### Clear Cache and Refresh + +```tsx +import { useDMContext } from '@/hooks/useDMContext'; + +function SettingsButton() { + const { clearCacheAndRefetch } = useDMContext(); + + const handleClearCache = async () => { + await clearCacheAndRefetch(); + // Clears IndexedDB cache and reloads all messages from relays + }; + + return ( + <button onClick={handleClearCache}> + Clear Message Cache + </button> + ); +} +``` + +## Architecture Notes + +### Data Flow +1. **Cache First**: Messages load instantly from encrypted IndexedDB cache +2. **Background Sync**: New messages fetched from relays in parallel +3. **Real-time Updates**: WebSocket subscriptions for live messages +4. **Optimistic UI**: Sent messages appear immediately, confirmed on relay response + +### Storage +- **IndexedDB**: All messages stored locally with NIP-44 encryption +- **Per-User Storage**: Separate encrypted store for each logged-in user +- **Automatic Sync**: Debounced writes (15s) + immediate on new messages + +### Performance +- **Parallel Queries**: NIP-04 and NIP-17 messages fetched simultaneously +- **Batched Loading**: Messages loaded in batches (1000/batch, 20k limit) +- **Pagination**: Conversation messages paginated (25/page) +- **Deduplication**: Automatic filtering of duplicate messages by ID + +### Security +- **NIP-44 Encryption**: Modern authenticated encryption for all NIP-17 messages +- **Local Encryption**: IndexedDB storage encrypted with user's NIP-44 key +- **Ephemeral Keys**: Random keys for NIP-17 gift wraps (sender anonymity) +- **No Plaintext**: Decrypted content never persisted unencrypted +- **Domain Isolation**: IndexedDB databases are namespaced by hostname to prevent data conflicts + +## Building Custom Messaging UIs + +For advanced use cases, you can use the individual DM components to build custom layouts: + +### Available Components + +**`DMConversationList`** - Conversation sidebar with tabs +```tsx +import { DMConversationList } from '@/components/dm/DMConversationList'; + +<DMConversationList + selectedPubkey={selectedPubkey} + onSelectConversation={(pubkey) => setSelectedPubkey(pubkey)} + onStatusClick={() => setShowStatus(true)} // optional + className="h-full" +/> +``` + +**`DMChatArea`** - Message thread and compose area +```tsx +import { DMChatArea } from '@/components/dm/DMChatArea'; + +<DMChatArea + pubkey={selectedPubkey} + onBack={() => setSelectedPubkey(null)} // optional, for mobile back button + className="h-full" +/> +``` + +**`DMStatusInfo`** - Debug/status panel +```tsx +import { DMStatusInfo } from '@/components/dm/DMStatusInfo'; + +<DMStatusInfo clearCacheAndRefetch={clearCacheAndRefetch} /> +``` + +### Custom Layout Example + +```tsx +import { useState } from 'react'; +import { DMConversationList } from '@/components/dm/DMConversationList'; +import { DMChatArea } from '@/components/dm/DMChatArea'; + +function CustomMessagingLayout() { + const [selectedPubkey, setSelectedPubkey] = useState<string | null>(null); + + return ( + <div className="flex h-screen"> + {/* Custom sidebar */} + <aside className="w-64 border-r"> + <DMConversationList + selectedPubkey={selectedPubkey} + onSelectConversation={setSelectedPubkey} + /> + </aside> + + {/* Custom main area */} + <main className="flex-1"> + {selectedPubkey ? ( + <DMChatArea pubkey={selectedPubkey} /> + ) : ( + <div className="flex items-center justify-center h-full"> + <p>Select a conversation to start messaging</p> + </div> + )} + </main> + </div> + ); +} +``` + diff --git a/docs/NOSTR_INFINITE_SCROLL.md b/docs/NOSTR_INFINITE_SCROLL.md new file mode 100644 index 0000000..becee36 --- /dev/null +++ b/docs/NOSTR_INFINITE_SCROLL.md @@ -0,0 +1,72 @@ +### Infinite Scroll for Nostr Feeds + +For feed-like interfaces, implement infinite scroll using TanStack Query's `useInfiniteQuery` with Nostr's timestamp-based pagination: + +```typescript +import { useNostr } from '@nostrify/react'; +import { useInfiniteQuery } from '@tanstack/react-query'; + +export function useGlobalFeed() { + const { nostr } = useNostr(); + + return useInfiniteQuery({ + queryKey: ['global-feed'], + queryFn: async ({ pageParam, signal }) => { + const filter = { kinds: [1], limit: 20 }; + if (pageParam) filter.until = pageParam; + + const events = await nostr.query([filter], { + signal: AbortSignal.any([signal, AbortSignal.timeout(1500)]) + }); + + return events; + }, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) return undefined; + return lastPage[lastPage.length - 1].created_at - 1; // Subtract 1 since 'until' is inclusive + }, + initialPageParam: undefined, + }); +} +``` + +Example usage with intersection observer for automatic loading: + +```tsx +import { useInView } from 'react-intersection-observer'; +import { useMemo } from 'react'; + +function GlobalFeed() { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGlobalFeed(); + const { ref, inView } = useInView(); + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + // Remove duplicate events by ID + const posts = useMemo(() => { + const seen = new Set(); + return data?.pages.flat().filter(event => { + if (!event.id || seen.has(event.id)) return false; + seen.add(event.id); + return true; + }) || []; + }, [data?.pages]); + + return ( + <div className="space-y-4"> + {posts.map((post) => ( + <PostCard key={post.id} post={post} /> + ))} + {hasNextPage && ( + <div ref={ref} className="py-4"> + {isFetchingNextPage && <Skeleton className="h-20 w-full" />} + </div> + )} + </div> + ); +} +``` \ No newline at end of file diff --git a/eslint-rules/README.md b/eslint-rules/README.md new file mode 100644 index 0000000..5254b9e --- /dev/null +++ b/eslint-rules/README.md @@ -0,0 +1,146 @@ +# Custom ESLint Rules + +This directory contains custom ESLint rules for the project. + +## no-inline-script + +This rule prevents the use of inline script tags in HTML files. Inline scripts can pose security risks and violate Content Security Policy (CSP) directives. + +### Examples + +❌ **Bad** - These will trigger the rule: +```html +<!-- Inline JavaScript code --> +<script> + console.log("This is inline JavaScript"); +</script> + +<!-- Inline JSON-LD structured data --> +<script type="application/ld+json"> + {"@context": "https://schema.org", "@type": "Organization"} +</script> +``` + +✅ **Good** - These are fine: +```html +<!-- External script files --> +<script src="/js/app.js"></script> +<script type="module" src="/src/main.tsx"></script> + +<!-- Empty script tags (no content) --> +<script id="data-container"></script> +``` + +### Configuration + +The rule is configured in `eslint.config.js` as: +```javascript +"custom/no-inline-script": "error" +``` + +### Purpose + +This rule helps maintain security best practices by: +- Preventing XSS vulnerabilities from inline scripts +- Enforcing Content Security Policy compliance +- Encouraging separation of concerns (HTML structure vs JavaScript logic) +- Making code easier to maintain and debug + +## no-placeholder-comments + +This rule detects and flags comments that start with "// In a real" (case-insensitive). These comments typically indicate placeholder implementations that should be replaced with real code. + +### Examples + +❌ **Bad** - These will trigger the rule: +```javascript +// In a real application, this would connect to a database +const data = []; + +// in a real world scenario, this would be different +const config = {}; + +/* In a real implementation, we would handle errors */ +const handleError = () => {}; +``` + +✅ **Good** - These are fine: +```javascript +// This is a regular comment +const data = []; + +// TODO: Implement database connection +const config = {}; + +// Note: In a real application, consider using a database +const handleError = () => {}; +``` + +### Configuration + +The rule is configured in `eslint.config.js` as: +```javascript +"custom/no-placeholder-comments": "error" +``` + +You can change the severity level to: +- `"off"` - Disable the rule +- `"warn"` - Show as warning +- `"error"` - Show as error (current setting) + +### Purpose + +This rule helps ensure that placeholder comments used during development are replaced with actual implementations before code is committed or deployed to production. + +## require-webmanifest + +This rule ensures that HTML files include a proper web manifest link tag and that the referenced manifest file exists. Web manifests are essential for Progressive Web Apps (PWAs) and provide metadata about the application. + +### Examples + +❌ **Bad** - These will trigger the rule: +```html +<!-- Missing manifest link entirely --> +<head> + <title>My App</title> +</head> + +<!-- Manifest file doesn't exist --> +<head> + <link rel="manifest" href="/nonexistent-manifest.json"> +</head> + +<!-- Invalid manifest link (missing rel or href) --> +<head> + <link href="/manifest.json"> +</head> +``` + +✅ **Good** - These are fine: +```html +<!-- Proper manifest link with existing file --> +<head> + <link rel="manifest" href="/manifest.json"> +</head> + +<!-- Alternative valid manifest link --> +<head> + <link rel="manifest" href="/public/site.webmanifest"> +</head> +``` + +### Configuration + +The rule is configured in `eslint.config.js` as: +```javascript +"custom/require-webmanifest": "error" +``` + +### Purpose + +This rule helps ensure: +- PWA compliance by requiring a web manifest +- Proper manifest file structure and accessibility +- Better user experience on mobile devices +- App installation capabilities +- Consistent branding and metadata across platforms \ No newline at end of file diff --git a/eslint-rules/index.js b/eslint-rules/index.js new file mode 100644 index 0000000..bbaeecd --- /dev/null +++ b/eslint-rules/index.js @@ -0,0 +1,11 @@ +import noInlineScript from './no-inline-script.js'; +import noPlaceholderComments from './no-placeholder-comments.js'; +import requireWebmanifest from './require-webmanifest.js'; + +export default { + rules: { + 'no-inline-script': noInlineScript, + 'no-placeholder-comments': noPlaceholderComments, + 'require-webmanifest': requireWebmanifest, + }, +}; \ No newline at end of file diff --git a/eslint-rules/no-inline-script.js b/eslint-rules/no-inline-script.js new file mode 100644 index 0000000..8227257 --- /dev/null +++ b/eslint-rules/no-inline-script.js @@ -0,0 +1,40 @@ +/** + * Rule to prevent inline script tags in HTML files + */ + +export default { + meta: { + type: 'problem', + docs: { + description: 'Prevent inline script tags in HTML files', + category: 'Security', + recommended: true, + }, + fixable: null, + schema: [], + messages: { + noInlineScript: 'Inline script tags are not allowed. Move script content to external files.', + }, + }, + + create(context) { + return { + // For HTML files, we need to check script tags + 'ScriptTag'(node) { + // Check if this is an inline script (has content but no src attribute) + const hasContent = node.value && node.value.value && node.value.value.trim().length > 0; + const hasSrc = node.attributes && node.attributes.some(attr => + attr.key && attr.key.value === 'src' + ); + + // If the script has content but no src attribute, it's an inline script + if (hasContent && !hasSrc) { + context.report({ + node, + messageId: 'noInlineScript', + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/eslint-rules/no-placeholder-comments.js b/eslint-rules/no-placeholder-comments.js new file mode 100644 index 0000000..b2f7b95 --- /dev/null +++ b/eslint-rules/no-placeholder-comments.js @@ -0,0 +1,45 @@ +/** + * Custom ESLint rule to detect placeholder comments starting with "// In a real" + * These comments indicate incomplete implementations that should be replaced with real code. + */ + +export default { + meta: { + type: "problem", + docs: { + description: "Disallow placeholder comments starting with '// In a real'", + category: "Best Practices", + recommended: true, + }, + fixable: null, + schema: [], + messages: { + placeholderComment: "Placeholder comment detected: '{{comment}}'. This should be replaced with a real implementation.", + }, + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + Program() { + const comments = sourceCode.getAllComments(); + + comments.forEach((comment) => { + const commentText = comment.value.trim(); + + // Check if comment starts with "In a real" (case-insensitive) + if (commentText.toLowerCase().startsWith("in a real")) { + context.report({ + node: comment, + messageId: "placeholderComment", + data: { + comment: `// ${commentText}`, + }, + }); + } + }); + }, + }; + }, +}; \ No newline at end of file diff --git a/eslint-rules/require-webmanifest.js b/eslint-rules/require-webmanifest.js new file mode 100644 index 0000000..312bafd --- /dev/null +++ b/eslint-rules/require-webmanifest.js @@ -0,0 +1,84 @@ +import fs from 'fs'; +import path from 'path'; + +export default { + meta: { + type: 'problem', + docs: { + description: 'Require web manifest file and proper HTML link tag', + category: 'Best Practices', + }, + fixable: null, + schema: [], + messages: { + missingManifestFile: 'Web manifest file not found. Expected {{expectedPath}}', + missingManifestLink: 'Missing web manifest link tag in HTML head', + invalidManifestLink: 'Web manifest link tag has incorrect rel or href attribute', + }, + }, + + create(context) { + const filename = context.getFilename(); + + // Only run this rule on HTML files + if (!filename.endsWith('.html')) { + return {}; + } + + return { + Program(node) { + const sourceCode = context.getSourceCode(); + const htmlContent = sourceCode.getText(); + + // Check for manifest link tag in HTML + const manifestLinkRegex = /<link[^>]*rel=["']manifest["'][^>]*>/i; + const manifestMatch = htmlContent.match(manifestLinkRegex); + + if (!manifestMatch) { + context.report({ + node, + messageId: 'missingManifestLink', + }); + return; + } + + // Extract href from the manifest link + const hrefMatch = manifestMatch[0].match(/href=["']([^"']+)["']/i); + if (!hrefMatch) { + context.report({ + node, + messageId: 'invalidManifestLink', + }); + return; + } + + const manifestPath = hrefMatch[1]; + + // Resolve the manifest file path relative to the project root + const htmlDir = path.dirname(filename); + let resolvedManifestPath; + + if (manifestPath.startsWith('/')) { + // Absolute path - check in public directory first, then project root + const publicPath = path.resolve(htmlDir, 'public' + manifestPath); + const rootPath = path.resolve(htmlDir, '.' + manifestPath); + resolvedManifestPath = fs.existsSync(publicPath) ? publicPath : rootPath; + } else { + // Relative path + resolvedManifestPath = path.resolve(htmlDir, manifestPath); + } + + // Check if the manifest file exists + if (!fs.existsSync(resolvedManifestPath)) { + context.report({ + node, + messageId: 'missingManifestFile', + data: { + expectedPath: manifestPath, + }, + }); + } + }, + }; + }, +}; \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..cbd3337 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,74 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import htmlEslint from "@html-eslint/eslint-plugin"; +import htmlParser from "@html-eslint/parser"; +import customRules from "./eslint-rules/index.js"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + linterOptions: { + reportUnusedDisableDirectives: "error", // Reports unused disable directives as errors + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + "custom": customRules, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true, + }, + ], + "custom/no-placeholder-comments": "error", + "no-warning-comments": [ + "error", + { terms: ["fixme"] }, + ], + }, + }, + { + files: ["**/*.html"], + plugins: { + "@html-eslint": htmlEslint, + "custom": customRules, + }, + languageOptions: { + parser: htmlParser, + }, + rules: { + "@html-eslint/require-title": "error", + "@html-eslint/require-meta-charset": "error", + "@html-eslint/require-meta-description": "error", + "@html-eslint/require-meta-viewport": "error", + "@html-eslint/require-open-graph-protocol": [ + "error", + [ + "og:type", + "og:title", + "og:description", + ], + ], + "custom/no-inline-script": "error", + "custom/require-webmanifest": "error", + }, + } +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..cd5ada1 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ +<!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'; frame-src 'self' https:; font-src 'self'; base-uri 'self'; manifest-src 'self'; connect-src 'self' blob: https: wss:; img-src 'self' data: blob: https:; media-src 'self' https:"> + <link rel="manifest" href="/manifest.webmanifest"> + </head> + <body> + <div id="root"></div> + <script type="module" src="/src/main.tsx"></script> + </body> +</html> diff --git a/kustomize/base/deployment.yaml b/kustomize/base/deployment.yaml deleted file mode 100644 index 5d43bf4..0000000 --- a/kustomize/base/deployment.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: lumina-deployment -spec: - replicas: 1 - selector: - matchLabels: - app: lumina - template: - metadata: - labels: - app: lumina - spec: - containers: - - name: lumina - image: ghcr.io/lumina-rocks/lumina:latest - ports: - - containerPort: 80 diff --git a/kustomize/base/kustomization.yaml b/kustomize/base/kustomization.yaml deleted file mode 100644 index 6d1374a..0000000 --- a/kustomize/base/kustomization.yaml +++ /dev/null @@ -1,3 +0,0 @@ -resources: - - deployment.yaml - - service.yaml diff --git a/kustomize/base/service.yaml b/kustomize/base/service.yaml deleted file mode 100644 index 62907a4..0000000 --- a/kustomize/base/service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: my-app -spec: - selector: - app: my-app - ports: - - protocol: TCP - port: 80 - targetPort: 80 diff --git a/kustomize/environments/beta/kustomization.yaml b/kustomize/environments/beta/kustomization.yaml deleted file mode 100644 index fb7dc89..0000000 --- a/kustomize/environments/beta/kustomization.yaml +++ /dev/null @@ -1,2 +0,0 @@ -resources: - - ../../base diff --git a/kustomize/environments/prod/kustomization.yaml b/kustomize/environments/prod/kustomization.yaml deleted file mode 100644 index fb7dc89..0000000 --- a/kustomize/environments/prod/kustomization.yaml +++ /dev/null @@ -1,2 +0,0 @@ -resources: - - ../../base diff --git a/next.config.mjs b/next.config.mjs deleted file mode 100644 index ea9ca17..0000000 --- a/next.config.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import withPWA from 'next-pwa'; - -const config = { - dest: 'public' -}; - -const nextConfig = { - output: 'standalone', - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '**', - port: '', - pathname: '/**', - }, - ], - }, - async headers() { - return [ - { - // matching all well-known routes - source: "/.well-known/:path*", - headers: [ - // { key: "Access-Control-Allow-Credentials", value: "true" }, - { key: "Access-Control-Allow-Origin", value: "*" }, - { key: "Access-Control-Allow-Methods", value: "GET" }, - // { key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" }, - ] - } - ] - } -}; - -export default withPWA(config)(nextConfig); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e14e30b..2d6d735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,77 +1,110 @@ { "name": "lumina", - "version": "0.1.28", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumina", - "version": "0.1.28", + "version": "2.0.0", "dependencies": { - "@getalby/sdk": "^5.0.0", - "@hookform/resolvers": "^3.4.0", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-dialog": "^1.1.10", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-navigation-menu": "^1.1.4", - "@radix-ui/react-scroll-area": "^1.0.5", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.4", - "@radix-ui/react-slot": "^1.2.0", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "blurhash": "^2.0.5", - "bolt11": "^1.4.1", + "@fontsource-variable/inter": "^5.2.6", + "@getalby/sdk": "^5.1.1", + "@hookform/resolvers": "^3.9.0", + "@nostrify/nostrify": "^0.47.1", + "@nostrify/react": "^0.2.17", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-aspect-ratio": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@tanstack/react-query": "^5.56.2", + "@unhead/addons": "^2.0.10", + "@unhead/react": "^2.0.10", + "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "embla-carousel-react": "^8.0.0-rc21", - "html5-qrcode": "^2.3.8", - "light-bolt11-decoder": "^3.1.1", - "lucide-react": "^0.475.0", - "next": "14.2.28", - "next-pwa": "^5.6.0", - "next-themes": "^0.2.1", - "nostr-react": "^0.7.0", - "nostr-tools": "^2.4.0", - "react": "^18", - "react-dom": "^18", - "react-hook-form": "^7.51.4", - "react-icons": "^5.1.0", - "react-qr-code": "^2.0.15", - "sharp": "^0.33.5", - "tailwind-merge": "^3.0.1", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.3.0", + "idb": "^8.0.3", + "input-otp": "^1.2.4", + "lucide-react": "^0.462.0", + "nostr-tools": "^2.13.0", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-intersection-observer": "^9.16.0", + "react-resizable-panels": "^2.1.3", + "react-router-dom": "^6.26.2", + "recharts": "^2.12.7", + "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.8.9", - "zod": "^3.23.8" + "vaul": "^0.9.3", + "zod": "^3.25.71" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10.0.1", - "eslint": "^8", - "eslint-config-next": "14.1.0", - "postcss": "^8", - "tailwindcss": "^3.3.0", - "typescript": "^5" + "@eslint/js": "^9.9.0", + "@html-eslint/eslint-plugin": "^0.41.0", + "@html-eslint/parser": "^0.41.0", + "@tailwindcss/typography": "^0.5.15", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.5.5", + "@types/qrcode": "^1.5.5", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "@webbtc/webln-types": "^3.0.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "jsdom": "^26.1.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^6.3.5", + "vitest": "^3.1.4" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "license": "MIT", "engines": { "node": ">=10" @@ -80,411 +113,77 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz", - "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dependencies": { - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", - "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", - "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -493,1236 +192,593 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", - "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", - "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", - "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", - "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", - "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", - "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", - "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", - "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", - "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", - "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", - "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, "node_modules/@babel/runtime": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", - "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1730,34 +786,154 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/js": { - "version": "8.56.0", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@floating-ui/core": { - "version": "1.6.0", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.1", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.1" + "@floating-ui/core": "^1.7.0", + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.8", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.6.1" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -1765,13 +941,24 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@fontsource-variable/inter": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.6.tgz", + "integrity": "sha512-jks/bficUPQ9nn7GvXvHtlQIPudW7Wx8CrlZoY8bhxgeobNxlQan8DclUJuYF2loYRrGpfrhCIZZspXYysiVGg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@getalby/lightning-tools": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.1.2.tgz", - "integrity": "sha512-BwGm8eGbPh59BVa1gI5yJMantBl/Fdps6X4p1ZACnmxz9vDINX8/3aFoOnDlF7yyA2boXWCsReVQSr26Q2yjiQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz", + "integrity": "sha512-8kBvENBTMh541VjGKhw3I29+549/C02gLSh3AQaMfoMNSZaMxfQW+7dcMcc7vbFaCKEcEe18ST5bUveTRBuXCQ==", "license": "MIT", "engines": { "node": ">=14" @@ -1782,13 +969,13 @@ } }, "node_modules/@getalby/sdk": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@getalby/sdk/-/sdk-5.0.0.tgz", - "integrity": "sha512-PyXgLZN3p+xZIPqyNyTwE0WxTCZI1YJK8TV/amOjdNkecQCR3LnVsusdtZfoszyS+pVo/HaSwF5zCQWUmz8I+Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@getalby/sdk/-/sdk-5.1.1.tgz", + "integrity": "sha512-t/kg2ljPx86qRYKqEVc5VYhDICFKtVPRlQKIz5cI/AqOLYVguLJz1AkQlDBaiOz2PW5FxoyGlLkTGmX7ONHH/Q==", "license": "MIT", "dependencies": { "@getalby/lightning-tools": "^5.1.2", - "nostr-tools": "2.9.4" + "nostr-tools": "2.15.0" }, "engines": { "node": ">=14" @@ -1799,28 +986,103 @@ } }, "node_modules/@hookform/resolvers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.0.tgz", - "integrity": "sha512-+oAqK3okmoEDnvUkJ3N/mvNMeeMv5Apgy1jkoRmlaaAF4vBgcJs9tHvtXU7VE4DvPosvAUUkPOaNFunzt1dbgA==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", "peerDependencies": { "react-hook-form": "^7.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", + "node_modules/@html-eslint/eslint-plugin": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.41.0.tgz", + "integrity": "sha512-kZhOdLOx5xGogw9CE/uGKkoo8t7fNK9PxU6kosIvu9Yveir3r2VhGYC4II/2OEGQ+O4cPk2LxV1fCpYt7XFMtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/plugin-kit": "^0.3.1", + "@html-eslint/parser": "^0.41.0", + "@html-eslint/template-parser": "^0.41.0", + "@html-eslint/template-syntax-parser": "^0.41.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0" + } + }, + "node_modules/@html-eslint/parser": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.41.0.tgz", + "integrity": "sha512-QQJeq2G11T/SGYcG09+XOajm+X8XX6bVVSouB3SVlGmm3exvJis0B6tBnb+5Rz2kC/q3C21z1ZuCc/AXcFKzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@html-eslint/template-syntax-parser": "^0.41.0", + "es-html-parser": "0.2.0" + } + }, + "node_modules/@html-eslint/template-parser": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@html-eslint/template-parser/-/template-parser-0.41.0.tgz", + "integrity": "sha512-ntZNMh+G13lPvwSSkr2U0XqWWDFLSZPeidcztgVCjxthwgSMdefL4au/YicnlB+h1WNBw8Pk0bAjczJXEbp85g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-html-parser": "0.2.0" + } + }, + "node_modules/@html-eslint/template-syntax-parser": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@html-eslint/template-syntax-parser/-/template-syntax-parser-0.41.0.tgz", + "integrity": "sha512-dgiE30uXWCoD89chi6KdjYOF+143bImJ3GNl3j2iy6/ZM8enTqCQVHZ48+Esf6Tx2rLBBBdxx4Rb0fG11myHsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1831,374 +1093,24 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=18.18" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -2212,146 +1124,42 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "license": "MIT", "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2364,177 +1172,34 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@next/env": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz", - "integrity": "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "14.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "10.3.10" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz", - "integrity": "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz", - "integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz", - "integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz", - "integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz", - "integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz", - "integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz", - "integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz", - "integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz", - "integrity": "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/ciphers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.2.tgz", - "integrity": "sha512-GADtQmZCdgbnNp+daPLc3OY3ibEtGGDV/+CzeM3MFnhiQ7ELQKlsHWYq0YbYUXx4jU3/Y1erAxU6r+hwpewqmQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves": { - "version": "1.1.0", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.3.1" + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -2542,6 +1207,8 @@ }, "node_modules/@noble/hashes": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "license": "MIT", "engines": { "node": ">= 16" @@ -2552,6 +1219,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -2563,6 +1232,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "license": "MIT", "engines": { "node": ">= 8" @@ -2570,6 +1241,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2579,8 +1252,69 @@ "node": ">= 8" } }, + "node_modules/@nostrify/nostrify": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@nostrify/nostrify/-/nostrify-0.47.1.tgz", + "integrity": "sha512-8jdAeKIyyDK/UQN92WgrQn9+B067HYcjfW35TpIjNLAT9qYe+KYFHaC9O6PD+I1DvLHziJMxk4Q18Fu4fqOiAQ==", + "dependencies": { + "@nostrify/types": "0.36.7", + "@scure/base": "^1.2.6", + "@std/encoding": "npm:@jsr/std__encoding@^0.224.1", + "@types/node": "^24.1.0", + "lru-cache": "^10.2.0", + "nostr-tools": "^2.13.0", + "websocket-ts": "^2.2.1", + "ws": "^8.18.3", + "zod": "^3.23.8" + } + }, + "node_modules/@nostrify/nostrify/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nostrify/nostrify/node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@nostrify/nostrify/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/@nostrify/react": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@nostrify/react/-/react-0.2.17.tgz", + "integrity": "sha512-IG6XPiNBe1nDFZ055EnWufxASPjgOZ2cE53Hjanj5xQQURwzA946yMhDDzaHmD9lvoBYjuijopmTiyFk2RObHQ==", + "dependencies": { + "@nostrify/nostrify": "0.47.1", + "@nostrify/types": "0.36.7", + "@tanstack/react-query": "^5.69.0", + "happy-dom": "^17.4.4", + "nostr-tools": "^2.13.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@nostrify/types": { + "version": "0.36.7", + "resolved": "https://registry.npmjs.org/@nostrify/types/-/types-0.36.7.tgz", + "integrity": "sha512-Jdf2eWdIzonMzic/st4ltre7XIcXVNTjmcbKpw4xGEfNmoq2Y4wj8KcSB6s0TUqxcLEyiXnyGP8EDx/gEVri9A==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", "optional": true, "engines": { @@ -2588,41 +1322,66 @@ } }, "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" }, "node_modules/@radix-ui/react-accordion": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", - "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.8.tgz", + "integrity": "sha512-c7OKBvO36PfQIUGIjj1Wko0hH937pYFU2tR5zbIJDUsmTzHoZVHHt4bmb7OOJbzTaWJtVELKWojBHa7OcnUHmQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collapsible": "1.0.3", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.8", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.11.tgz", + "integrity": "sha512-4KfkwrFnAw3Y5Jeoq6G+JYSKW0JfIS3uDdFC/79Jw9AsMayZMizSSMxk1gkrolYXsa/WzbbDfOA7/D8N5D+l1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.11", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2634,17 +1393,41 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", + "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.4.tgz", + "integrity": "sha512-ie2mUDtM38LBqVU+Xn+GIY44tWM5yVbT5uXO+th85WZxUUsgEdWNNZWecqqGzkQ4Af+Fq1mYT6TyQ/uUf5gfcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2656,20 +1439,52 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.0.4", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.7.tgz", + "integrity": "sha512-V7ODUt4mUoJTe3VUxZw6nfURxaPALVqmDQh501YmaQsk3D8AZQrOPRnfKn4H7JGDLBc0KqLhT94H79nV88ppNg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.2.3.tgz", + "integrity": "sha512-pHVzDYsnaDmBlAuwim45y3soIN8H4R7KbkSVirGhXO+R/kO2OLCe0eucUEbddaTcdMHHdzcIGHtZSMSQlA+apw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2681,25 +1496,25 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", - "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.8.tgz", + "integrity": "sha512-hxEsLvK9WxIAPyxdDRULL4hcaSjMZCfP7fHB0Z1uUnDoDBat1Zh46hwYfa69DeZAbJrPckjf0AGAtEZyvDyJbw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2711,20 +1526,21 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.0.3", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2735,34 +1551,14 @@ } } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2771,14 +1567,13 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.0.1", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2786,10 +1581,38 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.12.tgz", + "integrity": "sha512-5UFKuTMX8F2/KjHvyqu9IYT8bEtDSCJwwIx1PghBo4jh9S6jJVsceq9xIjqsOVcxsynGwV5eaqPE3n/Cu+DrSA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.12", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.10.tgz", - "integrity": "sha512-m6pZb0gEM5uHPSb+i2nKKGQi/HMSVjARMsLMWQfKDP+eJ6B+uqryHnXhpnohTWElw+vEcMk/o4wJODtdRKHwqg==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz", + "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", @@ -2800,7 +1623,7 @@ "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.6", - "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-controllable-state": "1.2.2", @@ -2822,16 +1645,10 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2843,22 +1660,7 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", @@ -2885,7 +1687,36 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.12.tgz", + "integrity": "sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.12", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", @@ -2900,7 +1731,7 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "node_modules/@radix-ui/react-focus-scope": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", @@ -2925,7 +1756,38 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.11.tgz", + "integrity": "sha512-q9h9grUpGZKR3MNhtVCLVnPGmx1YnzBgGR+O40mhSNGsUnkR+LChVH8c7FB0mkS+oudhd8KAkZGTJPJCjdAPIg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", @@ -2943,7 +1805,207 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "node_modules/@radix-ui/react-label": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.4.tgz", + "integrity": "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.12.tgz", + "integrity": "sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.7", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.12.tgz", + "integrity": "sha512-bM2vT5nxRqJH/d1vFQ9jLsW4qR70yFQw2ZD1TUPWUNskDsV0eYeMbbNJqxNjGMOVogEkOJaHtu11kzYdTJvVJg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.12", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.7", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.10.tgz", + "integrity": "sha512-kGDqMVPj2SRB1vJmXN/jnhC66REAXNyDmDRubbbmJ+360zSIJUDmWGMKIJOf72PHMwPENrbtJVb3CMAUJDjEIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.11.tgz", + "integrity": "sha512-yFMfZkVA5G3GJnBgb2PxrrcLKm1ZLWXrbYVgdyTl//0TYEIHS9LJbnyz7WWcZ0qCq7hIlJZpRtxeSeIG5T5oJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", + "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", @@ -2967,10 +2029,10 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", - "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -2991,7 +2053,7 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "node_modules/@radix-ui/react-primitive": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", @@ -3014,341 +2076,20 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", - "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-menu": "2.0.6", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-icons": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", - "integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==", - "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x" - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", - "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", - "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { + "node_modules/@radix-ui/react-progress": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.4.tgz", + "integrity": "sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -3359,27 +2100,28 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.1.3", + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.4.tgz", + "integrity": "sha512-N4J9QFdW5zcJNxxY/zwTXBN4Uc5VEuRM7ZLjNfnWoKmNvgrPtNNw4P8zY532O3qL6aPkaNO+gY9y6bfzmH4U1g==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.7", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -3390,113 +2132,27 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", - "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.7.tgz", + "integrity": "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -3508,26 +2164,26 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz", - "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.6.tgz", + "integrity": "sha512-lj8OMlpPERXrQIHlEQdlXHJoRT52AMpBrgyPYylOhXYq5e/glsEdtOc/kCQlsTdtgN5U0iDbrrolDadvektJGQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -3539,29 +2195,30 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", - "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.2.tgz", + "integrity": "sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA==", + "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.0", - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2", + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -3580,440 +2237,6 @@ } } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/number": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", - "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", - "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", - "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", - "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", - "dependencies": { - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-previous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", - "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "dependencies": { - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", - "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" - }, - "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-separator": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz", @@ -4037,13 +2260,23 @@ } } }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", - "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "node_modules/@radix-ui/react-slider": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.2.tgz", + "integrity": "sha512-oQnqfgSiYkxZ1MrF6672jw2/zZvpB+PJsrIc3Zm1zof1JHf/kj7WhmROw7JahLfOwYQ5/+Ip0rFORgF1tjSiaQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.0" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4078,34 +2311,19 @@ } } }, - "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-switch": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", - "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.2.tgz", + "integrity": "sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4122,184 +2340,26 @@ } } }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-previous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", - "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tabs": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", - "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.9.tgz", + "integrity": "sha512-KIjtwciYvquiW/wAFkELZCVnaNLBsYNhTNcvl+zfMAbMhRkcvNuCLXDDd22L0j7tagpzVh/QwbFpwAATg7ILPw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.7", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4311,29 +2371,83 @@ } }, "node_modules/@radix-ui/react-toast": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", - "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.11.tgz", + "integrity": "sha512-Ed2mlOmT+tktOsu2NZBK1bCSHh/uqULu1vWOkpQTVq53EoOuZUZw7FInQoDB3uil5wZc2oe0XN9a7uVZB7/6AQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.6.tgz", + "integrity": "sha512-3SeJxKeO3TO1zVw1Nl++Cp0krYk6zHDHMCUXXVkosIzl6Nxcvb07EerQpyD2wXQSJ5RZajrYAmPaydU8Hk1IyQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.7.tgz", + "integrity": "sha512-GRaPJhxrRSOqAcmcX3MwRL/SZACkoYdmoY9/sg7Bd5DhBYsB2t4co0NxTvVW8H7jUmieQDQwRtUlZ5Ta8UbgJA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.7", + "@radix-ui/react-toggle": "1.1.6", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4345,28 +2459,29 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.0.7", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.4.tgz", + "integrity": "sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4377,34 +2492,14 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4413,15 +2508,17 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4447,7 +2544,43 @@ } } }, - "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", @@ -4462,48 +2595,14 @@ } } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4512,15 +2611,16 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4529,15 +2629,16 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4546,17 +2647,18 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.0.tgz", + "integrity": "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -4568,93 +2670,344 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.0.1", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 10.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { - "@types/babel__core": { + "rollup": { "optional": true } } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.7.2", - "dev": true, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", + "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", + "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", + "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", + "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", + "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", + "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", + "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", + "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", + "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", + "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", + "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", + "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", + "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", + "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", + "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", + "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", + "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", + "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", + "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", + "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@scure/base": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", "funding": [ { "type": "individual", @@ -4665,6 +3018,8 @@ }, "node_modules/@scure/bip32": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", "license": "MIT", "dependencies": { "@noble/curves": "~1.1.0", @@ -4675,8 +3030,22 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", "license": "MIT", "dependencies": { "@noble/hashes": "~1.3.0", @@ -4686,200 +3055,608 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@surma/rollup-plugin-off-main-thread": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", - "dependencies": { - "ejs": "^3.1.6", - "json5": "^2.2.0", - "magic-string": "^0.25.0", - "string.prototype.matchall": "^4.0.6" - } + "node_modules/@std/encoding": { + "name": "@jsr/std__encoding", + "version": "0.224.3", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/0.224.3.tgz", + "integrity": "sha512-zAuX2QV1zwJ5RSmrnDGVerAtN3pBXpYYNlGzhERW9AiQ1UJd2/xruyB3i5NdTWy2OK2pjETswOj+0+prYTPlxQ==" }, - "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" + "node_modules/@swc/core": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" }, - "node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" + "@swc/counter": "^0.1.3" } }, - "node_modules/@types/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", "dependencies": { - "@types/node": "*" + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "node_modules/@tanstack/query-core": { + "version": "5.75.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.75.0.tgz", + "integrity": "sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.75.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.75.1.tgz", + "integrity": "sha512-tN+gG+eXCHYm+VpmdXUP1rfE9LUrRzgYozTkBZtJV1/WFM3vwWNKQC8G6b2RKcs+2cPg+hdToZHZfjL3bF4yIQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.75.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" } }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/json5": { - "version": "0.0.29", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" - }, "node_modules/@types/node": { - "version": "20.11.8", + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/prop-types": { - "version": "15.7.11", + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "devOptional": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "18.2.48", - "devOptional": true, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.18", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", + "node_modules/@types/react": { + "version": "18.3.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", + "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.19.1", - "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4" + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", + "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/type-utils": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", + "node_modules/@typescript-eslint/parser": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", + "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", + "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", + "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", + "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.31.1", + "@typescript-eslint/utils": "8.31.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/types": { - "version": "6.19.1", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", + "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", "dev": true, "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -4887,34 +3664,46 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", + "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/visitor-keys": "8.31.1", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -4927,239 +3716,223 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", + "node_modules/@typescript-eslint/utils": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", + "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.31.1", + "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/typescript-estree": "8.31.1" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", + "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.31.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", + "node_modules/@unhead/addons": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@unhead/addons/-/addons-2.0.10.tgz", + "integrity": "sha512-9+w/m+X5e7CDKXKGTym1N4MpBjrRC89cfl95RDgKwBcFJfQ3pZu50llIjx/j462VqtrNMXddBKcUnfWvQyapuw==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "ufo": "^1.6.1", + "unplugin": "^2.3.4", + "unplugin-ast": "^0.15.0" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/react": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.0.10.tgz", + "integrity": "sha512-U5tqhUYk4qmyLD8YpKOuYwWmbIGFpB7aacOzcGAhjJ59GH3W/BSFOFHj/6dcoYV5yzr1CZrINGGH7stRVMMLjQ==", + "license": "MIT", + "dependencies": { + "unhead": "2.0.10" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", + "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", "dev": true, - "license": "ISC" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + "@swc/core": "^1.11.21" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "node_modules/@vitest/expect": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "node_modules/@vitest/mocker": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" + "@vitest/spy": "3.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "node_modules/@vitest/pretty-format": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "peer": true, + "node_modules/@vitest/runner": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@xtuc/long": "4.2.2" + "@vitest/utils": "3.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "node_modules/@vitest/snapshot": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" + "@vitest/pretty-format": "3.1.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "node_modules/@vitest/spy": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "node_modules/@vitest/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" + "@vitest/pretty-format": "3.1.4", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "node_modules/@webbtc/webln-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@webbtc/webln-types/-/webln-types-3.0.0.tgz", + "integrity": "sha512-aXfTHLKz5lysd+6xTeWl+qHNh/p3qVYbeLo+yDN5cUDmhie2ZoGvkppfWxzbGkcFBzb6dJyQ2/i2cbmDHas+zQ==", + "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "funding": { + "type": "lightning", + "url": "lightning:hello@getalby.com" } }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true - }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -5169,14 +3942,29 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5189,62 +3977,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-regex": { - "version": "5.0.1", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5258,10 +4006,14 @@ }, "node_modules/any-promise": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -5273,10 +4025,14 @@ }, "node_modules/arg": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, @@ -5284,6 +4040,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -5293,167 +4050,44 @@ }, "node_modules/aria-query": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.7", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/array-union": { + "node_modules/ast-kit": { "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "dev": true, + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.1.0.tgz", + "integrity": "sha512-ROM2LlXbZBZVk97crfw8PGDOBzzsJvN2uJCmwswvPUNyfH14eg90mSN3xNqsri1JS1G9cz0VzeDUhxJkTrr4Ew==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "@babel/parser": "^7.27.3", + "pathe": "^2.0.3" }, "engines": { - "node": ">= 0.4" + "node": ">=20.18.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" - }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - } - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" + "url": "https://github.com/sponsors/sxzz" } }, "node_modules/autoprefixer": { - "version": "10.4.17", + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "dev": true, "funding": [ { @@ -5471,11 +4105,11 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -5488,173 +4122,49 @@ "postcss": "^8.1.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.7.0", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "3.2.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/babel-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", - "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", - "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.5.0", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", - "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/base-x": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", - "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, - "node_modules/bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { - "version": "2.2.0", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", "engines": { "node": ">=8" - } - }, - "node_modules/bip174": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", - "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bitcoinjs-lib": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz", - "integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==", - "dependencies": { - "@noble/hashes": "^1.2.0", - "bech32": "^2.0.0", - "bip174": "^2.1.1", - "bs58check": "^3.0.1", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.1.2" }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bitcoinjs-lib/node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "node_modules/blurhash": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", - "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", - "license": "MIT" - }, - "node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "node_modules/bolt11": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bolt11/-/bolt11-1.4.1.tgz", - "integrity": "sha512-jR0Y+MO+CK2at1Cg5mltLJ+6tdOwNKoTS/DJOBDdzVkQ+R9D6UgZMayTWOsuzY7OgV1gEqlyT5Tzk6t6r4XcNQ==", - "dependencies": { - "@types/bn.js": "^4.11.3", - "bech32": "^1.1.2", - "bitcoinjs-lib": "^6.0.0", - "bn.js": "^4.11.8", - "create-hash": "^1.2.0", - "lodash": "^4.17.11", - "safe-buffer": "^5.1.1", - "secp256k1": "^4.0.2" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5665,6 +4175,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -5672,15 +4183,11 @@ "node": ">=8" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5695,6 +4202,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5708,79 +4216,73 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "dependencies": { - "base-x": "^4.0.0" - } - }, - "node_modules/bs58check": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", - "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", - "dependencies": { - "@noble/hashes": "^1.2.0", - "bs58": "^5.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.5", + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001689", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", - "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5794,10 +4296,31 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5810,14 +4333,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { - "version": "3.5.3", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -5831,12 +4360,17 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -5845,25 +4379,6 @@ "node": ">= 6" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -5876,24 +4391,72 @@ "url": "https://polar.sh/cva" } }, - "node_modules/clean-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", "dependencies": { - "del": "^4.1.1" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": ">=4.0.0 <6.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "node_modules/client-only": { - "version": "0.0.1", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -5903,21 +4466,26 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" }, - "engines": { - "node": ">=12.5.0" + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5928,75 +4496,37 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/commander": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" } }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/core-js-compat": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", - "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", - "dependencies": { - "browserslist": "^4.22.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6006,16 +4536,17 @@ "node": ">= 8" } }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "engines": { - "node": ">=8" - } + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" }, "node_modules/cssesc": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -6024,20 +4555,165 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", + "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.1.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", - "devOptional": true, + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, - "license": "BSD-2-Clause" + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } }, "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", "funding": { "type": "github", @@ -6045,10 +4721,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6059,392 +4738,226 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" }, - "node_modules/define-data-property": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dependencies": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/del/node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/del/node_modules/globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "engines": { - "node": ">=6" - } - }, - "node_modules/del/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/dequal": { - "version": "2.0.3", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/detect-libc": { + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, "node_modules/detect-node-es": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, "node_modules/didyoumean": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" }, "node_modules/dlv": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, - "node_modules/doctrine": { - "version": "3.0.0", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" } }, "node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.73", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", - "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==" - }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } + "version": "1.5.149", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.149.tgz", + "integrity": "sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ==", + "dev": true, + "license": "ISC" }, "node_modules/embla-carousel": { - "version": "8.0.0-rc21", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "license": "MIT" }, "node_modules/embla-carousel-react": { - "version": "8.0.0-rc21", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", "license": "MIT", "dependencies": { - "embla-carousel": "8.0.0-rc21", - "embla-carousel-reactive-utils": "8.0.0-rc21" + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/embla-carousel-reactive-utils": { - "version": "8.0.0-rc21", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", "license": "MIT", "peerDependencies": { - "embla-carousel": "8.0.0-rc21" + "embla-carousel": "8.6.0" } }, "node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">= 4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.22.3", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-iterator-helpers": { - "version": "1.0.15", + "node_modules/es-html-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/es-html-parser/-/es-html-parser-0.2.0.tgz", + "integrity": "sha512-snJ7uJC8Dkx/yT0eYZrWcY57rkPU6Zui6YphPynw8r52AWf57gjqMC0GWe7OxSDipwXowFpa3rqckEeAPTOz7w==", "dev": true, - "license": "MIT", - "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" - } + "license": "MIT" }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "license": "MIT", - "peer": true - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } + "license": "MIT" }, - "node_modules/es-to-primitive": { - "version": "1.2.1", + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -6455,312 +4968,93 @@ } }, "node_modules/eslint": { - "version": "8.56.0", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-next": { - "version": "14.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "14.1.0", - "@rushstack/eslint-patch": "^1.3.3", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + "url": "https://eslint.org/donate" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0", - "typescript": ">=3.3.1" + "jiti": "*" }, "peerDependenciesMeta": { - "typescript": { + "jiti": { "optional": true } } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "dev": true, - "license": "ISC", - "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils/node_modules/debug/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", "dev": true, "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "eslint": ">=8.40" } }, "node_modules/eslint-scope": { - "version": "7.2.2", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6768,41 +5062,47 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "9.6.1", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6814,6 +5114,9 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -6824,46 +5127,76 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "peer": true, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=0.8.x" + "node": ">=12.0.0" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { - "version": "3.3.2", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -6871,6 +5204,8 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6881,78 +5216,45 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { - "version": "1.17.0", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "6.0.1", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "node": ">=16.0.0" } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6960,24 +5262,10 @@ "node": ">=8" } }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -6992,35 +5280,33 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, - "node_modules/for-each": { - "version": "0.3.3", - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/foreground-child": { - "version": "3.1.1", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -7032,6 +5318,8 @@ }, "node_modules/fraction.js": { "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "license": "MIT", "engines": { @@ -7042,26 +5330,11 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -7073,114 +5346,55 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-nonce": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { - "version": "10.3.10", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7189,15 +5403,19 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -7209,158 +5427,62 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/globals": { - "version": "13.24.0", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalthis": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, "node_modules/graphemer": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, - "node_modules/has-bigints": { - "version": "1.0.2", + "node_modules/happy-dom": { + "version": "17.6.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.6.3.tgz", + "integrity": "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "license": "MIT", + "engines": { + "node": ">=12" } }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "node_modules/hasown": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7369,36 +5491,106 @@ "node": ">= 0.4" } }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" } }, - "node_modules/html5-qrcode": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", - "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==", - "license": "Apache-2.0" + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.0", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7414,80 +5606,47 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.0.6", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.0.0", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-bigint": { - "version": "1.0.4", + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -7496,45 +5655,13 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { - "version": "2.13.1", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -7545,45 +5672,26 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7592,245 +5700,36 @@ "node": ">=0.10.0" } }, - "node_modules/is-map": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { + "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd/node_modules/is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, "node_modules/jackspeak": { - "version": "2.3.6", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -7838,119 +5737,25 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { - "version": "1.21.0", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", "bin": { "jiti": "bin/jiti.js" } }, - "node_modules/jotai": { - "version": "1.13.1", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@babel/core": "*", - "@babel/template": "*", - "jotai-devtools": "*", - "jotai-immer": "*", - "jotai-optics": "*", - "jotai-redux": "*", - "jotai-tanstack-query": "*", - "jotai-urql": "*", - "jotai-valtio": "*", - "jotai-xstate": "*", - "jotai-zustand": "*", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@babel/template": { - "optional": true - }, - "jotai-devtools": { - "optional": true - }, - "jotai-immer": { - "optional": true - }, - "jotai-optics": { - "optional": true - }, - "jotai-redux": { - "optional": true - }, - "jotai-tanstack-query": { - "optional": true - }, - "jotai-urql": { - "optional": true - }, - "jotai-valtio": { - "optional": true - }, - "jotai-xstate": { - "optional": true - }, - "jotai-zustand": { - "optional": true - } - } - }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -7960,121 +5765,93 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT", - "peer": true - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.22", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "engines": { - "node": ">=6" - } - }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8085,61 +5862,28 @@ "node": ">= 0.8.0" } }, - "node_modules/light-bolt11-decoder": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.1.1.tgz", - "integrity": "sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==", - "dependencies": { - "@scure/base": "1.1.1" - } - }, "node_modules/lilconfig": { - "version": "2.1.0", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -8155,25 +5899,34 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", "dev": true, "license": "MIT" }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -8182,61 +5935,67 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { - "version": "10.2.0", - "license": "ISC", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/lucide-react": { - "version": "0.475.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", - "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "version": "0.462.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz", + "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" } }, "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", "dependencies": { - "sourcemap-codec": "^1.4.8" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/magic-string-ast": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.9.1.tgz", + "integrity": "sha512-18dv2ZlSSgJ/jDWlZGKfnDJx56ilNlYq9F7NnwuWTErsmYmqJ2TWE4l1o2zlUHBYUGBy3tIhPCC1gxq8M5HkMA==", + "license": "MIT", "dependencies": { - "semver": "^6.0.0" + "magic-string": "^0.30.17" }, "engines": { - "node": ">=8" + "node": ">=20.18.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "license": "MIT", "engines": { "node": ">= 8" @@ -8246,6 +6005,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -8254,41 +6014,21 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimalistic-assert": { + "node_modules/min-indent": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, "node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -8297,27 +6037,38 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { - "version": "7.0.4", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, "node_modules/ms": { - "version": "2.1.2", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/mz": { "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -8326,15 +6077,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -8344,139 +6096,22 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT", - "peer": true - }, - "node_modules/next": { - "version": "14.2.28", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.28.tgz", - "integrity": "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA==", - "license": "MIT", - "dependencies": { - "@next/env": "14.2.28", - "@swc/helpers": "0.5.5", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", - "postcss": "8.4.31", - "styled-jsx": "5.1.1" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=18.17.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.28", - "@next/swc-darwin-x64": "14.2.28", - "@next/swc-linux-arm64-gnu": "14.2.28", - "@next/swc-linux-arm64-musl": "14.2.28", - "@next/swc-linux-x64-gnu": "14.2.28", - "@next/swc-linux-x64-musl": "14.2.28", - "@next/swc-win32-arm64-msvc": "14.2.28", - "@next/swc-win32-ia32-msvc": "14.2.28", - "@next/swc-win32-x64-msvc": "14.2.28" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-pwa": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/next-pwa/-/next-pwa-5.6.0.tgz", - "integrity": "sha512-XV8g8C6B7UmViXU8askMEYhWwQ4qc/XqJGnexbLV68hzKaGHZDMtHsm2TNxFcbR7+ypVuth/wwpiIlMwpRJJ5A==", - "dependencies": { - "babel-loader": "^8.2.5", - "clean-webpack-plugin": "^4.0.0", - "globby": "^11.0.4", - "terser-webpack-plugin": "^5.3.3", - "workbox-webpack-plugin": "^6.5.4", - "workbox-window": "^6.5.4" - }, - "peerDependencies": { - "next": ">=9.0.0" - } - }, - "node_modules/next-themes": { - "version": "0.2.1", - "license": "MIT", - "peerDependencies": { - "next": "*", - "react": "*", - "react-dom": "*" - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" - }, - "node_modules/node-gyp-build": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", - "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8484,59 +6119,18 @@ }, "node_modules/normalize-range": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/nostr-react": { - "version": "0.7.0", - "license": "MIT", - "dependencies": { - "jotai": "^1.12.1", - "nostr-tools": "^1.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": ">=16" - } - }, - "node_modules/nostr-react/node_modules/@noble/ciphers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz", - "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nostr-react/node_modules/nostr-tools": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", - "integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==", - "dependencies": { - "@noble/ciphers": "0.2.0", - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", - "@scure/base": "1.1.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/nostr-tools": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.9.4.tgz", - "integrity": "sha512-Powumwkp+EWbdK1T8IsEX4daTLQhtWJvitfZ6OP2BdU1jJZvNlUp3SQB541UYw4uc9jgLbxZW6EZSdZoSfIygQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.15.0.tgz", + "integrity": "sha512-Jj/+UFbu3JbTAWP4ipPFNuyD4W5eVRBNAP+kmnoRCYp3bLmTrlQ0Qhs5O1xSQJTFpjdZqoS0zZOUKdxUdjc+pw==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", @@ -8544,10 +6138,8 @@ "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" - }, - "optionalDependencies": { - "nostr-wasm": "v0.1.0" + "@scure/bip39": "1.2.1", + "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" @@ -8558,36 +6150,23 @@ } } }, - "node_modules/nostr-tools/node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/nostr-wasm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", - "optional": true + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8595,127 +6174,26 @@ }, "node_modules/object-hash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", "engines": { "node": ">= 6" } }, - "node_modules/object-inspect": { - "version": "1.13.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/object.hasown": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { - "version": "0.9.3", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -8723,6 +6201,8 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8737,6 +6217,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -8749,24 +6231,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "engines": { - "node": ">=6" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -8776,27 +6259,32 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" - }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -8804,36 +6292,52 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.10.1", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-type": { - "version": "4.0.0", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 14.16" } }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -8844,98 +6348,46 @@ }, "node_modules/pify": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pirates": { - "version": "4.0.6", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", "engines": { "node": ">= 6" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { + "node_modules/pngjs": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, "node_modules/postcss": { - "version": "8.4.33", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -8952,9 +6404,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -8962,6 +6414,8 @@ }, "node_modules/postcss-import": { "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -8977,6 +6431,8 @@ }, "node_modules/postcss-js": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -8994,6 +6450,8 @@ }, "node_modules/postcss-load-config": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "funding": [ { "type": "opencollective", @@ -9025,32 +6483,49 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/postcss-nested": { - "version": "6.0.1", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -9062,29 +6537,73 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, "engines": { - "node": ">=6" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -9092,21 +6611,43 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/qr.js": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", - "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==", - "license": "MIT" + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { "type": "github", @@ -9123,16 +6664,10 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { - "version": "18.2.0", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -9141,73 +6676,88 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { - "version": "18.2.0", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-hook-form": { - "version": "7.51.4", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz", - "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==", + "version": "7.56.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz", + "integrity": "sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==", + "license": "MIT", "engines": { - "node": ">=12.22.0" + "node": ">=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/react-hook-form" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" + "react": "^16.8.0 || ^17 || ^18 || ^19" } }, - "node_modules/react-icons": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.1.0.tgz", - "integrity": "sha512-D3zug1270S4hbSlIRJ0CUS97QE1yNNKDjzQe3HqY0aefp2CBn9VgzgES27sRR2gOvFK+0CNx/BW0ggOESp6fqQ==", + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", "peerDependencies": { - "react": "*" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-is": { - "version": "16.13.1", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, - "node_modules/react-qr-code": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", - "integrity": "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.8.1", - "qr.js": "0.0.0" - }, - "peerDependencies": { - "react": "*" - } - }, "node_modules/react-remove-scroll": { - "version": "2.5.5", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", "license": "MIT", "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -9219,6 +6769,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" @@ -9236,10 +6787,68 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" @@ -9257,28 +6866,35 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "license": "MIT", "dependencies": { "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -9287,244 +6903,158 @@ "node": ">=8.10.0" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.4", + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "license": "MIT" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "engines": { "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { - "version": "1.22.8", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/reusify": { - "version": "1.0.4", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, "node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", + "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.1", + "@rollup/rollup-android-arm64": "4.40.1", + "@rollup/rollup-darwin-arm64": "4.40.1", + "@rollup/rollup-darwin-x64": "4.40.1", + "@rollup/rollup-freebsd-arm64": "4.40.1", + "@rollup/rollup-freebsd-x64": "4.40.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", + "@rollup/rollup-linux-arm-musleabihf": "4.40.1", + "@rollup/rollup-linux-arm64-gnu": "4.40.1", + "@rollup/rollup-linux-arm64-musl": "4.40.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-musl": "4.40.1", + "@rollup/rollup-linux-s390x-gnu": "4.40.1", + "@rollup/rollup-linux-x64-gnu": "4.40.1", + "@rollup/rollup-linux-x64-musl": "4.40.1", + "@rollup/rollup-win32-arm64-msvc": "4.40.1", + "@rollup/rollup-win32-ia32-msvc": "4.40.1", + "@rollup/rollup-win32-x64-msvc": "4.40.1", "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dependencies": { - "randombytes": "^2.1.0" - } + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "funding": [ { "type": "github", @@ -9544,190 +7074,40 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-array-concat": { - "version": "1.1.0", - "license": "MIT", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" + "xmlchars": "^2.2.0" }, "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex-test": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=v12.22.7" } }, "node_modules/scheduler": { - "version": "0.23.0", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } }, - "node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/secp256k1": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", - "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", - "hasInstallScript": true, - "dependencies": { - "elliptic": "^6.5.7", - "node-addon-api": "^5.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/semver": { - "version": "6.3.1", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.1", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9736,8 +7116,16 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9748,25 +7136,24 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -9775,73 +7162,33 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { - "version": "1.0.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead" - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" }, "node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -9855,8 +7202,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width/node_modules/strip-ansi": { + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -9868,89 +7259,11 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/string-width/node_modules/strip-ansi/node_modules/ansi-regex": { + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", "version": "6.0.1", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9959,24 +7272,32 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "dev": true, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/strip-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, "engines": { - "node": ">=10" + "node": ">=8" } }, "node_modules/strip-json-comments": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -9986,29 +7307,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/styled-jsx": { - "version": "5.1.1", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, "node_modules/sucrase": { "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -10029,6 +7331,9 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -10039,6 +7344,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10047,10 +7354,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.1.tgz", - "integrity": "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", "license": "MIT", "funding": { "type": "github", @@ -10058,31 +7372,33 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.1", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -10094,171 +7410,30 @@ }, "node_modules/tailwindcss-animate": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, - "node_modules/tapable": { - "version": "2.2.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", - "dependencies": { - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "node": ">=4" } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -10266,6 +7441,8 @@ }, "node_modules/thenify-all": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -10274,10 +7451,126 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -10285,46 +7578,61 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "punycode": "^2.1.0" + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" } }, "node_modules/ts-api-utils": { - "version": "1.0.3", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16.13.0" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, "node_modules/tslib": { - "version": "2.6.2", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -10334,81 +7642,10 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" - }, "node_modules/typescript": { - "version": "5.3.3", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -10419,91 +7656,103 @@ "node": ">=14.17" } }, - "node_modules/unbox-primitive": { - "version": "1.0.2", + "node_modules/typescript-eslint": { + "version": "8.31.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", + "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "@typescript-eslint/eslint-plugin": "8.31.1", + "@typescript-eslint/parser": "8.31.1", + "@typescript-eslint/utils": "8.31.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/undici-types": { - "version": "5.26.5", + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "engines": { - "node": ">=4" + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unhead": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.0.10.tgz", + "integrity": "sha512-GT188rzTCeSKt55tYyQlHHKfUTtZvgubrXiwzGeXg6UjcKO3FsagaMzQp6TVDrpDY++3i7Qt0t3pnCc/ebg5yQ==", + "license": "MIT", + "dependencies": { + "hookable": "^5.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" } }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "node_modules/unplugin": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz", + "integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", + "license": "MIT", "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" + "acorn": "^8.14.1", + "picomatch": "^4.0.2", + "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=4" + "node": ">=18.12.0" } }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "node_modules/unplugin-ast": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/unplugin-ast/-/unplugin-ast-0.15.0.tgz", + "integrity": "sha512-3ReKQUmmYEcNhjoyiwfFuaJU0jkZNcNk8+iLdLVWk73iojVjJLiF/QhnpAFf3O7CJd6bqhWBzNyQ68Udp2fi5Q==", + "license": "MIT", "dependencies": { - "crypto-random-string": "^2.0.0" + "@babel/generator": "^7.27.1", + "ast-kit": "^2.0.0", + "magic-string-ast": "^0.9.1", + "unplugin": "^2.3.2" }, "engines": { - "node": ">=8" + "node": ">=20.18.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/unplugin/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "engines": { - "node": ">=4", - "yarn": "*" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10518,9 +7767,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -10531,6 +7781,9 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -10540,6 +7793,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -10560,6 +7814,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -10577,198 +7832,328 @@ } } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/varuint-bitcoin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", - "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "dependencies": { - "safe-buffer": "^5.1.1" + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vaul": { - "version": "0.8.9", + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", + "integrity": "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-dialog": "^1.0.4" + "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "license": "MIT", - "peer": true, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" } }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "node_modules/webpack": { - "version": "5.99.6", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", - "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { - "webpack": "bin/webpack.js" + "vite": "bin/vite.js" }, "engines": { - "node": ">=10.13.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "webpack-cli": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "node_modules/vite-node": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", + "dev": true, "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" } }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "node_modules/vite/node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.3" + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", + "happy-dom": "*", + "jsdom": "*" }, - "engines": { - "node": ">=8.0.0" + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", - "license": "MIT", - "peer": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/websocket-ts": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/websocket-ts/-/websocket-ts-2.2.1.tgz", + "integrity": "sha512-YKPDfxlK5qOheLZ2bTIiktZO1bpfGdNCPJmTEaPW7G9UXI1GKjDdeacOrsULUS000OPNxDVOyAuKLuIWPqWM0Q==", + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10780,347 +8165,43 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" }, - "node_modules/which-builtin-type": { - "version": "1.1.3", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/which-collection": { - "version": "1.0.1", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.13", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/workbox-background-sync": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", - "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-broadcast-update": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", - "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-build": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", - "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", - "dependencies": { - "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.11.1", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@rollup/plugin-replace": "^2.4.1", - "@surma/rollup-plugin-off-main-thread": "^2.2.3", - "ajv": "^8.6.0", - "common-tags": "^1.8.0", - "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "^9.0.1", - "glob": "^7.1.6", - "lodash": "^4.17.20", - "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", - "rollup-plugin-terser": "^7.0.0", - "source-map": "^0.8.0-beta.0", - "stringify-object": "^3.3.0", - "strip-comments": "^2.0.1", - "tempy": "^0.6.0", - "upath": "^1.2.0", - "workbox-background-sync": "6.6.0", - "workbox-broadcast-update": "6.6.0", - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-google-analytics": "6.6.0", - "workbox-navigation-preload": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-range-requests": "6.6.0", - "workbox-recipes": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0", - "workbox-streams": "6.6.0", - "workbox-sw": "6.6.0", - "workbox-window": "6.6.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", - "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", - "dependencies": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/workbox-build/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/workbox-build/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/workbox-build/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/workbox-cacheable-response": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", - "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", - "deprecated": "workbox-background-sync@6.6.0", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", - "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==" - }, - "node_modules/workbox-expiration": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", - "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-google-analytics": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", - "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", - "dependencies": { - "workbox-background-sync": "6.6.0", - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-navigation-preload": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", - "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-precaching": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", - "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", - "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-range-requests": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", - "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-recipes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", - "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", - "dependencies": { - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-routing": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", - "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-strategies": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", - "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-streams": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", - "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", - "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0" - } - }, - "node_modules/workbox-sw": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", - "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==" - }, - "node_modules/workbox-webpack-plugin": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz", - "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==", - "dependencies": { - "fast-json-stable-stringify": "^2.1.0", - "pretty-bytes": "^5.4.1", - "upath": "^1.2.0", - "webpack-sources": "^1.4.3", - "workbox-build": "6.6.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.9.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "node_modules/workbox-window": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", - "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", - "dependencies": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "6.6.0" + "node": ">=0.10.0" } }, "node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -11134,8 +8215,69 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -11144,47 +8286,194 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.0.1", + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "4.0.0", + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, "node_modules/yaml": { - "version": "2.3.4", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -11195,9 +8484,10 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.25.71", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.71.tgz", + "integrity": "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index da1c467..166e7f5 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,98 @@ { "name": "lumina", - "version": "0.1.28", "private": true, + "version": "2.0.0", + "type": "module", "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" + "dev": "npm i --silent && vite", + "build": "npm i --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'Project built successfully!'", + "test": "npm i --silent && tsc --noEmit && eslint && vitest run --reporter=dot --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'All tests passed!'", + "deploy": "npm run build && npx -y nostr-deploy-cli deploy --skip-setup" }, "dependencies": { - "@getalby/sdk": "^5.0.0", - "@hookform/resolvers": "^3.4.0", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-dialog": "^1.1.10", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-navigation-menu": "^1.1.4", - "@radix-ui/react-scroll-area": "^1.0.5", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.4", - "@radix-ui/react-slot": "^1.2.0", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "blurhash": "^2.0.5", - "bolt11": "^1.4.1", + "@fontsource-variable/inter": "^5.2.6", + "@getalby/sdk": "^5.1.1", + "@hookform/resolvers": "^3.9.0", + "@nostrify/nostrify": "^0.47.1", + "@nostrify/react": "^0.2.17", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-aspect-ratio": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@tanstack/react-query": "^5.56.2", + "@unhead/addons": "^2.0.10", + "@unhead/react": "^2.0.10", + "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "embla-carousel-react": "^8.0.0-rc21", - "html5-qrcode": "^2.3.8", - "light-bolt11-decoder": "^3.1.1", - "lucide-react": "^0.475.0", - "next": "14.2.28", - "next-pwa": "^5.6.0", - "next-themes": "^0.2.1", - "nostr-react": "^0.7.0", - "nostr-tools": "^2.4.0", - "react": "^18", - "react-dom": "^18", - "react-hook-form": "^7.51.4", - "react-icons": "^5.1.0", - "react-qr-code": "^2.0.15", - "sharp": "^0.33.5", - "tailwind-merge": "^3.0.1", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.3.0", + "idb": "^8.0.3", + "input-otp": "^1.2.4", + "lucide-react": "^0.462.0", + "nostr-tools": "^2.13.0", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-intersection-observer": "^9.16.0", + "react-resizable-panels": "^2.1.3", + "react-router-dom": "^6.26.2", + "recharts": "^2.12.7", + "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.8.9", - "zod": "^3.23.8" + "vaul": "^0.9.3", + "zod": "^3.25.71" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10.0.1", - "eslint": "^8", - "eslint-config-next": "14.1.0", - "postcss": "^8", - "tailwindcss": "^3.3.0", - "typescript": "^5" + "@eslint/js": "^9.9.0", + "@html-eslint/eslint-plugin": "^0.41.0", + "@html-eslint/parser": "^0.41.0", + "@tailwindcss/typography": "^0.5.15", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.5.5", + "@types/qrcode": "^1.5.5", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "@webbtc/webln-types": "^3.0.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "jsdom": "^26.1.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^6.3.5", + "vitest": "^3.1.4" } } diff --git a/postcss.config.js b/postcss.config.js index 12a703d..2e7af2b 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ -module.exports = { +export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, -}; +} diff --git a/public/.well-known/nostr.json b/public/.well-known/nostr.json deleted file mode 100644 index 61d6ad7..0000000 --- a/public/.well-known/nostr.json +++ /dev/null @@ -1 +0,0 @@ -{"names":{"_":"ff363e4afc398b7dd8ceb0b2e73e96fe9621ababc22ab150ffbb1aa0f34df8b2"}} \ No newline at end of file diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..4278771 --- /dev/null +++ b/public/_redirects @@ -0,0 +1,2 @@ +/assets/* /assets/:splat 200 +/* /index.html 200 diff --git a/public/android/android-launchericon-144-144.png b/public/android/android-launchericon-144-144.png deleted file mode 100644 index 65a3dc7..0000000 Binary files a/public/android/android-launchericon-144-144.png and /dev/null differ diff --git a/public/android/android-launchericon-192-192.png b/public/android/android-launchericon-192-192.png deleted file mode 100644 index 16ea86e..0000000 Binary files a/public/android/android-launchericon-192-192.png and /dev/null differ diff --git a/public/android/android-launchericon-48-48.png b/public/android/android-launchericon-48-48.png deleted file mode 100644 index b940d36..0000000 Binary files a/public/android/android-launchericon-48-48.png and /dev/null differ diff --git a/public/android/android-launchericon-512-512.png b/public/android/android-launchericon-512-512.png deleted file mode 100644 index 99b0e4e..0000000 Binary files a/public/android/android-launchericon-512-512.png and /dev/null differ diff --git a/public/android/android-launchericon-72-72.png b/public/android/android-launchericon-72-72.png deleted file mode 100644 index 7e69198..0000000 Binary files a/public/android/android-launchericon-72-72.png and /dev/null differ diff --git a/public/android/android-launchericon-96-96.png b/public/android/android-launchericon-96-96.png deleted file mode 100644 index 7257009..0000000 Binary files a/public/android/android-launchericon-96-96.png and /dev/null differ diff --git a/public/geyser_logo.svg b/public/geyser_logo.svg deleted file mode 100644 index 530a1e8..0000000 --- a/public/geyser_logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width='349' height='365' viewBox='0 0 349 365' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M346.396 125.679C346.193 124.459 345.986 123.24 345.755 122.028C345.017 118.123 345.017 118.123 345.017 118.123C343.98 113.895 339.304 110.436 334.628 110.435L221.265 110.425C216.587 110.424 211.581 114.065 210.14 118.514L186.054 192.873C185.343 195.069 184.408 197.774 185.441 199.236C186.502 200.735 189.57 200.963 191.939 200.963H238.093C238.093 200.963 240.249 200.945 240.536 202.442C240.843 204.031 239.19 205.743 239.19 205.743L157.833 291.25C154.987 294.242 150.017 299.46 146.792 302.845L125.923 324.742C125.923 324.742 125.497 325.221 124.999 325.68C124.712 325.946 124.436 326.178 124.173 326.383C121.278 328.632 119.909 327.374 121.11 323.26L131.729 286.87C133.04 282.381 135.182 275.033 136.493 270.544L142.575 249.702C143.595 246.206 144.753 242.242 145.481 239.743C145.688 239.033 146.17 236.844 145.442 235.737C144.751 234.686 143.587 234.487 142.28 234.487C140.864 234.487 139.196 234.487 137.442 234.487H93.4839C88.8059 234.487 87.2359 232.119 89.9939 229.225C92.7519 226.331 97.6479 221.193 100.875 217.807L135.051 181.949C138.278 178.563 143.246 173.345 146.094 170.352C148.942 167.359 153.91 162.139 157.133 158.75L162.904 152.684C166.129 149.295 171.404 143.75 174.627 140.362L186.471 127.916C189.694 124.527 194.758 119.205 197.721 116.089C200.686 112.974 203.889 109.608 204.838 108.609C205.787 107.611 209.201 104.022 212.426 100.633L278.317 31.3812C281.542 27.9922 280.911 23.2282 276.919 20.7932C276.919 20.7932 267.599 15.1122 258.755 11.4192C235.769 1.81919 210.816 -1.59681 185.61 0.678194C178.503 1.32019 171.393 2.41119 164.321 3.94219C145.237 8.07619 126.546 15.3542 108.954 25.4422C97.9699 31.7382 87.4599 39.1132 77.5829 47.4572C54.3189 67.1072 34.7959 91.9432 20.9249 120.702C14.0339 134.988 8.95193 149.42 5.51493 163.704C-1.69007 193.646 -1.63107 223.046 4.49993 249.499C8.57593 267.102 15.3589 283.516 24.5589 298.092C25.1199 298.982 25.6939 299.867 26.2739 300.743C31.2249 308.225 36.8479 315.217 43.0999 321.638C52.6369 331.432 63.6919 339.937 76.2069 346.811C82.4469 350.24 88.8519 353.149 95.3729 355.575C99.0779 356.951 102.824 358.176 106.605 359.24C113.087 361.068 119.679 362.438 126.337 363.378C184.333 371.547 251.78 346.032 298.382 290.078C307.3 279.37 315.068 268.043 321.671 256.283C330.341 240.845 336.993 224.657 341.544 208.154C345.339 194.392 347.665 180.431 348.483 166.541C349.298 152.728 348.617 139.014 346.396 125.679Z' fill='#FFFFFF'/></svg> \ No newline at end of file diff --git a/public/icons.json b/public/icons.json deleted file mode 100644 index 9d9dbbf..0000000 --- a/public/icons.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "icons": [ - { - "src": "windows11/SmallTile.scale-100.png", - "sizes": "71x71" - }, - { - "src": "windows11/SmallTile.scale-125.png", - "sizes": "89x89" - }, - { - "src": "windows11/SmallTile.scale-150.png", - "sizes": "107x107" - }, - { - "src": "windows11/SmallTile.scale-200.png", - "sizes": "142x142" - }, - { - "src": "windows11/SmallTile.scale-400.png", - "sizes": "284x284" - }, - { - "src": "windows11/Square150x150Logo.scale-100.png", - "sizes": "150x150" - }, - { - "src": "windows11/Square150x150Logo.scale-125.png", - "sizes": "188x188" - }, - { - "src": "windows11/Square150x150Logo.scale-150.png", - "sizes": "225x225" - }, - { - "src": "windows11/Square150x150Logo.scale-200.png", - "sizes": "300x300" - }, - { - "src": "windows11/Square150x150Logo.scale-400.png", - "sizes": "600x600" - }, - { - "src": "windows11/Wide310x150Logo.scale-100.png", - "sizes": "310x150" - }, - { - "src": "windows11/Wide310x150Logo.scale-125.png", - "sizes": "388x188" - }, - { - "src": "windows11/Wide310x150Logo.scale-150.png", - "sizes": "465x225" - }, - { - "src": "windows11/Wide310x150Logo.scale-200.png", - "sizes": "620x300" - }, - { - "src": "windows11/Wide310x150Logo.scale-400.png", - "sizes": "1240x600" - }, - { - "src": "windows11/LargeTile.scale-100.png", - "sizes": "310x310" - }, - { - "src": "windows11/LargeTile.scale-125.png", - "sizes": "388x388" - }, - { - "src": "windows11/LargeTile.scale-150.png", - "sizes": "465x465" - }, - { - "src": "windows11/LargeTile.scale-200.png", - "sizes": "620x620" - }, - { - "src": "windows11/LargeTile.scale-400.png", - "sizes": "1240x1240" - }, - { - "src": "windows11/Square44x44Logo.scale-100.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.scale-125.png", - "sizes": "55x55" - }, - { - "src": "windows11/Square44x44Logo.scale-150.png", - "sizes": "66x66" - }, - { - "src": "windows11/Square44x44Logo.scale-200.png", - "sizes": "88x88" - }, - { - "src": "windows11/Square44x44Logo.scale-400.png", - "sizes": "176x176" - }, - { - "src": "windows11/StoreLogo.scale-100.png", - "sizes": "50x50" - }, - { - "src": "windows11/StoreLogo.scale-125.png", - "sizes": "63x63" - }, - { - "src": "windows11/StoreLogo.scale-150.png", - "sizes": "75x75" - }, - { - "src": "windows11/StoreLogo.scale-200.png", - "sizes": "100x100" - }, - { - "src": "windows11/StoreLogo.scale-400.png", - "sizes": "200x200" - }, - { - "src": "windows11/SplashScreen.scale-100.png", - "sizes": "620x300" - }, - { - "src": "windows11/SplashScreen.scale-125.png", - "sizes": "775x375" - }, - { - "src": "windows11/SplashScreen.scale-150.png", - "sizes": "930x450" - }, - { - "src": "windows11/SplashScreen.scale-200.png", - "sizes": "1240x600" - }, - { - "src": "windows11/SplashScreen.scale-400.png", - "sizes": "2480x1200" - }, - { - "src": "windows11/Square44x44Logo.targetsize-16.png", - "sizes": "16x16" - }, - { - "src": "windows11/Square44x44Logo.targetsize-20.png", - "sizes": "20x20" - }, - { - "src": "windows11/Square44x44Logo.targetsize-24.png", - "sizes": "24x24" - }, - { - "src": "windows11/Square44x44Logo.targetsize-30.png", - "sizes": "30x30" - }, - { - "src": "windows11/Square44x44Logo.targetsize-32.png", - "sizes": "32x32" - }, - { - "src": "windows11/Square44x44Logo.targetsize-36.png", - "sizes": "36x36" - }, - { - "src": "windows11/Square44x44Logo.targetsize-40.png", - "sizes": "40x40" - }, - { - "src": "windows11/Square44x44Logo.targetsize-44.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.targetsize-48.png", - "sizes": "48x48" - }, - { - "src": "windows11/Square44x44Logo.targetsize-60.png", - "sizes": "60x60" - }, - { - "src": "windows11/Square44x44Logo.targetsize-64.png", - "sizes": "64x64" - }, - { - "src": "windows11/Square44x44Logo.targetsize-72.png", - "sizes": "72x72" - }, - { - "src": "windows11/Square44x44Logo.targetsize-80.png", - "sizes": "80x80" - }, - { - "src": "windows11/Square44x44Logo.targetsize-96.png", - "sizes": "96x96" - }, - { - "src": "windows11/Square44x44Logo.targetsize-256.png", - "sizes": "256x256" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-16.png", - "sizes": "16x16" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-20.png", - "sizes": "20x20" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-24.png", - "sizes": "24x24" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-30.png", - "sizes": "30x30" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-32.png", - "sizes": "32x32" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-36.png", - "sizes": "36x36" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-40.png", - "sizes": "40x40" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-44.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-48.png", - "sizes": "48x48" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-60.png", - "sizes": "60x60" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-64.png", - "sizes": "64x64" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-72.png", - "sizes": "72x72" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-80.png", - "sizes": "80x80" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-96.png", - "sizes": "96x96" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-256.png", - "sizes": "256x256" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png", - "sizes": "16x16" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png", - "sizes": "20x20" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png", - "sizes": "24x24" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png", - "sizes": "30x30" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png", - "sizes": "32x32" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png", - "sizes": "36x36" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png", - "sizes": "40x40" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png", - "sizes": "48x48" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png", - "sizes": "60x60" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png", - "sizes": "64x64" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png", - "sizes": "72x72" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png", - "sizes": "80x80" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png", - "sizes": "96x96" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png", - "sizes": "256x256" - }, - { - "src": "android/android-launchericon-512-512.png", - "sizes": "512x512" - }, - { - "src": "android/android-launchericon-192-192.png", - "sizes": "192x192" - }, - { - "src": "android/android-launchericon-144-144.png", - "sizes": "144x144" - }, - { - "src": "android/android-launchericon-96-96.png", - "sizes": "96x96" - }, - { - "src": "android/android-launchericon-72-72.png", - "sizes": "72x72" - }, - { - "src": "android/android-launchericon-48-48.png", - "sizes": "48x48" - }, - { - "src": "ios/16.png", - "sizes": "16x16" - }, - { - "src": "ios/20.png", - "sizes": "20x20" - }, - { - "src": "ios/29.png", - "sizes": "29x29" - }, - { - "src": "ios/32.png", - "sizes": "32x32" - }, - { - "src": "ios/40.png", - "sizes": "40x40" - }, - { - "src": "ios/50.png", - "sizes": "50x50" - }, - { - "src": "ios/57.png", - "sizes": "57x57" - }, - { - "src": "ios/58.png", - "sizes": "58x58" - }, - { - "src": "ios/60.png", - "sizes": "60x60" - }, - { - "src": "ios/64.png", - "sizes": "64x64" - }, - { - "src": "ios/72.png", - "sizes": "72x72" - }, - { - "src": "ios/76.png", - "sizes": "76x76" - }, - { - "src": "ios/80.png", - "sizes": "80x80" - }, - { - "src": "ios/87.png", - "sizes": "87x87" - }, - { - "src": "ios/100.png", - "sizes": "100x100" - }, - { - "src": "ios/114.png", - "sizes": "114x114" - }, - { - "src": "ios/120.png", - "sizes": "120x120" - }, - { - "src": "ios/128.png", - "sizes": "128x128" - }, - { - "src": "ios/144.png", - "sizes": "144x144" - }, - { - "src": "ios/152.png", - "sizes": "152x152" - }, - { - "src": "ios/167.png", - "sizes": "167x167" - }, - { - "src": "ios/180.png", - "sizes": "180x180" - }, - { - "src": "ios/192.png", - "sizes": "192x192" - }, - { - "src": "ios/256.png", - "sizes": "256x256" - }, - { - "src": "ios/512.png", - "sizes": "512x512" - }, - { - "src": "ios/1024.png", - "sizes": "1024x1024" - } - ] -} \ No newline at end of file diff --git a/public/ios/100.png b/public/ios/100.png deleted file mode 100644 index 3c131c3..0000000 Binary files a/public/ios/100.png and /dev/null differ diff --git a/public/ios/1024.png b/public/ios/1024.png deleted file mode 100644 index c75e115..0000000 Binary files a/public/ios/1024.png and /dev/null differ diff --git a/public/ios/114.png b/public/ios/114.png deleted file mode 100644 index cfae24c..0000000 Binary files a/public/ios/114.png and /dev/null differ diff --git a/public/ios/120.png b/public/ios/120.png deleted file mode 100644 index f1f0d77..0000000 Binary files a/public/ios/120.png and /dev/null differ diff --git a/public/ios/128.png b/public/ios/128.png deleted file mode 100644 index 12f485f..0000000 Binary files a/public/ios/128.png and /dev/null differ diff --git a/public/ios/144.png b/public/ios/144.png deleted file mode 100644 index 65a3dc7..0000000 Binary files a/public/ios/144.png and /dev/null differ diff --git a/public/ios/152.png b/public/ios/152.png deleted file mode 100644 index 757c911..0000000 Binary files a/public/ios/152.png and /dev/null differ diff --git a/public/ios/16.png b/public/ios/16.png deleted file mode 100644 index 93cde89..0000000 Binary files a/public/ios/16.png and /dev/null differ diff --git a/public/ios/167.png b/public/ios/167.png deleted file mode 100644 index 9021db2..0000000 Binary files a/public/ios/167.png and /dev/null differ diff --git a/public/ios/180.png b/public/ios/180.png deleted file mode 100644 index f0d5b22..0000000 Binary files a/public/ios/180.png and /dev/null differ diff --git a/public/ios/192.png b/public/ios/192.png deleted file mode 100644 index 16ea86e..0000000 Binary files a/public/ios/192.png and /dev/null differ diff --git a/public/ios/20.png b/public/ios/20.png deleted file mode 100644 index fd4af01..0000000 Binary files a/public/ios/20.png and /dev/null differ diff --git a/public/ios/256.png b/public/ios/256.png deleted file mode 100644 index 7d2a710..0000000 Binary files a/public/ios/256.png and /dev/null differ diff --git a/public/ios/29.png b/public/ios/29.png deleted file mode 100644 index 102938b..0000000 Binary files a/public/ios/29.png and /dev/null differ diff --git a/public/ios/32.png b/public/ios/32.png deleted file mode 100644 index d798e8d..0000000 Binary files a/public/ios/32.png and /dev/null differ diff --git a/public/ios/40.png b/public/ios/40.png deleted file mode 100644 index c48dcaa..0000000 Binary files a/public/ios/40.png and /dev/null differ diff --git a/public/ios/50.png b/public/ios/50.png deleted file mode 100644 index 1a05299..0000000 Binary files a/public/ios/50.png and /dev/null differ diff --git a/public/ios/512.png b/public/ios/512.png deleted file mode 100644 index 99b0e4e..0000000 Binary files a/public/ios/512.png and /dev/null differ diff --git a/public/ios/57.png b/public/ios/57.png deleted file mode 100644 index 7eabe10..0000000 Binary files a/public/ios/57.png and /dev/null differ diff --git a/public/ios/58.png b/public/ios/58.png deleted file mode 100644 index 5c2721a..0000000 Binary files a/public/ios/58.png and /dev/null differ diff --git a/public/ios/60.png b/public/ios/60.png deleted file mode 100644 index 42c28b9..0000000 Binary files a/public/ios/60.png and /dev/null differ diff --git a/public/ios/64.png b/public/ios/64.png deleted file mode 100644 index 6a82f48..0000000 Binary files a/public/ios/64.png and /dev/null differ diff --git a/public/ios/72.png b/public/ios/72.png deleted file mode 100644 index 7e69198..0000000 Binary files a/public/ios/72.png and /dev/null differ diff --git a/public/ios/76.png b/public/ios/76.png deleted file mode 100644 index b999569..0000000 Binary files a/public/ios/76.png and /dev/null differ diff --git a/public/ios/80.png b/public/ios/80.png deleted file mode 100644 index a044ae2..0000000 Binary files a/public/ios/80.png and /dev/null differ diff --git a/public/ios/87.png b/public/ios/87.png deleted file mode 100644 index ad82464..0000000 Binary files a/public/ios/87.png and /dev/null differ diff --git a/public/lumina-lightning.png b/public/lumina-lightning.png deleted file mode 100644 index a7a3240..0000000 Binary files a/public/lumina-lightning.png and /dev/null differ diff --git a/public/lumina.png b/public/lumina.png deleted file mode 100644 index 4122889..0000000 Binary files a/public/lumina.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index af5ad8a..0000000 --- a/public/manifest.json +++ /dev/null @@ -1,459 +0,0 @@ -{ - "name": "LUMINA", - "short_name": "LUMINA", - "icons": [ - { - "src": "windows11/SmallTile.scale-100.png", - "sizes": "71x71" - }, - { - "src": "windows11/SmallTile.scale-125.png", - "sizes": "89x89" - }, - { - "src": "windows11/SmallTile.scale-150.png", - "sizes": "107x107" - }, - { - "src": "windows11/SmallTile.scale-200.png", - "sizes": "142x142" - }, - { - "src": "windows11/SmallTile.scale-400.png", - "sizes": "284x284" - }, - { - "src": "windows11/Square150x150Logo.scale-100.png", - "sizes": "150x150" - }, - { - "src": "windows11/Square150x150Logo.scale-125.png", - "sizes": "188x188" - }, - { - "src": "windows11/Square150x150Logo.scale-150.png", - "sizes": "225x225" - }, - { - "src": "windows11/Square150x150Logo.scale-200.png", - "sizes": "300x300" - }, - { - "src": "windows11/Square150x150Logo.scale-400.png", - "sizes": "600x600" - }, - { - "src": "windows11/Wide310x150Logo.scale-100.png", - "sizes": "310x150" - }, - { - "src": "windows11/Wide310x150Logo.scale-125.png", - "sizes": "388x188" - }, - { - "src": "windows11/Wide310x150Logo.scale-150.png", - "sizes": "465x225" - }, - { - "src": "windows11/Wide310x150Logo.scale-200.png", - "sizes": "620x300" - }, - { - "src": "windows11/Wide310x150Logo.scale-400.png", - "sizes": "1240x600" - }, - { - "src": "windows11/LargeTile.scale-100.png", - "sizes": "310x310" - }, - { - "src": "windows11/LargeTile.scale-125.png", - "sizes": "388x388" - }, - { - "src": "windows11/LargeTile.scale-150.png", - "sizes": "465x465" - }, - { - "src": "windows11/LargeTile.scale-200.png", - "sizes": "620x620" - }, - { - "src": "windows11/LargeTile.scale-400.png", - "sizes": "1240x1240" - }, - { - "src": "windows11/Square44x44Logo.scale-100.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.scale-125.png", - "sizes": "55x55" - }, - { - "src": "windows11/Square44x44Logo.scale-150.png", - "sizes": "66x66" - }, - { - "src": "windows11/Square44x44Logo.scale-200.png", - "sizes": "88x88" - }, - { - "src": "windows11/Square44x44Logo.scale-400.png", - "sizes": "176x176" - }, - { - "src": "windows11/StoreLogo.scale-100.png", - "sizes": "50x50" - }, - { - "src": "windows11/StoreLogo.scale-125.png", - "sizes": "63x63" - }, - { - "src": "windows11/StoreLogo.scale-150.png", - "sizes": "75x75" - }, - { - "src": "windows11/StoreLogo.scale-200.png", - "sizes": "100x100" - }, - { - "src": "windows11/StoreLogo.scale-400.png", - "sizes": "200x200" - }, - { - "src": "windows11/SplashScreen.scale-100.png", - "sizes": "620x300" - }, - { - "src": "windows11/SplashScreen.scale-125.png", - "sizes": "775x375" - }, - { - "src": "windows11/SplashScreen.scale-150.png", - "sizes": "930x450" - }, - { - "src": "windows11/SplashScreen.scale-200.png", - "sizes": "1240x600" - }, - { - "src": "windows11/SplashScreen.scale-400.png", - "sizes": "2480x1200" - }, - { - "src": "windows11/Square44x44Logo.targetsize-16.png", - "sizes": "16x16" - }, - { - "src": "windows11/Square44x44Logo.targetsize-20.png", - "sizes": "20x20" - }, - { - "src": "windows11/Square44x44Logo.targetsize-24.png", - "sizes": "24x24" - }, - { - "src": "windows11/Square44x44Logo.targetsize-30.png", - "sizes": "30x30" - }, - { - "src": "windows11/Square44x44Logo.targetsize-32.png", - "sizes": "32x32" - }, - { - "src": "windows11/Square44x44Logo.targetsize-36.png", - "sizes": "36x36" - }, - { - "src": "windows11/Square44x44Logo.targetsize-40.png", - "sizes": "40x40" - }, - { - "src": "windows11/Square44x44Logo.targetsize-44.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.targetsize-48.png", - "sizes": "48x48" - }, - { - "src": "windows11/Square44x44Logo.targetsize-60.png", - "sizes": "60x60" - }, - { - "src": "windows11/Square44x44Logo.targetsize-64.png", - "sizes": "64x64" - }, - { - "src": "windows11/Square44x44Logo.targetsize-72.png", - "sizes": "72x72" - }, - { - "src": "windows11/Square44x44Logo.targetsize-80.png", - "sizes": "80x80" - }, - { - "src": "windows11/Square44x44Logo.targetsize-96.png", - "sizes": "96x96" - }, - { - "src": "windows11/Square44x44Logo.targetsize-256.png", - "sizes": "256x256" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-16.png", - "sizes": "16x16" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-20.png", - "sizes": "20x20" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-24.png", - "sizes": "24x24" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-30.png", - "sizes": "30x30" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-32.png", - "sizes": "32x32" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-36.png", - "sizes": "36x36" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-40.png", - "sizes": "40x40" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-44.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-48.png", - "sizes": "48x48" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-60.png", - "sizes": "60x60" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-64.png", - "sizes": "64x64" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-72.png", - "sizes": "72x72" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-80.png", - "sizes": "80x80" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-96.png", - "sizes": "96x96" - }, - { - "src": "windows11/Square44x44Logo.altform-unplated_targetsize-256.png", - "sizes": "256x256" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png", - "sizes": "16x16" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png", - "sizes": "20x20" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png", - "sizes": "24x24" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png", - "sizes": "30x30" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png", - "sizes": "32x32" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png", - "sizes": "36x36" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png", - "sizes": "40x40" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png", - "sizes": "44x44" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png", - "sizes": "48x48" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png", - "sizes": "60x60" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png", - "sizes": "64x64" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png", - "sizes": "72x72" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png", - "sizes": "80x80" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png", - "sizes": "96x96" - }, - { - "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png", - "sizes": "256x256" - }, - { - "src": "android/android-launchericon-512-512.png", - "sizes": "512x512" - }, - { - "src": "android/android-launchericon-192-192.png", - "sizes": "192x192" - }, - { - "src": "android/android-launchericon-144-144.png", - "sizes": "144x144" - }, - { - "src": "android/android-launchericon-96-96.png", - "sizes": "96x96" - }, - { - "src": "android/android-launchericon-72-72.png", - "sizes": "72x72" - }, - { - "src": "android/android-launchericon-48-48.png", - "sizes": "48x48" - }, - { - "src": "ios/16.png", - "sizes": "16x16" - }, - { - "src": "ios/20.png", - "sizes": "20x20" - }, - { - "src": "ios/29.png", - "sizes": "29x29" - }, - { - "src": "ios/32.png", - "sizes": "32x32" - }, - { - "src": "ios/40.png", - "sizes": "40x40" - }, - { - "src": "ios/50.png", - "sizes": "50x50" - }, - { - "src": "ios/57.png", - "sizes": "57x57" - }, - { - "src": "ios/58.png", - "sizes": "58x58" - }, - { - "src": "ios/60.png", - "sizes": "60x60" - }, - { - "src": "ios/64.png", - "sizes": "64x64" - }, - { - "src": "ios/72.png", - "sizes": "72x72" - }, - { - "src": "ios/76.png", - "sizes": "76x76" - }, - { - "src": "ios/80.png", - "sizes": "80x80" - }, - { - "src": "ios/87.png", - "sizes": "87x87" - }, - { - "src": "ios/100.png", - "sizes": "100x100" - }, - { - "src": "ios/114.png", - "sizes": "114x114" - }, - { - "src": "ios/120.png", - "sizes": "120x120" - }, - { - "src": "ios/128.png", - "sizes": "128x128" - }, - { - "src": "ios/144.png", - "sizes": "144x144" - }, - { - "src": "ios/152.png", - "sizes": "152x152" - }, - { - "src": "ios/167.png", - "sizes": "167x167" - }, - { - "src": "ios/180.png", - "sizes": "180x180" - }, - { - "src": "ios/192.png", - "sizes": "192x192" - }, - { - "src": "ios/256.png", - "sizes": "256x256" - }, - { - "src": "ios/512.png", - "sizes": "512x512" - }, - { - "src": "ios/1024.png", - "sizes": "1024x1024" - } - ], - "theme_color": "#000", - "background_color": "#000", - "start_url": "/", - "display": "standalone", - "orientation": "portrait" -} \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..14267e9 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> \ No newline at end of file diff --git a/public/windows11/LargeTile.scale-100.png b/public/windows11/LargeTile.scale-100.png deleted file mode 100644 index cfb45bf..0000000 Binary files a/public/windows11/LargeTile.scale-100.png and /dev/null differ diff --git a/public/windows11/LargeTile.scale-125.png b/public/windows11/LargeTile.scale-125.png deleted file mode 100644 index aa8a904..0000000 Binary files a/public/windows11/LargeTile.scale-125.png and /dev/null differ diff --git a/public/windows11/LargeTile.scale-150.png b/public/windows11/LargeTile.scale-150.png deleted file mode 100644 index b33ec63..0000000 Binary files a/public/windows11/LargeTile.scale-150.png and /dev/null differ diff --git a/public/windows11/LargeTile.scale-200.png b/public/windows11/LargeTile.scale-200.png deleted file mode 100644 index 2cd8a18..0000000 Binary files a/public/windows11/LargeTile.scale-200.png and /dev/null differ diff --git a/public/windows11/LargeTile.scale-400.png b/public/windows11/LargeTile.scale-400.png deleted file mode 100644 index a33f1b2..0000000 Binary files a/public/windows11/LargeTile.scale-400.png and /dev/null differ diff --git a/public/windows11/SmallTile.scale-100.png b/public/windows11/SmallTile.scale-100.png deleted file mode 100644 index 053e028..0000000 Binary files a/public/windows11/SmallTile.scale-100.png and /dev/null differ diff --git a/public/windows11/SmallTile.scale-125.png b/public/windows11/SmallTile.scale-125.png deleted file mode 100644 index 7f06ac1..0000000 Binary files a/public/windows11/SmallTile.scale-125.png and /dev/null differ diff --git a/public/windows11/SmallTile.scale-150.png b/public/windows11/SmallTile.scale-150.png deleted file mode 100644 index 3facd23..0000000 Binary files a/public/windows11/SmallTile.scale-150.png and /dev/null differ diff --git a/public/windows11/SmallTile.scale-200.png b/public/windows11/SmallTile.scale-200.png deleted file mode 100644 index dede603..0000000 Binary files a/public/windows11/SmallTile.scale-200.png and /dev/null differ diff --git a/public/windows11/SmallTile.scale-400.png b/public/windows11/SmallTile.scale-400.png deleted file mode 100644 index edece82..0000000 Binary files a/public/windows11/SmallTile.scale-400.png and /dev/null differ diff --git a/public/windows11/SplashScreen.scale-100.png b/public/windows11/SplashScreen.scale-100.png deleted file mode 100644 index f3f8388..0000000 Binary files a/public/windows11/SplashScreen.scale-100.png and /dev/null differ diff --git a/public/windows11/SplashScreen.scale-125.png b/public/windows11/SplashScreen.scale-125.png deleted file mode 100644 index 8fced21..0000000 Binary files a/public/windows11/SplashScreen.scale-125.png and /dev/null differ diff --git a/public/windows11/SplashScreen.scale-150.png b/public/windows11/SplashScreen.scale-150.png deleted file mode 100644 index 5cf3dd5..0000000 Binary files a/public/windows11/SplashScreen.scale-150.png and /dev/null differ diff --git a/public/windows11/SplashScreen.scale-200.png b/public/windows11/SplashScreen.scale-200.png deleted file mode 100644 index 77c761c..0000000 Binary files a/public/windows11/SplashScreen.scale-200.png and /dev/null differ diff --git a/public/windows11/SplashScreen.scale-400.png b/public/windows11/SplashScreen.scale-400.png deleted file mode 100644 index a4bb775..0000000 Binary files a/public/windows11/SplashScreen.scale-400.png and /dev/null differ diff --git a/public/windows11/Square150x150Logo.scale-100.png b/public/windows11/Square150x150Logo.scale-100.png deleted file mode 100644 index a20582e..0000000 Binary files a/public/windows11/Square150x150Logo.scale-100.png and /dev/null differ diff --git a/public/windows11/Square150x150Logo.scale-125.png b/public/windows11/Square150x150Logo.scale-125.png deleted file mode 100644 index c5bdd56..0000000 Binary files a/public/windows11/Square150x150Logo.scale-125.png and /dev/null differ diff --git a/public/windows11/Square150x150Logo.scale-150.png b/public/windows11/Square150x150Logo.scale-150.png deleted file mode 100644 index b9153db..0000000 Binary files a/public/windows11/Square150x150Logo.scale-150.png and /dev/null differ diff --git a/public/windows11/Square150x150Logo.scale-200.png b/public/windows11/Square150x150Logo.scale-200.png deleted file mode 100644 index 750505d..0000000 Binary files a/public/windows11/Square150x150Logo.scale-200.png and /dev/null differ diff --git a/public/windows11/Square150x150Logo.scale-400.png b/public/windows11/Square150x150Logo.scale-400.png deleted file mode 100644 index 9210530..0000000 Binary files a/public/windows11/Square150x150Logo.scale-400.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png deleted file mode 100644 index 10db5a7..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png deleted file mode 100644 index cd2b33f..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png deleted file mode 100644 index e479989..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png deleted file mode 100644 index 95723a8..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png deleted file mode 100644 index a726ccf..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png deleted file mode 100644 index b63e5dc..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png deleted file mode 100644 index 447b9fd..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png deleted file mode 100644 index 2f39e2e..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png deleted file mode 100644 index 98660f8..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png deleted file mode 100644 index 1c07dd8..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png deleted file mode 100644 index 69bfda5..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png deleted file mode 100644 index cd25ac4..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png deleted file mode 100644 index 699c92c..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png deleted file mode 100644 index 4e4807f..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png b/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png deleted file mode 100644 index 11e977d..0000000 Binary files a/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-16.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-16.png deleted file mode 100644 index 10db5a7..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-16.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-20.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-20.png deleted file mode 100644 index cd2b33f..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-20.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-24.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-24.png deleted file mode 100644 index e479989..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-24.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-256.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-256.png deleted file mode 100644 index 95723a8..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-256.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-30.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-30.png deleted file mode 100644 index a726ccf..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-30.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-32.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-32.png deleted file mode 100644 index b63e5dc..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-32.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-36.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-36.png deleted file mode 100644 index 447b9fd..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-36.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-40.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-40.png deleted file mode 100644 index 2f39e2e..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-40.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-44.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-44.png deleted file mode 100644 index 98660f8..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-44.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-48.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-48.png deleted file mode 100644 index 1c07dd8..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-48.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-60.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-60.png deleted file mode 100644 index 69bfda5..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-60.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-64.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-64.png deleted file mode 100644 index cd25ac4..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-64.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-72.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-72.png deleted file mode 100644 index 699c92c..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-72.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-80.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-80.png deleted file mode 100644 index 4e4807f..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-80.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.altform-unplated_targetsize-96.png b/public/windows11/Square44x44Logo.altform-unplated_targetsize-96.png deleted file mode 100644 index 11e977d..0000000 Binary files a/public/windows11/Square44x44Logo.altform-unplated_targetsize-96.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.scale-100.png b/public/windows11/Square44x44Logo.scale-100.png deleted file mode 100644 index 98660f8..0000000 Binary files a/public/windows11/Square44x44Logo.scale-100.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.scale-125.png b/public/windows11/Square44x44Logo.scale-125.png deleted file mode 100644 index 649d606..0000000 Binary files a/public/windows11/Square44x44Logo.scale-125.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.scale-150.png b/public/windows11/Square44x44Logo.scale-150.png deleted file mode 100644 index 485dd3d..0000000 Binary files a/public/windows11/Square44x44Logo.scale-150.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.scale-200.png b/public/windows11/Square44x44Logo.scale-200.png deleted file mode 100644 index af99cdf..0000000 Binary files a/public/windows11/Square44x44Logo.scale-200.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.scale-400.png b/public/windows11/Square44x44Logo.scale-400.png deleted file mode 100644 index 17783bc..0000000 Binary files a/public/windows11/Square44x44Logo.scale-400.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-16.png b/public/windows11/Square44x44Logo.targetsize-16.png deleted file mode 100644 index 10db5a7..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-16.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-20.png b/public/windows11/Square44x44Logo.targetsize-20.png deleted file mode 100644 index cd2b33f..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-20.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-24.png b/public/windows11/Square44x44Logo.targetsize-24.png deleted file mode 100644 index e479989..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-24.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-256.png b/public/windows11/Square44x44Logo.targetsize-256.png deleted file mode 100644 index 95723a8..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-256.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-30.png b/public/windows11/Square44x44Logo.targetsize-30.png deleted file mode 100644 index a726ccf..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-30.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-32.png b/public/windows11/Square44x44Logo.targetsize-32.png deleted file mode 100644 index b63e5dc..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-32.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-36.png b/public/windows11/Square44x44Logo.targetsize-36.png deleted file mode 100644 index 447b9fd..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-36.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-40.png b/public/windows11/Square44x44Logo.targetsize-40.png deleted file mode 100644 index 2f39e2e..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-40.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-44.png b/public/windows11/Square44x44Logo.targetsize-44.png deleted file mode 100644 index 98660f8..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-44.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-48.png b/public/windows11/Square44x44Logo.targetsize-48.png deleted file mode 100644 index 1c07dd8..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-48.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-60.png b/public/windows11/Square44x44Logo.targetsize-60.png deleted file mode 100644 index 69bfda5..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-60.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-64.png b/public/windows11/Square44x44Logo.targetsize-64.png deleted file mode 100644 index cd25ac4..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-64.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-72.png b/public/windows11/Square44x44Logo.targetsize-72.png deleted file mode 100644 index 699c92c..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-72.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-80.png b/public/windows11/Square44x44Logo.targetsize-80.png deleted file mode 100644 index 4e4807f..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-80.png and /dev/null differ diff --git a/public/windows11/Square44x44Logo.targetsize-96.png b/public/windows11/Square44x44Logo.targetsize-96.png deleted file mode 100644 index 11e977d..0000000 Binary files a/public/windows11/Square44x44Logo.targetsize-96.png and /dev/null differ diff --git a/public/windows11/StoreLogo.scale-100.png b/public/windows11/StoreLogo.scale-100.png deleted file mode 100644 index 1a05299..0000000 Binary files a/public/windows11/StoreLogo.scale-100.png and /dev/null differ diff --git a/public/windows11/StoreLogo.scale-125.png b/public/windows11/StoreLogo.scale-125.png deleted file mode 100644 index a76ec24..0000000 Binary files a/public/windows11/StoreLogo.scale-125.png and /dev/null differ diff --git a/public/windows11/StoreLogo.scale-150.png b/public/windows11/StoreLogo.scale-150.png deleted file mode 100644 index e4bbc64..0000000 Binary files a/public/windows11/StoreLogo.scale-150.png and /dev/null differ diff --git a/public/windows11/StoreLogo.scale-200.png b/public/windows11/StoreLogo.scale-200.png deleted file mode 100644 index 3c131c3..0000000 Binary files a/public/windows11/StoreLogo.scale-200.png and /dev/null differ diff --git a/public/windows11/StoreLogo.scale-400.png b/public/windows11/StoreLogo.scale-400.png deleted file mode 100644 index 5663afc..0000000 Binary files a/public/windows11/StoreLogo.scale-400.png and /dev/null differ diff --git a/public/windows11/Wide310x150Logo.scale-100.png b/public/windows11/Wide310x150Logo.scale-100.png deleted file mode 100644 index 0183221..0000000 Binary files a/public/windows11/Wide310x150Logo.scale-100.png and /dev/null differ diff --git a/public/windows11/Wide310x150Logo.scale-125.png b/public/windows11/Wide310x150Logo.scale-125.png deleted file mode 100644 index 90c6b6f..0000000 Binary files a/public/windows11/Wide310x150Logo.scale-125.png and /dev/null differ diff --git a/public/windows11/Wide310x150Logo.scale-150.png b/public/windows11/Wide310x150Logo.scale-150.png deleted file mode 100644 index 8510012..0000000 Binary files a/public/windows11/Wide310x150Logo.scale-150.png and /dev/null differ diff --git a/public/windows11/Wide310x150Logo.scale-200.png b/public/windows11/Wide310x150Logo.scale-200.png deleted file mode 100644 index f3f8388..0000000 Binary files a/public/windows11/Wide310x150Logo.scale-200.png and /dev/null differ diff --git a/public/windows11/Wide310x150Logo.scale-400.png b/public/windows11/Wide310x150Logo.scale-400.png deleted file mode 100644 index 77c761c..0000000 Binary files a/public/windows11/Wide310x150Logo.scale-400.png and /dev/null differ diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..bedec00 --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,8 @@ +import { render } from '@testing-library/react'; +import { test } from 'vitest'; + +import App from './App'; + +test('App', () => { + render(<App />); +}) \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..79d6461 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,70 @@ +// NOTE: This file should normally not be modified unless you are adding a new provider. +// To add new routes, edit the AppRouter.tsx file. + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { createHead, UnheadProvider } from '@unhead/react/client'; +import { InferSeoMetaPlugin } from '@unhead/addons'; +import { Suspense } from 'react'; +import NostrProvider from '@/components/NostrProvider'; +import { NostrSync } from '@/components/NostrSync'; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { NostrLoginProvider } from '@nostrify/react/login'; +import { AppProvider } from '@/components/AppProvider'; +import { NWCProvider } from '@/contexts/NWCContext'; +import { AppConfig } from '@/contexts/AppContext'; +import AppRouter from './AppRouter'; + +const head = createHead({ + plugins: [ + InferSeoMetaPlugin(), + ], +}); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + staleTime: 60000, // 1 minute + gcTime: Infinity, + }, + }, +}); + +const defaultConfig: AppConfig = { + theme: "light", + relayMetadata: { + relays: [ + { url: 'wss://relay.ditto.pub', read: true, write: true }, + { url: 'wss://relay.nostr.band', read: true, write: true }, + { url: 'wss://relay.damus.io', read: true, write: true }, + ], + updatedAt: 0, + }, +}; + +export function App() { + return ( + <UnheadProvider head={head}> + <AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig}> + <QueryClientProvider client={queryClient}> + <NostrLoginProvider storageKey='nostr:login'> + <NostrProvider> + <NostrSync /> + <NWCProvider> + <TooltipProvider> + <Toaster /> + <Suspense> + <AppRouter /> + </Suspense> + </TooltipProvider> + </NWCProvider> + </NostrProvider> + </NostrLoginProvider> + </QueryClientProvider> + </AppProvider> + </UnheadProvider> + ); +} + +export default App; diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx new file mode 100644 index 0000000..2db53ee --- /dev/null +++ b/src/AppRouter.tsx @@ -0,0 +1,22 @@ +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { ScrollToTop } from "./components/ScrollToTop"; + +import Index from "./pages/Index"; +import { NIP19Page } from "./pages/NIP19Page"; +import NotFound from "./pages/NotFound"; + +export function AppRouter() { + return ( + <BrowserRouter> + <ScrollToTop /> + <Routes> + <Route path="/" element={<Index />} /> + {/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */} + <Route path="/:nip19" element={<NIP19Page />} /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + <Route path="*" element={<NotFound />} /> + </Routes> + </BrowserRouter> + ); +} +export default AppRouter; \ No newline at end of file diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx new file mode 100644 index 0000000..8b72567 --- /dev/null +++ b/src/components/AppProvider.tsx @@ -0,0 +1,111 @@ +import { ReactNode, useEffect } from 'react'; +import { z } from 'zod'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata } from '@/contexts/AppContext'; + +interface AppProviderProps { + children: ReactNode; + /** Application storage key */ + storageKey: string; + /** Default app configuration */ + defaultConfig: AppConfig; +} + +// Zod schema for RelayMetadata validation +const RelayMetadataSchema = z.object({ + relays: z.array(z.object({ + url: z.string().url(), + read: z.boolean(), + write: z.boolean(), + })), + updatedAt: z.number(), +}) satisfies z.ZodType<RelayMetadata>; + +// Zod schema for AppConfig validation +const AppConfigSchema = z.object({ + theme: z.enum(['dark', 'light', 'system']), + relayMetadata: RelayMetadataSchema, +}) satisfies z.ZodType<AppConfig>; + +export function AppProvider(props: AppProviderProps) { + const { + children, + storageKey, + defaultConfig, + } = props; + + // App configuration state with localStorage persistence + const [rawConfig, setConfig] = useLocalStorage<Partial<AppConfig>>( + storageKey, + {}, + { + serialize: JSON.stringify, + deserialize: (value: string) => { + const parsed = JSON.parse(value); + return AppConfigSchema.partial().parse(parsed); + } + } + ); + + // Generic config updater with callback pattern + const updateConfig = (updater: (currentConfig: Partial<AppConfig>) => Partial<AppConfig>) => { + setConfig(updater); + }; + + const config = { ...defaultConfig, ...rawConfig }; + + const appContextValue: AppContextType = { + config, + updateConfig, + }; + + // Apply theme effects to document + useApplyTheme(config.theme); + + return ( + <AppContext.Provider value={appContextValue}> + {children} + </AppContext.Provider> + ); +} + +/** + * Hook to apply theme changes to the document root + */ +function useApplyTheme(theme: Theme) { + 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]); + + // Handle system theme changes when theme is set to "system" + useEffect(() => { + if (theme !== 'system') return; + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleChange = () => { + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + + const systemTheme = mediaQuery.matches ? 'dark' : 'light'; + root.classList.add(systemTheme); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [theme]); +} \ No newline at end of file diff --git a/src/components/DMProvider.tsx b/src/components/DMProvider.tsx new file mode 100644 index 0000000..43710cb --- /dev/null +++ b/src/components/DMProvider.tsx @@ -0,0 +1,1524 @@ +import { useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useNostr } from '@nostrify/react'; +import { useAppContext } from '@/hooks/useAppContext'; +import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { useToast } from '@/hooks/useToast'; +import { validateDMEvent } from '@/lib/dmUtils'; +import { LOADING_PHASES, type LoadingPhase, PROTOCOL_MODE, type ProtocolMode } from '@/lib/dmConstants'; +import { NSecSigner, type NostrEvent } from '@nostrify/nostrify'; +import { generateSecretKey } from 'nostr-tools'; +import type { MessageProtocol } from '@/lib/dmConstants'; +import { MESSAGE_PROTOCOL } from '@/lib/dmConstants'; +import { DMContext, DMContextType, FileAttachment } from '@/contexts/DMContext'; + +// ============================================================================ +// DM Types and Constants +// ============================================================================ + +interface ParticipantData { + messages: DecryptedMessage[]; + lastActivity: number; + lastMessage: DecryptedMessage | null; + hasNIP4: boolean; + hasNIP17: boolean; +} + +type MessagesState = Map<string, ParticipantData>; + +interface LastSyncData { + nip4: number | null; + nip17: number | null; +} + +interface SubscriptionStatus { + isNIP4Connected: boolean; + isNIP17Connected: boolean; +} + +interface ScanProgress { + current: number; + status: string; +} + +interface ScanProgressState { + nip4: ScanProgress | null; + nip17: ScanProgress | null; +} + +interface ConversationSummary { + id: string; + pubkey: string; + lastMessage: DecryptedMessage | null; + lastActivity: number; + hasNIP4Messages: boolean; + hasNIP17Messages: boolean; + isKnown: boolean; + isRequest: boolean; + lastMessageFromUser: boolean; +} + +interface MessageProcessingResult { + lastMessageTimestamp?: number; + messageCount: number; +} + +interface DecryptionResult { + decryptedContent: string; + error?: string; +} + +interface DecryptedMessage extends NostrEvent { + decryptedContent?: string; + error?: string; + isSending?: boolean; + clientFirstSeen?: number; + decryptedEvent?: NostrEvent; // For NIP-17: the inner kind 14/15 event + originalGiftWrapId?: string; // Store gift wrap ID for NIP-17 deduplication +} + +interface NIP17ProcessingResult { + processedMessage: DecryptedMessage; + conversationPartner: string; + sealEvent: NostrEvent; // Return the seal so we can cache it +} + +const DM_CONSTANTS = { + DEBOUNCED_WRITE_DELAY: 15000, + RECENT_MESSAGE_THRESHOLD: 5000, + SUBSCRIPTION_OVERLAP_SECONDS: 10, // Overlap for subscriptions to catch race conditions + SCAN_TOTAL_LIMIT: 20000, + SCAN_BATCH_SIZE: 1000, + NIP4_QUERY_TIMEOUT: 15000, + NIP17_QUERY_TIMEOUT: 30000, + ERROR_LOG_DEBOUNCE_DELAY: 2000, +} as const; + +const SCAN_STATUS_MESSAGES = { + NIP4_STARTING: 'Starting NIP-4 scan...', + NIP17_STARTING: 'Starting NIP-17 scan...', +} as const; + +const createErrorLogger = (name: string) => { + let count = 0; + let timeout: NodeJS.Timeout | null = null; + + return (_error: Error) => { + count++; + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + if (count > 0) { + console.error(`[DM] ${name} processing complete with ${count} errors`); + count = 0; + } + }, DM_CONSTANTS.ERROR_LOG_DEBOUNCE_DELAY); + }; +}; + +const nip17ErrorLogger = createErrorLogger('NIP-17'); + +export interface DMConfig { + enabled?: boolean; + protocolMode?: ProtocolMode; +} + +interface DMProviderProps { + children: ReactNode; + config?: DMConfig; +} + +// ============================================================================ +// Message Sending Types and Helpers (Internal) +// ============================================================================ + +/** + * Prepare message content with file URLs appended + */ +function prepareMessageContent(content: string, attachments: FileAttachment[] = []): string { + if (attachments.length === 0) return content; + + const fileUrls = attachments.map(file => file.url).join('\n'); + return content ? `${content}\n\n${fileUrls}` : fileUrls; +} + +/** + * Create imeta tags for file attachments (NIP-92) + */ +function createImetaTags(attachments: FileAttachment[] = []): string[][] { + return attachments.map(file => { + const imetaTag = ['imeta']; + imetaTag.push(`url ${file.url}`); + if (file.mimeType) imetaTag.push(`m ${file.mimeType}`); + if (file.size) imetaTag.push(`size ${file.size}`); + if (file.name) imetaTag.push(`alt ${file.name}`); + + // Add hash tags from file.tags + file.tags.forEach(tag => { + if (tag[0] === 'x') imetaTag.push(`x ${tag[1]}`); + if (tag[0] === 'ox') imetaTag.push(`ox ${tag[1]}`); + }); + + return imetaTag; + }); +} + +// ============================================================================ +// DMProvider Component +// ============================================================================ + +export function DMProvider({ children, config }: DMProviderProps) { + const { enabled = false, protocolMode = PROTOCOL_MODE.NIP17_ONLY } = config || {}; + const { user } = useCurrentUser(); + const { nostr } = useNostr(); + const { mutateAsync: createEvent } = useNostrPublish(); + const { toast } = useToast(); + const { config: appConfig } = useAppContext(); + + const userPubkey = useMemo(() => user?.pubkey, [user?.pubkey]); + + // Track relay metadata to detect changes + const previousRelayMetadata = useRef(appConfig.relayMetadata); + + // Determine if NIP-17 is enabled based on protocol mode + const enableNIP17 = protocolMode !== PROTOCOL_MODE.NIP04_ONLY; + + const [messages, setMessages] = useState<MessagesState>(new Map()); + const [lastSync, setLastSync] = useState<LastSyncData>({ + nip4: null, + nip17: null + }); + const [isLoading, setIsLoading] = useState(false); + const [loadingPhase, setLoadingPhase] = useState<LoadingPhase>(LOADING_PHASES.IDLE); + const [subscriptions, setSubscriptions] = useState<SubscriptionStatus>({ + isNIP4Connected: false, + isNIP17Connected: false + }); + const [hasInitialLoadCompleted, setHasInitialLoadCompleted] = useState(false); + const [shouldSaveImmediately, setShouldSaveImmediately] = useState(false); + const [scanProgress, setScanProgress] = useState<ScanProgressState>({ + nip4: null, + nip17: null + }); + + const nip4SubscriptionRef = useRef<{ close: () => void } | null>(null); + const nip17SubscriptionRef = useRef<{ close: () => void } | null>(null); + const debouncedWriteRef = useRef<NodeJS.Timeout | null>(null); + + // ============================================================================ + // Internal Message Sending Mutations + // ============================================================================ + + // Send NIP-04 Message (internal) + const sendNIP4Message = useMutation<NostrEvent, Error, { + recipientPubkey: string; + content: string; + attachments?: FileAttachment[]; + }>({ + mutationFn: async ({ recipientPubkey, content, attachments = [] }) => { + if (!user) { + throw new Error('User is not logged in'); + } + + if (!user.signer.nip04) { + throw new Error('NIP-04 encryption not available'); + } + + // Prepare content with file URLs + const messageContent = prepareMessageContent(content, attachments); + + // Encrypt the content + const encryptedContent = await user.signer.nip04.encrypt(recipientPubkey, messageContent); + + // Build tags with imeta tags for attachments + const tags: string[][] = [ + ['p', recipientPubkey], + ...createImetaTags(attachments) + ]; + + // Create and publish the event + return await createEvent({ + kind: 4, + content: encryptedContent, + tags, + }); + }, + onError: (error) => { + console.error('[DM] Failed to send NIP-04 message:', error); + toast({ + title: 'Failed to send message', + description: error.message, + variant: 'destructive', + }); + }, + }); + + // Send NIP-17 Message (internal) + const sendNIP17Message = useMutation<NostrEvent, Error, { + recipientPubkey: string; + content: string; + attachments?: FileAttachment[]; + }>({ + mutationFn: async ({ recipientPubkey, content, attachments = [] }) => { + if (!user) { + throw new Error('User is not logged in'); + } + + if (!user.signer.nip44) { + throw new Error('NIP-44 encryption not available'); + } + + // Step 1: Create the inner Kind 14 Private Direct Message + const now = Math.floor(Date.now() / 1000); + + // Generate randomized timestamps for gift wraps (NIP-59 metadata privacy) + // Randomize within ±2 days in the PAST only (relays reject future timestamps > +30min) + const randomizeTimestamp = (baseTime: number) => { + const twoDaysInSeconds = 2 * 24 * 60 * 60; + // Random offset between -2 days and 0 (never future) + const randomOffset = -Math.floor(Math.random() * twoDaysInSeconds); + return baseTime + randomOffset; + }; + + // Prepare content with file URLs + const messageContent = prepareMessageContent(content, attachments); + + // Build tags with imeta tags for attachments + const tags: string[][] = [ + ['p', recipientPubkey], + ...createImetaTags(attachments) + ]; + + // Use kind 15 for messages with file attachments, kind 14 for text-only + const messageKind = (attachments && attachments.length > 0) ? 15 : 14; + + const privateMessage: Omit<NostrEvent, 'id' | 'sig'> = { + kind: messageKind, + pubkey: user.pubkey, + created_at: now, + tags, + content: messageContent, + }; + + // Step 2: Create TWO Kind 13 Seal events (one for recipient, one for myself) + const recipientSeal: Omit<NostrEvent, 'id' | 'sig'> = { + kind: 13, + pubkey: user.pubkey, + created_at: now, + tags: [], + content: await user.signer.nip44.encrypt(recipientPubkey, JSON.stringify(privateMessage)), + }; + + const senderSeal: Omit<NostrEvent, 'id' | 'sig'> = { + kind: 13, + pubkey: user.pubkey, + created_at: now, + tags: [], + content: await user.signer.nip44.encrypt(user.pubkey, JSON.stringify(privateMessage)), + }; + + // Step 3: Create TWO Kind 1059 Gift Wrap events + // Per NIP-17/NIP-59: Gift wraps MUST be signed with random, ephemeral keys + // to hide the sender's identity and provide - some - metadata privacy + + // Generate random secret keys for each gift wrap + const recipientRandomKey = generateSecretKey(); + const senderRandomKey = generateSecretKey(); + + // Create signers with the random keys + const recipientRandomSigner = new NSecSigner(recipientRandomKey); + const senderRandomSigner = new NSecSigner(senderRandomKey); + + // Encrypt the seals using the RANDOM signers (so recipient can decrypt with the random pubkey) + // The recipient will decrypt using the gift wrap's pubkey (the random ephemeral key) + const recipientGiftWrapContent = await recipientRandomSigner.nip44!.encrypt(recipientPubkey, JSON.stringify(recipientSeal)); + const senderGiftWrapContent = await senderRandomSigner.nip44!.encrypt(user.pubkey, JSON.stringify(senderSeal)); + + // Sign both gift wraps with random keys and randomized timestamps + // Random keys hide the sender's identity; encryption to recipient allows decryption + const [recipientGiftWrap, senderGiftWrap] = await Promise.all([ + recipientRandomSigner.signEvent({ + kind: 1059, + created_at: randomizeTimestamp(now), // Randomized to hide real send time + tags: [['p', recipientPubkey]], + content: recipientGiftWrapContent, + }), + senderRandomSigner.signEvent({ + kind: 1059, + created_at: randomizeTimestamp(now), // Randomized to hide real send time + tags: [['p', user.pubkey]], + content: senderGiftWrapContent, + }), + ]); + + // Publish both to relays + try { + const results = await Promise.allSettled([ + nostr.event(recipientGiftWrap), + nostr.event(senderGiftWrap), + ]); + + // Check for failures and log detailed errors + const recipientResult = results[0]; + const senderResult = results[1]; + + if (recipientResult.status === 'rejected') { + console.error('[DM] Failed to publish recipient gift wrap'); + console.error('[DM] Recipient gift wrap event:', recipientGiftWrap); + + // Try to extract detailed errors from AggregateError + const error = recipientResult.reason; + if (error && typeof error === 'object' && 'errors' in error) { + console.error('[DM] Recipient individual relay errors:', error.errors); + } else { + console.error('[DM] Recipient error:', error); + } + } + + if (senderResult.status === 'rejected') { + console.error('[DM] Failed to publish sender gift wrap'); + console.error('[DM] Sender gift wrap event:', senderGiftWrap); + + // Try to extract detailed errors from AggregateError + const error = senderResult.reason; + if (error && typeof error === 'object' && 'errors' in error) { + console.error('[DM] Sender individual relay errors:', error.errors); + } else { + console.error('[DM] Sender error:', error); + } + } + + // If both failed, throw error + if (recipientResult.status === 'rejected' && senderResult.status === 'rejected') { + throw new Error(`Both gift wraps rejected. Recipient: ${recipientResult.reason}, Sender: ${senderResult.reason}`); + } + } catch (publishError) { + console.error('[DM] Publish error:', publishError); + throw publishError; + } + + return recipientGiftWrap; + }, + onError: (error) => { + console.error('[DM] Failed to send NIP-17 message:', error); + toast({ + title: 'Failed to send message', + description: error.message, + variant: 'destructive', + }); + }, + }); + + // ============================================================================ + // Message Loading and Processing + // ============================================================================ + + // Load past NIP-4 messages + const loadPastNIP4Messages = useCallback(async (sinceTimestamp?: number) => { + if (!user?.pubkey) return; + + let allMessages: NostrEvent[] = []; + let processedMessages = 0; + let currentSince = sinceTimestamp || 0; + + + setScanProgress(prev => ({ ...prev, nip4: { current: 0, status: SCAN_STATUS_MESSAGES.NIP4_STARTING } })); + + while (processedMessages < DM_CONSTANTS.SCAN_TOTAL_LIMIT) { + const batchLimit = Math.min(DM_CONSTANTS.SCAN_BATCH_SIZE, DM_CONSTANTS.SCAN_TOTAL_LIMIT - processedMessages); + + const filters = [ + { kinds: [4], '#p': [user.pubkey], limit: batchLimit, since: currentSince }, + { kinds: [4], authors: [user.pubkey], limit: batchLimit, since: currentSince } + ]; + + try { + const batchDMs = await nostr.query(filters, { signal: AbortSignal.timeout(DM_CONSTANTS.NIP4_QUERY_TIMEOUT) }); + const validBatchDMs = batchDMs.filter(validateDMEvent); + + if (validBatchDMs.length === 0) break; + + allMessages = [...allMessages, ...validBatchDMs]; + processedMessages += validBatchDMs.length; + + setScanProgress(prev => ({ + ...prev, + nip4: { + current: allMessages.length, + status: `Batch ${Math.floor(processedMessages / DM_CONSTANTS.SCAN_BATCH_SIZE) + 1} complete: ${validBatchDMs.length} messages` + } + })); + + const oldestToMe = validBatchDMs.filter(m => m.pubkey !== user.pubkey).length > 0 + ? Math.min(...validBatchDMs.filter(m => m.pubkey !== user.pubkey).map(m => m.created_at)) + : Infinity; + const oldestFromMe = validBatchDMs.filter(m => m.pubkey === user.pubkey).length > 0 + ? Math.min(...validBatchDMs.filter(m => m.pubkey === user.pubkey).map(m => m.created_at)) + : Infinity; + + const oldestInBatch = Math.min(oldestToMe, oldestFromMe); + if (oldestInBatch !== Infinity) { + currentSince = oldestInBatch; + } + + if (validBatchDMs.length < batchLimit * 2) break; + } catch (error) { + console.error('[DM] NIP-4 Error in batch query:', error); + break; + } + } + + setScanProgress(prev => ({ ...prev, nip4: null })); + return allMessages; + }, [user, nostr]); + + // Load past NIP-17 messages + const loadPastNIP17Messages = useCallback(async (sinceTimestamp?: number) => { + if (!user?.pubkey) return; + + let allNIP17Events: NostrEvent[] = []; + let processedMessages = 0; + + // Adjust since timestamp to account for NIP-17 timestamp fuzzing (±2 days) + // We need to query from (lastSync - 2 days) to catch messages with randomized past timestamps + // This may fetch duplicates, but they're filtered by message ID in addMessageToState + const TWO_DAYS_IN_SECONDS = 2 * 24 * 60 * 60; + let currentSince = sinceTimestamp ? sinceTimestamp - TWO_DAYS_IN_SECONDS : 0; + + + setScanProgress(prev => ({ ...prev, nip17: { current: 0, status: SCAN_STATUS_MESSAGES.NIP17_STARTING } })); + + while (processedMessages < DM_CONSTANTS.SCAN_TOTAL_LIMIT) { + const batchLimit = Math.min(DM_CONSTANTS.SCAN_BATCH_SIZE, DM_CONSTANTS.SCAN_TOTAL_LIMIT - processedMessages); + + const filters = [ + { kinds: [1059], '#p': [user.pubkey], limit: batchLimit, since: currentSince } + ]; + + try { + const batchEvents = await nostr.query(filters, { signal: AbortSignal.timeout(DM_CONSTANTS.NIP17_QUERY_TIMEOUT) }); + + if (batchEvents.length === 0) break; + + allNIP17Events = [...allNIP17Events, ...batchEvents]; + processedMessages += batchEvents.length; + + setScanProgress(prev => ({ + ...prev, + nip17: { + current: allNIP17Events.length, + status: `Batch ${Math.floor(processedMessages / DM_CONSTANTS.SCAN_BATCH_SIZE) + 1} complete: ${batchEvents.length} messages` + } + })); + + if (batchEvents.length > 0) { + const oldestInBatch = Math.min(...batchEvents.map(m => m.created_at)); + currentSince = oldestInBatch; + } + + if (batchEvents.length < batchLimit) break; + } catch (error) { + console.error('[DM] NIP-17 Error in batch query:', error); + break; + } + } + + setScanProgress(prev => ({ ...prev, nip17: null })); + return allNIP17Events; + }, [user, nostr]); + + // Query relays for messages + const queryRelaysForMessagesSince = useCallback(async (protocol: MessageProtocol, sinceTimestamp?: number): Promise<MessageProcessingResult> => { + if (protocol === MESSAGE_PROTOCOL.NIP17 && !enableNIP17) { + return { lastMessageTimestamp: sinceTimestamp, messageCount: 0 }; + } + + if (!userPubkey) { + return { lastMessageTimestamp: sinceTimestamp, messageCount: 0 }; + } + + if (protocol === MESSAGE_PROTOCOL.NIP04) { + const messages = await loadPastNIP4Messages(sinceTimestamp); + + if (messages && messages.length > 0) { + const newState = new Map(); + + for (const message of messages) { + const isFromUser = message.pubkey === user?.pubkey; + const recipientPTag = message.tags?.find(([name]) => name === 'p')?.[1]; + const otherPubkey = isFromUser ? recipientPTag : message.pubkey; + + if (!otherPubkey || otherPubkey === user?.pubkey) continue; + + const { decryptedContent, error } = await decryptNIP4Message(message, otherPubkey); + + const decryptedMessage: DecryptedMessage = { + ...message, + content: message.content, + decryptedContent: decryptedContent, + error: error, + }; + + const messageAge = Date.now() - (message.created_at * 1000); + if (messageAge < 5000) { + decryptedMessage.clientFirstSeen = Date.now(); + } + + if (!newState.has(otherPubkey)) { + newState.set(otherPubkey, createEmptyParticipant()); + } + + const participant = newState.get(otherPubkey)!; + participant.messages.push(decryptedMessage); + participant.hasNIP4 = true; + } + + newState.forEach(participant => { + sortAndUpdateParticipantState(participant); + }); + + mergeMessagesIntoState(newState); + + const currentTime = Math.floor(Date.now() / 1000); + setLastSync(prev => ({ ...prev, nip4: currentTime })); + + const newestMessage = messages.reduce((newest, msg) => + msg.created_at > newest.created_at ? msg : newest + ); + return { lastMessageTimestamp: newestMessage.created_at, messageCount: messages.length }; + } else { + // No new messages, but we still successfully queried relays - update lastSync + const currentTime = Math.floor(Date.now() / 1000); + setLastSync(prev => ({ ...prev, nip4: currentTime })); + return { lastMessageTimestamp: sinceTimestamp, messageCount: 0 }; + } + } else if (protocol === MESSAGE_PROTOCOL.NIP17) { + const messages = await loadPastNIP17Messages(sinceTimestamp); + + if (messages && messages.length > 0) { + const newState = new Map(); + + for (const giftWrap of messages) { + try { + const { processedMessage, conversationPartner, sealEvent } = await processNIP17GiftWrap(giftWrap); + + // Skip messages with decryption errors + if (processedMessage.error) { + continue; + } + + // Store the seal (kind 13) as-is + add decryptedEvent for inner message access + const messageWithAnimation: DecryptedMessage = { + ...sealEvent, // Seal fields (kind 13, seal pubkey, encrypted content, etc.) + created_at: processedMessage.created_at, // Use real timestamp from inner message + decryptedEvent: { + ...processedMessage, + content: processedMessage.decryptedContent, + } as NostrEvent, + decryptedContent: processedMessage.decryptedContent, + originalGiftWrapId: giftWrap.id, // Store gift wrap ID for deduplication + }; + + // Use real message timestamp for recency check + const messageAge = Date.now() - (processedMessage.created_at * 1000); + if (messageAge < 5000) { + messageWithAnimation.clientFirstSeen = Date.now(); + } + + if (!newState.has(conversationPartner)) { + newState.set(conversationPartner, createEmptyParticipant()); + } + + newState.get(conversationPartner)!.messages.push(messageWithAnimation); + newState.get(conversationPartner)!.hasNIP17 = true; + } catch (error) { + console.error('[DM] Error processing gift wrap from relay:', error); + } + } + + newState.forEach(participant => { + sortAndUpdateParticipantState(participant); + }); + + mergeMessagesIntoState(newState); + + const currentTime = Math.floor(Date.now() / 1000); + setLastSync(prev => ({ ...prev, nip17: currentTime })); + + const newestMessage = messages.reduce((newest, msg) => + msg.created_at > newest.created_at ? msg : newest + ); + return { lastMessageTimestamp: newestMessage.created_at, messageCount: messages.length }; + } else { + // No new messages, but we still successfully queried relays - update lastSync + const currentTime = Math.floor(Date.now() / 1000); + setLastSync(prev => ({ ...prev, nip17: currentTime })); + return { lastMessageTimestamp: sinceTimestamp, messageCount: 0 }; + } + } + + return { lastMessageTimestamp: sinceTimestamp, messageCount: 0 }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enableNIP17, userPubkey, loadPastNIP4Messages, loadPastNIP17Messages, user]); + + // Decrypt NIP-4 message + const decryptNIP4Message = useCallback(async (event: NostrEvent, otherPubkey: string): Promise<DecryptionResult> => { + try { + if (user?.signer?.nip04) { + const decryptedContent = await user.signer.nip04.decrypt(otherPubkey, event.content); + return { decryptedContent }; + } else { + return { + decryptedContent: '', + error: 'No NIP-04 decryption available' + }; + } + } catch (error) { + console.error(`[DM] Failed to decrypt NIP-4 message ${event.id}:`, error); + return { + decryptedContent: '', + error: 'Decryption failed' + }; + } + }, [user]); + + // Create empty participant + const createEmptyParticipant = useCallback(() => ({ + messages: [], + lastActivity: 0, + lastMessage: null, + hasNIP4: false, + hasNIP17: false, + }), []); + + // Sort and update participant state + const sortAndUpdateParticipantState = useCallback((participant: { messages: DecryptedMessage[]; lastActivity: number; lastMessage: DecryptedMessage | null }) => { + participant.messages.sort((a, b) => a.created_at - b.created_at); + if (participant.messages.length > 0) { + participant.lastActivity = participant.messages[participant.messages.length - 1].created_at; + participant.lastMessage = participant.messages[participant.messages.length - 1]; + } + }, []); + + // Merge messages into state + const mergeMessagesIntoState = useCallback((newState: MessagesState) => { + setMessages(prev => { + const finalMap = new Map(prev); + + newState.forEach((value, key) => { + const existing = finalMap.get(key); + if (existing) { + // For NIP-17 messages with originalGiftWrapId, dedupe by gift wrap ID + // For NIP-04 and cached NIP-17 messages, dedupe by message ID + const existingMessageIds = new Set( + existing.messages.map(msg => msg.originalGiftWrapId || msg.id) + ); + const newMessages = value.messages.filter(msg => + !existingMessageIds.has(msg.originalGiftWrapId || msg.id) + ); + + const mergedMessages = [...existing.messages, ...newMessages]; + mergedMessages.sort((a, b) => a.created_at - b.created_at); + + // Recalculate lastActivity and lastMessage after merging + const lastMessage = mergedMessages.length > 0 ? mergedMessages[mergedMessages.length - 1] : null; + const lastActivity = lastMessage ? lastMessage.created_at : existing.lastActivity; + + finalMap.set(key, { + ...existing, + messages: mergedMessages, + lastActivity, + lastMessage, + hasNIP4: existing.hasNIP4 || value.hasNIP4, + hasNIP17: existing.hasNIP17 || value.hasNIP17, + }); + } else { + finalMap.set(key, value); + } + }); + + return finalMap; + }); + }, []); + + // Add message to state + const addMessageToState = useCallback((message: DecryptedMessage, conversationPartner: string, protocol: MessageProtocol) => { + setMessages(prev => { + const newMap = new Map(prev); + const existing = newMap.get(conversationPartner); + + if (existing) { + // For NIP-17 messages with originalGiftWrapId, dedupe by gift wrap ID + // For NIP-04 and cached NIP-17 messages, dedupe by message ID + const messageId = message.originalGiftWrapId || message.id; + if (existing.messages.some(msg => (msg.originalGiftWrapId || msg.id) === messageId)) { + return prev; + } + + const optimisticIndex = existing.messages.findIndex(msg => + msg.isSending && + msg.pubkey === message.pubkey && + msg.decryptedContent === message.decryptedContent && + Math.abs(msg.created_at - message.created_at) <= 30 + ); + + let updatedMessages: DecryptedMessage[]; + if (optimisticIndex !== -1) { + const existingMessage = existing.messages[optimisticIndex]; + updatedMessages = [...existing.messages]; + updatedMessages[optimisticIndex] = { + ...message, + created_at: existingMessage.created_at, + clientFirstSeen: existingMessage.clientFirstSeen + }; + } else { + updatedMessages = [...existing.messages, message]; + } + + updatedMessages.sort((a, b) => a.created_at - b.created_at); + + const actualLastMessage = updatedMessages[updatedMessages.length - 1]; + + newMap.set(conversationPartner, { + ...existing, + messages: updatedMessages, + lastActivity: actualLastMessage.created_at, + lastMessage: actualLastMessage, + hasNIP4: protocol === MESSAGE_PROTOCOL.NIP04 ? true : existing.hasNIP4, + hasNIP17: protocol === MESSAGE_PROTOCOL.NIP17 ? true : existing.hasNIP17, + }); + } else { + const newConversation = { + messages: [message], + lastActivity: message.created_at, + lastMessage: message, + hasNIP4: protocol === MESSAGE_PROTOCOL.NIP04, + hasNIP17: protocol === MESSAGE_PROTOCOL.NIP17, + }; + + newMap.set(conversationPartner, newConversation); + } + + return newMap; + }); + }, []); + + // Process incoming NIP-4 message + const processIncomingNIP4Message = useCallback(async (event: NostrEvent) => { + if (!user?.pubkey) return; + + if (!validateDMEvent(event)) return; + + const isFromUser = event.pubkey === user.pubkey; + const recipientPTag = event.tags?.find(([name]) => name === 'p')?.[1]; + const otherPubkey = isFromUser ? recipientPTag : event.pubkey; + + if (!otherPubkey || otherPubkey === user.pubkey) return; + + const { decryptedContent, error } = await decryptNIP4Message(event, otherPubkey); + + const decryptedMessage: DecryptedMessage = { + ...event, + content: event.content, + decryptedContent: decryptedContent, + error: error, + }; + + const messageAge = Date.now() - (event.created_at * 1000); + if (messageAge < 5000) { + decryptedMessage.clientFirstSeen = Date.now(); + } + + addMessageToState(decryptedMessage, otherPubkey, MESSAGE_PROTOCOL.NIP04); + }, [user, decryptNIP4Message, addMessageToState]); + + // Process NIP-17 Gift Wrap + const processNIP17GiftWrap = useCallback(async (event: NostrEvent): Promise<NIP17ProcessingResult> => { + if (!user?.signer?.nip44) { + return { + processedMessage: { + ...event, + content: '', + decryptedContent: '', + error: 'No NIP-44 decryption available', + }, + conversationPartner: event.pubkey, + sealEvent: event, // Return the event itself as fallback + }; + } + + try { + // Decrypt using the ephemeral sender's pubkey (event.pubkey) + const sealContent = await user.signer.nip44.decrypt(event.pubkey, event.content); + const sealEvent = JSON.parse(sealContent) as NostrEvent; + + if (sealEvent.kind !== 13) { + console.log(`[DM] ⚠️ NIP-17 INVALID SEAL - expected kind 13, got ${sealEvent.kind}`, { + giftWrapId: event.id, + sealKind: sealEvent.kind, + }); + return { + processedMessage: { + ...event, + content: '', + decryptedContent: '', + error: `Invalid Seal format - expected kind 13, got ${sealEvent.kind}`, + }, + conversationPartner: event.pubkey, + sealEvent: event, // Return the gift wrap as fallback + }; + } + + const messageContent = await user.signer.nip44.decrypt(sealEvent.pubkey, sealEvent.content); + const messageEvent = JSON.parse(messageContent) as NostrEvent; + + // Accept both kind 14 (text) and kind 15 (files/attachments) + if (messageEvent.kind !== 14 && messageEvent.kind !== 15) { + console.log(`[DM] ⚠️ NIP-17 MESSAGE WITH UNSUPPORTED INNER EVENT KIND:`, { + giftWrapId: event.id, + innerKind: messageEvent.kind, + expectedKinds: [14, 15], + sealPubkey: sealEvent.pubkey, + messageEvent: messageEvent, + }); + return { + processedMessage: { + ...event, + content: '', + decryptedContent: '', + error: `Invalid message format - expected kind 14 or 15, got ${messageEvent.kind}`, + }, + conversationPartner: event.pubkey, + sealEvent, // Return the seal + }; + } + + let conversationPartner: string; + if (sealEvent.pubkey === user.pubkey) { + const recipient = messageEvent.tags.find(([name]) => name === 'p')?.[1]; + if (!recipient || recipient === user.pubkey) { + return { + processedMessage: { + ...event, + content: '', + decryptedContent: '', + error: 'Invalid recipient - malformed p tag', + }, + conversationPartner: event.pubkey, + sealEvent, // Return the seal + }; + } else { + conversationPartner = recipient; + } + } else { + conversationPartner = sealEvent.pubkey; + } + + return { + processedMessage: { + ...messageEvent, + id: messageEvent.id || `missing-nip17-inner-${messageEvent.created_at}-${messageEvent.pubkey.substring(0, 8)}-${messageEvent.content.substring(0, 16)}`, + decryptedContent: messageEvent.content, // Plaintext from inner message + }, + conversationPartner, + sealEvent, // Return the seal (kind 13) for storage + }; + } catch (error) { + console.error('[DM] Failed to process NIP-17 gift wrap:', { + giftWrapId: event.id, + error: error instanceof Error ? error.message : String(error), + }); + nip17ErrorLogger(error as Error); + return { + processedMessage: { + ...event, + content: '', + decryptedContent: '', + error: error instanceof Error ? error.message : 'Failed to decrypt or parse NIP-17 message', + }, + conversationPartner: event.pubkey, + sealEvent: event, // Return the gift wrap as fallback + }; + } + }, [user]); + + // Process incoming NIP-17 message + const processIncomingNIP17Message = useCallback(async (event: NostrEvent) => { + if (!user?.pubkey) return; + + if (event.kind !== 1059) return; + + try { + const { processedMessage, conversationPartner, sealEvent } = await processNIP17GiftWrap(event); + + // Check if decryption failed + if (processedMessage.error) { + console.error('[DM] NIP-17 message decryption failed:', { + giftWrapId: event.id, + error: processedMessage.error, + }); + nip17ErrorLogger(new Error(processedMessage.error)); + return; + } + + // Store the seal (kind 13) as-is + add decryptedEvent for inner message access + const messageWithAnimation: DecryptedMessage = { + ...sealEvent, // Seal fields (kind 13, seal pubkey, encrypted content, etc.) + created_at: processedMessage.created_at, // Use real timestamp from inner message + decryptedEvent: { + ...processedMessage, + content: processedMessage.decryptedContent, + } as NostrEvent, + decryptedContent: processedMessage.decryptedContent, + originalGiftWrapId: event.id, // Store gift wrap ID for deduplication + }; + + // Use real message timestamp for recency check + const messageAge = Date.now() - (processedMessage.created_at * 1000); + if (messageAge < 5000) { + messageWithAnimation.clientFirstSeen = Date.now(); + } + + addMessageToState(messageWithAnimation, conversationPartner, MESSAGE_PROTOCOL.NIP17); + } catch (error) { + console.error('[DM] Exception in processIncomingNIP17Message:', { + giftWrapId: event.id, + error: error instanceof Error ? error.message : String(error), + }); + nip17ErrorLogger(error as Error); + } + }, [user, processNIP17GiftWrap, addMessageToState]); + + // Start NIP-4 subscription + const startNIP4Subscription = useCallback(async (sinceTimestamp?: number) => { + if (!user?.pubkey || !nostr) return; + + if (nip4SubscriptionRef.current) { + nip4SubscriptionRef.current.close(); + } + + try { + let subscriptionSince = sinceTimestamp || Math.floor(Date.now() / 1000); + if (!sinceTimestamp && lastSync.nip4) { + subscriptionSince = lastSync.nip4 - DM_CONSTANTS.SUBSCRIPTION_OVERLAP_SECONDS; + } + + const filters = [ + { kinds: [4], '#p': [user.pubkey], since: subscriptionSince }, + { kinds: [4], authors: [user.pubkey], since: subscriptionSince } + ]; + + const subscription = nostr.req(filters); + let isActive = true; + + (async () => { + try { + for await (const msg of subscription) { + if (!isActive) break; + if (msg[0] === 'EVENT') { + await processIncomingNIP4Message(msg[2]); + } + } + } catch (error) { + if (isActive) { + console.error('[DM] NIP-4 subscription error:', error); + } + } + })(); + + nip4SubscriptionRef.current = { + close: () => { + isActive = false; + } + }; + + setSubscriptions(prev => ({ ...prev, isNIP4Connected: true })); + } catch (error) { + console.error('[DM] Failed to start NIP-4 subscription:', error); + setSubscriptions(prev => ({ ...prev, isNIP4Connected: false })); + } + }, [user, nostr, lastSync.nip4, processIncomingNIP4Message]); + + // Start NIP-17 subscription + const startNIP17Subscription = useCallback(async (sinceTimestamp?: number) => { + if (!user?.pubkey || !nostr || !enableNIP17) return; + + if (nip17SubscriptionRef.current) { + nip17SubscriptionRef.current.close(); + } + + try { + let subscriptionSince = sinceTimestamp || Math.floor(Date.now() / 1000); + if (!sinceTimestamp && lastSync.nip17) { + subscriptionSince = lastSync.nip17 - DM_CONSTANTS.SUBSCRIPTION_OVERLAP_SECONDS; + } + + // Adjust for NIP-17 timestamp fuzzing (±2 days) + // Subscribe from (lastSync - 2 days) to catch messages with randomized past timestamps + const TWO_DAYS_IN_SECONDS = 2 * 24 * 60 * 60; + subscriptionSince = subscriptionSince - TWO_DAYS_IN_SECONDS; + + const filters = [{ + kinds: [1059], + '#p': [user.pubkey], + since: subscriptionSince, + }]; + + const subscription = nostr.req(filters); + let isActive = true; + + (async () => { + try { + for await (const msg of subscription) { + if (!isActive) break; + if (msg[0] === 'EVENT') { + await processIncomingNIP17Message(msg[2]); + } + } + } catch (error) { + if (isActive) { + console.error('[DM] NIP-17 subscription error:', error); + } + } + })(); + + nip17SubscriptionRef.current = { + close: () => { + isActive = false; + } + }; + + setSubscriptions(prev => ({ ...prev, isNIP17Connected: true })); + } catch (error) { + console.error('[DM] Failed to start NIP-17 subscription:', error); + setSubscriptions(prev => ({ ...prev, isNIP17Connected: false })); + } + }, [user, nostr, lastSync.nip17, enableNIP17, processIncomingNIP17Message]); + + // Load all cached messages at once (both protocols) + const loadAllCachedMessages = useCallback(async (): Promise<{ nip4Since?: number; nip17Since?: number }> => { + if (!userPubkey) return {}; + + try { + const { readMessagesFromDB } = await import('@/lib/dmMessageStore'); + + const cachedStore = await readMessagesFromDB(userPubkey); + + if (!cachedStore || Object.keys(cachedStore.participants).length === 0) { + return {}; + } + + const filteredParticipants = enableNIP17 + ? cachedStore.participants + : Object.fromEntries( + Object.entries(cachedStore.participants).filter(([_, participant]) => !participant.hasNIP17) + ); + + const newState = new Map(); + + // Decrypt each message individually (they're stored in original encrypted form) + for (const [participantPubkey, participant] of Object.entries(filteredParticipants)) { + const processedMessages = await Promise.all(participant.messages.map(async (msg) => { + // Decrypt based on message kind + let decryptedContent: string | undefined; + let error: string | undefined; + + if (msg.kind === 4) { + // NIP-04 message + const otherPubkey = msg.pubkey === user?.pubkey + ? msg.tags.find(([name]) => name === 'p')?.[1] + : msg.pubkey; + + if (otherPubkey && user?.signer?.nip04) { + try { + decryptedContent = await user.signer.nip04.decrypt(otherPubkey, msg.content); + } catch { + error = 'Decryption failed'; + } + } + } else if (msg.kind === 13) { + // NIP-17 seal - decrypt to get the inner kind 14/15 event + if (user?.signer?.nip44) { + try { + const sealContent = await user.signer.nip44.decrypt(msg.pubkey, msg.content); + const decryptedEvent = JSON.parse(sealContent) as NostrEvent; + + // Keep seal structure but add decryptedEvent for access to inner fields + return { + ...msg, + decryptedEvent, // Full inner event (kind 14/15) + decryptedContent: decryptedEvent.content, // Plaintext message + } as NostrEvent & { decryptedEvent?: NostrEvent; decryptedContent?: string; error?: string }; + } catch { + error = 'Decryption failed'; + } + } + } + + return { + ...msg, + id: msg.id || `missing-${msg.kind}-${msg.created_at}-${msg.pubkey.substring(0, 8)}-${msg.content?.substring(0, 16) || 'nocontent'}`, + decryptedContent, + error, + } as NostrEvent & { decryptedContent?: string; error?: string }; + })); + + newState.set(participantPubkey, { + messages: processedMessages, + lastActivity: participant.lastActivity, + lastMessage: processedMessages.length > 0 ? processedMessages[processedMessages.length - 1] : null, + hasNIP4: participant.hasNIP4, + hasNIP17: participant.hasNIP17, + }); + } + + setMessages(newState); + if (cachedStore.lastSync) { + setLastSync(cachedStore.lastSync); + } + + return { + nip4Since: cachedStore.lastSync?.nip4 || undefined, + nip17Since: cachedStore.lastSync?.nip17 || undefined, + }; + } catch (error) { + console.error('[DM] Error loading cached messages:', error); + return {}; + } + }, [userPubkey, enableNIP17, user]); + + // Start message loading + const startMessageLoading = useCallback(async () => { + if (isLoading) return; + + setIsLoading(true); + setLoadingPhase(LOADING_PHASES.CACHE); + + try { + // ===== PHASE 1: Load cache and show immediately ===== + const { nip4Since, nip17Since } = await loadAllCachedMessages(); + + // Mark as completed BEFORE releasing isLoading to prevent re-trigger + setHasInitialLoadCompleted(true); + + // Show cached messages immediately! Don't wait for relays + setLoadingPhase(LOADING_PHASES.READY); + setIsLoading(false); + + // ===== PHASE 2: Query relays in background (non-blocking, parallel) ===== + setLoadingPhase(LOADING_PHASES.RELAYS); + + // Run NIP-04 and NIP-17 queries IN PARALLEL + const [nip4Result, nip17Result] = await Promise.all([ + queryRelaysForMessagesSince(MESSAGE_PROTOCOL.NIP04, nip4Since), + enableNIP17 ? queryRelaysForMessagesSince(MESSAGE_PROTOCOL.NIP17, nip17Since) : Promise.resolve({ lastMessageTimestamp: undefined, messageCount: 0 }) + ]); + + const totalNewMessages = nip4Result.messageCount + (nip17Result?.messageCount || 0); + if (totalNewMessages > 0) { + setShouldSaveImmediately(true); + } + + // ===== PHASE 3: Setup subscriptions ===== + setLoadingPhase(LOADING_PHASES.SUBSCRIPTIONS); + + await Promise.all([ + startNIP4Subscription(nip4Result.lastMessageTimestamp), + enableNIP17 ? startNIP17Subscription(nip17Result?.lastMessageTimestamp) : Promise.resolve() + ]); + + setLoadingPhase(LOADING_PHASES.READY); + } catch (error) { + console.error('[DM] Error in message loading:', error); + setHasInitialLoadCompleted(true); + setLoadingPhase(LOADING_PHASES.READY); + setIsLoading(false); + } + }, [loadAllCachedMessages, queryRelaysForMessagesSince, startNIP4Subscription, startNIP17Subscription, enableNIP17, isLoading]); + + // Clear cache and refetch from relays + const clearCacheAndRefetch = useCallback(async () => { + if (!enabled || !userPubkey) return; + + try { + // Close existing subscriptions + if (nip4SubscriptionRef.current) { + nip4SubscriptionRef.current.close(); + nip4SubscriptionRef.current = null; + } + if (nip17SubscriptionRef.current) { + nip17SubscriptionRef.current.close(); + nip17SubscriptionRef.current = null; + } + + // Clear IndexedDB cache + const { deleteMessagesFromDB } = await import('@/lib/dmMessageStore'); + await deleteMessagesFromDB(userPubkey); + + // Reset all state + setMessages(new Map()); + setLastSync({ nip4: null, nip17: null }); + setSubscriptions({ isNIP4Connected: false, isNIP17Connected: false }); + setScanProgress({ nip4: null, nip17: null }); + setLoadingPhase(LOADING_PHASES.IDLE); + + // Trigger reload by setting hasInitialLoadCompleted to false + setHasInitialLoadCompleted(false); + } catch (error) { + console.error('[DM] Error clearing cache:', error); + throw error; + } + }, [enabled, userPubkey]); + + // Main effect to load messages + useEffect(() => { + if (!enabled || !userPubkey || hasInitialLoadCompleted || isLoading) return; + startMessageLoading(); + }, [enabled, userPubkey, hasInitialLoadCompleted, isLoading, startMessageLoading]); + + // Cleanup effect + useEffect(() => { + if (!enabled) return; + + return () => { + if (nip4SubscriptionRef.current) { + nip4SubscriptionRef.current.close(); + nip4SubscriptionRef.current = null; + } + if (nip17SubscriptionRef.current) { + nip17SubscriptionRef.current.close(); + nip17SubscriptionRef.current = null; + } + }; + }, [enabled, userPubkey]); + + // Cleanup subscriptions + useEffect(() => { + if (!enabled) return; + + return () => { + if (nip4SubscriptionRef.current) { + nip4SubscriptionRef.current.close(); + } + if (nip17SubscriptionRef.current) { + nip17SubscriptionRef.current.close(); + } + if (debouncedWriteRef.current) { + clearTimeout(debouncedWriteRef.current); + } + setSubscriptions({ isNIP4Connected: false, isNIP17Connected: false }); + }; + }, [enabled]); + + // Detect relay changes and reload messages + useEffect(() => { + const relayChanged = JSON.stringify(previousRelayMetadata.current) !== JSON.stringify(appConfig.relayMetadata); + + previousRelayMetadata.current = appConfig.relayMetadata; + + if (relayChanged && enabled && userPubkey && hasInitialLoadCompleted) { + clearCacheAndRefetch(); + } + }, [enabled, userPubkey, appConfig.relayMetadata, hasInitialLoadCompleted, clearCacheAndRefetch]); + + // Detect hard refresh shortcut (Ctrl+Shift+R / Cmd+Shift+R) to clear cache + useEffect(() => { + if (!enabled || !userPubkey) return; + + const handleHardRefresh = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'R' || e.key === 'r')) { + try { + sessionStorage.setItem('dm-clear-cache-on-load', 'true'); + } catch (error) { + console.warn('[DM] SessionStorage unavailable, cache won\'t clear on hard refresh:', error); + } + } + }; + + window.addEventListener('keydown', handleHardRefresh); + return () => window.removeEventListener('keydown', handleHardRefresh); + }, [enabled, userPubkey]); + + // Clear cache after hard refresh + useEffect(() => { + if (!enabled || !userPubkey) return; + + try { + const shouldClearCache = sessionStorage.getItem('dm-clear-cache-on-load'); + if (shouldClearCache) { + sessionStorage.removeItem('dm-clear-cache-on-load'); + clearCacheAndRefetch(); + } + } catch (error) { + console.warn('[DM] Could not check sessionStorage for cache clear flag:', error); + } + }, [enabled, userPubkey, clearCacheAndRefetch]); + + // Conversations summary + const conversations = useMemo(() => { + const conversationsList: ConversationSummary[] = []; + + messages.forEach((participant, participantPubkey) => { + if (!participant.messages.length) return; + + const userHasSentMessage = participant.messages.some(msg => msg.pubkey === user?.pubkey); + const isKnown = userHasSentMessage; + const isRequest = !userHasSentMessage; + + const lastMessage = participant.messages[participant.messages.length - 1]; + const isFromUser = lastMessage.pubkey === user?.pubkey; + + conversationsList.push({ + id: participantPubkey, + pubkey: participantPubkey, + lastMessage: participant.lastMessage, + lastActivity: participant.lastActivity, + hasNIP4Messages: participant.hasNIP4, + hasNIP17Messages: participant.hasNIP17, + isKnown: isKnown, + isRequest: isRequest, + lastMessageFromUser: isFromUser, + }); + }); + + return conversationsList.sort((a, b) => b.lastActivity - a.lastActivity); + }, [messages, user?.pubkey]); + + // Write to store + const writeAllMessagesToStore = useCallback(async () => { + if (!userPubkey) return; + + try { + const { writeMessagesToDB } = await import('@/lib/dmMessageStore'); + + const messageStore = { + participants: {} as Record<string, { + messages: NostrEvent[]; + lastActivity: number; + hasNIP4: boolean; + hasNIP17: boolean; + }>, + lastSync: { + nip4: lastSync.nip4, + nip17: lastSync.nip17, + } + }; + + messages.forEach((participant, participantPubkey) => { + messageStore.participants[participantPubkey] = { + messages: participant.messages.map(msg => ({ + // Store messages in their ORIGINAL ENCRYPTED form + // Just strip the decrypted fields (decryptedContent, decryptedEvent) + // Keep originalGiftWrapId for NIP-17 deduplication on cache load + id: msg.id, + pubkey: msg.pubkey, + content: msg.content, // Encrypted content (NIP-04 or seal) + created_at: msg.created_at, + kind: msg.kind, // 4 for NIP-04, 13 for NIP-17 + tags: msg.tags, + sig: msg.sig, + ...(msg.originalGiftWrapId && { originalGiftWrapId: msg.originalGiftWrapId }), + } as NostrEvent)), + lastActivity: participant.lastActivity, + hasNIP4: participant.hasNIP4, + hasNIP17: participant.hasNIP17, + }; + }); + + await writeMessagesToDB(userPubkey, messageStore); + + const currentTime = Math.floor(Date.now() / 1000); + setLastSync(prev => ({ + nip4: prev.nip4 || currentTime, + nip17: prev.nip17 || currentTime + })); + } catch (error) { + console.error('[DM] Error writing messages to IndexedDB:', error); + } + }, [messages, userPubkey, lastSync]); + + // Trigger debounced write + const triggerDebouncedWrite = useCallback(() => { + if (debouncedWriteRef.current) { + clearTimeout(debouncedWriteRef.current); + } + debouncedWriteRef.current = setTimeout(() => { + writeAllMessagesToStore(); + debouncedWriteRef.current = null; + }, DM_CONSTANTS.DEBOUNCED_WRITE_DELAY); + }, [writeAllMessagesToStore]); + + // Watch messages and save + useEffect(() => { + if (!enabled || messages.size === 0) return; + + if (shouldSaveImmediately) { + setShouldSaveImmediately(false); + writeAllMessagesToStore(); + } else { + triggerDebouncedWrite(); + } + }, [enabled, messages, shouldSaveImmediately, writeAllMessagesToStore, triggerDebouncedWrite]); + + // Send message + const sendMessage = useCallback(async (params: { + recipientPubkey: string; + content: string; + protocol?: MessageProtocol; + attachments?: FileAttachment[]; + }) => { + if (!enabled) return; + + const { recipientPubkey, content, protocol = MESSAGE_PROTOCOL.NIP04, attachments } = params; + if (!userPubkey) return; + + const optimisticId = `optimistic-${Date.now()}-${Math.random()}`; + const optimisticMessage: DecryptedMessage = { + id: optimisticId, + kind: protocol === MESSAGE_PROTOCOL.NIP04 ? 4 : 14, // Use kind 14 for NIP-17 (the real message kind) + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), // Real timestamp + tags: [['p', recipientPubkey]], + content: '', + decryptedContent: content, + sig: '', + isSending: true, + clientFirstSeen: Date.now(), + }; + + addMessageToState(optimisticMessage, recipientPubkey, protocol === MESSAGE_PROTOCOL.NIP04 ? MESSAGE_PROTOCOL.NIP04 : MESSAGE_PROTOCOL.NIP17); + + try { + if (protocol === MESSAGE_PROTOCOL.NIP04) { + await sendNIP4Message.mutateAsync({ recipientPubkey, content, attachments }); + } else if (protocol === MESSAGE_PROTOCOL.NIP17) { + await sendNIP17Message.mutateAsync({ recipientPubkey, content, attachments }); + } + } catch (error) { + console.error(`[DM] Failed to send ${protocol} message:`, error); + } + }, [enabled, userPubkey, addMessageToState, sendNIP4Message, sendNIP17Message]); + + const isDoingInitialLoad = isLoading && (loadingPhase === LOADING_PHASES.CACHE || loadingPhase === LOADING_PHASES.RELAYS); + + const contextValue: DMContextType = { + messages, + isLoading, + loadingPhase, + isDoingInitialLoad, + lastSync, + conversations, + sendMessage, + protocolMode, + scanProgress, + subscriptions, + clearCacheAndRefetch, + }; + + return ( + <DMContext.Provider value={contextValue}> + {children} + </DMContext.Provider> + ); +} + diff --git a/src/components/EditProfileForm.tsx b/src/components/EditProfileForm.tsx new file mode 100644 index 0000000..1219ae9 --- /dev/null +++ b/src/components/EditProfileForm.tsx @@ -0,0 +1,349 @@ +import React, { useEffect, useRef } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { useToast } from '@/hooks/useToast'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { Loader2, Upload } from 'lucide-react'; +import { NSchema as n, type NostrMetadata } from '@nostrify/nostrify'; +import { useQueryClient } from '@tanstack/react-query'; +import { useUploadFile } from '@/hooks/useUploadFile'; + +export const EditProfileForm: React.FC = () => { + const queryClient = useQueryClient(); + + const { user, metadata } = useCurrentUser(); + const { mutateAsync: publishEvent, isPending } = useNostrPublish(); + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); + const { toast } = useToast(); + + // Initialize the form with default values + const form = useForm<NostrMetadata>({ + resolver: zodResolver(n.metadata()), + defaultValues: { + name: '', + about: '', + picture: '', + banner: '', + website: '', + nip05: '', + bot: false, + }, + }); + + // Update form values when user data is loaded + useEffect(() => { + if (metadata) { + form.reset({ + name: metadata.name || '', + about: metadata.about || '', + picture: metadata.picture || '', + banner: metadata.banner || '', + website: metadata.website || '', + nip05: metadata.nip05 || '', + bot: metadata.bot || false, + }); + } + }, [metadata, form]); + + // Handle file uploads for profile picture and banner + const uploadPicture = async (file: File, field: 'picture' | 'banner') => { + try { + // The first tuple in the array contains the URL + const [[_, url]] = await uploadFile(file); + form.setValue(field, url); + toast({ + title: 'Success', + description: `${field === 'picture' ? 'Profile picture' : 'Banner'} uploaded successfully`, + }); + } catch (error) { + console.error(`Failed to upload ${field}:`, error); + toast({ + title: 'Error', + description: `Failed to upload ${field === 'picture' ? 'profile picture' : 'banner'}. Please try again.`, + variant: 'destructive', + }); + } + }; + + const onSubmit = async (values: NostrMetadata) => { + if (!user) { + toast({ + title: 'Error', + description: 'You must be logged in to update your profile', + variant: 'destructive', + }); + return; + } + + try { + // Combine existing metadata with new values + const data = { ...metadata, ...values }; + + // Clean up empty values + for (const key in data) { + if (data[key] === '') { + delete data[key]; + } + } + + // Publish the metadata event (kind 0) + await publishEvent({ + kind: 0, + content: JSON.stringify(data), + }); + + // Invalidate queries to refresh the data + queryClient.invalidateQueries({ queryKey: ['logins'] }); + queryClient.invalidateQueries({ queryKey: ['author', user.pubkey] }); + + toast({ + title: 'Success', + description: 'Your profile has been updated', + }); + } catch (error) { + console.error('Failed to update profile:', error); + toast({ + title: 'Error', + description: 'Failed to update your profile. Please try again.', + variant: 'destructive', + }); + } + }; + + return ( + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Name</FormLabel> + <FormControl> + <Input placeholder="Your name" {...field} /> + </FormControl> + <FormDescription> + This is your display name that will be displayed to others. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="about" + render={({ field }) => ( + <FormItem> + <FormLabel>Bio</FormLabel> + <FormControl> + <Textarea + placeholder="Tell others about yourself" + className="resize-none" + {...field} + /> + </FormControl> + <FormDescription> + A short description about yourself. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <FormField + control={form.control} + name="picture" + render={({ field }) => ( + <ImageUploadField + field={field} + label="Profile Picture" + placeholder="https://example.com/profile.jpg" + description="URL to your profile picture. You can upload an image or provide a URL." + previewType="square" + onUpload={(file) => uploadPicture(file, 'picture')} + /> + )} + /> + + <FormField + control={form.control} + name="banner" + render={({ field }) => ( + <ImageUploadField + field={field} + label="Banner Image" + placeholder="https://example.com/banner.jpg" + description="URL to a wide banner image for your profile. You can upload an image or provide a URL." + previewType="wide" + onUpload={(file) => uploadPicture(file, 'banner')} + /> + )} + /> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <FormField + control={form.control} + name="website" + render={({ field }) => ( + <FormItem> + <FormLabel>Website</FormLabel> + <FormControl> + <Input placeholder="https://yourwebsite.com" {...field} /> + </FormControl> + <FormDescription> + Your personal website or social media link. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="nip05" + render={({ field }) => ( + <FormItem> + <FormLabel>NIP-05 Identifier</FormLabel> + <FormControl> + <Input placeholder="you@example.com" {...field} /> + </FormControl> + <FormDescription> + Your verified Nostr identifier. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <FormField + control={form.control} + name="bot" + render={({ field }) => ( + <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4"> + <div className="space-y-0.5"> + <FormLabel className="text-base">Bot Account</FormLabel> + <FormDescription> + Mark this account as automated or a bot. + </FormDescription> + </div> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + </FormItem> + )} + /> + + <Button + type="submit" + className="w-full md:w-auto" + disabled={isPending || isUploading} + > + {(isPending || isUploading) && ( + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + )} + Save Profile + </Button> + </form> + </Form> + ); +}; + +// Reusable component for image upload fields +interface ImageUploadFieldProps { + field: { + value: string | undefined; + onChange: (value: string) => void; + name: string; + onBlur: () => void; + }; + label: string; + placeholder: string; + description: string; + previewType: 'square' | 'wide'; + onUpload: (file: File) => void; +} + +const ImageUploadField: React.FC<ImageUploadFieldProps> = ({ + field, + label, + placeholder, + description, + previewType, + onUpload, +}) => { + const fileInputRef = useRef<HTMLInputElement>(null); + + return ( + <FormItem> + <FormLabel>{label}</FormLabel> + <div className="flex flex-col gap-2"> + <FormControl> + <Input + placeholder={placeholder} + name={field.name} + value={field.value ?? ''} + onChange={e => field.onChange(e.target.value)} + onBlur={field.onBlur} + /> + </FormControl> + <div className="flex items-center gap-2"> + <input + type="file" + ref={fileInputRef} + accept="image/*" + className="hidden" + onChange={(e) => { + const file = e.target.files?.[0]; + if (file) { + onUpload(file); + } + }} + /> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => fileInputRef.current?.click()} + > + <Upload className="h-4 w-4 mr-2" /> + Upload Image + </Button> + {field.value && ( + <div className={`h-10 ${previewType === 'square' ? 'w-10' : 'w-24'} rounded overflow-hidden`}> + <img + src={field.value} + alt={`${label} preview`} + className="h-full w-full object-cover" + /> + </div> + )} + </div> + </div> + <FormDescription> + {description} + </FormDescription> + <FormMessage /> + </FormItem> + ); +}; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..ffa1f37 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,113 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; +} + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; +} + + + +export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> { + return { + hasError: true, + error, + }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Error caught by ErrorBoundary:', error, errorInfo); + + this.setState({ + error, + errorInfo, + }); + } + + handleReset = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( + <div className="min-h-screen bg-background flex items-center justify-center p-4"> + <div className="max-w-md w-full space-y-4"> + <div className="text-center"> + <h2 className="text-2xl font-bold text-foreground mb-2"> + Something went wrong + </h2> + <p className="text-muted-foreground"> + An unexpected error occurred. The error has been reported. + </p> + </div> + + <div className="bg-muted p-4 rounded-lg"> + <details className="text-sm"> + <summary className="cursor-pointer font-medium text-foreground"> + Error details + </summary> + <div className="mt-2 space-y-2"> + <div> + <strong className="text-foreground">Message:</strong> + <p className="text-muted-foreground mt-1"> + {this.state.error?.message} + </p> + </div> + {this.state.error?.stack && ( + <div> + <strong className="text-foreground">Stack trace:</strong> + <pre className="text-xs text-muted-foreground mt-1 overflow-auto max-h-32"> + {this.state.error.stack} + </pre> + </div> + )} + </div> + </details> + </div> + + <div className="flex gap-2"> + <button + onClick={this.handleReset} + className="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors" + > + Try again + </button> + <button + onClick={() => window.location.reload()} + className="flex-1 px-4 py-2 bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/90 transition-colors" + > + Reload page + </button> + </div> + </div> + </div> + ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/src/components/NostrProvider.tsx b/src/components/NostrProvider.tsx new file mode 100644 index 0000000..2b8a7ef --- /dev/null +++ b/src/components/NostrProvider.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useRef } from 'react'; +import { NostrEvent, NostrFilter, NPool, NRelay1 } from '@nostrify/nostrify'; +import { NostrContext } from '@nostrify/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAppContext } from '@/hooks/useAppContext'; + +interface NostrProviderProps { + children: React.ReactNode; +} + +const NostrProvider: React.FC<NostrProviderProps> = (props) => { + const { children } = props; + const { config } = useAppContext(); + + const queryClient = useQueryClient(); + + // Create NPool instance only once + const pool = useRef<NPool | undefined>(undefined); + + // Use refs so the pool always has the latest data + const relayMetadata = useRef(config.relayMetadata); + + // Invalidate Nostr queries when relay metadata changes + useEffect(() => { + relayMetadata.current = config.relayMetadata; + queryClient.invalidateQueries({ queryKey: ['nostr'] }); + }, [config.relayMetadata, queryClient]); + + // Initialize NPool only once + if (!pool.current) { + pool.current = new NPool({ + open(url: string) { + return new NRelay1(url); + }, + reqRouter(filters: NostrFilter[]) { + const routes = new Map<string, NostrFilter[]>(); + + // Route to all read relays + const readRelays = relayMetadata.current.relays + .filter(r => r.read) + .map(r => r.url); + + for (const url of readRelays) { + routes.set(url, filters); + } + + return routes; + }, + eventRouter(_event: NostrEvent) { + // Get write relays from metadata + const writeRelays = relayMetadata.current.relays + .filter(r => r.write) + .map(r => r.url); + + const allRelays = new Set<string>(writeRelays); + + return [...allRelays]; + }, + }); + } + + return ( + <NostrContext.Provider value={{ nostr: pool.current }}> + {children} + </NostrContext.Provider> + ); +}; + +export default NostrProvider; \ No newline at end of file diff --git a/src/components/NostrSync.tsx b/src/components/NostrSync.tsx new file mode 100644 index 0000000..2ebf856 --- /dev/null +++ b/src/components/NostrSync.tsx @@ -0,0 +1,62 @@ +import { useEffect } from 'react'; +import { useNostr } from '@nostrify/react'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAppContext } from '@/hooks/useAppContext'; + +/** + * NostrSync - Syncs user's Nostr data + * + * This component runs globally to sync various Nostr data when the user logs in. + * Currently syncs: + * - NIP-65 relay list (kind 10002) + */ +export function NostrSync() { + const { nostr } = useNostr(); + const { user } = useCurrentUser(); + const { config, updateConfig } = useAppContext(); + + useEffect(() => { + if (!user) return; + + const syncRelaysFromNostr = async () => { + try { + const events = await nostr.query( + [{ kinds: [10002], authors: [user.pubkey], limit: 1 }], + { signal: AbortSignal.timeout(5000) } + ); + + if (events.length > 0) { + const event = events[0]; + + // Only update if the event is newer than our stored data + if (event.created_at > config.relayMetadata.updatedAt) { + const fetchedRelays = event.tags + .filter(([name]) => name === 'r') + .map(([_, url, marker]) => ({ + url, + read: !marker || marker === 'read', + write: !marker || marker === 'write', + })); + + if (fetchedRelays.length > 0) { + console.log('Syncing relay list from Nostr:', fetchedRelays); + updateConfig((current) => ({ + ...current, + relayMetadata: { + relays: fetchedRelays, + updatedAt: event.created_at, + }, + })); + } + } + } + } catch (error) { + console.error('Failed to sync relays from Nostr:', error); + } + }; + + syncRelaysFromNostr(); + }, [user, config.relayMetadata.updatedAt, nostr, updateConfig]); + + return null; +} \ No newline at end of file diff --git a/src/components/NoteContent.test.tsx b/src/components/NoteContent.test.tsx new file mode 100644 index 0000000..a3244d1 --- /dev/null +++ b/src/components/NoteContent.test.tsx @@ -0,0 +1,136 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TestApp } from '@/test/TestApp'; +import { NoteContent } from './NoteContent'; +import type { NostrEvent } from '@nostrify/nostrify'; + +describe('NoteContent', () => { + it('linkifies URLs in kind 1 events', () => { + const event: NostrEvent = { + id: 'test-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1, + tags: [], + content: 'Check out this link: https://example.com', + sig: 'test-sig', + }; + + render( + <TestApp> + <NoteContent event={event} /> + </TestApp> + ); + + const link = screen.getByRole('link', { name: 'https://example.com' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('target', '_blank'); + }); + + it('linkifies URLs in kind 1111 events (comments)', () => { + const event: NostrEvent = { + id: 'test-comment-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1111, + tags: [ + ['a', '30040:pubkey:identifier'], + ['k', '30040'], + ['p', 'pubkey'], + ], + content: 'I think the log events should be different kind numbers instead of having a `log-type` tag. That way you can use normal Nostr filters to filter the log types. Also, the `note` type should just b a kind 1111: https://nostrbook.dev/kinds/1111', + sig: 'test-sig', + }; + + render( + <TestApp> + <NoteContent event={event} /> + </TestApp> + ); + + const link = screen.getByRole('link', { name: 'https://nostrbook.dev/kinds/1111' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://nostrbook.dev/kinds/1111'); + expect(link).toHaveAttribute('target', '_blank'); + }); + + it('handles text without URLs correctly', () => { + const event: NostrEvent = { + id: 'test-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1111, + tags: [], + content: 'This is just plain text without any links.', + sig: 'test-sig', + }; + + render( + <TestApp> + <NoteContent event={event} /> + </TestApp> + ); + + expect(screen.getByText('This is just plain text without any links.')).toBeInTheDocument(); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + }); + + it('renders hashtags as links', () => { + const event: NostrEvent = { + id: 'test-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1, + tags: [], + content: 'This is a post about #nostr and #bitcoin development.', + sig: 'test-sig', + }; + + render( + <TestApp> + <NoteContent event={event} /> + </TestApp> + ); + + const nostrHashtag = screen.getByRole('link', { name: '#nostr' }); + const bitcoinHashtag = screen.getByRole('link', { name: '#bitcoin' }); + + expect(nostrHashtag).toBeInTheDocument(); + expect(bitcoinHashtag).toBeInTheDocument(); + expect(nostrHashtag).toHaveAttribute('href', '/t/nostr'); + expect(bitcoinHashtag).toHaveAttribute('href', '/t/bitcoin'); + }); + + it('generates deterministic names for users without metadata and styles them differently', () => { + // Use a valid npub for testing + const event: NostrEvent = { + id: 'test-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1, + tags: [], + content: `Mentioning nostr:npub1zg69v7ys40x77y352eufp27daufrg4ncjz4ummcjx3t83y9tehhsqepuh0`, + sig: 'test-sig', + }; + + render( + <TestApp> + <NoteContent event={event} /> + </TestApp> + ); + + // The mention should be rendered with a deterministic name + const mention = screen.getByRole('link'); + expect(mention).toBeInTheDocument(); + + // Should have muted styling for generated names (gray instead of blue) + expect(mention).toHaveClass('text-gray-500'); + expect(mention).not.toHaveClass('text-blue-500'); + + // The text should start with @ and contain a generated name (not a truncated npub) + const linkText = mention.textContent; + expect(linkText).not.toMatch(/^@npub1/); // Should not be a truncated npub + expect(linkText).toEqual("@Swift Falcon"); + }); +}); \ No newline at end of file diff --git a/src/components/NoteContent.tsx b/src/components/NoteContent.tsx new file mode 100644 index 0000000..a7a7c30 --- /dev/null +++ b/src/components/NoteContent.tsx @@ -0,0 +1,137 @@ +import { useMemo } from 'react'; +import { type NostrEvent } from '@nostrify/nostrify'; +import { Link } from 'react-router-dom'; +import { nip19 } from 'nostr-tools'; +import { useAuthor } from '@/hooks/useAuthor'; +import { genUserName } from '@/lib/genUserName'; +import { cn } from '@/lib/utils'; + +interface NoteContentProps { + event: NostrEvent; + className?: string; +} + +/** Parses content of text note events so that URLs and hashtags are linkified. */ +export function NoteContent({ + event, + className, +}: NoteContentProps) { + // Process the content to render mentions, links, etc. + const content = useMemo(() => { + const text = event.content; + + // Regex to find URLs, Nostr references, and hashtags + const regex = /(https?:\/\/[^\s]+)|nostr:(npub1|note1|nprofile1|nevent1)([023456789acdefghjklmnpqrstuvwxyz]+)|(#\w+)/g; + + const parts: React.ReactNode[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + let keyCounter = 0; + + while ((match = regex.exec(text)) !== null) { + const [fullMatch, url, nostrPrefix, nostrData, hashtag] = match; + const index = match.index; + + // Add text before this match + if (index > lastIndex) { + parts.push(text.substring(lastIndex, index)); + } + + if (url) { + // Handle URLs + parts.push( + <a + key={`url-${keyCounter++}`} + href={url} + target="_blank" + rel="noopener noreferrer" + className="text-blue-500 hover:underline" + > + {url} + </a> + ); + } else if (nostrPrefix && nostrData) { + // Handle Nostr references + try { + const nostrId = `${nostrPrefix}${nostrData}`; + const decoded = nip19.decode(nostrId); + + if (decoded.type === 'npub') { + const pubkey = decoded.data; + parts.push( + <NostrMention key={`mention-${keyCounter++}`} pubkey={pubkey} /> + ); + } else { + // For other types, just show as a link + parts.push( + <Link + key={`nostr-${keyCounter++}`} + to={`/${nostrId}`} + className="text-blue-500 hover:underline" + > + {fullMatch} + </Link> + ); + } + } catch { + // If decoding fails, just render as text + parts.push(fullMatch); + } + } else if (hashtag) { + // Handle hashtags + const tag = hashtag.slice(1); // Remove the # + parts.push( + <Link + key={`hashtag-${keyCounter++}`} + to={`/t/${tag}`} + className="text-blue-500 hover:underline" + > + {hashtag} + </Link> + ); + } + + lastIndex = index + fullMatch.length; + } + + // Add any remaining text + if (lastIndex < text.length) { + parts.push(text.substring(lastIndex)); + } + + // If no special content was found, just use the plain text + if (parts.length === 0) { + parts.push(text); + } + + return parts; + }, [event]); + + return ( + <div className={cn("whitespace-pre-wrap break-words", className)}> + {content.length > 0 ? content : event.content} + </div> + ); +} + +// Helper component to display user mentions +function NostrMention({ pubkey }: { pubkey: string }) { + const author = useAuthor(pubkey); + const npub = nip19.npubEncode(pubkey); + const hasRealName = !!author.data?.metadata?.name; + const displayName = author.data?.metadata?.name ?? genUserName(pubkey); + + return ( + <Link + to={`/${npub}`} + className={cn( + "font-medium hover:underline", + hasRealName + ? "text-blue-500" + : "text-gray-500 hover:text-gray-700" + )} + > + @{displayName} + </Link> + ); +} \ No newline at end of file diff --git a/src/components/RelayListManager.tsx b/src/components/RelayListManager.tsx new file mode 100644 index 0000000..2ae7b6a --- /dev/null +++ b/src/components/RelayListManager.tsx @@ -0,0 +1,286 @@ +import { useState, useEffect } from 'react'; +import { Plus, X, Wifi, Settings } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { useAppContext } from '@/hooks/useAppContext'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { useToast } from '@/hooks/useToast'; + +interface Relay { + url: string; + read: boolean; + write: boolean; +} + +export function RelayListManager() { + const { config, updateConfig } = useAppContext(); + const { user } = useCurrentUser(); + const { mutate: publishEvent } = useNostrPublish(); + const { toast } = useToast(); + + const [relays, setRelays] = useState<Relay[]>(config.relayMetadata.relays); + const [newRelayUrl, setNewRelayUrl] = useState(''); + + // Sync local state with config when it changes (e.g., from NostrProvider sync) + useEffect(() => { + setRelays(config.relayMetadata.relays); + }, [config.relayMetadata.relays]); + + const normalizeRelayUrl = (url: string): string => { + url = url.trim(); + try { + return new URL(url).toString(); + } catch { + try { + return new URL(`wss://${url}`).toString(); + } catch { + return url; + } + } + }; + + const isValidRelayUrl = (url: string): boolean => { + const trimmed = url.trim(); + if (!trimmed) return false; + + const normalized = normalizeRelayUrl(trimmed); + try { + new URL(normalized); + return true; + } catch { + return false; + } + }; + + const handleAddRelay = () => { + if (!isValidRelayUrl(newRelayUrl)) { + toast({ + title: 'Invalid relay URL', + description: 'Please enter a valid relay URL (e.g., wss://relay.example.com)', + variant: 'destructive', + }); + return; + } + + const normalized = normalizeRelayUrl(newRelayUrl); + + if (relays.some(r => r.url === normalized)) { + toast({ + title: 'Relay already exists', + description: 'This relay is already in your list.', + variant: 'destructive', + }); + return; + } + + const newRelays = [...relays, { url: normalized, read: true, write: true }]; + setRelays(newRelays); + setNewRelayUrl(''); + + saveRelays(newRelays); + }; + + const handleRemoveRelay = (url: string) => { + const newRelays = relays.filter(r => r.url !== url); + setRelays(newRelays); + saveRelays(newRelays); + }; + + const handleToggleRead = (url: string) => { + const newRelays = relays.map(r => + r.url === url ? { ...r, read: !r.read } : r + ); + setRelays(newRelays); + saveRelays(newRelays); + }; + + const handleToggleWrite = (url: string) => { + const newRelays = relays.map(r => + r.url === url ? { ...r, write: !r.write } : r + ); + setRelays(newRelays); + saveRelays(newRelays); + }; + + const saveRelays = (newRelays: Relay[]) => { + const now = Math.floor(Date.now() / 1000); + + // Update local config + updateConfig((current) => ({ + ...current, + relayMetadata: { + relays: newRelays, + updatedAt: now, + }, + })); + + // Publish to Nostr if user is logged in + if (user) { + publishNIP65RelayList(newRelays); + } + }; + + const publishNIP65RelayList = (relayList: Relay[]) => { + const tags = relayList.map(relay => { + if (relay.read && relay.write) { + return ['r', relay.url]; + } else if (relay.read) { + return ['r', relay.url, 'read']; + } else if (relay.write) { + return ['r', relay.url, 'write']; + } + // If neither read nor write, don't include (shouldn't happen) + return null; + }).filter((tag): tag is string[] => tag !== null); + + publishEvent( + { + kind: 10002, + content: '', + tags, + }, + { + onSuccess: () => { + toast({ + title: 'Relay list published', + description: 'Your relay list has been published to Nostr.', + }); + }, + onError: (error) => { + console.error('Failed to publish relay list:', error); + toast({ + title: 'Failed to publish relay list', + description: 'There was an error publishing your relay list to Nostr.', + variant: 'destructive', + }); + }, + } + ); + }; + + const renderRelayUrl = (url: string): string => { + try { + const parsed = new URL(url); + if (parsed.protocol === 'wss:') { + if (parsed.pathname === '/') { + return parsed.host; + } else { + return parsed.host + parsed.pathname; + } + } else { + return parsed.href; + } + } catch { + return url; + } + } + + return ( + <div className="space-y-4"> + {/* Relay List */} + <div className="space-y-2"> + {relays.map((relay) => ( + <div + key={relay.url} + className="flex items-center gap-3 p-3 rounded-md border bg-muted/20" + > + <Wifi className="h-4 w-4 text-muted-foreground shrink-0" /> + <span className="font-mono text-sm flex-1 truncate" title={relay.url}> + {renderRelayUrl(relay.url)} + </span> + + {/* Settings Popover */} + <Popover> + <PopoverTrigger asChild> + <Button + variant="ghost" + size="icon" + className="size-5 text-muted-foreground hover:text-foreground shrink-0" + > + <Settings className="h-4 w-4" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-48" align="end"> + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <Label htmlFor={`read-${relay.url}`} className="text-sm cursor-pointer"> + Read + </Label> + <Switch + id={`read-${relay.url}`} + checked={relay.read} + onCheckedChange={() => handleToggleRead(relay.url)} + className="data-[state=checked]:bg-green-500 scale-75" + /> + </div> + <div className="flex items-center justify-between"> + <Label htmlFor={`write-${relay.url}`} className="text-sm cursor-pointer"> + Write + </Label> + <Switch + id={`write-${relay.url}`} + checked={relay.write} + onCheckedChange={() => handleToggleWrite(relay.url)} + className="data-[state=checked]:bg-blue-500 scale-75" + /> + </div> + </div> + </PopoverContent> + </Popover> + + {/* Remove Button */} + <Button + variant="ghost" + size="icon" + onClick={() => handleRemoveRelay(relay.url)} + className="size-5 text-muted-foreground hover:text-destructive hover:bg-transparent shrink-0" + disabled={relays.length <= 1} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + + {/* Add Relay Form */} + <div className="flex gap-2"> + <div className="flex-1"> + <Label htmlFor="new-relay-url" className="sr-only"> + Relay URL + </Label> + <Input + id="new-relay-url" + placeholder="Enter relay URL (e.g., wss://relay.example.com)" + value={newRelayUrl} + onChange={(e) => setNewRelayUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleAddRelay(); + } + }} + /> + </div> + <Button + onClick={handleAddRelay} + disabled={!newRelayUrl.trim()} + variant="outline" + size="sm" + className="h-10 shrink-0" + > + <Plus className="h-4 w-4 mr-2" /> + Add Relay + </Button> + </div> + + {!user && ( + <p className="text-xs text-muted-foreground"> + Log in to sync your relay list with Nostr + </p> + )} + </div> + ); +} \ No newline at end of file diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..5e38302 --- /dev/null +++ b/src/components/ScrollToTop.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +export function ScrollToTop() { + const { pathname } = useLocation(); + + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + + return null; +} \ No newline at end of file diff --git a/src/components/WalletModal.tsx b/src/components/WalletModal.tsx new file mode 100644 index 0000000..280052c --- /dev/null +++ b/src/components/WalletModal.tsx @@ -0,0 +1,393 @@ +import { useState, forwardRef } from 'react'; +import { Wallet, Plus, Trash2, Zap, Globe, WalletMinimal, CheckCircle, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, + DrawerClose, +} from '@/components/ui/drawer'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { useNWC } from '@/hooks/useNWCContext'; +import { useWallet } from '@/hooks/useWallet'; +import { useToast } from '@/hooks/useToast'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import type { NWCConnection, NWCInfo } from '@/hooks/useNWC'; +import type { WebLNProvider } from "@webbtc/webln-types"; + +interface WalletModalProps { + children?: React.ReactNode; + className?: string; +} + +// Extracted AddWalletContent to prevent re-renders +const AddWalletContent = forwardRef<HTMLDivElement, { + alias: string; + setAlias: (value: string) => void; + connectionUri: string; + setConnectionUri: (value: string) => void; +}>(({ alias, setAlias, connectionUri, setConnectionUri }, ref) => ( + <div className="space-y-4 px-4" ref={ref}> + <div> + <Label htmlFor="alias">Wallet Name (optional)</Label> + <Input + id="alias" + placeholder="My Lightning Wallet" + value={alias} + onChange={(e) => setAlias(e.target.value)} + /> + </div> + <div> + <Label htmlFor="connection-uri">Connection URI</Label> + <Textarea + id="connection-uri" + placeholder="nostr+walletconnect://..." + value={connectionUri} + onChange={(e) => setConnectionUri(e.target.value)} + rows={3} + /> + </div> + </div> +)); +AddWalletContent.displayName = 'AddWalletContent'; + +// Extracted WalletContent to prevent re-renders +const WalletContent = forwardRef<HTMLDivElement, { + webln: WebLNProvider | null; + hasNWC: boolean; + connections: NWCConnection[]; + connectionInfo: Record<string, NWCInfo>; + activeConnection: string | null; + handleSetActive: (cs: string) => void; + handleRemoveConnection: (cs: string) => void; + setAddDialogOpen: (open: boolean) => void; +}>(({ + webln, + hasNWC, + connections, + connectionInfo, + activeConnection, + handleSetActive, + handleRemoveConnection, + setAddDialogOpen +}, ref) => ( + <div className="space-y-6 px-4 pb-4" ref={ref}> + {/* Current Status */} + <div className="space-y-3"> + <h3 className="font-medium">Current Status</h3> + <div className="grid gap-3"> + {/* WebLN */} + <div className="flex items-center justify-between p-3 border rounded-lg"> + <div className="flex items-center gap-3"> + <Globe className="h-4 w-4 text-muted-foreground" /> + <div> + <p className="text-sm font-medium">WebLN</p> + <p className="text-xs text-muted-foreground">Browser extension</p> + </div> + </div> + <div className="flex items-center gap-2"> + {webln && <CheckCircle className="h-4 w-4 text-green-600" />} + <Badge variant={webln ? "default" : "secondary"} className="text-xs"> + {webln ? "Ready" : "Not Found"} + </Badge> + </div> + </div> + {/* NWC */} + <div className="flex items-center justify-between p-3 border rounded-lg"> + <div className="flex items-center gap-3"> + <WalletMinimal className="h-4 w-4 text-muted-foreground" /> + <div> + <p className="text-sm font-medium">Nostr Wallet Connect</p> + <p className="text-xs text-muted-foreground"> + {connections.length > 0 + ? `${connections.length} wallet${connections.length !== 1 ? 's' : ''} connected` + : "Remote wallet connection" + } + </p> + </div> + </div> + <div className="flex items-center gap-2"> + {hasNWC && <CheckCircle className="h-4 w-4 text-green-600" />} + <Badge variant={hasNWC ? "default" : "secondary"} className="text-xs"> + {hasNWC ? "Ready" : "None"} + </Badge> + </div> + </div> + </div> + </div> + <Separator /> + {/* NWC Management */} + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h3 className="font-medium">Nostr Wallet Connect</h3> + <Button size="sm" variant="outline" onClick={() => setAddDialogOpen(true)}> + <Plus className="h-4 w-4 mr-1" /> + Add + </Button> + </div> + {/* Connected Wallets List */} + {connections.length === 0 ? ( + <div className="text-center py-6 text-muted-foreground"> + <p className="text-sm">No wallets connected</p> + </div> + ) : ( + <div className="space-y-2"> + {connections.map((connection) => { + const info = connectionInfo[connection.connectionString]; + const isActive = activeConnection === connection.connectionString; + return ( + <div key={connection.connectionString} className={`flex items-center justify-between p-3 border rounded-lg ${isActive ? 'ring-2 ring-primary' : ''}`}> + <div className="flex items-center gap-3"> + <WalletMinimal className="h-4 w-4 text-muted-foreground" /> + <div> + <p className="text-sm font-medium"> + {connection.alias || info?.alias || 'Lightning Wallet'} + </p> + <p className="text-xs text-muted-foreground"> + NWC Connection + </p> + </div> + </div> + <div className="flex items-center gap-2"> + {isActive && <CheckCircle className="h-4 w-4 text-green-600" />} + {!isActive && ( + <Button + size="sm" + variant="ghost" + onClick={() => handleSetActive(connection.connectionString)} + > + <Zap className="h-3 w-3" /> + </Button> + )} + <Button + size="sm" + variant="ghost" + onClick={() => handleRemoveConnection(connection.connectionString)} + > + <Trash2 className="h-3 w-3" /> + </Button> + </div> + </div> + ); + })} + </div> + )} + </div> + {/* Help */} + {!webln && connections.length === 0 && ( + <> + <Separator /> + <div className="text-center py-4 space-y-2"> + <p className="text-sm text-muted-foreground"> + Install a WebLN extension or connect a NWC wallet for zaps. + </p> + </div> + </> + )} + </div> +)); +WalletContent.displayName = 'WalletContent'; + +export function WalletModal({ children, className }: WalletModalProps) { + const [open, setOpen] = useState(false); + const [addDialogOpen, setAddDialogOpen] = useState(false); + const [connectionUri, setConnectionUri] = useState(''); + const [alias, setAlias] = useState(''); + const [isConnecting, setIsConnecting] = useState(false); + const isMobile = useIsMobile(); + + const { + connections, + activeConnection, + connectionInfo, + addConnection, + removeConnection, + setActiveConnection + } = useNWC(); + + const { webln } = useWallet(); + + const hasNWC = connections.length > 0 && connections.some(c => c.isConnected); + const { toast } = useToast(); + + const handleAddConnection = async () => { + if (!connectionUri.trim()) { + toast({ + title: 'Connection URI required', + description: 'Please enter a valid NWC connection URI.', + variant: 'destructive', + }); + return; + } + + setIsConnecting(true); + try { + const success = await addConnection(connectionUri.trim(), alias.trim() || undefined); + if (success) { + setConnectionUri(''); + setAlias(''); + setAddDialogOpen(false); + } + } finally { + setIsConnecting(false); + } + }; + + const handleRemoveConnection = (connectionString: string) => { + removeConnection(connectionString); + }; + + const handleSetActive = (connectionString: string) => { + setActiveConnection(connectionString); + toast({ + title: 'Active wallet changed', + description: 'The selected wallet is now active for zaps.', + }); + }; + + const walletContentProps = { + webln, + hasNWC, + connections, + connectionInfo, + activeConnection, + handleSetActive, + handleRemoveConnection, + setAddDialogOpen, + }; + + const addWalletDialog = ( + <Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}> + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle>Connect NWC Wallet</DialogTitle> + <DialogDescription> + Enter your connection string from a compatible wallet. + </DialogDescription> + </DialogHeader> + <AddWalletContent + alias={alias} + setAlias={setAlias} + connectionUri={connectionUri} + setConnectionUri={setConnectionUri} + /> + <DialogFooter className="px-4"> + <Button + onClick={handleAddConnection} + disabled={isConnecting || !connectionUri.trim()} + className="w-full" + > + {isConnecting ? 'Connecting...' : 'Connect'} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); + + if (isMobile) { + return ( + <> + <Drawer open={open} onOpenChange={setOpen}> + <DrawerTrigger asChild> + {children || ( + <Button variant="outline" size="sm" className={className}> + <Wallet className="h-4 w-4 mr-2" /> + Wallet Settings + </Button> + )} + </DrawerTrigger> + <DrawerContent className="h-full"> + <DrawerHeader className="text-center relative"> + <DrawerClose asChild> + <Button variant="ghost" size="sm" className="absolute right-4 top-4"> + <X className="h-4 w-4" /> + <span className="sr-only">Close</span> + </Button> + </DrawerClose> + <DrawerTitle className="flex items-center justify-center gap-2 pt-2"> + <Wallet className="h-5 w-5" /> + Lightning Wallet + </DrawerTitle> + <DrawerDescription> + Connect your lightning wallet to send zaps instantly. + </DrawerDescription> + </DrawerHeader> + <div className="overflow-y-auto"> + <WalletContent {...walletContentProps} /> + </div> + </DrawerContent> + </Drawer> + {/* Render Add Wallet as a separate Drawer for mobile */} + <Drawer open={addDialogOpen} onOpenChange={setAddDialogOpen}> + <DrawerContent> + <DrawerHeader> + <DrawerTitle>Connect NWC Wallet</DrawerTitle> + <DrawerDescription> + Enter your connection string from a compatible wallet. + </DrawerDescription> + </DrawerHeader> + <AddWalletContent + alias={alias} + setAlias={setAlias} + connectionUri={connectionUri} + setConnectionUri={setConnectionUri} + /> + <div className="p-4"> + <Button + onClick={handleAddConnection} + disabled={isConnecting || !connectionUri.trim()} + className="w-full" + > + {isConnecting ? 'Connecting...' : 'Connect'} + </Button> + </div> + </DrawerContent> + </Drawer> + </> + ); + } + + return ( + <> + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild> + {children || ( + <Button variant="outline" size="sm" className={className}> + <Wallet className="h-4 w-4 mr-2" /> + Wallet Settings + </Button> + )} + </DialogTrigger> + <DialogContent className="sm:max-w-[500px] max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Wallet className="h-5 w-5" /> + Lightning Wallet + </DialogTitle> + <DialogDescription> + Connect your lightning wallet to send zaps instantly. + </DialogDescription> + </DialogHeader> + <WalletContent {...walletContentProps} /> + </DialogContent> + </Dialog> + {addWalletDialog} + </> + ); +} \ No newline at end of file diff --git a/src/components/ZapButton.tsx b/src/components/ZapButton.tsx new file mode 100644 index 0000000..bb3bbd8 --- /dev/null +++ b/src/components/ZapButton.tsx @@ -0,0 +1,58 @@ +import { ZapDialog } from '@/components/ZapDialog'; +import { useZaps } from '@/hooks/useZaps'; +import { useWallet } from '@/hooks/useWallet'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAuthor } from '@/hooks/useAuthor'; +import { Zap } from 'lucide-react'; +import type { Event } from 'nostr-tools'; + +interface ZapButtonProps { + target: Event; + className?: string; + showCount?: boolean; + zapData?: { count: number; totalSats: number; isLoading?: boolean }; +} + +export function ZapButton({ + target, + className = "text-xs ml-1", + showCount = true, + zapData: externalZapData +}: ZapButtonProps) { + const { user } = useCurrentUser(); + const { data: author } = useAuthor(target?.pubkey || ''); + const { webln, activeNWC } = useWallet(); + + // Only fetch data if not provided externally + const { totalSats: fetchedTotalSats, isLoading } = useZaps( + externalZapData ? [] : target ?? [], // Empty array prevents fetching if external data provided + webln, + activeNWC + ); + + // Don't show zap button if user is not logged in, is the author, or author has no lightning address + if (!user || !target || user.pubkey === target.pubkey || (!author?.metadata?.lud16 && !author?.metadata?.lud06)) { + return null; + } + + // Use external data if provided, otherwise use fetched data + const totalSats = externalZapData?.totalSats ?? fetchedTotalSats; + const showLoading = externalZapData?.isLoading || isLoading; + + return ( + <ZapDialog target={target}> + <div className={`flex items-center gap-1 ${className}`}> + <Zap className="h-4 w-4" /> + <span className="text-xs"> + {showLoading ? ( + '...' + ) : showCount && totalSats > 0 ? ( + `${totalSats.toLocaleString()}` + ) : ( + 'Zap' + )} + </span> + </div> + </ZapDialog> + ); +} \ No newline at end of file diff --git a/src/components/ZapDialog.tsx b/src/components/ZapDialog.tsx new file mode 100644 index 0000000..c50bf20 --- /dev/null +++ b/src/components/ZapDialog.tsx @@ -0,0 +1,463 @@ +import { useState, useEffect, useRef, forwardRef } from 'react'; +import { Zap, Copy, Check, ExternalLink, Sparkle, Sparkles, Star, Rocket, ArrowLeft, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, + DrawerClose, +} from '@/components/ui/drawer'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAuthor } from '@/hooks/useAuthor'; +import { useToast } from '@/hooks/useToast'; +import { useZaps } from '@/hooks/useZaps'; +import { useWallet } from '@/hooks/useWallet'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import type { Event } from 'nostr-tools'; +import QRCode from 'qrcode'; +import type { WebLNProvider } from "@webbtc/webln-types"; + +interface ZapDialogProps { + target: Event; + children?: React.ReactNode; + className?: string; +} + +const presetAmounts = [ + { amount: 1, icon: Sparkle }, + { amount: 50, icon: Sparkles }, + { amount: 100, icon: Zap }, + { amount: 250, icon: Star }, + { amount: 1000, icon: Rocket }, +]; + +interface ZapContentProps { + invoice: string | null; + amount: number | string; + comment: string; + isZapping: boolean; + qrCodeUrl: string; + copied: boolean; + webln: WebLNProvider | null; + handleZap: () => void; + handleCopy: () => void; + openInWallet: () => void; + setAmount: (amount: number | string) => void; + setComment: (comment: string) => void; + inputRef: React.RefObject<HTMLInputElement>; + zap: (amount: number, comment: string) => void; +} + +// Moved ZapContent outside of ZapDialog to prevent re-renders causing focus loss +const ZapContent = forwardRef<HTMLDivElement, ZapContentProps>(({ + invoice, + amount, + comment, + isZapping, + qrCodeUrl, + copied, + webln, + handleZap, + handleCopy, + openInWallet, + setAmount, + setComment, + inputRef, + zap, +}, ref) => ( + <div ref={ref}> + {invoice ? ( + <div className="flex flex-col h-full min-h-0"> + {/* Payment amount display */} + <div className="text-center pt-4"> + <div className="text-2xl font-bold">{amount} sats</div> + </div> + + <Separator className="my-4" /> + + <div className="flex flex-col justify-center min-h-0 flex-1 px-2"> + {/* QR Code */} + <div className="flex justify-center"> + <Card className="p-3 [@media(max-height:680px)]:max-w-[65vw] max-w-[95vw] mx-auto"> + <CardContent className="p-0 flex justify-center"> + {qrCodeUrl ? ( + <img + src={qrCodeUrl} + alt="Lightning Invoice QR Code" + className="w-full h-auto aspect-square max-w-full object-contain" + /> + ) : ( + <div className="w-full aspect-square bg-muted animate-pulse rounded" /> + )} + </CardContent> + </Card> + </div> + + {/* Invoice input */} + <div className="space-y-2 mt-4"> + <Label htmlFor="invoice">Lightning Invoice</Label> + <div className="flex gap-2 min-w-0"> + <Input + id="invoice" + value={invoice} + readOnly + className="font-mono text-xs min-w-0 flex-1 overflow-hidden text-ellipsis" + onClick={(e) => e.currentTarget.select()} + /> + <Button + variant="outline" + size="icon" + onClick={handleCopy} + className="shrink-0" + > + {copied ? ( + <Check className="h-4 w-4 text-green-600" /> + ) : ( + <Copy className="h-4 w-4" /> + )} + </Button> + </div> + </div> + + {/* Payment buttons */} + <div className="space-y-3 mt-4"> + {webln && ( + <Button + onClick={() => { + const finalAmount = typeof amount === 'string' ? parseInt(amount, 10) : amount; + zap(finalAmount, comment); + }} + disabled={isZapping} + className="w-full" + size="lg" + > + <Zap className="h-4 w-4 mr-2" /> + {isZapping ? "Processing..." : "Pay with WebLN"} + </Button> + )} + + <Button + variant="outline" + onClick={openInWallet} + className="w-full" + size="lg" + > + <ExternalLink className="h-4 w-4 mr-2" /> + Open in Lightning Wallet + </Button> + + <div className="text-xs sm:text-[.65rem] text-muted-foreground text-center"> + Scan the QR code or copy the invoice to pay with any Lightning wallet. + </div> + </div> + </div> + </div> + ) : ( + <> + <div className="grid gap-3 px-4 py-4 w-full overflow-hidden"> + <ToggleGroup + type="single" + value={String(amount)} + onValueChange={(value) => { + if (value) { + setAmount(parseInt(value, 10)); + } + }} + className="grid grid-cols-5 gap-1 w-full" + > + {presetAmounts.map(({ amount: presetAmount, icon: Icon }) => ( + <ToggleGroupItem + key={presetAmount} + value={String(presetAmount)} + className="flex flex-col h-auto min-w-0 text-xs px-1 py-2" + > + <Icon className="h-4 w-4 mb-1" /> + <span className="truncate">{presetAmount}</span> + </ToggleGroupItem> + ))} + </ToggleGroup> + <div className="flex items-center gap-2"> + <div className="h-px flex-1 bg-muted" /> + <span className="text-xs text-muted-foreground">OR</span> + <div className="h-px flex-1 bg-muted" /> + </div> + <Input + ref={inputRef} + id="custom-amount" + type="number" + placeholder="Custom amount" + value={amount} + onChange={(e) => setAmount(e.target.value)} + className="w-full text-sm" + /> + <Textarea + id="custom-comment" + placeholder="Add a comment (optional)" + value={comment} + onChange={(e) => setComment(e.target.value)} + className="w-full resize-none text-sm" + rows={2} + /> + </div> + <div className="px-4 pb-4"> + <Button onClick={handleZap} className="w-full" disabled={isZapping} size="default"> + {isZapping ? ( + 'Creating invoice...' + ) : ( + <> + <Zap className="h-4 w-4 mr-2" /> + Zap {amount} sats + </> + )} + </Button> + </div> + </> + )} + </div> +)); +ZapContent.displayName = 'ZapContent'; + +export function ZapDialog({ target, children, className }: ZapDialogProps) { + const [open, setOpen] = useState(false); + const { user } = useCurrentUser(); + const { data: author } = useAuthor(target.pubkey); + const { toast } = useToast(); + const { webln, activeNWC } = useWallet(); + const { zap, isZapping, invoice, setInvoice } = useZaps(target, webln, activeNWC, () => setOpen(false)); + const [amount, setAmount] = useState<number | string>(100); + const [comment, setComment] = useState<string>(''); + const [copied, setCopied] = useState(false); + const [qrCodeUrl, setQrCodeUrl] = useState<string>(''); + const inputRef = useRef<HTMLInputElement>(null); + const isMobile = useIsMobile(); + + useEffect(() => { + if (target) { + setComment('Zapped with MKStack!'); + } + }, [target]); + + // Generate QR code + useEffect(() => { + let isCancelled = false; + + const generateQR = async () => { + if (!invoice) { + setQrCodeUrl(''); + return; + } + + try { + const url = await QRCode.toDataURL(invoice.toUpperCase(), { + width: 512, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF', + }, + }); + + if (!isCancelled) { + setQrCodeUrl(url); + } + } catch (err) { + if (!isCancelled) { + console.error('Failed to generate QR code:', err); + } + } + }; + + generateQR(); + + return () => { + isCancelled = true; + }; + }, [invoice]); + + const handleCopy = async () => { + if (invoice) { + await navigator.clipboard.writeText(invoice); + setCopied(true); + toast({ + title: 'Invoice copied', + description: 'Lightning invoice copied to clipboard', + }); + setTimeout(() => setCopied(false), 2000); + } + }; + + const openInWallet = () => { + if (invoice) { + const lightningUrl = `lightning:${invoice}`; + window.open(lightningUrl, '_blank'); + } + }; + + useEffect(() => { + if (open) { + setAmount(100); + setInvoice(null); + setCopied(false); + setQrCodeUrl(''); + } else { + // Clean up state when dialog closes + setAmount(100); + setInvoice(null); + setCopied(false); + setQrCodeUrl(''); + } + }, [open, setInvoice]); + + const handleZap = () => { + const finalAmount = typeof amount === 'string' ? parseInt(amount, 10) : amount; + zap(finalAmount, comment); + }; + + const contentProps = { + invoice, + amount, + comment, + isZapping, + qrCodeUrl, + copied, + webln, + handleZap, + handleCopy, + openInWallet, + setAmount, + setComment, + inputRef, + zap, + }; + + if (!user || user.pubkey === target.pubkey || !author?.metadata?.lud06 && !author?.metadata?.lud16) { + return null; + } + + if (isMobile) { + // Use drawer for entire mobile flow, make it full-screen when showing invoice + return ( + <Drawer + open={open} + onOpenChange={(newOpen) => { + // Reset invoice when closing + if (!newOpen) { + setInvoice(null); + setQrCodeUrl(''); + } + setOpen(newOpen); + }} + dismissible={true} // Always allow dismissal via drag + snapPoints={invoice ? [0.5, 0.75, 0.98] : [0.98]} + activeSnapPoint={invoice ? 0.98 : 0.98} + modal={true} + shouldScaleBackground={false} + fadeFromIndex={0} + > + <DrawerTrigger asChild> + <div className={`cursor-pointer ${className || ''}`}> + {children} + </div> + </DrawerTrigger> + <DrawerContent + key={invoice ? 'payment' : 'form'} + className={cn( + "transition-all duration-300", + invoice ? "h-full max-h-screen" : "max-h-[98vh]" + )} + data-testid="zap-modal" + > + <DrawerHeader className="text-center relative"> + {/* Back button when showing invoice */} + {invoice && ( + <Button + variant="ghost" + size="sm" + onClick={() => { + setInvoice(null); + setQrCodeUrl(''); + }} + className="absolute left-4 top-4 flex items-center gap-2" + > + <ArrowLeft className="h-4 w-4" /> + </Button> + )} + + {/* Close button */} + <DrawerClose asChild> + <Button + variant="ghost" + size="sm" + className="absolute right-4 top-4" + > + <X className="h-4 w-4" /> + <span className="sr-only">Close</span> + </Button> + </DrawerClose> + + <DrawerTitle className="text-lg break-words pt-2"> + {invoice ? 'Lightning Payment' : 'Send a Zap'} + </DrawerTitle> + <DrawerDescription className="text-sm break-words text-center"> + {invoice ? ( + 'Pay with Bitcoin Lightning Network' + ) : ( + 'Zaps are small Bitcoin payments that support the creator of this item. If you enjoyed this, consider sending a zap!' + )} + </DrawerDescription> + </DrawerHeader> + <div className="flex-1 overflow-y-auto px-4 pb-4"> + <ZapContent {...contentProps} /> + </div> + </DrawerContent> + </Drawer> + ); + } + + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild> + <div className={`cursor-pointer ${className || ''}`}> + {children} + </div> + </DialogTrigger> + <DialogContent className="sm:max-w-[425px] max-h-[95vh] overflow-hidden" data-testid="zap-modal"> + <DialogHeader> + <DialogTitle className="text-lg break-words"> + {invoice ? 'Lightning Payment' : 'Send a Zap'} + </DialogTitle> + <DialogDescription className="text-sm text-center break-words"> + {invoice ? ( + 'Pay with Bitcoin Lightning Network' + ) : ( + <> + Zaps are small Bitcoin payments that support the creator of this item. If you enjoyed this, consider sending a zap! + </> + )} + </DialogDescription> + </DialogHeader> + <div className="overflow-y-auto"> + <ZapContent {...contentProps} /> + </div> + </DialogContent> + </Dialog> + ); +} diff --git a/src/components/auth/AccountSwitcher.tsx b/src/components/auth/AccountSwitcher.tsx new file mode 100644 index 0000000..afc655d --- /dev/null +++ b/src/components/auth/AccountSwitcher.tsx @@ -0,0 +1,89 @@ +// NOTE: This file is stable and usually should not be modified. +// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. + +import { ChevronDown, LogOut, UserIcon, UserPlus, Wallet } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu.tsx'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.tsx'; +import { WalletModal } from '@/components/WalletModal'; +import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts'; +import { genUserName } from '@/lib/genUserName'; + +interface AccountSwitcherProps { + onAddAccountClick: () => void; +} + +export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) { + const { currentUser, otherUsers, setLogin, removeLogin } = useLoggedInAccounts(); + + if (!currentUser) return null; + + const getDisplayName = (account: Account): string => { + return account.metadata.name ?? genUserName(account.pubkey); + } + + return ( + <DropdownMenu modal={false}> + <DropdownMenuTrigger asChild> + <button className='flex items-center gap-3 p-3 rounded-full hover:bg-accent transition-all w-full text-foreground'> + <Avatar className='w-10 h-10'> + <AvatarImage src={currentUser.metadata.picture} alt={getDisplayName(currentUser)} /> + <AvatarFallback>{getDisplayName(currentUser).charAt(0)}</AvatarFallback> + </Avatar> + <div className='flex-1 text-left hidden md:block truncate'> + <p className='font-medium text-sm truncate'>{getDisplayName(currentUser)}</p> + </div> + <ChevronDown className='w-4 h-4 text-muted-foreground' /> + </button> + </DropdownMenuTrigger> + <DropdownMenuContent className='w-56 p-2 animate-scale-in'> + <div className='font-medium text-sm px-2 py-1.5'>Switch Account</div> + {otherUsers.map((user) => ( + <DropdownMenuItem + key={user.id} + onClick={() => setLogin(user.id)} + className='flex items-center gap-2 cursor-pointer p-2 rounded-md' + > + <Avatar className='w-8 h-8'> + <AvatarImage src={user.metadata.picture} alt={getDisplayName(user)} /> + <AvatarFallback>{getDisplayName(user)?.charAt(0) || <UserIcon />}</AvatarFallback> + </Avatar> + <div className='flex-1 truncate'> + <p className='text-sm font-medium'>{getDisplayName(user)}</p> + </div> + {user.id === currentUser.id && <div className='w-2 h-2 rounded-full bg-primary'></div>} + </DropdownMenuItem> + ))} + <DropdownMenuSeparator /> + <WalletModal> + <DropdownMenuItem + className='flex items-center gap-2 cursor-pointer p-2 rounded-md' + onSelect={(e) => e.preventDefault()} + > + <Wallet className='w-4 h-4' /> + <span>Wallet Settings</span> + </DropdownMenuItem> + </WalletModal> + <DropdownMenuItem + onClick={onAddAccountClick} + className='flex items-center gap-2 cursor-pointer p-2 rounded-md' + > + <UserPlus className='w-4 h-4' /> + <span>Add another account</span> + </DropdownMenuItem> + <DropdownMenuItem + onClick={() => removeLogin(currentUser.id)} + className='flex items-center gap-2 cursor-pointer p-2 rounded-md text-red-500' + > + <LogOut className='w-4 h-4' /> + <span>Log out</span> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ); +} \ No newline at end of file diff --git a/src/components/auth/LoginArea.tsx b/src/components/auth/LoginArea.tsx new file mode 100644 index 0000000..f84414a --- /dev/null +++ b/src/components/auth/LoginArea.tsx @@ -0,0 +1,63 @@ +// NOTE: This file is stable and usually should not be modified. +// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. + +import { useState } from 'react'; +import { User, UserPlus } from 'lucide-react'; +import { Button } from '@/components/ui/button.tsx'; +import LoginDialog from './LoginDialog'; +import SignupDialog from './SignupDialog'; +import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts'; +import { AccountSwitcher } from './AccountSwitcher'; +import { cn } from '@/lib/utils'; + +export interface LoginAreaProps { + className?: string; +} + +export function LoginArea({ className }: LoginAreaProps) { + const { currentUser } = useLoggedInAccounts(); + const [loginDialogOpen, setLoginDialogOpen] = useState(false); + const [signupDialogOpen, setSignupDialogOpen] = useState(false); + + const handleLogin = () => { + setLoginDialogOpen(false); + setSignupDialogOpen(false); + }; + + return ( + <div className={cn("inline-flex items-center justify-center", className)}> + {currentUser ? ( + <AccountSwitcher onAddAccountClick={() => setLoginDialogOpen(true)} /> + ) : ( + <div className="flex gap-3 justify-center"> + <Button + onClick={() => setLoginDialogOpen(true)} + className='flex items-center gap-2 px-4 py-2 rounded-full bg-primary text-primary-foreground w-full font-medium transition-all hover:bg-primary/90 animate-scale-in' + > + <User className='w-4 h-4' /> + <span className='truncate'>Log in</span> + </Button><Button + onClick={() => setSignupDialogOpen(true)} + variant="outline" + className="flex items-center gap-2 px-4 py-2 rounded-full font-medium transition-all" + > + <UserPlus className="w-4 h-4" /> + <span>Sign Up</span> + </Button> + </div> + )} + + <LoginDialog + isOpen={loginDialogOpen} + onClose={() => setLoginDialogOpen(false)} + onLogin={handleLogin} + onSignup={() => setSignupDialogOpen(true)} + /> + + <SignupDialog + isOpen={signupDialogOpen} + onClose={() => setSignupDialogOpen(false)} + /> + </div> + ); +} \ No newline at end of file diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx new file mode 100644 index 0000000..ff40ec0 --- /dev/null +++ b/src/components/auth/LoginDialog.tsx @@ -0,0 +1,376 @@ +// NOTE: This file is stable and usually should not be modified. +// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. + +import React, { useRef, useState, useEffect } from 'react'; +import { Shield, Upload, AlertTriangle, UserPlus, KeyRound, Sparkles, Cloud } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Dialog, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { useLoginActions } from '@/hooks/useLoginActions'; +import { cn } from '@/lib/utils'; + +interface LoginDialogProps { + isOpen: boolean; + onClose: () => void; + onLogin: () => void; + onSignup?: () => void; +} + +const validateNsec = (nsec: string) => { + return /^nsec1[a-zA-Z0-9]{58}$/.test(nsec); +}; + +const validateBunkerUri = (uri: string) => { + return uri.startsWith('bunker://'); +}; + +const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onSignup }) => { + const [isLoading, setIsLoading] = useState(false); + const [isFileLoading, setIsFileLoading] = useState(false); + const [nsec, setNsec] = useState(''); + const [bunkerUri, setBunkerUri] = useState(''); + const [errors, setErrors] = useState<{ + nsec?: string; + bunker?: string; + file?: string; + extension?: string; + }>({}); + const fileInputRef = useRef<HTMLInputElement>(null); + const login = useLoginActions(); + + // Reset all state when dialog opens/closes + useEffect(() => { + if (isOpen) { + // Reset state when dialog opens + setIsLoading(false); + setIsFileLoading(false); + setNsec(''); + setBunkerUri(''); + setErrors({}); + // Reset file input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }, [isOpen]); + + const handleExtensionLogin = async () => { + setIsLoading(true); + setErrors(prev => ({ ...prev, extension: undefined })); + + try { + if (!('nostr' in window)) { + throw new Error('Nostr extension not found. Please install a NIP-07 extension.'); + } + await login.extension(); + onLogin(); + onClose(); + } catch (e: unknown) { + const error = e as Error; + console.error('Bunker login failed:', error); + console.error('Nsec login failed:', error); + console.error('Extension login failed:', error); + setErrors(prev => ({ + ...prev, + extension: error instanceof Error ? error.message : 'Extension login failed' + })); + } finally { + setIsLoading(false); + } + }; + + const executeLogin = (key: string) => { + setIsLoading(true); + setErrors({}); + + // Use a timeout to allow the UI to update before the synchronous login call + setTimeout(() => { + try { + login.nsec(key); + onLogin(); + onClose(); + } catch { + setErrors({ nsec: "Failed to login with this key. Please check that it's correct." }); + setIsLoading(false); + } + }, 50); + }; + + const handleKeyLogin = () => { + if (!nsec.trim()) { + setErrors(prev => ({ ...prev, nsec: 'Please enter your secret key' })); + return; + } + + if (!validateNsec(nsec)) { + setErrors(prev => ({ ...prev, nsec: 'Invalid secret key format. Must be a valid nsec starting with nsec1.' })); + return; + } + executeLogin(nsec); + }; + + const handleBunkerLogin = async () => { + if (!bunkerUri.trim()) { + setErrors(prev => ({ ...prev, bunker: 'Please enter a bunker URI' })); + return; + } + + if (!validateBunkerUri(bunkerUri)) { + setErrors(prev => ({ ...prev, bunker: 'Invalid bunker URI format. Must start with bunker://' })); + return; + } + + setIsLoading(true); + setErrors(prev => ({ ...prev, bunker: undefined })); + + try { + await login.bunker(bunkerUri); + onLogin(); + onClose(); + // Clear the URI from memory + setBunkerUri(''); + } catch { + setErrors(prev => ({ + ...prev, + bunker: 'Failed to connect to bunker. Please check the URI.' + })); + } finally { + setIsLoading(false); + } + }; + + const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { + const file = e.target.files?.[0]; + if (!file) return; + + setIsFileLoading(true); + setErrors({}); + + const reader = new FileReader(); + reader.onload = (event) => { + setIsFileLoading(false); + const content = event.target?.result as string; + if (content) { + const trimmedContent = content.trim(); + if (validateNsec(trimmedContent)) { + executeLogin(trimmedContent); + } else { + setErrors({ file: 'File does not contain a valid secret key.' }); + } + } else { + setErrors({ file: 'Could not read file content.' }); + } + }; + reader.onerror = () => { + setIsFileLoading(false); + setErrors({ file: 'Failed to read file.' }); + }; + reader.readAsText(file); + }; + + const handleSignupClick = () => { + onClose(); + if (onSignup) { + onSignup(); + } + }; + + const defaultTab = 'nostr' in window ? 'extension' : 'key'; + + return ( + <Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent + className={cn("max-w-[95vw] sm:max-w-md max-h-[90vh] max-h-[90dvh] p-0 overflow-hidden rounded-2xl overflow-y-scroll")} + > + <DialogHeader className={cn('px-6 pt-6 pb-1 relative')}> + + <DialogDescription className="text-center"> + Sign up or log in to continue + </DialogDescription> + </DialogHeader> + <div className='px-6 pt-2 pb-4 space-y-4 overflow-y-auto flex-1'> + {/* Prominent Sign Up Section */} + <div className='relative p-4 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-950/50 dark:to-indigo-950/50 border border-blue-200 dark:border-blue-800 overflow-hidden'> + <div className='relative z-10 text-center space-y-3'> + <div className='flex justify-center items-center gap-2 mb-2'> + <Sparkles className='w-5 h-5 text-blue-600' /> + <span className='font-semibold text-blue-800 dark:text-blue-200'> + New to Nostr? + </span> + </div> + <p className='text-sm text-blue-700 dark:text-blue-300'> + Create a new account to get started. It's free and open. + </p> + <Button + onClick={handleSignupClick} + className='w-full rounded-full py-3 text-base font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transform transition-all duration-200 hover:scale-105 shadow-lg border-0' + > + <UserPlus className='w-4 h-4 mr-2' /> + <span>Sign Up</span> + </Button> + </div> + </div> + + {/* Divider */} + <div className='relative'> + <div className='absolute inset-0 flex items-center'> + <div className='w-full border-t border-gray-300 dark:border-gray-600'></div> + </div> + <div className='relative flex justify-center text-sm'> + <span className='px-3 bg-background text-muted-foreground'> + <span>Or log in</span> + </span> + </div> + </div> + + {/* Login Methods */} + <Tabs defaultValue={defaultTab} className="w-full"> + <TabsList className="grid w-full grid-cols-3 bg-muted/80 rounded-lg mb-4"> + <TabsTrigger value="extension" className="flex items-center gap-2"> + <Shield className="w-4 h-4" /> + <span>Extension</span> + </TabsTrigger> + <TabsTrigger value="key" className="flex items-center gap-2"> + <KeyRound className="w-4 h-4" /> + <span>Key</span> + </TabsTrigger> + <TabsTrigger value="bunker" className="flex items-center gap-2"> + <Cloud className="w-4 h-4" /> + <span>Bunker</span> + </TabsTrigger> + </TabsList> + <TabsContent value='extension' className='space-y-3 bg-muted'> + {errors.extension && ( + <Alert variant="destructive"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription>{errors.extension}</AlertDescription> + </Alert> + )} + <div className='text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800'> + <Shield className='w-12 h-12 mx-auto mb-3 text-primary' /> + <p className='text-sm text-gray-600 dark:text-gray-300 mb-4'> + Login with one click using the browser extension + </p> + <div className="flex justify-center"> + <Button + className='w-full rounded-full py-4' + onClick={handleExtensionLogin} + disabled={isLoading} + > + {isLoading ? 'Logging in...' : 'Login with Extension'} + </Button> + </div> + </div> + </TabsContent> + + <TabsContent value='key' className='space-y-4'> + <div className='space-y-4'> + <div className='space-y-2'> + <label htmlFor='nsec' className='text-sm font-medium'> + Secret Key (nsec) + </label> + <Input + id='nsec' + type="password" + value={nsec} + onChange={(e) => { + setNsec(e.target.value); + if (errors.nsec) setErrors(prev => ({ ...prev, nsec: undefined })); + }} + className={`rounded-lg ${ + errors.nsec ? 'border-red-500 focus-visible:ring-red-500' : '' + }`} + placeholder='nsec1...' + autoComplete="off" + /> + {errors.nsec && ( + <p className="text-sm text-red-500">{errors.nsec}</p> + )} + </div> + + <Button + className='w-full rounded-full py-3' + onClick={handleKeyLogin} + disabled={isLoading || !nsec.trim()} + > + {isLoading ? 'Verifying...' : 'Log In'} + </Button> + + <div className='relative'> + <div className='absolute inset-0 flex items-center'> + <div className='w-full border-t border-muted'></div> + </div> + <div className='relative flex justify-center text-xs'> + <span className='px-2 bg-background text-muted-foreground'> + or + </span> + </div> + </div> + + <div className='text-center'> + <input + type='file' + accept='.txt' + className='hidden' + ref={fileInputRef} + onChange={handleFileUpload} + /> + <Button + variant='outline' + className='w-full' + onClick={() => fileInputRef.current?.click()} + disabled={isLoading || isFileLoading} + > + <Upload className='w-4 h-4 mr-2' /> + {isFileLoading ? 'Reading File...' : 'Upload Your Key File'} + </Button> + {errors.file && ( + <p className="text-sm text-red-500 mt-2">{errors.file}</p> + )} + </div> + </div> + </TabsContent> + + <TabsContent value='bunker' className='space-y-3 bg-muted'> + <div className='space-y-2'> + <label htmlFor='bunkerUri' className='text-sm font-medium text-gray-700 dark:text-gray-400'> + Bunker URI + </label> + <Input + id='bunkerUri' + value={bunkerUri} + onChange={(e) => { + setBunkerUri(e.target.value); + if (errors.bunker) setErrors(prev => ({ ...prev, bunker: undefined })); + }} + className={`rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary ${ + errors.bunker ? 'border-red-500' : '' + }`} + placeholder='bunker://' + autoComplete="off" + /> + {errors.bunker && ( + <p className="text-sm text-red-500">{errors.bunker}</p> + )} + </div> + + <div className="flex justify-center"> + <Button + className='w-full rounded-full py-4' + onClick={handleBunkerLogin} + disabled={isLoading || !bunkerUri.trim()} + > + {isLoading ? 'Connecting...' : 'Login with Bunker'} + </Button> + </div> + </TabsContent> + </Tabs> + </div> + </DialogContent> + </Dialog> + ); + }; + +export default LoginDialog; diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx new file mode 100644 index 0000000..b80f333 --- /dev/null +++ b/src/components/auth/SignupDialog.tsx @@ -0,0 +1,708 @@ +// NOTE: This file is stable and usually should not be modified. +// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. + +import React, { useState, useEffect, useRef } from 'react'; +import { Download, Key, UserPlus, FileText, Shield, User, Sparkles, LogIn, CheckCircle, Upload, Globe } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Card, CardContent } from '@/components/ui/card'; +import { toast } from '@/hooks/useToast'; +import { useLoginActions } from '@/hooks/useLoginActions'; +import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { useUploadFile } from '@/hooks/useUploadFile'; +import { generateSecretKey, nip19 } from 'nostr-tools'; +import { cn } from '@/lib/utils'; + +interface SignupDialogProps { + isOpen: boolean; + onClose: () => void; + onComplete?: () => void; +} + +const sanitizeFilename = (filename: string) => { + return filename.replace(/[^a-z0-9_.-]/gi, '_'); +} + +const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete }) => { + const [step, setStep] = useState<'welcome' | 'generate' | 'download' | 'profile' | 'done'>('welcome'); + const [isLoading, setIsLoading] = useState(false); + const [nsec, setNsec] = useState(''); + const [showSparkles, setShowSparkles] = useState(false); + const [keySecured, setKeySecured] = useState<'none' | 'downloaded'>('none'); + const [profileData, setProfileData] = useState({ + name: '', + about: '', + picture: '' + }); + const login = useLoginActions(); + const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish(); + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); + const avatarFileInputRef = useRef<HTMLInputElement>(null); + + // Generate a proper nsec key using nostr-tools + const generateKey = () => { + setIsLoading(true); + setShowSparkles(true); + + // Add a dramatic pause for the key generation effect + setTimeout(() => { + try { + // Generate a new secret key + const sk = generateSecretKey(); + + // Convert to nsec format + setNsec(nip19.nsecEncode(sk)); + setStep('download'); + + toast({ + title: 'Your Secret Key is Ready!', + description: 'A new secret key has been generated for you.', + }); + } catch { + toast({ + title: 'Error', + description: 'Failed to generate key. Please try again.', + variant: 'destructive', + }); + } finally { + setIsLoading(false); + setShowSparkles(false); + } + }, 2000); + }; + + const downloadKey = () => { + try { + // Create a blob with the key text + const blob = new Blob([nsec], { type: 'text/plain; charset=utf-8' }); + const url = globalThis.URL.createObjectURL(blob); + + // Sanitize filename + const filename = sanitizeFilename('nostr-nsec-key.txt'); + + // Create a temporary link element and trigger download + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + + // Clean up immediately + globalThis.URL.revokeObjectURL(url); + document.body.removeChild(a); + + // Mark as secured + setKeySecured('downloaded'); + + toast({ + title: 'Secret Key Saved!', + description: 'Your key has been safely stored.', + }); + } catch { + toast({ + title: 'Download failed', + description: 'Could not download the key file. Please copy it manually.', + variant: 'destructive', + }); + } + }; + + + + const finishKeySetup = () => { + try { + login.nsec(nsec); + setStep('profile'); + } catch { + toast({ + title: 'Login Failed', + description: 'Failed to login with the generated key. Please try again.', + variant: 'destructive', + }); + } + }; + + const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Reset file input + e.target.value = ''; + + // Validate file type + if (!file.type.startsWith('image/')) { + toast({ + title: 'Invalid file type', + description: 'Please select an image file for your avatar.', + variant: 'destructive', + }); + return; + } + + // Validate file size (max 5MB) + if (file.size > 5 * 1024 * 1024) { + toast({ + title: 'File too large', + description: 'Avatar image must be smaller than 5MB.', + variant: 'destructive', + }); + return; + } + + try { + const tags = await uploadFile(file); + // Get the URL from the first tag + const url = tags[0]?.[1]; + if (url) { + setProfileData(prev => ({ ...prev, picture: url })); + toast({ + title: 'Avatar uploaded!', + description: 'Your avatar has been uploaded successfully.', + }); + } + } catch { + toast({ + title: 'Upload failed', + description: 'Failed to upload avatar. Please try again.', + variant: 'destructive', + }); + } + }; + + const finishSignup = async (skipProfile = false) => { + // Mark signup completion time for fallback welcome modal + localStorage.setItem('signup_completed', Date.now().toString()); + + try { + // Publish profile if user provided information + if (!skipProfile && (profileData.name || profileData.about || profileData.picture)) { + const metadata: Record<string, string> = {}; + if (profileData.name) metadata.name = profileData.name; + if (profileData.about) metadata.about = profileData.about; + if (profileData.picture) metadata.picture = profileData.picture; + + await publishEvent({ + kind: 0, + content: JSON.stringify(metadata), + }); + + toast({ + title: 'Profile Created!', + description: 'Your profile has been set up.', + }); + } + + // Close signup and show welcome modal + onClose(); + if (onComplete) { + // Add a longer delay to ensure login state has fully propagated + setTimeout(() => { + onComplete(); + }, 600); + } else { + // Fallback for when used without onComplete + setStep('done'); + setTimeout(() => { + onClose(); + toast({ + title: 'Welcome!', + description: 'Your account is ready.', + }); + }, 3000); + } + } catch { + toast({ + title: 'Profile Setup Failed', + description: 'Your account was created but profile setup failed. You can update it later.', + variant: 'destructive', + }); + + // Still proceed to completion even if profile failed + onClose(); + if (onComplete) { + // Add a longer delay to ensure login state has fully propagated + setTimeout(() => { + onComplete(); + }, 600); + } else { + // Fallback for when used without onComplete + setStep('done'); + setTimeout(() => { + onClose(); + toast({ + title: 'Welcome!', + description: 'Your account is ready.', + }); + }, 3000); + } + } + }; + + const getTitle = () => { + if (step === 'welcome') return ( + <span className="flex items-center justify-center gap-2"> + Create Your Account + </span> + ); + if (step === 'generate') return ( + <span className="flex items-center justify-center gap-2"> + Generating Your Key + </span> + ); + if (step === 'download') return ( + <span className="flex items-center justify-center gap-2"> + Secret Key + </span> + ); + if (step === 'profile') return ( + <span className="flex items-center justify-center gap-2"> + Create Your Profile + </span> + ); + return ( + <span className="flex items-center justify-center gap-2"> + Welcome! + </span> + ); + }; + + // Reset state when dialog opens + useEffect(() => { + if (isOpen) { + setStep('welcome'); + setIsLoading(false); + setNsec(''); + setShowSparkles(false); + setKeySecured('none'); + setProfileData({ name: '', about: '', picture: '' }); + } + }, [isOpen]); + + // Add sparkle animation effect + useEffect(() => { + if (showSparkles) { + const interval = setInterval(() => { + // This will trigger re-renders for sparkle animation + }, 100); + return () => clearInterval(interval); + } + }, [showSparkles]); + + return ( + <Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent + className={cn("max-w-[95vw] sm:max-w-md max-h-[90vh] max-h-[90dvh] p-0 overflow-hidden rounded-2xl flex flex-col")} + > + <DialogHeader className={cn('px-6 pt-6 pb-1 relative flex-shrink-0')}> + <DialogTitle className={cn('font-semibold text-center text-lg')}> + {getTitle()} + </DialogTitle> + </DialogHeader> + <div className='px-6 pt-2 pb-4 space-y-4 overflow-y-scroll flex-1'> + {/* Welcome Step - New engaging introduction */} + {step === 'welcome' && ( + <div className='text-center space-y-4'> + {/* Hero illustration */} + <div className='relative p-6 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-950/50 dark:to-indigo-950/50'> + <div className='flex justify-center items-center space-x-4 mb-3'> + <div className='relative'> + <UserPlus className='w-12 h-12 text-blue-600' /> + <Sparkles className='w-4 h-4 text-yellow-500 absolute -top-1 -right-1 animate-pulse' /> + </div> + <Globe className='w-16 h-16 text-blue-700 animate-spin-slow' /> + <div className='relative'> + <FileText className='w-12 h-12 text-blue-600' /> + <Sparkles className='w-4 h-4 text-yellow-500 absolute -top-1 -left-1 animate-pulse' style={{animationDelay: '0.3s'}} /> + </div> + </div> + + {/* Benefits */} + <div className='grid grid-cols-1 gap-2 text-sm'> + <div className='flex items-center justify-center gap-2 text-blue-700 dark:text-blue-300'> + <Shield className='w-4 h-4' /> + Decentralized and censorship-resistant + </div> + <div className='flex items-center justify-center gap-2 text-blue-700 dark:text-blue-300'> + <User className='w-4 h-4' /> + You are in control of your data + </div> + <div className='flex items-center justify-center gap-2 text-blue-700 dark:text-blue-300'> + <Globe className='w-4 h-4' /> + Join a global network + </div> + </div> + </div> + + <div className='space-y-3'> + <Button + className='w-full rounded-full py-6 text-lg font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transform transition-all duration-200 hover:scale-105 shadow-lg' + onClick={() => setStep('generate')} + > + <LogIn className='w-5 h-5 mr-2' /> + Get Started + </Button> + </div> + </div> + )} + + {/* Generate Step - Enhanced with animations */} + {step === 'generate' && ( + <div className='text-center space-y-4'> + <div className='relative p-6 rounded-2xl bg-gradient-to-br from-blue-50 to-purple-100 dark:from-blue-950/50 dark:to-purple-950/50 overflow-hidden'> + {/* Animated background elements */} + {showSparkles && ( + <div className='absolute inset-0'> + {[...Array(12)].map((_, i) => ( + <Sparkles + key={i} + className={`absolute w-4 h-4 text-yellow-400 animate-ping`} + style={{ + left: `${Math.random() * 80 + 10}%`, + top: `${Math.random() * 80 + 10}%`, + animationDelay: `${Math.random() * 2}s` + }} + /> + ))} + </div> + )} + + <div className='relative z-10'> + {isLoading ? ( + <div className='space-y-3'> + <div className='relative'> + <Key className='w-20 h-20 text-primary mx-auto animate-pulse' /> + <div className='absolute inset-0 flex items-center justify-center'> + <div className='w-24 h-24 border-4 border-yellow-400 border-t-transparent rounded-full animate-spin'></div> + </div> + </div> + <div className='space-y-2'> + <p className='text-lg font-semibold text-primary flex items-center justify-center gap-2'> + <Sparkles className='w-5 h-5' /> + Generating your secret key... + </p> + <p className='text-sm text-muted-foreground'> + Creating your secure key + </p> + </div> + </div> + ) : ( + <div className='space-y-3'> + <Key className='w-20 h-20 text-primary mx-auto' /> + <div className='space-y-2'> + <p className='text-lg font-semibold'> + Ready to generate your secret key? + </p> + <p className='text-sm text-muted-foreground px-5'> + This key will be your password to access applications within the Nostr network. + </p> + + </div> + </div> + )} + </div> + </div> + + {!isLoading && ( + <Button + className='w-full rounded-full py-6 text-lg font-semibold bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 transform transition-all duration-200 hover:scale-105 shadow-lg' + onClick={generateKey} + disabled={isLoading} + > + <Sparkles className='w-5 h-5 mr-2' /> + Generate My Secret Key + </Button> + )} + </div> + )} + + {/* Download Step - Whimsical and magical */} + {step === 'download' && ( + <div className='text-center space-y-4'> + {/* Key reveal */} + <div className='relative p-6 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-950/50 dark:to-indigo-950/50 overflow-hidden'> + {/* Sparkles */} + <div className='absolute inset-0 pointer-events-none'> + <Sparkles className='absolute top-3 left-4 w-3 h-3 text-yellow-400 animate-pulse' style={{animationDelay: '0s'}} /> + <Sparkles className='absolute top-6 right-6 w-3 h-3 text-yellow-500 animate-pulse' style={{animationDelay: '0.5s'}} /> + <Sparkles className='absolute bottom-4 left-6 w-3 h-3 text-yellow-400 animate-pulse' style={{animationDelay: '1s'}} /> + <Sparkles className='absolute bottom-3 right-4 w-3 h-3 text-yellow-500 animate-pulse' style={{animationDelay: '1.5s'}} /> + </div> + + <div className='relative z-10 flex justify-center items-center mb-3'> + <div className='relative'> + <div className='w-16 h-16 bg-gradient-to-br from-blue-200 to-indigo-300 rounded-full flex items-center justify-center shadow-lg animate-pulse'> + <Key className='w-8 h-8 text-indigo-800' /> + </div> + <div className='absolute -top-1 -right-1 w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center animate-bounce'> + <Sparkles className='w-3 h-3 text-white' /> + </div> + </div> + </div> + + <div className='relative z-10 space-y-2'> + <p className='text-base font-semibold'> + Your secret key has been generated! + </p> + + {/* Warning */} + <div className='relative mx-auto max-w-sm'> + <div className='p-3 bg-gradient-to-r from-amber-100 via-yellow-50 to-amber-100 dark:from-amber-950/40 dark:via-yellow-950/20 dark:to-amber-950/40 rounded-lg border-2 border-amber-300 dark:border-amber-700 shadow-md'> + <div className='flex items-center gap-2 mb-1'> + <FileText className='w-3 h-3 text-amber-700' /> + <span className='text-xs font-bold text-amber-800 dark:text-amber-200'> + Important Warning + </span> + </div> + <p className='text-xs text-red-700 dark:text-amber-300 italic'> + This key is your primary and only means of accessing your account. Store it safely and securely. + </p> + </div> + </div> + </div> + </div> + + {/* Key vault */} + + + {/* Security options */} + <div className='space-y-3'> + + + <div className='grid grid-cols-1 gap-2'> + {/* Download Option */} + <Card className={`cursor-pointer transition-all duration-200 ${ + keySecured === 'downloaded' + ? 'ring-2 ring-green-500 bg-green-50 dark:bg-green-950/20' + : 'hover:bg-primary/5 hover:border-primary/20' + }`}> + <CardContent className='p-3'> + <Button + variant="ghost" + className='w-full h-auto p-0 justify-start hover:bg-transparent' + onClick={downloadKey} + > + <div className='flex items-center gap-3 w-full'> + <div className={`p-1.5 rounded-lg ${ + keySecured === 'downloaded' + ? 'bg-green-100 dark:bg-green-900' + : 'bg-primary/10' + }`}> + {keySecured === 'downloaded' ? ( + <CheckCircle className='w-4 h-4 text-green-600' /> + ) : ( + <Download className='w-4 h-4 text-primary' /> + )} + </div> + <div className='flex-1 text-left'> + <div className='font-medium text-sm'> + Download as File + </div> + <div className='text-xs text-muted-foreground'> + Save as nostr-nsec-key.txt file + </div> + </div> + {keySecured === 'downloaded' && ( + <div className='text-xs font-medium text-green-600'> + ✓ Downloaded + </div> + )} + </div> + </Button> + </CardContent> + </Card> + + + </div> + + {/* Continue button */} + <Button + className={`w-full rounded-full py-4 text-base font-semibold transform transition-all duration-200 shadow-lg ${ + keySecured === 'downloaded' + ? 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 dark:from-blue-950/50 dark:to-purple-950/50 hover:scale-105' + : 'bg-gradient-to-r from-blue-600/60 to-indigo-600/60 text-muted cursor-not-allowed' + }`} + onClick={finishKeySetup} + disabled={keySecured !== 'downloaded'} + > + <LogIn className='w-4 h-4 mr-2 flex-shrink-0' /> + <span className="text-center leading-tight"> + {keySecured === 'none' ? ( + <> + Please download your key first + </> + ) : ( + <> + <span className="hidden sm:inline">My Key is Safe - Continue</span> + <span className="sm:hidden">Key Secured - Continue</span> + </> + )} + </span> + </Button> + </div> + </div> + )} + + {/* Profile Step - Optional profile setup */} + {step === 'profile' && ( + <div className='text-center space-y-4'> + {/* Profile setup illustration */} + <div className='relative p-6 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-950/50 dark:to-indigo-950/50 overflow-hidden'> + {/* Sparkles */} + <div className='absolute inset-0 pointer-events-none'> + <Sparkles className='absolute top-3 left-4 w-3 h-3 text-yellow-400 animate-pulse' style={{animationDelay: '0s'}} /> + <Sparkles className='absolute top-6 right-6 w-3 h-3 text-yellow-500 animate-pulse' style={{animationDelay: '0.5s'}} /> + <Sparkles className='absolute bottom-4 left-6 w-3 h-3 text-yellow-400 animate-pulse' style={{animationDelay: '1s'}} /> + </div> + + <div className='relative z-10 flex justify-center items-center mb-3'> + <div className='relative'> + <div className='w-16 h-16 bg-gradient-to-br from-blue-200 to-indigo-300 rounded-full flex items-center justify-center shadow-lg'> + <User className='w-8 h-8 text-blue-800' /> + </div> + <div className='absolute -top-1 -right-1 w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center animate-bounce'> + <Sparkles className='w-3 h-3 text-white' /> + </div> + </div> + </div> + + <div className='relative z-10 space-y-2'> + <p className='text-base font-semibold'> + Almost there! Let's set up your profile + </p> + + <p className='text-sm text-muted-foreground'> + Your profile is your identity on Nostr. + </p> + </div> + </div> + + {/* Publishing status indicator */} + {isPublishing && ( + <div className='relative p-4 rounded-xl bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30 border border-blue-200 dark:border-blue-800'> + <div className='flex items-center justify-center gap-3'> + <div className='w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin' /> + <span className='text-sm font-medium text-blue-700 dark:text-blue-300'> + Publishing your profile... + </span> + </div> + </div> + )} + + {/* Profile form */} + <div className={`space-y-4 text-left ${isPublishing ? 'opacity-50 pointer-events-none' : ''}`}> + <div className='space-y-2'> + <label htmlFor='profile-name' className='text-sm font-medium'> + Display Name + </label> + <Input + id='profile-name' + value={profileData.name} + onChange={(e) => setProfileData(prev => ({ ...prev, name: e.target.value }))} + placeholder='Your name' + className='rounded-lg' + disabled={isPublishing} + /> + </div> + + <div className='space-y-2'> + <label htmlFor='profile-about' className='text-sm font-medium'> + Bio + </label> + <Textarea + id='profile-about' + value={profileData.about} + onChange={(e) => setProfileData(prev => ({ ...prev, about: e.target.value }))} + placeholder='Tell others about yourself...' + className='rounded-lg resize-none' + rows={3} + disabled={isPublishing} + /> + </div> + + <div className='space-y-2'> + <label htmlFor='profile-picture' className='text-sm font-medium'> + Avatar + </label> + <div className='flex gap-2'> + <Input + id='profile-picture' + value={profileData.picture} + onChange={(e) => setProfileData(prev => ({ ...prev, picture: e.target.value }))} + placeholder='https://example.com/your-avatar.jpg' + className='rounded-lg flex-1' + disabled={isPublishing} + /> + <input + type='file' + accept='image/*' + className='hidden' + ref={avatarFileInputRef} + onChange={handleAvatarUpload} + /> + <Button + type='button' + variant='outline' + size='icon' + onClick={() => avatarFileInputRef.current?.click()} + disabled={isUploading || isPublishing} + className='rounded-lg shrink-0' + title='Upload avatar image' + > + {isUploading ? ( + <div className='w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin' /> + ) : ( + <Upload className='w-4 h-4' /> + )} + </Button> + </div> + </div> + </div> + + {/* Action buttons */} + <div className='space-y-3'> + <Button + className='w-full rounded-full py-4 text-base font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 transform transition-all duration-200 hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none' + onClick={() => finishSignup(false)} + disabled={isPublishing || isUploading} + > + {isPublishing ? ( + <> + <div className='w-4 h-4 mr-2 border-2 border-current border-t-transparent rounded-full animate-spin' /> + Creating Profile... + </> + ) : ( + <> + <User className='w-4 h-4 mr-2' /> + Create Profile & Finish + </> + )} + </Button> + + <Button + variant='outline' + className='w-full rounded-full py-3 disabled:opacity-50 disabled:cursor-not-allowed' + onClick={() => finishSignup(true)} + disabled={isPublishing || isUploading} + > + {isPublishing ? ( + <> + <div className='w-4 h-4 mr-2 border-2 border-current border-t-transparent rounded-full animate-spin' /> + Setting up account... + </> + ) : ( + 'Skip for now' + )} + </Button> + </div> + </div> + )} + </div> + </DialogContent> + </Dialog> + ); +}; + +export default SignupDialog; diff --git a/src/components/comments/Comment.tsx b/src/components/comments/Comment.tsx new file mode 100644 index 0000000..54658ae --- /dev/null +++ b/src/components/comments/Comment.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { NostrEvent } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; +import { useAuthor } from '@/hooks/useAuthor'; +import { useComments } from '@/hooks/useComments'; +import { CommentForm } from './CommentForm'; +import { NoteContent } from '@/components/NoteContent'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { DropdownMenu, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { MessageSquare, ChevronDown, ChevronRight, MoreHorizontal } from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; +import { genUserName } from '@/lib/genUserName'; + +interface CommentProps { + root: NostrEvent | URL; + comment: NostrEvent; + depth?: number; + maxDepth?: number; + limit?: number; +} + +export function Comment({ root, comment, depth = 0, maxDepth = 3, limit }: CommentProps) { + const [showReplyForm, setShowReplyForm] = useState(false); + const [showReplies, setShowReplies] = useState(depth < 2); // Auto-expand first 2 levels + + const author = useAuthor(comment.pubkey); + const { data: commentsData } = useComments(root, limit); + + const metadata = author.data?.metadata; + const displayName = metadata?.name ?? genUserName(comment.pubkey) + const timeAgo = formatDistanceToNow(new Date(comment.created_at * 1000), { addSuffix: true }); + + // Get direct replies to this comment + const replies = commentsData?.getDirectReplies(comment.id) || []; + const hasReplies = replies.length > 0; + + return ( + <div className={`space-y-3 ${depth > 0 ? 'ml-6 border-l-2 border-muted pl-4' : ''}`}> + <Card className="bg-card/50"> + <CardContent className="p-4"> + <div className="space-y-3"> + {/* Comment Header */} + <div className="flex items-start justify-between"> + <div className="flex items-center space-x-3"> + <Link to={`/${nip19.npubEncode(comment.pubkey)}`}> + <Avatar className="h-8 w-8 hover:ring-2 hover:ring-primary/30 transition-all cursor-pointer"> + <AvatarImage src={metadata?.picture} /> + <AvatarFallback className="text-xs"> + {displayName.charAt(0)} + </AvatarFallback> + </Avatar> + </Link> + <div> + <Link + to={`/${nip19.npubEncode(comment.pubkey)}`} + className="font-medium text-sm hover:text-primary transition-colors" + > + {displayName} + </Link> + <p className="text-xs text-muted-foreground">{timeAgo}</p> + </div> + </div> + </div> + + {/* Comment Content */} + <div className="text-sm"> + <NoteContent event={comment} className="text-sm" /> + </div> + + {/* Comment Actions */} + <div className="flex items-center justify-between pt-2"> + <div className="flex items-center space-x-2"> + <Button + variant="ghost" + size="sm" + onClick={() => setShowReplyForm(!showReplyForm)} + className="h-8 px-2 text-xs" + > + <MessageSquare className="h-3 w-3 mr-1" /> + Reply + </Button> + + {hasReplies && ( + <Collapsible open={showReplies} onOpenChange={setShowReplies}> + <CollapsibleTrigger asChild> + <Button variant="ghost" size="sm" className="h-8 px-2 text-xs"> + {showReplies ? ( + <ChevronDown className="h-3 w-3 mr-1" /> + ) : ( + <ChevronRight className="h-3 w-3 mr-1" /> + )} + {replies.length} {replies.length === 1 ? 'reply' : 'replies'} + </Button> + </CollapsibleTrigger> + </Collapsible> + )} + </div> + + {/* Comment menu */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="ghost" + size="sm" + className="h-8 px-2 text-xs" + aria-label="Comment options" + > + <MoreHorizontal className="h-3 w-3" /> + </Button> + </DropdownMenuTrigger> + </DropdownMenu> + </div> + </div> + </CardContent> + </Card> + + {/* Reply Form */} + {showReplyForm && ( + <div className="ml-6"> + <CommentForm + root={root} + reply={comment} + onSuccess={() => setShowReplyForm(false)} + placeholder="Write a reply..." + compact + /> + </div> + )} + + {/* Replies */} + {hasReplies && ( + <Collapsible open={showReplies} onOpenChange={setShowReplies}> + <CollapsibleContent className="space-y-3"> + {replies.map((reply) => ( + <Comment + key={reply.id} + root={root} + comment={reply} + depth={depth + 1} + maxDepth={maxDepth} + limit={limit} + /> + ))} + </CollapsibleContent> + </Collapsible> + )} + </div> + ); +} \ No newline at end of file diff --git a/src/components/comments/CommentForm.tsx b/src/components/comments/CommentForm.tsx new file mode 100644 index 0000000..7c973f2 --- /dev/null +++ b/src/components/comments/CommentForm.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent } from '@/components/ui/card'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { usePostComment } from '@/hooks/usePostComment'; +import { LoginArea } from '@/components/auth/LoginArea'; +import { NostrEvent } from '@nostrify/nostrify'; +import { MessageSquare, Send } from 'lucide-react'; + +interface CommentFormProps { + root: NostrEvent | URL; + reply?: NostrEvent | URL; + onSuccess?: () => void; + placeholder?: string; + compact?: boolean; +} + +export function CommentForm({ + root, + reply, + onSuccess, + placeholder = "Write a comment...", + compact = false +}: CommentFormProps) { + const [content, setContent] = useState(''); + const { user } = useCurrentUser(); + const { mutate: postComment, isPending } = usePostComment(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!content.trim() || !user) return; + + postComment( + { content: content.trim(), root, reply }, + { + onSuccess: () => { + setContent(''); + onSuccess?.(); + }, + } + ); + }; + + if (!user) { + return ( + <Card className={compact ? "border-dashed" : ""}> + <CardContent className={compact ? "p-4" : "p-6"}> + <div className="text-center space-y-4"> + <div className="flex items-center justify-center space-x-2 text-muted-foreground"> + <MessageSquare className="h-5 w-5" /> + <span>Sign in to {reply ? 'reply' : 'comment'}</span> + </div> + <LoginArea /> + </div> + </CardContent> + </Card> + ); + } + + return ( + <Card className={compact ? "border-dashed" : ""}> + <CardContent className={compact ? "p-4" : "p-6"}> + <form onSubmit={handleSubmit} className="space-y-4"> + <Textarea + value={content} + onChange={(e) => setContent(e.target.value)} + placeholder={placeholder} + className={compact ? "min-h-[80px]" : "min-h-[100px]"} + disabled={isPending} + /> + <div className="flex justify-between items-center"> + <span className="text-sm text-muted-foreground"> + {reply ? 'Replying to comment' : 'Adding to the discussion'} + </span> + <Button + type="submit" + disabled={!content.trim() || isPending} + size={compact ? "sm" : "default"} + > + <Send className="h-4 w-4 mr-2" /> + {isPending ? 'Posting...' : (reply ? 'Reply' : 'Comment')} + </Button> + </div> + </form> + </CardContent> + </Card> + ); +} \ No newline at end of file diff --git a/src/components/comments/CommentsSection.tsx b/src/components/comments/CommentsSection.tsx new file mode 100644 index 0000000..a269934 --- /dev/null +++ b/src/components/comments/CommentsSection.tsx @@ -0,0 +1,100 @@ +import { useComments } from '@/hooks/useComments'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { MessageSquare } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { NostrEvent } from '@nostrify/nostrify'; +import { CommentForm } from './CommentForm'; +import { Comment } from './Comment'; + +interface CommentsSectionProps { + root: NostrEvent | URL; + title?: string; + emptyStateMessage?: string; + emptyStateSubtitle?: string; + className?: string; + limit?: number; +} + +export function CommentsSection({ + root, + title = "Comments", + emptyStateMessage = "No comments yet", + emptyStateSubtitle = "Be the first to share your thoughts!", + className, + limit = 500, +}: CommentsSectionProps) { + const { data: commentsData, isLoading, error } = useComments(root, limit); + const comments = commentsData?.topLevelComments || []; + + if (error) { + return ( + <Card className="rounded-none sm:rounded-lg mx-0 sm:mx-0"> + <CardContent className="px-2 py-6 sm:p-6"> + <div className="text-center text-muted-foreground"> + <MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" /> + <p>Failed to load comments</p> + </div> + </CardContent> + </Card> + ); + } + + return ( + <Card className={cn("rounded-none sm:rounded-lg mx-0 sm:mx-0", className)}> + <CardHeader className="px-2 pt-6 pb-4 sm:p-6"> + <CardTitle className="flex items-center space-x-2"> + <MessageSquare className="h-5 w-5" /> + <span>{title}</span> + {!isLoading && ( + <span className="text-sm font-normal text-muted-foreground"> + ({comments.length}) + </span> + )} + </CardTitle> + </CardHeader> + <CardContent className="px-2 pb-6 pt-4 sm:p-6 sm:pt-0 space-y-6"> + {/* Comment Form */} + <CommentForm root={root} /> + + {/* Comments List */} + {isLoading ? ( + <div className="space-y-4"> + {[...Array(3)].map((_, i) => ( + <Card key={i} className="bg-card/50"> + <CardContent className="p-4"> + <div className="space-y-3"> + <div className="flex items-center space-x-3"> + <Skeleton className="h-8 w-8 rounded-full" /> + <div className="space-y-1"> + <Skeleton className="h-4 w-24" /> + <Skeleton className="h-3 w-16" /> + </div> + </div> + <Skeleton className="h-16 w-full" /> + </div> + </CardContent> + </Card> + ))} + </div> + ) : comments.length === 0 ? ( + <div className="text-center py-8 text-muted-foreground"> + <MessageSquare className="h-12 w-12 mx-auto mb-4 opacity-30" /> + <p className="text-lg font-medium mb-2">{emptyStateMessage}</p> + <p className="text-sm">{emptyStateSubtitle}</p> + </div> + ) : ( + <div className="space-y-4"> + {comments.map((comment) => ( + <Comment + key={comment.id} + root={root} + comment={comment} + /> + ))} + </div> + )} + </CardContent> + </Card> + ); +} \ No newline at end of file diff --git a/src/components/dm/DMChatArea.tsx b/src/components/dm/DMChatArea.tsx new file mode 100644 index 0000000..b5b6c5f --- /dev/null +++ b/src/components/dm/DMChatArea.tsx @@ -0,0 +1,409 @@ +import { useState, useRef, useEffect, useCallback, memo } from 'react'; +import { useConversationMessages } from '@/hooks/useConversationMessages'; +import { useDMContext } from '@/hooks/useDMContext'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAuthor } from '@/hooks/useAuthor'; +import { genUserName } from '@/lib/genUserName'; +import { MESSAGE_PROTOCOL, PROTOCOL_MODE, type MessageProtocol } from '@/lib/dmConstants'; +import { formatConversationTime, formatFullDateTime } from '@/lib/dmUtils'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Textarea } from '@/components/ui/textarea'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { ArrowLeft, Send, Loader2, AlertTriangle, Key, ShieldCheck } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { NoteContent } from '@/components/NoteContent'; +import type { NostrEvent } from '@nostrify/nostrify'; + +interface DMChatAreaProps { + pubkey: string | null; + onBack?: () => void; + className?: string; +} + +const MessageBubble = memo(({ + message, + isFromCurrentUser +}: { + message: { + id: string; + pubkey: string; + kind: number; + tags: string[][]; + decryptedContent?: string; + decryptedEvent?: NostrEvent; + error?: string; + created_at: number; + isSending?: boolean; + }; + isFromCurrentUser: boolean; +}) => { + // For NIP-17, use inner message kind (14/15); for NIP-04, use message kind (4) + const actualKind = message.decryptedEvent?.kind || message.kind; + const isNIP4Message = message.kind === 4; + const isFileAttachment = actualKind === 15; // Kind 15 = files/attachments + + // Create a NostrEvent object for NoteContent (only used for kind 15) + // For NIP-17 file attachments, use the decryptedEvent which has the actual tags + const messageEvent: NostrEvent = message.decryptedEvent || { + id: message.id, + pubkey: message.pubkey, + created_at: message.created_at, + kind: message.kind, + tags: message.tags, + content: message.decryptedContent || '', + sig: '', // Not needed for display + }; + + return ( + <div className={cn("flex mb-4", isFromCurrentUser ? "justify-end" : "justify-start")}> + <div className={cn( + "max-w-[70%] rounded-lg px-4 py-2", + isFromCurrentUser + ? "bg-primary text-primary-foreground" + : "bg-muted" + )}> + {message.error ? ( + <Tooltip delayDuration={200}> + <TooltipTrigger asChild> + <p className="text-sm italic opacity-70 cursor-help">🔒 Failed to decrypt</p> + </TooltipTrigger> + <TooltipContent> + <p className="text-xs">{message.error}</p> + </TooltipContent> + </Tooltip> + ) : isFileAttachment ? ( + // Kind 15: Use NoteContent to render files/media with imeta tags + <div className="text-sm"> + <NoteContent event={messageEvent} className="whitespace-pre-wrap break-words" /> + </div> + ) : ( + // Kind 4 (NIP-04) and Kind 14 (NIP-17 text): Display plain text + <p className="text-sm whitespace-pre-wrap break-words"> + {message.decryptedContent} + </p> + )} + <div className="flex items-center gap-2 mt-1"> + <TooltipProvider> + <Tooltip delayDuration={200}> + <TooltipTrigger asChild> + <span className={cn( + "text-xs opacity-70 cursor-default", + isFromCurrentUser ? "text-primary-foreground" : "text-muted-foreground" + )}> + {formatConversationTime(message.created_at)} + </span> + </TooltipTrigger> + <TooltipContent> + <p className="text-xs">{formatFullDateTime(message.created_at)}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + + <TooltipProvider> + <Tooltip delayDuration={200}> + <TooltipTrigger asChild> + <span className={cn( + "flex-shrink-0 opacity-50", + isFromCurrentUser ? "text-primary-foreground" : "text-muted-foreground" + )}> + {message.kind === 4 ? ( + <Key className="h-3 w-3" /> + ) : ( + <ShieldCheck className="h-3 w-3" /> + )} + </span> + </TooltipTrigger> + <TooltipContent> + <p className="text-xs"> + {message.kind === 4 && "NIP-04 Kind 4 (Legacy DM)"} + {message.kind === 14 && "NIP-17 Kind 14 (Private Message)"} + {message.kind === 15 && "NIP-17 Kind 15 (Media)"} + {message.kind !== 4 && message.kind !== 14 && message.kind !== 15 && `Kind ${message.kind}`} + </p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + {isNIP4Message && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <div className="flex-shrink-0"> + <AlertTriangle className="h-3 w-3 text-yellow-600 dark:text-yellow-500" /> + </div> + </TooltipTrigger> + <TooltipContent> + <p className="text-xs">Uses outdated NIP-04 encryption</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + )} + {message.isSending && ( + <Loader2 className="h-3 w-3 animate-spin opacity-70" /> + )} + </div> + </div> + </div> + ); +}); + +MessageBubble.displayName = 'MessageBubble'; + +const ChatHeader = ({ pubkey, onBack }: { pubkey: string; onBack?: () => void }) => { + const author = useAuthor(pubkey); + const metadata = author.data?.metadata; + + const displayName = metadata?.name || genUserName(pubkey); + const avatarUrl = metadata?.picture; + const initials = displayName.slice(0, 2).toUpperCase(); + + return ( + <div className="p-4 border-b flex items-center gap-3"> + {onBack && ( + <Button + variant="ghost" + size="icon" + onClick={onBack} + className="md:hidden" + > + <ArrowLeft className="h-5 w-5" /> + </Button> + )} + + <Avatar className="h-10 w-10"> + <AvatarImage src={avatarUrl} alt={displayName} /> + <AvatarFallback>{initials}</AvatarFallback> + </Avatar> + + <div className="flex-1 min-w-0"> + <h2 className="font-semibold truncate">{displayName}</h2> + {metadata?.nip05 && ( + <p className="text-xs text-muted-foreground truncate">{metadata.nip05}</p> + )} + </div> + </div> + ); +}; + +const EmptyState = ({ isLoading }: { isLoading: boolean }) => { + return ( + <div className="h-full flex items-center justify-center p-8"> + <div className="text-center text-muted-foreground max-w-sm"> + {isLoading ? ( + <> + <Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" /> + <p className="text-sm">Loading conversations...</p> + <p className="text-xs mt-2"> + Fetching encrypted messages from relays + </p> + </> + ) : ( + <> + <p className="text-sm">Select a conversation to start messaging</p> + <p className="text-xs mt-2"> + Your messages are encrypted and stored locally + </p> + </> + )} + </div> + </div> + ); +}; + +export const DMChatArea = ({ pubkey, onBack, className }: DMChatAreaProps) => { + const { user } = useCurrentUser(); + const { sendMessage, protocolMode, isLoading } = useDMContext(); + const { messages, hasMoreMessages, loadEarlierMessages } = useConversationMessages(pubkey || ''); + + const [messageText, setMessageText] = useState(''); + const [isSending, setIsSending] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + + // Determine default protocol based on mode + const getDefaultProtocol = () => { + if (protocolMode === PROTOCOL_MODE.NIP04_ONLY) return MESSAGE_PROTOCOL.NIP04; + if (protocolMode === PROTOCOL_MODE.NIP17_ONLY) return MESSAGE_PROTOCOL.NIP17; + if (protocolMode === PROTOCOL_MODE.NIP04_OR_NIP17) return MESSAGE_PROTOCOL.NIP17; + // Fallback to NIP-17 for any unexpected mode + return MESSAGE_PROTOCOL.NIP17; + }; + + const [selectedProtocol, setSelectedProtocol] = useState<MessageProtocol>(getDefaultProtocol()); + const scrollAreaRef = useRef<HTMLDivElement>(null); + + // Determine if selection is allowed + const allowSelection = protocolMode === PROTOCOL_MODE.NIP04_OR_NIP17; + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + if (scrollAreaRef.current) { + const scrollContainer = scrollAreaRef.current.querySelector('[data-radix-scroll-area-viewport]'); + if (scrollContainer) { + scrollContainer.scrollTop = scrollContainer.scrollHeight; + } + } + }, [messages.length]); + + const handleSend = useCallback(async () => { + if (!messageText.trim() || !pubkey || !user) return; + + setIsSending(true); + try { + await sendMessage({ + recipientPubkey: pubkey, + content: messageText.trim(), + protocol: selectedProtocol, + }); + setMessageText(''); + } catch (error) { + console.error('Failed to send message:', error); + } finally { + setIsSending(false); + } + }, [messageText, pubkey, user, sendMessage, selectedProtocol]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, [handleSend]); + + const handleLoadMore = useCallback(async () => { + if (!scrollAreaRef.current || isLoadingMore) return; + + const scrollContainer = scrollAreaRef.current.querySelector('[data-radix-scroll-area-viewport]'); + if (!scrollContainer) return; + + // Store current scroll position and height + const previousScrollHeight = scrollContainer.scrollHeight; + const previousScrollTop = scrollContainer.scrollTop; + + setIsLoadingMore(true); + + // Load more messages + loadEarlierMessages(); + + // Wait for DOM to update, then restore relative scroll position + setTimeout(() => { + if (scrollContainer) { + const newScrollHeight = scrollContainer.scrollHeight; + const heightDifference = newScrollHeight - previousScrollHeight; + scrollContainer.scrollTop = previousScrollTop + heightDifference; + } + setIsLoadingMore(false); + }, 0); + }, [loadEarlierMessages, isLoadingMore]); + + if (!pubkey) { + return ( + <Card className={cn("h-full", className)}> + <EmptyState isLoading={isLoading} /> + </Card> + ); + } + + if (!user) { + return ( + <Card className={cn("h-full flex items-center justify-center", className)}> + <div className="text-center text-muted-foreground"> + <p className="text-sm">Please log in to view messages</p> + </div> + </Card> + ); + } + + return ( + <Card className={cn("h-full flex flex-col", className)}> + <ChatHeader pubkey={pubkey} onBack={onBack} /> + + <ScrollArea ref={scrollAreaRef} className="flex-1 p-4"> + {messages.length === 0 ? ( + <div className="h-full flex items-center justify-center"> + <div className="text-center text-muted-foreground"> + <p className="text-sm">No messages yet</p> + <p className="text-xs mt-1">Send a message to start the conversation</p> + </div> + </div> + ) : ( + <div> + {hasMoreMessages && ( + <div className="flex justify-center mb-4"> + <Button + variant="outline" + size="sm" + onClick={handleLoadMore} + disabled={isLoadingMore} + className="text-xs" + > + {isLoadingMore ? ( + <> + <Loader2 className="h-3 w-3 animate-spin mr-2" /> + Loading... + </> + ) : ( + 'Load Earlier Messages' + )} + </Button> + </div> + )} + {messages.map((message) => ( + <MessageBubble + key={message.id} + message={message} + isFromCurrentUser={message.pubkey === user.pubkey} + /> + ))} + </div> + )} + </ScrollArea> + + <div className="p-4 border-t"> + <div className="flex gap-2"> + <Textarea + value={messageText} + onChange={(e) => setMessageText(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Type a message... (Enter to send, Shift+Enter for new line)" + className="min-h-[80px] resize-none" + disabled={isSending} + /> + <div className="flex flex-col gap-2"> + <Button + onClick={handleSend} + disabled={!messageText.trim() || isSending} + size="icon" + className="h-[44px] w-[90px]" + > + {isSending ? ( + <Loader2 className="h-5 w-5 animate-spin" /> + ) : ( + <Send className="h-5 w-5" /> + )} + </Button> + <Select + value={selectedProtocol} + onValueChange={(value) => setSelectedProtocol(value as MessageProtocol)} + disabled={!allowSelection} + > + <SelectTrigger className="h-[32px] w-[90px] text-xs px-2"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value={MESSAGE_PROTOCOL.NIP17} className="text-xs"> + NIP-17 + </SelectItem> + <SelectItem value={MESSAGE_PROTOCOL.NIP04} className="text-xs"> + NIP-04 + </SelectItem> + </SelectContent> + </Select> + </div> + </div> + </div> + </Card> + ); +}; diff --git a/src/components/dm/DMConversationList.tsx b/src/components/dm/DMConversationList.tsx new file mode 100644 index 0000000..1cc91a6 --- /dev/null +++ b/src/components/dm/DMConversationList.tsx @@ -0,0 +1,266 @@ +import { useMemo, useState, memo } from 'react'; +import { AlertTriangle, Info, Loader2 } from 'lucide-react'; +import { useDMContext } from '@/hooks/useDMContext'; +import { useAuthor } from '@/hooks/useAuthor'; +import { genUserName } from '@/lib/genUserName'; +import { formatConversationTime, formatFullDateTime } from '@/lib/dmUtils'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { LOADING_PHASES } from '@/lib/dmConstants'; + +interface DMConversationListProps { + selectedPubkey: string | null; + onSelectConversation: (pubkey: string) => void; + className?: string; + onStatusClick?: () => void; +} + +interface ConversationItemProps { + pubkey: string; + isSelected: boolean; + onClick: () => void; + lastMessage: { decryptedContent?: string; error?: string } | null; + lastActivity: number; + hasNIP4Messages: boolean; +} + +const ConversationItemComponent = ({ + pubkey, + isSelected, + onClick, + lastMessage, + lastActivity, + hasNIP4Messages +}: ConversationItemProps) => { + const author = useAuthor(pubkey); + const metadata = author.data?.metadata; + + const displayName = metadata?.name || genUserName(pubkey); + const avatarUrl = metadata?.picture; + const initials = displayName.slice(0, 2).toUpperCase(); + + const lastMessagePreview = lastMessage?.error + ? '🔒 Encrypted message' + : lastMessage?.decryptedContent || 'No messages yet'; + + // Show skeleton only for name/avatar while loading (we already have message data) + const isLoadingProfile = author.isLoading && !metadata; + + return ( + <button + onClick={onClick} + className={cn( + "w-full text-left p-3 rounded-lg transition-colors hover:bg-accent block overflow-hidden", + isSelected && "bg-accent" + )} + > + <div className="flex items-start gap-3 max-w-full"> + {isLoadingProfile ? ( + <Skeleton className="h-10 w-10 rounded-full flex-shrink-0" /> + ) : ( + <Avatar className="h-10 w-10 flex-shrink-0"> + <AvatarImage src={avatarUrl} alt={displayName} /> + <AvatarFallback>{initials}</AvatarFallback> + </Avatar> + )} + + <div className="flex-1 min-w-0"> + <div className="flex items-center justify-between gap-2 mb-1"> + <div className="flex items-center gap-1.5 min-w-0 flex-1"> + {isLoadingProfile ? ( + <Skeleton className="h-[1.25rem] w-24" /> + ) : ( + <span className="font-medium text-sm truncate">{displayName}</span> + )} + {hasNIP4Messages && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <div className="flex-shrink-0"> + <AlertTriangle className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-500" /> + </div> + </TooltipTrigger> + <TooltipContent side="left"> + <p className="text-xs max-w-[200px]">Some messages use outdated NIP-04 encryption</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + )} + </div> + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <span className="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0 cursor-default"> + {formatConversationTime(lastActivity)} + </span> + </TooltipTrigger> + <TooltipContent side="left"> + <p className="text-xs">{formatFullDateTime(lastActivity)}</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + + <p className="text-sm text-muted-foreground truncate"> + {lastMessagePreview} + </p> + </div> + </div> + </button> + ); +}; + +const ConversationItem = memo(ConversationItemComponent); +ConversationItem.displayName = 'ConversationItem'; + +const ConversationListSkeleton = () => { + return ( + <div className="space-y-2 p-4"> + {[1, 2, 3, 4, 5].map((i) => ( + <div key={i} className="flex items-start gap-3 p-3"> + <Skeleton className="h-10 w-10 rounded-full flex-shrink-0" /> + <div className="flex-1 space-y-2"> + <div className="flex justify-between"> + <Skeleton className="h-4 w-24" /> + <Skeleton className="h-3 w-12" /> + </div> + <Skeleton className="h-3 w-full" /> + </div> + </div> + ))} + </div> + ); +}; + +export const DMConversationList = ({ + selectedPubkey, + onSelectConversation, + className, + onStatusClick +}: DMConversationListProps) => { + const { conversations, isLoading, loadingPhase } = useDMContext(); + const [activeTab, setActiveTab] = useState<'known' | 'requests'>('known'); + + // Filter conversations by type + const { knownConversations, requestConversations } = useMemo(() => { + return { + knownConversations: conversations.filter(c => c.isKnown), + requestConversations: conversations.filter(c => c.isRequest), + }; + }, [conversations]); + + // Get the current list based on active tab + const currentConversations = activeTab === 'known' ? knownConversations : requestConversations; + + // Show skeleton during initial load (cache + relays) if we have no conversations yet + const isInitialLoad = (loadingPhase === LOADING_PHASES.CACHE || loadingPhase === LOADING_PHASES.RELAYS) && conversations.length === 0; + + return ( + <Card className={cn("h-full flex flex-col overflow-hidden", className)}> + {/* Header - always visible */} + <div className="p-4 border-b flex-shrink-0 flex items-center justify-between"> + <div className="flex items-center gap-2"> + <h2 className="font-semibold text-lg">Messages</h2> + {(loadingPhase === LOADING_PHASES.CACHE || + loadingPhase === LOADING_PHASES.RELAYS || + loadingPhase === LOADING_PHASES.SUBSCRIPTIONS) && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <div className="flex items-center"> + <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> + </div> + </TooltipTrigger> + <TooltipContent> + <p className="text-xs"> + {loadingPhase === LOADING_PHASES.CACHE && 'Loading from cache...'} + {loadingPhase === LOADING_PHASES.RELAYS && 'Querying relays for new messages...'} + {loadingPhase === LOADING_PHASES.SUBSCRIPTIONS && 'Setting up subscriptions...'} + </p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + )} + </div> + {onStatusClick && ( + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={onStatusClick} + aria-label="View messaging status" + > + <Info className="h-4 w-4" /> + </Button> + )} + </div> + + {/* Tab buttons - always visible */} + <div className="px-2 pt-2 flex-shrink-0"> + <div className="grid grid-cols-2 gap-1 bg-muted p-1 rounded-lg"> + <button + onClick={() => setActiveTab('known')} + className={cn( + "text-xs py-2 px-3 rounded-md transition-colors", + activeTab === 'known' + ? "bg-background shadow-sm font-medium" + : "text-muted-foreground hover:text-foreground" + )} + > + Active {knownConversations.length > 0 && `(${knownConversations.length})`} + </button> + <button + onClick={() => setActiveTab('requests')} + className={cn( + "text-xs py-2 px-3 rounded-md transition-colors", + activeTab === 'requests' + ? "bg-background shadow-sm font-medium" + : "text-muted-foreground hover:text-foreground" + )} + > + Requests {requestConversations.length > 0 && `(${requestConversations.length})`} + </button> + </div> + </div> + + {/* Content area - show skeleton during initial load, otherwise show conversations */} + <div className="flex-1 min-h-0 mt-2 overflow-hidden"> + {(isLoading || isInitialLoad) ? ( + <ConversationListSkeleton /> + ) : conversations.length === 0 ? ( + <div className="flex items-center justify-center h-full text-center text-muted-foreground px-4"> + <div> + <p className="text-sm">No conversations yet</p> + <p className="text-xs mt-1">Start a new conversation to get started</p> + </div> + </div> + ) : currentConversations.length === 0 ? ( + <div className="flex items-center justify-center h-32 text-center text-muted-foreground px-4"> + <p className="text-sm">No {activeTab} conversations</p> + </div> + ) : ( + <ScrollArea className="h-full block"> + <div className="block w-full px-2 py-2 space-y-1"> + {currentConversations.map((conversation) => ( + <ConversationItem + key={conversation.pubkey} + pubkey={conversation.pubkey} + isSelected={selectedPubkey === conversation.pubkey} + onClick={() => onSelectConversation(conversation.pubkey)} + lastMessage={conversation.lastMessage} + lastActivity={conversation.lastActivity} + hasNIP4Messages={conversation.hasNIP4Messages} + /> + ))} + </div> + </ScrollArea> + )} + </div> + </Card> + ); +}; diff --git a/src/components/dm/DMMessagingInterface.tsx b/src/components/dm/DMMessagingInterface.tsx new file mode 100644 index 0000000..200ed24 --- /dev/null +++ b/src/components/dm/DMMessagingInterface.tsx @@ -0,0 +1,84 @@ +import { useState, useCallback } from 'react'; +import { DMConversationList } from '@/components/dm/DMConversationList'; +import { DMChatArea } from '@/components/dm/DMChatArea'; +import { DMStatusInfo } from '@/components/dm/DMStatusInfo'; +import { useDMContext } from '@/hooks/useDMContext'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import { cn } from '@/lib/utils'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +interface DMMessagingInterfaceProps { + className?: string; +} + +export const DMMessagingInterface = ({ className }: DMMessagingInterfaceProps) => { + const [selectedPubkey, setSelectedPubkey] = useState<string | null>(null); + const [statusModalOpen, setStatusModalOpen] = useState(false); + const isMobile = useIsMobile(); + const { clearCacheAndRefetch } = useDMContext(); + + // On mobile, show only one panel at a time + const showConversationList = !isMobile || !selectedPubkey; + const showChatArea = !isMobile || selectedPubkey; + + const handleSelectConversation = useCallback((pubkey: string) => { + setSelectedPubkey(pubkey); + }, []); + + const handleBack = useCallback(() => { + setSelectedPubkey(null); + }, []); + + return ( + <> + {/* Status Modal */} + <Dialog open={statusModalOpen} onOpenChange={setStatusModalOpen}> + <DialogContent className="max-w-md"> + <DialogHeader> + <DialogTitle>Messaging Status</DialogTitle> + <DialogDescription> + View loading status, cache info, and connection details + </DialogDescription> + </DialogHeader> + <DMStatusInfo clearCacheAndRefetch={clearCacheAndRefetch} /> + </DialogContent> + </Dialog> + + <div className={cn("flex gap-4 overflow-hidden", className)}> + {/* Conversation List - Left Sidebar */} + <div className={cn( + "md:w-80 md:flex-shrink-0", + isMobile && !showConversationList && "hidden", + isMobile && showConversationList && "w-full" + )}> + <DMConversationList + selectedPubkey={selectedPubkey} + onSelectConversation={handleSelectConversation} + className="h-full" + onStatusClick={() => setStatusModalOpen(true)} + /> + </div> + + {/* Chat Area - Right Panel */} + <div className={cn( + "flex-1 md:min-w-0", + isMobile && !showChatArea && "hidden", + isMobile && showChatArea && "w-full" + )}> + <DMChatArea + pubkey={selectedPubkey} + onBack={isMobile ? handleBack : undefined} + className="h-full" + /> + </div> + </div> + </> + ); +}; + diff --git a/src/components/dm/DMStatusInfo.tsx b/src/components/dm/DMStatusInfo.tsx new file mode 100644 index 0000000..c44dfd4 --- /dev/null +++ b/src/components/dm/DMStatusInfo.tsx @@ -0,0 +1,214 @@ +import { useState } from 'react'; +import { RefreshCw, Database, Wifi, CheckCircle2, Loader2 } from 'lucide-react'; +import { useDMContext } from '@/hooks/useDMContext'; +import { LOADING_PHASES } from '@/lib/dmConstants'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { useToast } from '@/hooks/useToast'; + +interface DMStatusInfoProps { + clearCacheAndRefetch?: () => Promise<void>; +} + +export const DMStatusInfo = ({ clearCacheAndRefetch }: DMStatusInfoProps) => { + const [isClearing, setIsClearing] = useState(false); + const { toast } = useToast(); + const { + loadingPhase, + subscriptions, + scanProgress, + isDoingInitialLoad, + lastSync, + conversations, + } = useDMContext(); + + const handleClearCache = async () => { + if (!clearCacheAndRefetch) return; + + setIsClearing(true); + try { + await clearCacheAndRefetch(); + toast({ + title: 'Cache cleared', + description: 'Refetching messages from relays...', + }); + setIsClearing(false); + } catch (error) { + console.error('Error clearing cache:', error); + toast({ + title: 'Error', + description: 'Failed to clear cache. Please try again.', + variant: 'destructive', + }); + setIsClearing(false); + } + }; + + const getLoadingPhaseInfo = () => { + switch (loadingPhase) { + case LOADING_PHASES.IDLE: + return { label: 'Idle', description: 'Not yet initialized', icon: Loader2, color: 'text-muted-foreground' }; + case LOADING_PHASES.CACHE: + return { label: 'Loading from cache', description: 'Reading cached messages...', icon: Database, color: 'text-blue-500' }; + case LOADING_PHASES.RELAYS: + return { label: 'Loading from relays', description: 'Fetching messages from Nostr relays...', icon: Wifi, color: 'text-yellow-500' }; + case LOADING_PHASES.SUBSCRIPTIONS: + return { label: 'Connecting subscriptions', description: 'Setting up real-time message sync...', icon: RefreshCw, color: 'text-orange-500' }; + case LOADING_PHASES.READY: + return { label: 'Ready', description: 'All systems operational', icon: CheckCircle2, color: 'text-green-500' }; + default: + return { label: 'Unknown', description: 'Status unknown', icon: Loader2, color: 'text-muted-foreground' }; + } + }; + + const phaseInfo = getLoadingPhaseInfo(); + const PhaseIcon = phaseInfo.icon; + + const formatTimestamp = (timestamp: number | null) => { + if (!timestamp) return 'Never'; + const date = new Date(timestamp * 1000); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`; + return date.toLocaleDateString(); + }; + + return ( + <div className="space-y-4"> + {/* Loading Phase */} + <Card> + <CardContent className="pt-6"> + <div className="flex items-start gap-3"> + <PhaseIcon className={`h-5 w-5 ${phaseInfo.color} ${loadingPhase !== LOADING_PHASES.READY ? 'animate-pulse' : ''}`} /> + <div className="flex-1 space-y-1"> + <div className="flex items-center gap-2"> + <p className="font-medium">{phaseInfo.label}</p> + {isDoingInitialLoad && ( + <Badge variant="secondary" className="text-xs"> + Initial Load + </Badge> + )} + </div> + <p className="text-sm text-muted-foreground">{phaseInfo.description}</p> + </div> + </div> + </CardContent> + </Card> + + {/* Scan Progress */} + {(scanProgress.nip4 !== null || scanProgress.nip17 !== null) && ( + <Card> + <CardContent className="pt-6"> + <div className="space-y-3"> + <p className="text-sm font-medium">Scanning Messages</p> + {scanProgress.nip4 !== null && ( + <div className="space-y-1"> + <div className="flex items-center justify-between text-xs"> + <span className="text-muted-foreground">NIP-4 (Legacy)</span> + <span className="text-muted-foreground">{scanProgress.nip4.current} events</span> + </div> + <p className="text-xs text-muted-foreground">{scanProgress.nip4.status}</p> + </div> + )} + {scanProgress.nip17 !== null && ( + <div className="space-y-1"> + <div className="flex items-center justify-between text-xs"> + <span className="text-muted-foreground">NIP-17 (Private)</span> + <span className="text-muted-foreground">{scanProgress.nip17.current} events</span> + </div> + <p className="text-xs text-muted-foreground">{scanProgress.nip17.status}</p> + </div> + )} + </div> + </CardContent> + </Card> + )} + + {/* Subscriptions */} + <Card> + <CardContent className="pt-6"> + <div className="space-y-3"> + <p className="text-sm font-medium">Real-time Subscriptions</p> + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">NIP-4 (Legacy DMs)</span> + <Badge variant={subscriptions.isNIP4Connected ? 'default' : 'secondary'}> + {subscriptions.isNIP4Connected ? 'Connected' : 'Disconnected'} + </Badge> + </div> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">NIP-17 (Private DMs)</span> + <Badge variant={subscriptions.isNIP17Connected ? 'default' : 'secondary'}> + {subscriptions.isNIP17Connected ? 'Connected' : 'Disconnected'} + </Badge> + </div> + </div> + </div> + </CardContent> + </Card> + + {/* Cache Info */} + <Card> + <CardContent className="pt-6"> + <div className="space-y-3"> + <p className="text-sm font-medium">Cache Information</p> + <div className="space-y-2 text-sm"> + <div className="flex items-center justify-between"> + <span className="text-muted-foreground">Conversations</span> + <span className="font-medium">{conversations.length}</span> + </div> + <div className="flex items-center justify-between"> + <span className="text-muted-foreground">Last NIP-4 sync</span> + <span className="font-medium">{formatTimestamp(lastSync.nip4)}</span> + </div> + <div className="flex items-center justify-between"> + <span className="text-muted-foreground">Last NIP-17 sync</span> + <span className="font-medium">{formatTimestamp(lastSync.nip17)}</span> + </div> + </div> + </div> + </CardContent> + </Card> + + {/* Actions */} + {clearCacheAndRefetch && ( + <> + <Separator /> + <div className="space-y-3"> + <div className="space-y-1"> + <p className="text-sm font-medium">Cache Management</p> + <p className="text-xs text-muted-foreground"> + Clear all cached messages and refetch from relays. This will force a fresh sync. + </p> + </div> + <Button + onClick={handleClearCache} + disabled={isClearing} + variant="outline" + className="w-full" + > + {isClearing ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + Clearing... + </> + ) : ( + <> + <RefreshCw className="mr-2 h-4 w-4" /> + Clear Cache & Refetch + </> + )} + </Button> + </div> + </> + )} + </div> + ); +}; + diff --git a/components/ui/accordion.tsx b/src/components/ui/accordion.tsx similarity index 99% rename from components/ui/accordion.tsx rename to src/components/ui/accordion.tsx index 24c788c..e6a723d 100644 --- a/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as AccordionPrimitive from "@radix-ui/react-accordion" import { ChevronDown } from "lucide-react" diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..4bd7937 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button-variants" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.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} + /> +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> +>(({ className, ...props }, ref) => ( + <AlertDialogPortal> + <AlertDialogOverlay /> + <AlertDialogPrimitive.Content + ref={ref} + className={cn( + "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", + className + )} + {...props} + /> + </AlertDialogPortal> +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col space-y-2 text-center sm:text-left", + className + )} + {...props} + /> +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn( + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", + className + )} + {...props} + /> +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Title + ref={ref} + className={cn("text-lg font-semibold", className)} + {...props} + /> +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Description + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Action>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Action + ref={ref} + className={cn(buttonVariants(), className)} + {...props} + /> +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef<typeof AlertDialogPrimitive.Cancel>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> +>(({ className, ...props }, ref) => ( + <AlertDialogPrimitive.Cancel + ref={ref} + className={cn( + buttonVariants({ variant: "outline" }), + "mt-2 sm:mt-0", + className + )} + {...props} + /> +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/src/components/ui/alert.tsx similarity index 100% rename from components/ui/alert.tsx rename to src/components/ui/alert.tsx diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/components/ui/avatar.tsx b/src/components/ui/avatar.tsx similarity index 94% rename from components/ui/avatar.tsx rename to src/components/ui/avatar.tsx index 51e507b..2a948cb 100644 --- a/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar" @@ -26,7 +24,7 @@ const AvatarImage = React.forwardRef< >(({ className, ...props }, ref) => ( <AvatarPrimitive.Image ref={ref} - className={cn("aspect-square h-full w-full", className)} + className={cn("aspect-square h-full w-full object-cover", className)} {...props} /> )) diff --git a/components/ui/badge.tsx b/src/components/ui/badge-variants.ts similarity index 58% rename from components/ui/badge.tsx rename to src/components/ui/badge-variants.ts index f000e3e..f6b20a6 100644 --- a/components/ui/badge.tsx +++ b/src/components/ui/badge-variants.ts @@ -1,9 +1,6 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const badgeVariants = cva( +export const badgeVariants = cva( "inline-flex items-center rounded-full 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: { @@ -21,16 +18,4 @@ const badgeVariants = cva( 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 } +) \ No newline at end of file diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..0abefc2 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,17 @@ +import * as React from "react" +import { type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { badgeVariants } from "./badge-variants" + +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 } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..71a5c32 --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) +Breadcrumb.displayName = "Breadcrumb" + +const BreadcrumbList = React.forwardRef< + HTMLOListElement, + React.ComponentPropsWithoutRef<"ol"> +>(({ className, ...props }, ref) => ( + <ol + ref={ref} + className={cn( + "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", + className + )} + {...props} + /> +)) +BreadcrumbList.displayName = "BreadcrumbList" + +const BreadcrumbItem = React.forwardRef< + HTMLLIElement, + React.ComponentPropsWithoutRef<"li"> +>(({ className, ...props }, ref) => ( + <li + ref={ref} + className={cn("inline-flex items-center gap-1.5", className)} + {...props} + /> +)) +BreadcrumbItem.displayName = "BreadcrumbItem" + +const BreadcrumbLink = React.forwardRef< + HTMLAnchorElement, + React.ComponentPropsWithoutRef<"a"> & { + asChild?: boolean + } +>(({ asChild, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a" + + return ( + <Comp + ref={ref} + className={cn("transition-colors hover:text-foreground", className)} + {...props} + /> + ) +}) +BreadcrumbLink.displayName = "BreadcrumbLink" + +const BreadcrumbPage = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef<"span"> +>(({ className, ...props }, ref) => ( + <span + ref={ref} + role="link" + aria-disabled="true" + aria-current="page" + className={cn("font-normal text-foreground", className)} + {...props} + /> +)) +BreadcrumbPage.displayName = "BreadcrumbPage" + +const BreadcrumbSeparator = ({ + children, + className, + ...props +}: React.ComponentProps<"li">) => ( + <li + role="presentation" + aria-hidden="true" + className={cn("[&>svg]:size-3.5", className)} + {...props} + > + {children ?? <ChevronRight />} + </li> +) +BreadcrumbSeparator.displayName = "BreadcrumbSeparator" + +const BreadcrumbEllipsis = ({ + className, + ...props +}: React.ComponentProps<"span">) => ( + <span + role="presentation" + aria-hidden="true" + className={cn("flex h-9 w-9 items-center justify-center", className)} + {...props} + > + <MoreHorizontal className="h-4 w-4" /> + <span className="sr-only">More</span> + </span> +) +BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" + +export { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, + BreadcrumbEllipsis, +} diff --git a/src/components/ui/button-variants.ts b/src/components/ui/button-variants.ts new file mode 100644 index 0000000..63a20d3 --- /dev/null +++ b/src/components/ui/button-variants.ts @@ -0,0 +1,30 @@ +import { cva } from "class-variance-authority" + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 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 hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..25fa2bd --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "./button-variants" + +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 } diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..169142b --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,64 @@ +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { DayPicker } from "react-day-picker"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button-variants"; + +export type CalendarProps = React.ComponentProps<typeof DayPicker>; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + <DayPicker + showOutsideDays={showOutsideDays} + className={cn("p-3", className)} + classNames={{ + months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", + month: "space-y-4", + caption: "flex justify-center pt-1 relative items-center", + caption_label: "text-sm font-medium", + nav: "space-x-1 flex items-center", + nav_button: cn( + buttonVariants({ variant: "outline" }), + "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" + ), + nav_button_previous: "absolute left-1", + nav_button_next: "absolute right-1", + table: "w-full border-collapse space-y-1", + head_row: "flex", + head_cell: + "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", + row: "flex w-full mt-2", + cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", + day: cn( + buttonVariants({ variant: "ghost" }), + "h-9 w-9 p-0 font-normal aria-selected:opacity-100" + ), + day_range_end: "day-range-end", + day_selected: + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", + day_today: "bg-accent text-accent-foreground", + day_outside: + "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", + day_disabled: "text-muted-foreground opacity-50", + day_range_middle: + "aria-selected:bg-accent aria-selected:text-accent-foreground", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />, + IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />, + }} + {...props} + /> + ); +} +Calendar.displayName = "Calendar"; + +export { Calendar }; diff --git a/components/ui/card.tsx b/src/components/ui/card.tsx similarity index 86% rename from components/ui/card.tsx rename to src/components/ui/card.tsx index 7dea9a1..afa13ec 100644 --- a/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -76,12 +76,4 @@ const CardFooter = React.forwardRef< )) CardFooter.displayName = "CardFooter" -const SmallCardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div ref={ref} className={cn(className)} {...props} /> -)) -SmallCardContent.displayName = "CardContent" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent, SmallCardContent } +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/carousel.tsx b/src/components/ui/carousel.tsx similarity index 99% rename from components/ui/carousel.tsx rename to src/components/ui/carousel.tsx index ec505d0..9c2b9bf 100644 --- a/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import useEmblaCarousel, { type UseEmblaCarouselType, diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx new file mode 100644 index 0000000..a21d77e --- /dev/null +++ b/src/components/ui/chart.tsx @@ -0,0 +1,363 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record<keyof typeof THEMES, string> } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext<ChartContextProps | null>(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a <ChartContainer />") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + <ChartContext.Provider value={{ config }}> + <div + data-chart={chartId} + ref={ref} + className={cn( + "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", + className + )} + {...props} + > + <ChartStyle id={chartId} config={config} /> + <RechartsPrimitive.ResponsiveContainer> + {children} + </RechartsPrimitive.ResponsiveContainer> + </div> + </ChartContext.Provider> + ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( + <style + dangerouslySetInnerHTML={{ + __html: Object.entries(THEMES) + .map( + ([theme, prefix]) => ` +${prefix} [data-chart=${id}] { +${colorConfig + .map(([key, itemConfig]) => { + const color = + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || + itemConfig.color + return color ? ` --color-${key}: ${color};` : null + }) + .join("\n")} +} +` + ) + .join("\n"), + }} + /> + ) +} + +const ChartTooltip = RechartsPrimitive.Tooltip + +const ChartTooltipContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<typeof RechartsPrimitive.Tooltip> & + React.ComponentProps<"div"> & { + hideLabel?: boolean + hideIndicator?: boolean + indicator?: "line" | "dot" | "dashed" + nameKey?: string + labelKey?: string + } +>( + ( + { + active, + payload, + className, + indicator = "dot", + hideLabel = false, + hideIndicator = false, + label, + labelFormatter, + labelClassName, + formatter, + color, + nameKey, + labelKey, + }, + ref + ) => { + const { config } = useChart() + + const tooltipLabel = React.useMemo(() => { + if (hideLabel || !payload?.length) { + return null + } + + const [item] = payload + const key = `${labelKey || item.dataKey || item.name || "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) + const value = + !labelKey && typeof label === "string" + ? config[label as keyof typeof config]?.label || label + : itemConfig?.label + + if (labelFormatter) { + return ( + <div className={cn("font-medium", labelClassName)}> + {labelFormatter(value, payload)} + </div> + ) + } + + if (!value) { + return null + } + + return <div className={cn("font-medium", labelClassName)}>{value}</div> + }, [ + label, + labelFormatter, + payload, + hideLabel, + labelClassName, + config, + labelKey, + ]) + + if (!active || !payload?.length) { + return null + } + + const nestLabel = payload.length === 1 && indicator !== "dot" + + return ( + <div + ref={ref} + className={cn( + "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", + className + )} + > + {!nestLabel ? tooltipLabel : null} + <div className="grid gap-1.5"> + {payload.map((item, index) => { + const key = `${nameKey || item.name || item.dataKey || "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) + const indicatorColor = color || item.payload.fill || item.color + + return ( + <div + key={item.dataKey} + className={cn( + "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", + indicator === "dot" && "items-center" + )} + > + {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + <itemConfig.icon /> + ) : ( + !hideIndicator && ( + <div + className={cn( + "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", + { + "h-2.5 w-2.5": indicator === "dot", + "w-1": indicator === "line", + "w-0 border-[1.5px] border-dashed bg-transparent": + indicator === "dashed", + "my-0.5": nestLabel && indicator === "dashed", + } + )} + style={ + { + "--color-bg": indicatorColor, + "--color-border": indicatorColor, + } as React.CSSProperties + } + /> + ) + )} + <div + className={cn( + "flex flex-1 justify-between leading-none", + nestLabel ? "items-end" : "items-center" + )} + > + <div className="grid gap-1.5"> + {nestLabel ? tooltipLabel : null} + <span className="text-muted-foreground"> + {itemConfig?.label || item.name} + </span> + </div> + {item.value && ( + <span className="font-mono font-medium tabular-nums text-foreground"> + {item.value.toLocaleString()} + </span> + )} + </div> + </> + )} + </div> + ) + })} + </div> + </div> + ) + } +) +ChartTooltipContent.displayName = "ChartTooltip" + +const ChartLegend = RechartsPrimitive.Legend + +const ChartLegendContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & + Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { + hideIcon?: boolean + nameKey?: string + } +>( + ( + { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, + ref + ) => { + const { config } = useChart() + + if (!payload?.length) { + return null + } + + return ( + <div + ref={ref} + className={cn( + "flex items-center justify-center gap-4", + verticalAlign === "top" ? "pb-3" : "pt-3", + className + )} + > + {payload.map((item) => { + const key = `${nameKey || item.dataKey || "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) + + return ( + <div + key={item.value} + className={cn( + "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" + )} + > + {itemConfig?.icon && !hideIcon ? ( + <itemConfig.icon /> + ) : ( + <div + className="h-2 w-2 shrink-0 rounded-[2px]" + style={{ + backgroundColor: item.color, + }} + /> + )} + {itemConfig?.label} + </div> + ) + })} + </div> + ) + } +) +ChartLegendContent.displayName = "ChartLegend" + +// Helper to extract item config from a payload. +function getPayloadConfigFromPayload( + config: ChartConfig, + payload: unknown, + key: string +) { + if (typeof payload !== "object" || payload === null) { + return undefined + } + + const payloadPayload = + "payload" in payload && + typeof payload.payload === "object" && + payload.payload !== null + ? payload.payload + : undefined + + let configLabelKey: string = key + + if ( + key in payload && + typeof payload[key as keyof typeof payload] === "string" + ) { + configLabelKey = payload[key as keyof typeof payload] as string + } else if ( + payloadPayload && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" + ) { + configLabelKey = payloadPayload[ + key as keyof typeof payloadPayload + ] as string + } + + return configLabelKey in config + ? config[configLabelKey] + : config[key as keyof typeof config] +} + +export { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + ChartLegend, + ChartLegendContent, + ChartStyle, +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..ddbdd01 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef<typeof CheckboxPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> +>(({ className, ...props }, ref) => ( + <CheckboxPrimitive.Root + ref={ref} + className={cn( + "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", + className + )} + {...props} + > + <CheckboxPrimitive.Indicator + className={cn("flex items-center justify-center text-current")} + > + <Check className="h-4 w-4" /> + </CheckboxPrimitive.Indicator> + </CheckboxPrimitive.Root> +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..a23e7a2 --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..69d63a4 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef<typeof CommandPrimitive>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive> +>(({ className, ...props }, ref) => ( + <CommandPrimitive + ref={ref} + className={cn( + "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", + className + )} + {...props} + /> +)) +Command.displayName = CommandPrimitive.displayName + +type CommandDialogProps = DialogProps + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + <Dialog {...props}> + <DialogContent className="overflow-hidden p-0 shadow-lg"> + <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> + {children} + </Command> + </DialogContent> + </Dialog> + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Input>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> +>(({ className, ...props }, ref) => ( + <div className="flex items-center border-b px-3" cmdk-input-wrapper=""> + <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> + <CommandPrimitive.Input + ref={ref} + className={cn( + "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + /> + </div> +)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.List>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.List + ref={ref} + className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} + {...props} + /> +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Empty>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> +>((props, ref) => ( + <CommandPrimitive.Empty + ref={ref} + className="py-6 text-center text-sm" + {...props} + /> +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Group>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Group + ref={ref} + className={cn( + "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", + className + )} + {...props} + /> +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Separator + ref={ref} + className={cn("-mx-1 h-px bg-border", className)} + {...props} + /> +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50", + className + )} + {...props} + /> +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..3e52999 --- /dev/null +++ b/src/components/ui/context-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + <ContextMenuPrimitive.SubTrigger + ref={ref} + className={cn( + "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", + inset && "pl-8", + className + )} + {...props} + > + {children} + <ChevronRight className="ml-auto h-4 w-4" /> + </ContextMenuPrimitive.SubTrigger> +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> +>(({ className, ...props }, ref) => ( + <ContextMenuPrimitive.SubContent + ref={ref} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> +>(({ className, ...props }, ref) => ( + <ContextMenuPrimitive.Portal> + <ContextMenuPrimitive.Content + ref={ref} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> + </ContextMenuPrimitive.Portal> +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + <ContextMenuPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + inset && "pl-8", + className + )} + {...props} + /> +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> +>(({ className, children, checked, ...props }, ref) => ( + <ContextMenuPrimitive.CheckboxItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className + )} + checked={checked} + {...props} + > + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <ContextMenuPrimitive.ItemIndicator> + <Check className="h-4 w-4" /> + </ContextMenuPrimitive.ItemIndicator> + </span> + {children} + </ContextMenuPrimitive.CheckboxItem> +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.RadioItem>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> +>(({ className, children, ...props }, ref) => ( + <ContextMenuPrimitive.RadioItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className + )} + {...props} + > + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <ContextMenuPrimitive.ItemIndicator> + <Circle className="h-2 w-2 fill-current" /> + </ContextMenuPrimitive.ItemIndicator> + </span> + {children} + </ContextMenuPrimitive.RadioItem> +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + <ContextMenuPrimitive.Label + ref={ref} + className={cn( + "px-2 py-1.5 text-sm font-semibold text-foreground", + inset && "pl-8", + className + )} + {...props} + /> +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <ContextMenuPrimitive.Separator + ref={ref} + className={cn("-mx-1 my-1 h-px bg-border", className)} + {...props} + /> +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/components/ui/dialog.tsx b/src/components/ui/dialog.tsx similarity index 99% rename from components/ui/dialog.tsx rename to src/components/ui/dialog.tsx index 01ff19c..c23630e 100644 --- a/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react" diff --git a/components/ui/drawer.tsx b/src/components/ui/drawer.tsx similarity index 99% rename from components/ui/drawer.tsx rename to src/components/ui/drawer.tsx index 6a0ef53..c17b0cc 100644 --- a/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import { Drawer as DrawerPrimitive } from "vaul" diff --git a/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx similarity index 99% rename from components/ui/dropdown-menu.tsx rename to src/components/ui/dropdown-menu.tsx index f69a0d6..769ff7a 100644 --- a/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react" diff --git a/src/components/ui/form-utils.ts b/src/components/ui/form-utils.ts new file mode 100644 index 0000000..c765ff2 --- /dev/null +++ b/src/components/ui/form-utils.ts @@ -0,0 +1,48 @@ +import * as React from "react" +import { + FieldPath, + FieldValues, + useFormContext, +} from "react-hook-form" + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> +> = { + name: TName +} + +export const FormFieldContext = React.createContext<FormFieldContextValue>( + {} as FormFieldContextValue +) + +type FormItemContextValue = { + id: string +} + +export const FormItemContext = React.createContext<FormItemContextValue>( + {} as FormItemContextValue +) + +export const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within <FormField>") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} \ No newline at end of file diff --git a/components/ui/form.tsx b/src/components/ui/form.tsx similarity index 74% rename from components/ui/form.tsx rename to src/components/ui/form.tsx index 4603f8b..b7a6301 100644 --- a/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -7,25 +7,14 @@ import { FieldPath, FieldValues, FormProvider, - useFormContext, } from "react-hook-form" import { cn } from "@/lib/utils" import { Label } from "@/components/ui/label" +import { FormFieldContext, FormItemContext, useFormField } from "./form-utils" const Form = FormProvider -type FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> -> = { - name: TName -} - -const FormFieldContext = React.createContext<FormFieldContextValue>( - {} as FormFieldContextValue -) - const FormField = < TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> @@ -39,37 +28,6 @@ const FormField = < ) } -const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState, formState } = useFormContext() - - const fieldState = getFieldState(fieldContext.name, formState) - - if (!fieldContext) { - throw new Error("useFormField should be used within <FormField>") - } - - const { id } = itemContext - - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - } -} - -type FormItemContextValue = { - id: string -} - -const FormItemContext = React.createContext<FormItemContextValue>( - {} as FormItemContextValue -) - const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes<HTMLDivElement> @@ -165,7 +123,6 @@ const FormMessage = React.forwardRef< FormMessage.displayName = "FormMessage" export { - useFormField, Form, FormItem, FormLabel, diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx new file mode 100644 index 0000000..863ff01 --- /dev/null +++ b/src/components/ui/hover-card.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef<typeof HoverCardPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + <HoverCardPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + className={cn( + "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx new file mode 100644 index 0000000..ebf5c28 --- /dev/null +++ b/src/components/ui/input-otp.tsx @@ -0,0 +1,69 @@ +import * as React from "react" +import { OTPInput, OTPInputContext } from "input-otp" +import { Dot } from "lucide-react" + +import { cn } from "@/lib/utils" + +const InputOTP = React.forwardRef< + React.ElementRef<typeof OTPInput>, + React.ComponentPropsWithoutRef<typeof OTPInput> +>(({ className, containerClassName, ...props }, ref) => ( + <OTPInput + ref={ref} + containerClassName={cn( + "flex items-center gap-2 has-[:disabled]:opacity-50", + containerClassName + )} + className={cn("disabled:cursor-not-allowed", className)} + {...props} + /> +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( + <div ref={ref} className={cn("flex items-center", className)} {...props} /> +)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext) + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] + + return ( + <div + ref={ref} + className={cn( + "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md", + isActive && "z-10 ring-2 ring-ring ring-offset-background", + className + )} + {...props} + > + {char} + {hasFakeCaret && ( + <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> + <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> + </div> + )} + </div> + ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( + <div ref={ref} role="separator" {...props}> + <Dot /> + </div> +)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..68551b9 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( + ({ className, type, ...props }, ref) => { + return ( + <input + type={type} + className={cn( + "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + className + )} + ref={ref} + {...props} + /> + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/components/ui/label.tsx b/src/components/ui/label.tsx similarity index 98% rename from components/ui/label.tsx rename to src/components/ui/label.tsx index 5341821..683faa7 100644 --- a/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { cva, type VariantProps } from "class-variance-authority" diff --git a/src/components/ui/menubar.tsx b/src/components/ui/menubar.tsx new file mode 100644 index 0000000..d11c299 --- /dev/null +++ b/src/components/ui/menubar.tsx @@ -0,0 +1,234 @@ +import * as React from "react" +import * as MenubarPrimitive from "@radix-ui/react-menubar" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const MenubarMenu = MenubarPrimitive.Menu + +const MenubarGroup = MenubarPrimitive.Group + +const MenubarPortal = MenubarPrimitive.Portal + +const MenubarSub = MenubarPrimitive.Sub + +const MenubarRadioGroup = MenubarPrimitive.RadioGroup + +const Menubar = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.Root + ref={ref} + className={cn( + "flex h-10 items-center space-x-1 rounded-md border bg-background p-1", + className + )} + {...props} + /> +)) +Menubar.displayName = MenubarPrimitive.Root.displayName + +const MenubarTrigger = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.Trigger + ref={ref} + className={cn( + "flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", + className + )} + {...props} + /> +)) +MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName + +const MenubarSubTrigger = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + <MenubarPrimitive.SubTrigger + ref={ref} + className={cn( + "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", + inset && "pl-8", + className + )} + {...props} + > + {children} + <ChevronRight className="ml-auto h-4 w-4" /> + </MenubarPrimitive.SubTrigger> +)) +MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName + +const MenubarSubContent = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.SubContent + ref={ref} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> +)) +MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName + +const MenubarContent = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> +>( + ( + { className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, + ref + ) => ( + <MenubarPrimitive.Portal> + <MenubarPrimitive.Content + ref={ref} + align={align} + alignOffset={alignOffset} + sideOffset={sideOffset} + className={cn( + "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> + </MenubarPrimitive.Portal> + ) +) +MenubarContent.displayName = MenubarPrimitive.Content.displayName + +const MenubarItem = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + <MenubarPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + inset && "pl-8", + className + )} + {...props} + /> +)) +MenubarItem.displayName = MenubarPrimitive.Item.displayName + +const MenubarCheckboxItem = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.CheckboxItem>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> +>(({ className, children, checked, ...props }, ref) => ( + <MenubarPrimitive.CheckboxItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className + )} + checked={checked} + {...props} + > + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <MenubarPrimitive.ItemIndicator> + <Check className="h-4 w-4" /> + </MenubarPrimitive.ItemIndicator> + </span> + {children} + </MenubarPrimitive.CheckboxItem> +)) +MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName + +const MenubarRadioItem = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.RadioItem>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> +>(({ className, children, ...props }, ref) => ( + <MenubarPrimitive.RadioItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className + )} + {...props} + > + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <MenubarPrimitive.ItemIndicator> + <Circle className="h-2 w-2 fill-current" /> + </MenubarPrimitive.ItemIndicator> + </span> + {children} + </MenubarPrimitive.RadioItem> +)) +MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName + +const MenubarLabel = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + <MenubarPrimitive.Label + ref={ref} + className={cn( + "px-2 py-1.5 text-sm font-semibold", + inset && "pl-8", + className + )} + {...props} + /> +)) +MenubarLabel.displayName = MenubarPrimitive.Label.displayName + +const MenubarSeparator = React.forwardRef< + React.ElementRef<typeof MenubarPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <MenubarPrimitive.Separator + ref={ref} + className={cn("-mx-1 my-1 h-px bg-muted", className)} + {...props} + /> +)) +MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName + +const MenubarShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ) +} +MenubarShortcut.displayname = "MenubarShortcut" + +export { + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarLabel, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarPortal, + MenubarSubContent, + MenubarSubTrigger, + MenubarGroup, + MenubarSub, + MenubarShortcut, +} diff --git a/src/components/ui/navigation-menu-variants.ts b/src/components/ui/navigation-menu-variants.ts new file mode 100644 index 0000000..403e12f --- /dev/null +++ b/src/components/ui/navigation-menu-variants.ts @@ -0,0 +1,5 @@ +import { cva } from "class-variance-authority" + +export const navigationMenuTriggerStyle = cva( + "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" +) \ No newline at end of file diff --git a/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx similarity index 90% rename from components/ui/navigation-menu.tsx rename to src/components/ui/navigation-menu.tsx index 1419f56..0e1499b 100644 --- a/components/ui/navigation-menu.tsx +++ b/src/components/ui/navigation-menu.tsx @@ -1,9 +1,9 @@ import * as React from "react" import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" -import { cva } from "class-variance-authority" import { ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" +import { navigationMenuTriggerStyle } from "./navigation-menu-variants" const NavigationMenu = React.forwardRef< React.ElementRef<typeof NavigationMenuPrimitive.Root>, @@ -40,10 +40,6 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName const NavigationMenuItem = NavigationMenuPrimitive.Item -const navigationMenuTriggerStyle = cva( - "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" -) - const NavigationMenuTrigger = React.forwardRef< React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> @@ -116,7 +112,6 @@ NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName export { - navigationMenuTriggerStyle, NavigationMenu, NavigationMenuList, NavigationMenuItem, diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx new file mode 100644 index 0000000..5bacb4a --- /dev/null +++ b/src/components/ui/pagination.tsx @@ -0,0 +1,118 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" +import { ButtonProps } from "@/components/ui/button" +import { buttonVariants } from "@/components/ui/button-variants" + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( + <nav + role="navigation" + aria-label="pagination" + className={cn("mx-auto flex w-full justify-center", className)} + {...props} + /> +) +Pagination.displayName = "Pagination" + +const PaginationContent = React.forwardRef< + HTMLUListElement, + React.ComponentProps<"ul"> +>(({ className, ...props }, ref) => ( + <ul + ref={ref} + className={cn("flex flex-row items-center gap-1", className)} + {...props} + /> +)) +PaginationContent.displayName = "PaginationContent" + +const PaginationItem = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> +>(({ className, ...props }, ref) => ( + <li ref={ref} className={cn("", className)} {...props} /> +)) +PaginationItem.displayName = "PaginationItem" + +type PaginationLinkProps = { + isActive?: boolean +} & Pick<ButtonProps, "size"> & + React.ComponentProps<"a"> + +const PaginationLink = ({ + className, + isActive, + size = "icon", + ...props +}: PaginationLinkProps) => ( + <a + aria-current={isActive ? "page" : undefined} + className={cn( + buttonVariants({ + variant: isActive ? "outline" : "ghost", + size, + }), + className + )} + {...props} + /> +) +PaginationLink.displayName = "PaginationLink" + +const PaginationPrevious = ({ + className, + ...props +}: React.ComponentProps<typeof PaginationLink>) => ( + <PaginationLink + aria-label="Go to previous page" + size="default" + className={cn("gap-1 pl-2.5", className)} + {...props} + > + <ChevronLeft className="h-4 w-4" /> + <span>Previous</span> + </PaginationLink> +) +PaginationPrevious.displayName = "PaginationPrevious" + +const PaginationNext = ({ + className, + ...props +}: React.ComponentProps<typeof PaginationLink>) => ( + <PaginationLink + aria-label="Go to next page" + size="default" + className={cn("gap-1 pr-2.5", className)} + {...props} + > + <span>Next</span> + <ChevronRight className="h-4 w-4" /> + </PaginationLink> +) +PaginationNext.displayName = "PaginationNext" + +const PaginationEllipsis = ({ + className, + ...props +}: React.ComponentProps<"span">) => ( + <span + aria-hidden + className={cn("flex h-9 w-9 items-center justify-center", className)} + {...props} + > + <MoreHorizontal className="h-4 w-4" /> + <span className="sr-only">More pages</span> + </span> +) +PaginationEllipsis.displayName = "PaginationEllipsis" + +export { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..bbba7e0 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef<typeof PopoverPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + <PopoverPrimitive.Portal> + <PopoverPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + className={cn( + "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> + </PopoverPrimitive.Portal> +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..105fb65 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef<typeof ProgressPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> +>(({ className, value, ...props }, ref) => ( + <ProgressPrimitive.Root + ref={ref} + className={cn( + "relative h-4 w-full overflow-hidden rounded-full bg-secondary", + className + )} + {...props} + > + <ProgressPrimitive.Indicator + className="h-full w-full flex-1 bg-primary transition-all" + style={{ transform: `translateX(-${100 - (value || 0)}%)` }} + /> + </ProgressPrimitive.Root> +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..43b43b4 --- /dev/null +++ b/src/components/ui/radio-group.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef<typeof RadioGroupPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> +>(({ className, ...props }, ref) => { + return ( + <RadioGroupPrimitive.Root + className={cn("grid gap-2", className)} + {...props} + ref={ref} + /> + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef<typeof RadioGroupPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> +>(({ className, ...props }, ref) => { + return ( + <RadioGroupPrimitive.Item + ref={ref} + className={cn( + "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + > + <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> + <Circle className="h-2.5 w-2.5 fill-current text-current" /> + </RadioGroupPrimitive.Indicator> + </RadioGroupPrimitive.Item> + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx new file mode 100644 index 0000000..cd3cb0e --- /dev/null +++ b/src/components/ui/resizable.tsx @@ -0,0 +1,43 @@ +import { GripVertical } from "lucide-react" +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "@/lib/utils" + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => ( + <ResizablePrimitive.PanelGroup + className={cn( + "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", + className + )} + {...props} + /> +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { + withHandle?: boolean +}) => ( + <ResizablePrimitive.PanelResizeHandle + className={cn( + "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( + <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border"> + <GripVertical className="h-2.5 w-2.5" /> + </div> + )} + </ResizablePrimitive.PanelResizeHandle> +) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx similarity index 97% rename from components/ui/scroll-area.tsx rename to src/components/ui/scroll-area.tsx index 0b4a48d..cff30bf 100644 --- a/components/ui/scroll-area.tsx +++ b/src/components/ui/scroll-area.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" @@ -14,7 +12,7 @@ const ScrollArea = React.forwardRef< className={cn("relative overflow-hidden", className)} {...props} > - <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> + <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block [&>div]:!w-full"> {children} </ScrollAreaPrimitive.Viewport> <ScrollBar /> diff --git a/components/ui/select.tsx b/src/components/ui/select.tsx similarity index 99% rename from components/ui/select.tsx rename to src/components/ui/select.tsx index cbe5a36..fe56d4d 100644 --- a/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as SelectPrimitive from "@radix-ui/react-select" import { Check, ChevronDown, ChevronUp } from "lucide-react" diff --git a/components/ui/separator.tsx b/src/components/ui/separator.tsx similarity index 98% rename from components/ui/separator.tsx rename to src/components/ui/separator.tsx index 12d81c4..6d7f122 100644 --- a/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as SeparatorPrimitive from "@radix-ui/react-separator" diff --git a/components/ui/sheet.tsx b/src/components/ui/sheet.tsx similarity index 95% rename from components/ui/sheet.tsx rename to src/components/ui/sheet.tsx index a37f17b..7b1dbe4 100644 --- a/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -1,9 +1,7 @@ -"use client" - -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 * as React from "react" import { cn } from "@/lib/utils" @@ -51,7 +49,7 @@ const sheetVariants = cva( interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, - VariantProps<typeof sheetVariants> {} + VariantProps<typeof sheetVariants> { } const SheetContent = React.forwardRef< React.ElementRef<typeof SheetPrimitive.Content>, @@ -127,14 +125,7 @@ const SheetDescription = React.forwardRef< SheetDescription.displayName = SheetPrimitive.Description.displayName export { - Sheet, - SheetPortal, - SheetOverlay, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, + Sheet, SheetClose, + SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger } + diff --git a/src/components/ui/sidebar-utils.ts b/src/components/ui/sidebar-utils.ts new file mode 100644 index 0000000..01c777b --- /dev/null +++ b/src/components/ui/sidebar-utils.ts @@ -0,0 +1,52 @@ +import * as React from "react" +import { cva } from "class-variance-authority" + +export const SIDEBAR_COOKIE_NAME = "sidebar:state" +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +export const SIDEBAR_WIDTH = "16rem" +export const SIDEBAR_WIDTH_MOBILE = "18rem" +export const SIDEBAR_WIDTH_ICON = "3rem" +export const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +export type SidebarContext = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +export const SidebarContext = React.createContext<SidebarContext | null>(null) + +export function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +export const sidebarMenuButtonVariants = cva( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + }, + size: { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) \ No newline at end of file diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..51a2b57 --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,724 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps } from "class-variance-authority" +import { PanelLeft } from "lucide-react" + +import { useIsMobile } from "@/hooks/useIsMobile" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { Sheet, SheetContent } from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + SIDEBAR_COOKIE_NAME, + SIDEBAR_COOKIE_MAX_AGE, + SIDEBAR_WIDTH, + SIDEBAR_WIDTH_MOBILE, + SIDEBAR_WIDTH_ICON, + SIDEBAR_KEYBOARD_SHORTCUT, + SidebarContext, + useSidebar, + sidebarMenuButtonVariants, + type SidebarContext as SidebarContextType, +} from "./sidebar-utils" + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo<SidebarContextType>( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + <SidebarContext.Provider value={contextValue}> + <TooltipProvider delayDuration={0}> + <div + style={ + { + "--sidebar-width": SIDEBAR_WIDTH, + "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, + ...style, + } as React.CSSProperties + } + className={cn( + "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", + className + )} + ref={ref} + {...props} + > + {children} + </div> + </TooltipProvider> + </SidebarContext.Provider> + ) + } +) +SidebarProvider.displayName = "SidebarProvider" + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( + <div + className={cn( + "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", + className + )} + ref={ref} + {...props} + > + {children} + </div> + ) + } + + if (isMobile) { + return ( + <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> + <SheetContent + data-sidebar="sidebar" + data-mobile="true" + className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" + style={ + { + "--sidebar-width": SIDEBAR_WIDTH_MOBILE, + } as React.CSSProperties + } + side={side} + > + <div className="flex h-full w-full flex-col">{children}</div> + </SheetContent> + </Sheet> + ) + } + + return ( + <div + ref={ref} + className="group peer hidden md:block text-sidebar-foreground" + data-state={state} + data-collapsible={state === "collapsed" ? collapsible : ""} + data-variant={variant} + data-side={side} + > + {/* This is what handles the sidebar gap on desktop */} + <div + className={cn( + "duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear", + "group-data-[collapsible=offcanvas]:w-0", + "group-data-[side=right]:rotate-180", + variant === "floating" || variant === "inset" + ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" + : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]" + )} + /> + <div + className={cn( + "duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex", + side === "left" + ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" + : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", + // Adjust the padding for floating and inset variants. + variant === "floating" || variant === "inset" + ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" + : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l", + className + )} + {...props} + > + <div + data-sidebar="sidebar" + className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow" + > + {children} + </div> + </div> + </div> + ) + } +) +Sidebar.displayName = "Sidebar" + +const SidebarTrigger = React.forwardRef< + React.ElementRef<typeof Button>, + React.ComponentProps<typeof Button> +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + <Button + ref={ref} + data-sidebar="trigger" + variant="ghost" + size="icon" + className={cn("h-7 w-7", className)} + onClick={(event) => { + onClick?.(event) + toggleSidebar() + }} + {...props} + > + <PanelLeft /> + <span className="sr-only">Toggle Sidebar</span> + </Button> + ) +}) +SidebarTrigger.displayName = "SidebarTrigger" + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + <button + ref={ref} + data-sidebar="rail" + aria-label="Toggle Sidebar" + tabIndex={-1} + onClick={toggleSidebar} + title="Toggle Sidebar" + className={cn( + "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", + "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize", + "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", + "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar", + "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", + "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", + className + )} + {...props} + /> + ) +}) +SidebarRail.displayName = "SidebarRail" + +const SidebarInset = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"main"> +>(({ className, ...props }, ref) => { + return ( + <main + ref={ref} + className={cn( + "relative flex min-h-svh flex-1 flex-col bg-background", + "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow", + className + )} + {...props} + /> + ) +}) +SidebarInset.displayName = "SidebarInset" + +const SidebarInput = React.forwardRef< + React.ElementRef<typeof Input>, + React.ComponentProps<typeof Input> +>(({ className, ...props }, ref) => { + return ( + <Input + ref={ref} + data-sidebar="input" + className={cn( + "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", + className + )} + {...props} + /> + ) +}) +SidebarInput.displayName = "SidebarInput" + +const SidebarHeader = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + <div + ref={ref} + data-sidebar="header" + className={cn("flex flex-col gap-2 p-2", className)} + {...props} + /> + ) +}) +SidebarHeader.displayName = "SidebarHeader" + +const SidebarFooter = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + <div + ref={ref} + data-sidebar="footer" + className={cn("flex flex-col gap-2 p-2", className)} + {...props} + /> + ) +}) +SidebarFooter.displayName = "SidebarFooter" + +const SidebarSeparator = React.forwardRef< + React.ElementRef<typeof Separator>, + React.ComponentProps<typeof Separator> +>(({ className, ...props }, ref) => { + return ( + <Separator + ref={ref} + data-sidebar="separator" + className={cn("mx-2 w-auto bg-sidebar-border", className)} + {...props} + /> + ) +}) +SidebarSeparator.displayName = "SidebarSeparator" + +const SidebarContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + <div + ref={ref} + data-sidebar="content" + className={cn( + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", + className + )} + {...props} + /> + ) +}) +SidebarContent.displayName = "SidebarContent" + +const SidebarGroup = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + <div + ref={ref} + data-sidebar="group" + className={cn("relative flex w-full min-w-0 flex-col p-2", className)} + {...props} + /> + ) +}) +SidebarGroup.displayName = "SidebarGroup" + +const SidebarGroupLabel = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "div" + + return ( + <Comp + ref={ref} + data-sidebar="group-label" + className={cn( + "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className + )} + {...props} + /> + ) +}) +SidebarGroupLabel.displayName = "SidebarGroupLabel" + +const SidebarGroupAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + + return ( + <Comp + ref={ref} + data-sidebar="group-action" + className={cn( + "absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 after:md:hidden", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +}) +SidebarGroupAction.displayName = "SidebarGroupAction" + +const SidebarGroupContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => ( + <div + ref={ref} + data-sidebar="group-content" + className={cn("w-full text-sm", className)} + {...props} + /> +)) +SidebarGroupContent.displayName = "SidebarGroupContent" + +const SidebarMenu = React.forwardRef< + HTMLUListElement, + React.ComponentProps<"ul"> +>(({ className, ...props }, ref) => ( + <ul + ref={ref} + data-sidebar="menu" + className={cn("flex w-full min-w-0 flex-col gap-1", className)} + {...props} + /> +)) +SidebarMenu.displayName = "SidebarMenu" + +const SidebarMenuItem = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> +>(({ className, ...props }, ref) => ( + <li + ref={ref} + data-sidebar="menu-item" + className={cn("group/menu-item relative", className)} + {...props} + /> +)) +SidebarMenuItem.displayName = "SidebarMenuItem" + + + +const SidebarMenuButton = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean + isActive?: boolean + tooltip?: string | React.ComponentProps<typeof TooltipContent> + } & VariantProps<typeof sidebarMenuButtonVariants> +>( + ( + { + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props + }, + ref + ) => { + const Comp = asChild ? Slot : "button" + const { isMobile, state } = useSidebar() + + const button = ( + <Comp + ref={ref} + data-sidebar="menu-button" + data-size={size} + data-active={isActive} + className={cn(sidebarMenuButtonVariants({ variant, size }), className)} + {...props} + /> + ) + + if (!tooltip) { + return button + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + } + } + + return ( + <Tooltip> + <TooltipTrigger asChild>{button}</TooltipTrigger> + <TooltipContent + side="right" + align="center" + hidden={state !== "collapsed" || isMobile} + {...tooltip} + /> + </Tooltip> + ) + } +) +SidebarMenuButton.displayName = "SidebarMenuButton" + +const SidebarMenuAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean + showOnHover?: boolean + } +>(({ className, asChild = false, showOnHover = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + + return ( + <Comp + ref={ref} + data-sidebar="menu-action" + className={cn( + "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 after:md:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", + className + )} + {...props} + /> + ) +}) +SidebarMenuAction.displayName = "SidebarMenuAction" + +const SidebarMenuBadge = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => ( + <div + ref={ref} + data-sidebar="menu-badge" + className={cn( + "absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none", + "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> +)) +SidebarMenuBadge.displayName = "SidebarMenuBadge" + +const SidebarMenuSkeleton = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + showIcon?: boolean + } +>(({ className, showIcon = false, ...props }, ref) => { + // Random width between 50 to 90%. + const width = React.useMemo(() => { + return `${Math.floor(Math.random() * 40) + 50}%` + }, []) + + return ( + <div + ref={ref} + data-sidebar="menu-skeleton" + className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)} + {...props} + > + {showIcon && ( + <Skeleton + className="size-4 rounded-md" + data-sidebar="menu-skeleton-icon" + /> + )} + <Skeleton + className="h-4 flex-1 max-w-[--skeleton-width]" + data-sidebar="menu-skeleton-text" + style={ + { + "--skeleton-width": width, + } as React.CSSProperties + } + /> + </div> + ) +}) +SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton" + +const SidebarMenuSub = React.forwardRef< + HTMLUListElement, + React.ComponentProps<"ul"> +>(({ className, ...props }, ref) => ( + <ul + ref={ref} + data-sidebar="menu-sub" + className={cn( + "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> +)) +SidebarMenuSub.displayName = "SidebarMenuSub" + +const SidebarMenuSubItem = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> +>(({ ...props }, ref) => <li ref={ref} {...props} />) +SidebarMenuSubItem.displayName = "SidebarMenuSubItem" + +const SidebarMenuSubButton = React.forwardRef< + HTMLAnchorElement, + React.ComponentProps<"a"> & { + asChild?: boolean + size?: "sm" | "md" + isActive?: boolean + } +>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a" + + return ( + <Comp + ref={ref} + data-sidebar="menu-sub-button" + data-size={size} + data-active={isActive} + className={cn( + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +}) +SidebarMenuSubButton.displayName = "SidebarMenuSubButton" + +export { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, +} diff --git a/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx similarity index 100% rename from components/ui/skeleton.tsx rename to src/components/ui/skeleton.tsx diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx new file mode 100644 index 0000000..e161dae --- /dev/null +++ b/src/components/ui/slider.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" + +import { cn } from "@/lib/utils" + +const Slider = React.forwardRef< + React.ElementRef<typeof SliderPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> +>(({ className, ...props }, ref) => ( + <SliderPrimitive.Root + ref={ref} + className={cn( + "relative flex w-full touch-none select-none items-center", + className + )} + {...props} + > + <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> + <SliderPrimitive.Range className="absolute h-full bg-primary" /> + </SliderPrimitive.Track> + <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" /> + </SliderPrimitive.Root> +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } diff --git a/components/ui/switch.tsx b/src/components/ui/switch.tsx similarity index 98% rename from components/ui/switch.tsx rename to src/components/ui/switch.tsx index bc69cf2..aa58baa 100644 --- a/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as SwitchPrimitives from "@radix-ui/react-switch" diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes<HTMLTableElement> +>(({ className, ...props }, ref) => ( + <div className="relative w-full overflow-auto"> + <table + ref={ref} + className={cn("w-full caption-bottom text-sm", className)} + {...props} + /> + </div> +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes<HTMLTableSectionElement> +>(({ className, ...props }, ref) => ( + <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes<HTMLTableSectionElement> +>(({ className, ...props }, ref) => ( + <tbody + ref={ref} + className={cn("[&_tr:last-child]:border-0", className)} + {...props} + /> +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes<HTMLTableSectionElement> +>(({ className, ...props }, ref) => ( + <tfoot + ref={ref} + className={cn( + "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes<HTMLTableRowElement> +>(({ className, ...props }, ref) => ( + <tr + ref={ref} + className={cn( + "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", + className + )} + {...props} + /> +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes<HTMLTableCellElement> +>(({ className, ...props }, ref) => ( + <th + ref={ref} + className={cn( + "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes<HTMLTableCellElement> +>(({ className, ...props }, ref) => ( + <td + ref={ref} + className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes<HTMLTableCaptionElement> +>(({ className, ...props }, ref) => ( + <caption + ref={ref} + className={cn("mt-4 text-sm text-muted-foreground", className)} + {...props} + /> +)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..f57fffd --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef<typeof TabsPrimitive.List>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> +>(({ className, ...props }, ref) => ( + <TabsPrimitive.List + ref={ref} + className={cn( + "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", + className + )} + {...props} + /> +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef<typeof TabsPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> +>(({ className, ...props }, ref) => ( + <TabsPrimitive.Trigger + ref={ref} + className={cn( + "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", + className + )} + {...props} + /> +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef<typeof TabsPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> +>(({ className, ...props }, ref) => ( + <TabsPrimitive.Content + ref={ref} + className={cn( + "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + className + )} + {...props} + /> +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/components/ui/textarea.tsx b/src/components/ui/textarea.tsx similarity index 87% rename from components/ui/textarea.tsx rename to src/components/ui/textarea.tsx index 9f9a6dc..12c9136 100644 --- a/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -2,8 +2,7 @@ import * as React from "react" import { cn } from "@/lib/utils" -export interface TextareaProps - extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} +export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( ({ className, ...props }, ref) => { diff --git a/components/ui/toast.tsx b/src/components/ui/toast.tsx similarity index 99% rename from components/ui/toast.tsx rename to src/components/ui/toast.tsx index 521b94b..a822477 100644 --- a/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as ToastPrimitives from "@radix-ui/react-toast" import { cva, type VariantProps } from "class-variance-authority" diff --git a/components/ui/toaster.tsx b/src/components/ui/toaster.tsx similarity index 91% rename from components/ui/toaster.tsx rename to src/components/ui/toaster.tsx index e223385..18edc7c 100644 --- a/components/ui/toaster.tsx +++ b/src/components/ui/toaster.tsx @@ -1,5 +1,4 @@ -"use client" - +import { useToast } from "@/hooks/useToast" import { Toast, ToastClose, @@ -8,7 +7,6 @@ import { ToastTitle, ToastViewport, } from "@/components/ui/toast" -import { useToast } from "@/components/ui/use-toast" export function Toaster() { const { toasts } = useToast() diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx new file mode 100644 index 0000000..172f316 --- /dev/null +++ b/src/components/ui/toggle-group.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" +import { type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { toggleVariants } from "@/components/ui/toggle-variants" + +const ToggleGroupContext = React.createContext< + VariantProps<typeof toggleVariants> +>({ + size: "default", + variant: "default", +}) + +const ToggleGroup = React.forwardRef< + React.ElementRef<typeof ToggleGroupPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & + VariantProps<typeof toggleVariants> +>(({ className, variant, size, children, ...props }, ref) => ( + <ToggleGroupPrimitive.Root + ref={ref} + className={cn("flex items-center justify-center gap-1", className)} + {...props} + > + <ToggleGroupContext.Provider value={{ variant, size }}> + {children} + </ToggleGroupContext.Provider> + </ToggleGroupPrimitive.Root> +)) + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName + +const ToggleGroupItem = React.forwardRef< + React.ElementRef<typeof ToggleGroupPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & + VariantProps<typeof toggleVariants> +>(({ className, children, variant, size, ...props }, ref) => { + const context = React.useContext(ToggleGroupContext) + + return ( + <ToggleGroupPrimitive.Item + ref={ref} + className={cn( + toggleVariants({ + variant: context.variant || variant, + size: context.size || size, + }), + className + )} + {...props} + > + {children} + </ToggleGroupPrimitive.Item> + ) +}) + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName + +export { ToggleGroup, ToggleGroupItem } diff --git a/src/components/ui/toggle-variants.ts b/src/components/ui/toggle-variants.ts new file mode 100644 index 0000000..0c8f15f --- /dev/null +++ b/src/components/ui/toggle-variants.ts @@ -0,0 +1,23 @@ +import { cva } from "class-variance-authority" + +export const toggleVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-10 px-3", + sm: "h-9 px-2.5", + lg: "h-11 px-5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) \ No newline at end of file diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx new file mode 100644 index 0000000..a2c2275 --- /dev/null +++ b/src/components/ui/toggle.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import * as TogglePrimitive from "@radix-ui/react-toggle" +import { type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { toggleVariants } from "./toggle-variants" + +const Toggle = React.forwardRef< + React.ElementRef<typeof TogglePrimitive.Root>, + React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & + VariantProps<typeof toggleVariants> +>(({ className, variant, size, ...props }, ref) => ( + <TogglePrimitive.Root + ref={ref} + className={cn(toggleVariants({ variant, size, className }))} + {...props} + /> +)) + +Toggle.displayName = TogglePrimitive.Root.displayName + +export { Toggle } diff --git a/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx similarity index 98% rename from components/ui/tooltip.tsx rename to src/components/ui/tooltip.tsx index 30fc44d..e121f0a 100644 --- a/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -1,5 +1,3 @@ -"use client" - import * as React from "react" import * as TooltipPrimitive from "@radix-ui/react-tooltip" diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts new file mode 100644 index 0000000..6af4057 --- /dev/null +++ b/src/contexts/AppContext.ts @@ -0,0 +1,26 @@ +import { createContext } from "react"; + +export type Theme = "dark" | "light" | "system"; + +export interface RelayMetadata { + /** List of relays with read/write permissions */ + relays: { url: string; read: boolean; write: boolean }[]; + /** Unix timestamp of when the relay list was last updated */ + updatedAt: number; +} + +export interface AppConfig { + /** Current theme */ + theme: Theme; + /** NIP-65 relay list metadata */ + relayMetadata: RelayMetadata; +} + +export interface AppContextType { + /** Current application configuration */ + config: AppConfig; + /** Update configuration using a callback that receives current config and returns new config */ + updateConfig: (updater: (currentConfig: Partial<AppConfig>) => Partial<AppConfig>) => void; +} + +export const AppContext = createContext<AppContextType | undefined>(undefined); diff --git a/src/contexts/DMContext.ts b/src/contexts/DMContext.ts new file mode 100644 index 0000000..ce59eed --- /dev/null +++ b/src/contexts/DMContext.ts @@ -0,0 +1,138 @@ +import { createContext } from 'react'; +import { type LoadingPhase, type ProtocolMode } from '@/lib/dmConstants'; +import { type NostrEvent } from '@nostrify/nostrify'; +import type { MessageProtocol } from '@/lib/dmConstants'; + +// ============================================================================ +// DM Types and Constants +// ============================================================================ + +interface ParticipantData { + messages: DecryptedMessage[]; + lastActivity: number; + lastMessage: DecryptedMessage | null; + hasNIP4: boolean; + hasNIP17: boolean; +} + +type MessagesState = Map<string, ParticipantData>; + +interface LastSyncData { + nip4: number | null; + nip17: number | null; +} + +interface SubscriptionStatus { + isNIP4Connected: boolean; + isNIP17Connected: boolean; +} + +interface ScanProgress { + current: number; + status: string; +} + +interface ScanProgressState { + nip4: ScanProgress | null; + nip17: ScanProgress | null; +} + +interface ConversationSummary { + id: string; + pubkey: string; + lastMessage: DecryptedMessage | null; + lastActivity: number; + hasNIP4Messages: boolean; + hasNIP17Messages: boolean; + isKnown: boolean; + isRequest: boolean; + lastMessageFromUser: boolean; +} + +interface DecryptedMessage extends NostrEvent { + decryptedContent?: string; + error?: string; + isSending?: boolean; + clientFirstSeen?: number; + decryptedEvent?: NostrEvent; // For NIP-17: the inner kind 14/15 event + originalGiftWrapId?: string; // Store gift wrap ID for NIP-17 deduplication +} + +/** + * File attachment for direct messages (NIP-92 compatible). + * + * All fields are required. Use with `useUploadFile` hook to upload files + * and generate the proper tags format. + * + * @example + * ```tsx + * import { useUploadFile } from '@/hooks/useUploadFile'; + * import type { FileAttachment } from '@/contexts/DMContext'; + * + * const { mutateAsync: uploadFile } = useUploadFile(); + * + * const tags = await uploadFile(file); + * const attachment: FileAttachment = { + * url: tags[0][1], + * mimeType: file.type, + * size: file.size, + * name: file.name, + * tags: tags + * }; + * + * await sendMessage({ + * recipientPubkey: 'hex-pubkey', + * content: 'Check out this file!', + * attachments: [attachment] + * }); + * ``` + * + * @property url - Blossom server URL where file is hosted + * @property mimeType - MIME type of the file (e.g., 'image/png') + * @property size - File size in bytes + * @property name - Original filename + * @property tags - NIP-94 file metadata tags (includes hashes) + */ +export interface FileAttachment { + url: string; + mimeType: string; + size: number; + name: string; + tags: string[][]; +} + +/** + * Direct Messaging context interface providing access to all DM functionality. + * + * @property messages - Raw message state (Map of pubkey -> participant data) + * @property isLoading - True during initial load phases + * @property loadingPhase - Current loading phase (CACHE, RELAYS, SUBSCRIPTIONS, READY, IDLE) + * @property isDoingInitialLoad - True only during cache/relay loading (not subscriptions) + * @property lastSync - Unix timestamps of last successful sync for each protocol + * @property subscriptions - Connection status for real-time message subscriptions + * @property conversations - Array of conversation summaries sorted by last activity + * @property sendMessage - Send an encrypted direct message (NIP-04 or NIP-17) + * @property protocolMode - Current protocol mode (NIP04_ONLY, NIP17_ONLY, or BOTH) + * @property scanProgress - Progress info for large message history scans + * @property clearCacheAndRefetch - Clear IndexedDB cache and reload all messages from relays + */ +export interface DMContextType { + messages: MessagesState; + isLoading: boolean; + loadingPhase: LoadingPhase; + isDoingInitialLoad: boolean; + lastSync: LastSyncData; + subscriptions: SubscriptionStatus; + conversations: ConversationSummary[]; + sendMessage: (params: { + recipientPubkey: string; + content: string; + protocol?: MessageProtocol; + attachments?: FileAttachment[]; + }) => Promise<void>; + protocolMode: ProtocolMode; + scanProgress: ScanProgressState; + clearCacheAndRefetch: () => Promise<void>; +} + +export const DMContext = createContext<DMContextType | null>(null); \ No newline at end of file diff --git a/src/contexts/NWCContext.tsx b/src/contexts/NWCContext.tsx new file mode 100644 index 0000000..43e866b --- /dev/null +++ b/src/contexts/NWCContext.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react'; +import { useNWCInternal as useNWCHook } from '@/hooks/useNWC'; +import { NWCContext } from '@/hooks/useNWCContext'; + +export function NWCProvider({ children }: { children: ReactNode }) { + const nwc = useNWCHook(); + return <NWCContext.Provider value={nwc}>{children}</NWCContext.Provider>; +} \ No newline at end of file diff --git a/src/hooks/useAppContext.ts b/src/hooks/useAppContext.ts new file mode 100644 index 0000000..7554806 --- /dev/null +++ b/src/hooks/useAppContext.ts @@ -0,0 +1,14 @@ +import { useContext } from "react"; +import { AppContext, type AppContextType } from "@/contexts/AppContext"; + +/** + * Hook to access and update application configuration + * @returns Application context with config and update methods + */ +export function useAppContext(): AppContextType { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error('useAppContext must be used within an AppProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/hooks/useAuthor.ts b/src/hooks/useAuthor.ts new file mode 100644 index 0000000..271d4c2 --- /dev/null +++ b/src/hooks/useAuthor.ts @@ -0,0 +1,34 @@ +import { type NostrEvent, type NostrMetadata, NSchema as n } from '@nostrify/nostrify'; +import { useNostr } from '@nostrify/react'; +import { useQuery } from '@tanstack/react-query'; + +export function useAuthor(pubkey: string | undefined) { + const { nostr } = useNostr(); + + return useQuery<{ event?: NostrEvent; metadata?: NostrMetadata }>({ + queryKey: ['author', pubkey ?? ''], + queryFn: async ({ signal }) => { + if (!pubkey) { + return {}; + } + + const [event] = await nostr.query( + [{ kinds: [0], authors: [pubkey!], limit: 1 }], + { signal: AbortSignal.any([signal, AbortSignal.timeout(1500)]) }, + ); + + if (!event) { + throw new Error('No event found'); + } + + try { + const metadata = n.json().pipe(n.metadata()).parse(event.content); + return { metadata, event }; + } catch { + return { event }; + } + }, + staleTime: 5 * 60 * 1000, // Keep cached data fresh for 5 minutes + retry: 3, + }); +} diff --git a/src/hooks/useComments.ts b/src/hooks/useComments.ts new file mode 100644 index 0000000..5fdf42b --- /dev/null +++ b/src/hooks/useComments.ts @@ -0,0 +1,98 @@ +import { NKinds, NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import { useNostr } from '@nostrify/react'; +import { useQuery } from '@tanstack/react-query'; + +export function useComments(root: NostrEvent | URL, limit?: number) { + const { nostr } = useNostr(); + + return useQuery({ + queryKey: ['nostr', 'comments', root instanceof URL ? root.toString() : root.id, limit], + queryFn: async (c) => { + const filter: NostrFilter = { kinds: [1111] }; + + if (root instanceof URL) { + filter['#I'] = [root.toString()]; + } else if (NKinds.addressable(root.kind)) { + const d = root.tags.find(([name]) => name === 'd')?.[1] ?? ''; + filter['#A'] = [`${root.kind}:${root.pubkey}:${d}`]; + } else if (NKinds.replaceable(root.kind)) { + filter['#A'] = [`${root.kind}:${root.pubkey}:`]; + } else { + filter['#E'] = [root.id]; + } + + if (typeof limit === 'number') { + filter.limit = limit; + } + + // Query for all kind 1111 comments that reference this addressable event regardless of depth + const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]); + const events = await nostr.query([filter], { signal }); + + // Helper function to get tag value + const getTagValue = (event: NostrEvent, tagName: string): string | undefined => { + const tag = event.tags.find(([name]) => name === tagName); + return tag?.[1]; + }; + + // Filter top-level comments (those with lowercase tag matching the root) + const topLevelComments = events.filter(comment => { + if (root instanceof URL) { + return getTagValue(comment, 'i') === root.toString(); + } else if (NKinds.addressable(root.kind)) { + const d = getTagValue(root, 'd') ?? ''; + return getTagValue(comment, 'a') === `${root.kind}:${root.pubkey}:${d}`; + } else if (NKinds.replaceable(root.kind)) { + return getTagValue(comment, 'a') === `${root.kind}:${root.pubkey}:`; + } else { + return getTagValue(comment, 'e') === root.id; + } + }); + + // Helper function to get all descendants of a comment + const getDescendants = (parentId: string): NostrEvent[] => { + const directReplies = events.filter(comment => { + const eTag = getTagValue(comment, 'e'); + return eTag === parentId; + }); + + const allDescendants = [...directReplies]; + + // Recursively get descendants of each direct reply + for (const reply of directReplies) { + allDescendants.push(...getDescendants(reply.id)); + } + + return allDescendants; + }; + + // Create a map of comment ID to its descendants + const commentDescendants = new Map<string, NostrEvent[]>(); + for (const comment of events) { + commentDescendants.set(comment.id, getDescendants(comment.id)); + } + + // Sort top-level comments by creation time (newest first) + const sortedTopLevel = topLevelComments.sort((a, b) => b.created_at - a.created_at); + + return { + allComments: events, + topLevelComments: sortedTopLevel, + getDescendants: (commentId: string) => { + const descendants = commentDescendants.get(commentId) || []; + // Sort descendants by creation time (oldest first for threaded display) + return descendants.sort((a, b) => a.created_at - b.created_at); + }, + getDirectReplies: (commentId: string) => { + const directReplies = events.filter(comment => { + const eTag = getTagValue(comment, 'e'); + return eTag === commentId; + }); + // Sort direct replies by creation time (oldest first for threaded display) + return directReplies.sort((a, b) => a.created_at - b.created_at); + } + }; + }, + enabled: !!root, + }); +} \ No newline at end of file diff --git a/src/hooks/useConversationMessages.ts b/src/hooks/useConversationMessages.ts new file mode 100644 index 0000000..2fbf9e1 --- /dev/null +++ b/src/hooks/useConversationMessages.ts @@ -0,0 +1,87 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useDMContext } from "@/hooks/useDMContext"; + +const MESSAGES_PER_PAGE = 25; + +/** + * Hook to access paginated messages for a specific conversation. + * + * Returns the most recent messages (default 25) with the ability to load earlier messages. + * Automatically resets to default page size when switching conversations. + * + * @example + * ```tsx + * import { useConversationMessages } from '@/contexts/DMContext'; + * + * function MessageThread({ recipientPubkey }: { recipientPubkey: string }) { + * const { + * messages, + * hasMoreMessages, + * loadEarlierMessages, + * totalCount + * } = useConversationMessages(recipientPubkey); + * + * return ( + * <div> + * {hasMoreMessages && ( + * <button onClick={loadEarlierMessages}> + * Load Earlier ({totalCount - messages.length} more) + * </button> + * )} + * {messages.map(msg => ( + * <div key={msg.id}>{msg.decryptedContent}</div> + * ))} + * </div> + * ); + * } + * ``` + * + * @param conversationId - The pubkey of the conversation participant + * @returns Paginated message data with loading function + */ +export function useConversationMessages(conversationId: string) { + const { messages: allMessages } = useDMContext(); + const [visibleCount, setVisibleCount] = useState(MESSAGES_PER_PAGE); + + const result = useMemo(() => { + const conversationData = allMessages.get(conversationId); + + if (!conversationData) { + return { + messages: [], + hasMoreMessages: false, + totalCount: 0, + lastMessage: null, + lastActivity: 0, + }; + } + + const totalMessages = conversationData.messages.length; + const hasMore = totalMessages > visibleCount; + + // Return the most recent N messages (slice from the end) + const visibleMessages = conversationData.messages.slice(-visibleCount); + + return { + messages: visibleMessages, + hasMoreMessages: hasMore, + totalCount: totalMessages, + lastMessage: conversationData.lastMessage, + lastActivity: conversationData.lastActivity, + }; + }, [allMessages, conversationId, visibleCount]); + + const loadEarlierMessages = useCallback(() => { + setVisibleCount(prev => prev + MESSAGES_PER_PAGE); + }, []); + + // Reset visible count when conversation changes + useEffect(() => { + setVisibleCount(MESSAGES_PER_PAGE); + }, [conversationId]); + + return { + ...result, + loadEarlierMessages, + }; +} \ No newline at end of file diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts new file mode 100644 index 0000000..e79b3a3 --- /dev/null +++ b/src/hooks/useCurrentUser.ts @@ -0,0 +1,48 @@ +import { type NLoginType, NUser, useNostrLogin } from '@nostrify/react/login'; +import { useNostr } from '@nostrify/react'; +import { useCallback, useMemo } from 'react'; + +import { useAuthor } from './useAuthor.ts'; + +export function useCurrentUser() { + const { nostr } = useNostr(); + const { logins } = useNostrLogin(); + + const loginToUser = useCallback((login: NLoginType): NUser => { + switch (login.type) { + case 'nsec': // Nostr login with secret key + return NUser.fromNsecLogin(login); + case 'bunker': // Nostr login with NIP-46 "bunker://" URI + return NUser.fromBunkerLogin(login, nostr); + case 'extension': // Nostr login with NIP-07 browser extension + return NUser.fromExtensionLogin(login); + // Other login types can be defined here + default: + throw new Error(`Unsupported login type: ${login.type}`); + } + }, [nostr]); + + const users = useMemo(() => { + const users: NUser[] = []; + + for (const login of logins) { + try { + const user = loginToUser(login); + users.push(user); + } catch (error) { + console.warn('Skipped invalid login', login.id, error); + } + } + + return users; + }, [logins, loginToUser]); + + const user = users[0] as NUser | undefined; + const author = useAuthor(user?.pubkey); + + return { + user, + users, + ...author.data, + }; +} diff --git a/src/hooks/useDMContext.ts b/src/hooks/useDMContext.ts new file mode 100644 index 0000000..a29a47f --- /dev/null +++ b/src/hooks/useDMContext.ts @@ -0,0 +1,45 @@ +import { useContext } from "react"; +import { DMContext, DMContextType } from "@/contexts/DMContext"; + +/** + * Hook to access the direct messaging system. + * + * Provides access to conversations, message sending, loading states, and cache management. + * Must be used within a DMProvider. + * + * @example + * ```tsx + * import { useDMContext } from '@/hooks/useDMContext'; + * import { MESSAGE_PROTOCOL } from '@/lib/dmConstants'; + * + * function MyComponent() { + * const { conversations, sendMessage, isLoading } = useDMContext(); + * + * // Send a message + * await sendMessage({ + * recipientPubkey: 'hex-pubkey', + * content: 'Hello!', + * protocol: MESSAGE_PROTOCOL.NIP17 + * }); + * + * // Display conversations + * return ( + * <div> + * {isLoading ? 'Loading...' : conversations.map(c => ( + * <div key={c.pubkey}>{c.lastMessage?.decryptedContent}</div> + * ))} + * </div> + * ); + * } + * ``` + * + * @returns DMContextType - The direct messaging context + * @throws Error if used outside DMProvider + */ +export function useDMContext(): DMContextType { + const context = useContext(DMContext); + if (!context) { + throw new Error('useDMContext must be used within DMProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/hooks/useIsMobile.tsx b/src/hooks/useIsMobile.tsx new file mode 100644 index 0000000..0fae217 --- /dev/null +++ b/src/hooks/useIsMobile.tsx @@ -0,0 +1,19 @@ +import { useEffect, useState } from "react" + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile(): boolean { + const [isMobile, setIsMobile] = useState(window.innerWidth < MOBILE_BREAKPOINT); + + useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + } + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +} diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..73c2959 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,54 @@ +import { useState, useEffect } from 'react'; + +/** + * Generic hook for managing localStorage state + */ +export function useLocalStorage<T>( + key: string, + defaultValue: T, + serializer?: { + serialize: (value: T) => string; + deserialize: (value: string) => T; + } +) { + const serialize = serializer?.serialize || JSON.stringify; + const deserialize = serializer?.deserialize || JSON.parse; + + const [state, setState] = useState<T>(() => { + try { + const item = localStorage.getItem(key); + return item ? deserialize(item) : defaultValue; + } catch (error) { + console.warn(`Failed to load ${key} from localStorage:`, error); + return defaultValue; + } + }); + + const setValue = (value: T | ((prev: T) => T)) => { + try { + const valueToStore = value instanceof Function ? value(state) : value; + setState(valueToStore); + localStorage.setItem(key, serialize(valueToStore)); + } catch (error) { + console.warn(`Failed to save ${key} to localStorage:`, error); + } + }; + + // Sync with localStorage changes from other tabs + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === key && e.newValue !== null) { + try { + setState(deserialize(e.newValue)); + } catch (error) { + console.warn(`Failed to sync ${key} from localStorage:`, error); + } + } + }; + + window.addEventListener('storage', handleStorageChange); + return () => window.removeEventListener('storage', handleStorageChange); + }, [key, deserialize]); + + return [state, setValue] as const; +} \ No newline at end of file diff --git a/src/hooks/useLoggedInAccounts.ts b/src/hooks/useLoggedInAccounts.ts new file mode 100644 index 0000000..e1b1d8c --- /dev/null +++ b/src/hooks/useLoggedInAccounts.ts @@ -0,0 +1,56 @@ +import { useNostr } from '@nostrify/react'; +import { useNostrLogin } from '@nostrify/react/login'; +import { useQuery } from '@tanstack/react-query'; +import { NSchema as n, NostrEvent, NostrMetadata } from '@nostrify/nostrify'; + +export interface Account { + id: string; + pubkey: string; + event?: NostrEvent; + metadata: NostrMetadata; +} + +export function useLoggedInAccounts() { + const { nostr } = useNostr(); + const { logins, setLogin, removeLogin } = useNostrLogin(); + + const { data: authors = [] } = useQuery({ + queryKey: ['nostr', 'logins', logins.map((l) => l.id).join(';')], + queryFn: async ({ signal }) => { + const events = await nostr.query( + [{ kinds: [0], authors: logins.map((l) => l.pubkey) }], + { signal: AbortSignal.any([signal, AbortSignal.timeout(1500)]) }, + ); + + return logins.map(({ id, pubkey }): Account => { + const event = events.find((e) => e.pubkey === pubkey); + try { + const metadata = n.json().pipe(n.metadata()).parse(event?.content); + return { id, pubkey, metadata, event }; + } catch { + return { id, pubkey, metadata: {}, event }; + } + }); + }, + retry: 3, + }); + + // Current user is the first login + const currentUser: Account | undefined = (() => { + const login = logins[0]; + if (!login) return undefined; + const author = authors.find((a) => a.id === login.id); + return { metadata: {}, ...author, id: login.id, pubkey: login.pubkey }; + })(); + + // Other users are all logins except the current one + const otherUsers = (authors || []).slice(1) as Account[]; + + return { + authors, + currentUser, + otherUsers, + setLogin, + removeLogin, + }; +} \ No newline at end of file diff --git a/src/hooks/useLoginActions.ts b/src/hooks/useLoginActions.ts new file mode 100644 index 0000000..f0d98a9 --- /dev/null +++ b/src/hooks/useLoginActions.ts @@ -0,0 +1,34 @@ +import { useNostr } from '@nostrify/react'; +import { NLogin, useNostrLogin } from '@nostrify/react/login'; + +// NOTE: This file should not be edited except for adding new login methods. + +export function useLoginActions() { + const { nostr } = useNostr(); + const { logins, addLogin, removeLogin } = useNostrLogin(); + + return { + // Login with a Nostr secret key + nsec(nsec: string): void { + const login = NLogin.fromNsec(nsec); + addLogin(login); + }, + // Login with a NIP-46 "bunker://" URI + async bunker(uri: string): Promise<void> { + const login = await NLogin.fromBunker(uri, nostr); + addLogin(login); + }, + // Login with a NIP-07 browser extension + async extension(): Promise<void> { + const login = await NLogin.fromExtension(); + addLogin(login); + }, + // Log out the current user + async logout(): Promise<void> { + const login = logins[0]; + if (login) { + removeLogin(login.id); + } + } + }; +} diff --git a/src/hooks/useNWC.ts b/src/hooks/useNWC.ts new file mode 100644 index 0000000..43b14c7 --- /dev/null +++ b/src/hooks/useNWC.ts @@ -0,0 +1,221 @@ +import { useState, useCallback } from 'react'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useToast } from '@/hooks/useToast'; +import { LN } from '@getalby/sdk'; + +export interface NWCConnection { + connectionString: string; + alias?: string; + isConnected: boolean; + client?: LN; +} + +export interface NWCInfo { + alias?: string; + color?: string; + pubkey?: string; + network?: string; + methods?: string[]; + notifications?: string[]; +} + +export function useNWCInternal() { + const { toast } = useToast(); + const [connections, setConnections] = useLocalStorage<NWCConnection[]>('nwc-connections', []); + const [activeConnection, setActiveConnection] = useLocalStorage<string | null>('nwc-active-connection', null); + const [connectionInfo, setConnectionInfo] = useState<Record<string, NWCInfo>>({}); + + // Add new connection + const addConnection = async (uri: string, alias?: string): Promise<boolean> => { + const parseNWCUri = (uri: string): { connectionString: string } | null => { + try { + if (!uri.startsWith('nostr+walletconnect://') && !uri.startsWith('nostrwalletconnect://')) { + console.error('Invalid NWC URI protocol:', { protocol: uri.split('://')[0] }); + return null; + } + return { connectionString: uri }; + } catch (error) { + console.error('Failed to parse NWC URI:', error); + return null; + } + }; + + const parsed = parseNWCUri(uri); + if (!parsed) { + toast({ + title: 'Invalid NWC URI', + description: 'Please check the connection string and try again.', + variant: 'destructive', + }); + return false; + } + + const existingConnection = connections.find(c => c.connectionString === parsed.connectionString); + if (existingConnection) { + toast({ + title: 'Connection already exists', + description: 'This wallet is already connected.', + variant: 'destructive', + }); + return false; + } + + try { + let timeoutId: NodeJS.Timeout | undefined; + const testPromise = new Promise((resolve, reject) => { + try { + const client = new LN(parsed.connectionString); + resolve(client); + } catch (error) { + reject(error); + } + }); + const timeoutPromise = new Promise<never>((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('Connection test timeout')), 10000); + }); + + try { + await Promise.race([testPromise, timeoutPromise]) as LN; + if (timeoutId) clearTimeout(timeoutId); + } catch (error) { + if (timeoutId) clearTimeout(timeoutId); + throw error; + } + + const connection: NWCConnection = { + connectionString: parsed.connectionString, + alias: alias || 'NWC Wallet', + isConnected: true, + }; + + setConnectionInfo(prev => ({ + ...prev, + [parsed.connectionString]: { + alias: connection.alias, + methods: ['pay_invoice'], + }, + })); + + const newConnections = [...connections, connection]; + setConnections(newConnections); + + if (connections.length === 0 || !activeConnection) + setActiveConnection(parsed.connectionString); + + toast({ + title: 'Wallet connected', + description: `Successfully connected to ${connection.alias}.`, + }); + + return true; + } catch (error) { + console.error('NWC connection failed:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + + toast({ + title: 'Connection failed', + description: `Could not connect to the wallet: ${errorMessage}`, + variant: 'destructive', + }); + return false; + } + }; + + // Remove connection + const removeConnection = (connectionString: string) => { + const filtered = connections.filter(c => c.connectionString !== connectionString); + setConnections(filtered); + + if (activeConnection === connectionString) { + const newActive = filtered.length > 0 ? filtered[0].connectionString : null; + setActiveConnection(newActive); + } + + setConnectionInfo(prev => { + const newInfo = { ...prev }; + delete newInfo[connectionString]; + return newInfo; + }); + + toast({ + title: 'Wallet disconnected', + description: 'The wallet connection has been removed.', + }); + }; + + // Get active connection + const getActiveConnection = useCallback((): NWCConnection | null => { + if (!activeConnection && connections.length > 0) { + setActiveConnection(connections[0].connectionString); + return connections[0]; + } + + if (!activeConnection) return null; + + const found = connections.find(c => c.connectionString === activeConnection); + return found || null; + }, [activeConnection, connections, setActiveConnection]); + + // Send payment using the SDK + const sendPayment = useCallback(async ( + connection: NWCConnection, + invoice: string + ): Promise<{ preimage: string }> => { + if (!connection.connectionString) { + throw new Error('Invalid connection: missing connection string'); + } + + let client: LN; + try { + client = new LN(connection.connectionString); + } catch (error) { + console.error('Failed to create NWC client:', error); + throw new Error(`Failed to create NWC client: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + + try { + let timeoutId: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise<never>((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('Payment timeout after 15 seconds')), 15000); + }); + + const paymentPromise = client.pay(invoice); + + try { + const response = await Promise.race([paymentPromise, timeoutPromise]) as { preimage: string }; + if (timeoutId) clearTimeout(timeoutId); + return response; + } catch (error) { + if (timeoutId) clearTimeout(timeoutId); + throw error; + } + } catch (error) { + console.error('NWC payment failed:', error); + + if (error instanceof Error) { + if (error.message.includes('timeout')) { + throw new Error('Payment timed out. Please try again.'); + } else if (error.message.includes('insufficient')) { + throw new Error('Insufficient balance in connected wallet.'); + } else if (error.message.includes('invalid')) { + throw new Error('Invalid invoice or connection. Please check your wallet.'); + } else { + throw new Error(`Payment failed: ${error.message}`); + } + } + + throw new Error('Payment failed with unknown error'); + } + }, []); + + return { + connections, + activeConnection, + connectionInfo, + addConnection, + removeConnection, + setActiveConnection, + getActiveConnection, + sendPayment, + }; +} \ No newline at end of file diff --git a/src/hooks/useNWCContext.ts b/src/hooks/useNWCContext.ts new file mode 100644 index 0000000..08304dc --- /dev/null +++ b/src/hooks/useNWCContext.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; +import { createContext } from 'react'; +import { useNWCInternal } from '@/hooks/useNWC'; + +type NWCContextType = ReturnType<typeof useNWCInternal>; + +export const NWCContext = createContext<NWCContextType | null>(null); + +export function useNWC(): NWCContextType { + const context = useContext(NWCContext); + if (!context) { + throw new Error('useNWC must be used within a NWCProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/hooks/useNostr.ts b/src/hooks/useNostr.ts new file mode 100644 index 0000000..68db540 --- /dev/null +++ b/src/hooks/useNostr.ts @@ -0,0 +1,6 @@ +// This file exists because LLMs get confused and try to create this file if it doesn't exist. +// The `useNostr` hook should be imported directly from `@nostrify/react`, not from this file. + +// This file SHOULD NOT be edited or removed. + +export { useNostr } from "@nostrify/react"; diff --git a/src/hooks/useNostrPublish.ts b/src/hooks/useNostrPublish.ts new file mode 100644 index 0000000..b9a3bc2 --- /dev/null +++ b/src/hooks/useNostrPublish.ts @@ -0,0 +1,42 @@ +import { useNostr } from "@nostrify/react"; +import { useMutation, type UseMutationResult } from "@tanstack/react-query"; + +import { useCurrentUser } from "./useCurrentUser"; + +import type { NostrEvent } from "@nostrify/nostrify"; + +export function useNostrPublish(): UseMutationResult<NostrEvent> { + const { nostr } = useNostr(); + const { user } = useCurrentUser(); + + return useMutation({ + mutationFn: async (t: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>) => { + if (user) { + const tags = t.tags ?? []; + + // Add the client tag if it doesn't exist + if (location.protocol === "https:" && !tags.some(([name]) => name === "client")) { + tags.push(["client", location.hostname]); + } + + const event = await user.signer.signEvent({ + kind: t.kind, + content: t.content ?? "", + tags, + created_at: t.created_at ?? Math.floor(Date.now() / 1000), + }); + + await nostr.event(event, { signal: AbortSignal.timeout(5000) }); + return event; + } else { + throw new Error("User is not logged in"); + } + }, + onError: (error) => { + console.error("Failed to publish event:", error); + }, + onSuccess: (data) => { + console.log("Event published successfully:", data); + }, + }); +} \ No newline at end of file diff --git a/src/hooks/usePostComment.ts b/src/hooks/usePostComment.ts new file mode 100644 index 0000000..84a2405 --- /dev/null +++ b/src/hooks/usePostComment.ts @@ -0,0 +1,92 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNostrPublish } from '@/hooks/useNostrPublish'; +import { NKinds, type NostrEvent } from '@nostrify/nostrify'; + +interface PostCommentParams { + root: NostrEvent | URL; // The root event to comment on + reply?: NostrEvent | URL; // Optional reply to another comment + content: string; +} + +/** Post a NIP-22 (kind 1111) comment on an event. */ +export function usePostComment() { + const { mutateAsync: publishEvent } = useNostrPublish(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ root, reply, content }: PostCommentParams) => { + const tags: string[][] = []; + + // d-tag identifiers + const dRoot = root instanceof URL ? '' : root.tags.find(([name]) => name === 'd')?.[1] ?? ''; + const dReply = reply instanceof URL ? '' : reply?.tags.find(([name]) => name === 'd')?.[1] ?? ''; + + // Root event tags + if (root instanceof URL) { + tags.push(['I', root.toString()]); + } else if (NKinds.addressable(root.kind)) { + tags.push(['A', `${root.kind}:${root.pubkey}:${dRoot}`]); + } else if (NKinds.replaceable(root.kind)) { + tags.push(['A', `${root.kind}:${root.pubkey}:`]); + } else { + tags.push(['E', root.id]); + } + if (root instanceof URL) { + tags.push(['K', root.hostname]); + } else { + tags.push(['K', root.kind.toString()]); + tags.push(['P', root.pubkey]); + } + + // Reply event tags + if (reply) { + if (reply instanceof URL) { + tags.push(['i', reply.toString()]); + } else if (NKinds.addressable(reply.kind)) { + tags.push(['a', `${reply.kind}:${reply.pubkey}:${dReply}`]); + } else if (NKinds.replaceable(reply.kind)) { + tags.push(['a', `${reply.kind}:${reply.pubkey}:`]); + } else { + tags.push(['e', reply.id]); + } + if (reply instanceof URL) { + tags.push(['k', reply.hostname]); + } else { + tags.push(['k', reply.kind.toString()]); + tags.push(['p', reply.pubkey]); + } + } else { + // If this is a top-level comment, use the root event's tags + if (root instanceof URL) { + tags.push(['i', root.toString()]); + } else if (NKinds.addressable(root.kind)) { + tags.push(['a', `${root.kind}:${root.pubkey}:${dRoot}`]); + } else if (NKinds.replaceable(root.kind)) { + tags.push(['a', `${root.kind}:${root.pubkey}:`]); + } else { + tags.push(['e', root.id]); + } + if (root instanceof URL) { + tags.push(['k', root.hostname]); + } else { + tags.push(['k', root.kind.toString()]); + tags.push(['p', root.pubkey]); + } + } + + const event = await publishEvent({ + kind: 1111, + content, + tags, + }); + + return event; + }, + onSuccess: (_, { root }) => { + // Invalidate and refetch comments + queryClient.invalidateQueries({ + queryKey: ['nostr', 'comments', root instanceof URL ? root.toString() : root.id] + }); + }, + }); +} \ No newline at end of file diff --git a/src/hooks/useShakespeare.ts b/src/hooks/useShakespeare.ts new file mode 100644 index 0000000..1ceaaed --- /dev/null +++ b/src/hooks/useShakespeare.ts @@ -0,0 +1,372 @@ +import { useCallback, useState } from 'react'; +import { useCurrentUser } from './useCurrentUser'; +import type { NUser } from '@nostrify/react/login'; + +// Types for Shakespeare API (compatible with OpenAI ChatCompletionMessageParam) +export interface ChatMessage { + role: 'user' | 'assistant' | 'system'; + content: string | Array<{ + type: 'text' | 'image_url'; + text?: string; + image_url?: { + url: string; + }; + }>; +} + +export interface ChatCompletionRequest { + model: string; + messages: ChatMessage[]; + stream?: boolean; + temperature?: number; + max_tokens?: number; +} + +export interface ChatCompletionResponse { + id: string; + object: string; + created: number; + model: string; + choices: Array<{ + index: number; + message: ChatMessage; + finish_reason: string; + }>; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +export interface Model { + id: string; + name: string; + description: string; + object: string; + owned_by: string; + created: number; + context_window: number; + pricing: { + prompt: string; + completion: string; + }; +} + +export interface ModelsResponse { + object: string; + data: Model[]; +} + +// Configuration +const SHAKESPEARE_API_URL = 'https://ai.shakespeare.diy/v1'; + +// Helper function to create NIP-98 token +async function createNIP98Token( + method: string, + url: string, + body?: unknown, + user?: NUser +): Promise<string> { + if (!user?.signer) { + throw new Error('User signer is required for NIP-98 authentication'); + } + + // Create the tags array + const tags: string[][] = [ + ['u', url], + ['method', method] + ]; + + // Add payload hash for requests with body (following NIP-98 spec) + if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + const bodyString = JSON.stringify(body); + const encoder = new TextEncoder(); + const data = encoder.encode(bodyString); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const payloadHash = Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + tags.push(['payload', payloadHash]); + } + + // Create the HTTP request event + const event = await user.signer.signEvent({ + kind: 27235, // NIP-98 HTTP Auth + content: '', + tags, + created_at: Math.floor(Date.now() / 1000) + }); + + // Return the token (base64 encoded event) + return btoa(JSON.stringify(event)); +} + +// Helper function to handle API errors with user-friendly messages +async function handleAPIError(response: Response) { + if (response.status === 401) { + throw new Error('Authentication failed. Please make sure you are logged in with a Nostr account.'); + } else if (response.status === 402) { + throw new Error('Insufficient credits. Please add credits to your account to use premium models, or use the free "tybalt" model.'); + } else if (response.status === 400) { + try { + const error = await response.json(); + if (error.error?.type === 'invalid_request_error') { + // Handle specific validation errors + if (error.error.code === 'minimum_amount_not_met') { + throw new Error(`Minimum credit amount is $${error.error.minimum_amount}. Please increase your payment amount.`); + } else if (error.error.code === 'unsupported_method') { + throw new Error('Payment method not supported. Please use "stripe" or "lightning".'); + } else if (error.error.code === 'invalid_url') { + throw new Error('Invalid redirect URL provided for Stripe payment.'); + } + } + throw new Error(`Invalid request: ${error.error?.message || error.details || error.error || 'Please check your request parameters.'}`); + } catch { + throw new Error('Invalid request. Please check your parameters and try again.'); + } + } else if (response.status === 404) { + throw new Error('Resource not found. Please check the payment ID or try again.'); + } else if (response.status >= 500) { + throw new Error('Server error. Please try again in a few moments.'); + } else if (!response.ok) { + try { + const errorData = await response.json(); + throw new Error(`API error: ${errorData.error?.message || errorData.details || errorData.error || response.statusText}`); + } catch { + throw new Error(`Network error: ${response.statusText}. Please check your connection and try again.`); + } + } +} + +export function useShakespeare() { + const { user } = useCurrentUser(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + // Clear error helper + const clearError = useCallback(() => { + setError(null); + }, []); + + // Chat completion function + const sendChatMessage = useCallback(async ( + messages: ChatMessage[], + model: string = 'shakespeare', + options?: Partial<ChatCompletionRequest> + ): Promise<ChatCompletionResponse> => { + if (!user) { + throw new Error('User must be logged in to use AI features'); + } + + setIsLoading(true); + setError(null); + + try { + const requestBody: ChatCompletionRequest = { + model, + messages, + ...options + }; + + const token = await createNIP98Token( + 'POST', + `${SHAKESPEARE_API_URL}/chat/completions`, + requestBody, + user + ); + + const response = await fetch(`${SHAKESPEARE_API_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Authorization': `Nostr ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + await handleAPIError(response); + return await response.json(); + } catch (err) { + let errorMessage = 'An unexpected error occurred'; + + if (err instanceof Error) { + errorMessage = err.message; + } else if (typeof err === 'string') { + errorMessage = err; + } + + // Add context for common issues + if (errorMessage.includes('Failed to fetch') || errorMessage.includes('Network')) { + errorMessage = 'Network error: Please check your internet connection and try again.'; + } else if (errorMessage.includes('signer')) { + errorMessage = 'Authentication error: Please make sure you are logged in with a Nostr account that supports signing.'; + } + + setError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsLoading(false); + } + }, [user]); + + // Streaming chat completion function + const sendStreamingMessage = useCallback(async ( + messages: ChatMessage[], + model: string = 'shakespeare', + onChunk: (chunk: string) => void, + options?: Partial<ChatCompletionRequest> + ): Promise<void> => { + if (!user) { + throw new Error('User must be logged in to use AI features'); + } + + setIsLoading(true); + setError(null); + + try { + const requestBody: ChatCompletionRequest = { + model, + messages, + stream: true, + ...options + }; + + const token = await createNIP98Token( + 'POST', + `${SHAKESPEARE_API_URL}/chat/completions`, + requestBody, + user + ); + + const response = await fetch(`${SHAKESPEARE_API_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Authorization': `Nostr ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + await handleAPIError(response); + + if (!response.body) { + throw new Error('No response body'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') return; + + try { + const parsed = JSON.parse(data); + const content = parsed.choices?.[0]?.delta?.content; + if (content) { + onChunk(content); + } + } catch { + // Ignore parsing errors for incomplete chunks + } + } + } + } + } finally { + reader.releaseLock(); + } + } catch (err) { + let errorMessage = 'An unexpected error occurred'; + + if (err instanceof Error) { + errorMessage = err.message; + } else if (typeof err === 'string') { + errorMessage = err; + } + + // Add context for common issues + if (errorMessage.includes('Failed to fetch') || errorMessage.includes('Network')) { + errorMessage = 'Network error: Please check your internet connection and try again.'; + } else if (errorMessage.includes('signer')) { + errorMessage = 'Authentication error: Please make sure you are logged in with a Nostr account that supports signing.'; + } + + setError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsLoading(false); + } + }, [user]); + + // Get available models + const getAvailableModels = useCallback(async (): Promise<ModelsResponse> => { + if (!user) { + throw new Error('User must be logged in to use AI features'); + } + + setIsLoading(true); + setError(null); + + try { + const token = await createNIP98Token( + 'GET', + `${SHAKESPEARE_API_URL}/models`, + undefined, + user + ); + + const response = await fetch(`${SHAKESPEARE_API_URL}/models`, { + method: 'GET', + headers: { + 'Authorization': `Nostr ${token}`, + }, + }); + + await handleAPIError(response); + return await response.json(); + } catch (err) { + let errorMessage = 'An unexpected error occurred'; + + if (err instanceof Error) { + errorMessage = err.message; + } else if (typeof err === 'string') { + errorMessage = err; + } + + // Add context for common issues + if (errorMessage.includes('Failed to fetch') || errorMessage.includes('Network')) { + errorMessage = 'Network error: Please check your internet connection and try again.'; + } else if (errorMessage.includes('signer')) { + errorMessage = 'Authentication error: Please make sure you are logged in with a Nostr account that supports signing.'; + } + + setError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsLoading(false); + } + }, [user]); + + return { + // State + isLoading, + error, + isAuthenticated: !!user, + + // Actions + sendChatMessage, + sendStreamingMessage, + getAvailableModels, + clearError, + }; +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..24f7a5d --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,20 @@ +import { type Theme } from "@/contexts/AppContext"; +import { useAppContext } from "@/hooks/useAppContext"; + +/** + * Hook to get and set the active theme + * @returns Theme context with theme and setTheme + */ +export function useTheme(): { theme: Theme; setTheme: (theme: Theme) => void } { + const { config, updateConfig } = useAppContext(); + + return { + theme: config.theme, + setTheme: (theme: Theme) => { + updateConfig((currentConfig) => ({ + ...currentConfig, + theme, + })); + } + } +} \ No newline at end of file diff --git a/components/ui/use-toast.ts b/src/hooks/useToast.ts similarity index 89% rename from components/ui/use-toast.ts rename to src/hooks/useToast.ts index 02e111d..62cc2e2 100644 --- a/components/ui/use-toast.ts +++ b/src/hooks/useToast.ts @@ -1,6 +1,3 @@ -"use client" - -// Inspired by react-hot-toast library import * as React from "react" import type { @@ -18,13 +15,6 @@ type ToasterToast = ToastProps & { action?: ToastActionElement } -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - let count = 0 function genId() { @@ -32,23 +22,21 @@ function genId() { return count.toString() } -type ActionType = typeof actionTypes - type Action = | { - type: ActionType["ADD_TOAST"] + type: "ADD_TOAST"; toast: ToasterToast } | { - type: ActionType["UPDATE_TOAST"] + type: "UPDATE_TOAST"; toast: Partial<ToasterToast> } | { - type: ActionType["DISMISS_TOAST"] + type: "DISMISS_TOAST"; toastId?: ToasterToast["id"] } | { - type: ActionType["REMOVE_TOAST"] + type: "REMOVE_TOAST"; toastId?: ToasterToast["id"] } diff --git a/src/hooks/useUploadFile.ts b/src/hooks/useUploadFile.ts new file mode 100644 index 0000000..a3c9793 --- /dev/null +++ b/src/hooks/useUploadFile.ts @@ -0,0 +1,26 @@ +import { useMutation } from "@tanstack/react-query"; +import { BlossomUploader } from '@nostrify/nostrify/uploaders'; + +import { useCurrentUser } from "./useCurrentUser"; + +export function useUploadFile() { + const { user } = useCurrentUser(); + + return useMutation({ + mutationFn: async (file: File) => { + if (!user) { + throw new Error('Must be logged in to upload files'); + } + + const uploader = new BlossomUploader({ + servers: [ + 'https://blossom.primal.net/', + ], + signer: user.signer, + }); + + const tags = await uploader.upload(file); + return tags; + }, + }); +} \ No newline at end of file diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts new file mode 100644 index 0000000..0cf9e88 --- /dev/null +++ b/src/hooks/useWallet.ts @@ -0,0 +1,41 @@ +import { useMemo } from 'react'; +import { useNWC } from '@/hooks/useNWCContext'; +import type { WebLNProvider } from '@webbtc/webln-types'; + +export interface WalletStatus { + hasNWC: boolean; + webln: WebLNProvider | null; + activeNWC: ReturnType<typeof useNWC>['getActiveConnection'] extends () => infer T ? T : null; + preferredMethod: 'nwc' | 'webln' | 'manual'; +} + +export function useWallet() { + const { connections, getActiveConnection } = useNWC(); + + // Get the active connection directly - no memoization to avoid stale state + const activeNWC = getActiveConnection(); + + // Access WebLN directly from browser global scope + const webln = (globalThis as { webln?: WebLNProvider }).webln || null; + + // Calculate status values reactively + const hasNWC = useMemo(() => { + return connections.length > 0 && connections.some(c => c.isConnected); + }, [connections]); + + // Determine preferred payment method + const preferredMethod: WalletStatus['preferredMethod'] = activeNWC + ? 'nwc' + : webln + ? 'webln' + : 'manual'; + + const status: WalletStatus = { + hasNWC, + webln, + activeNWC, + preferredMethod, + }; + + return status; +} \ No newline at end of file diff --git a/src/hooks/useZaps.ts b/src/hooks/useZaps.ts new file mode 100644 index 0000000..ec16d45 --- /dev/null +++ b/src/hooks/useZaps.ts @@ -0,0 +1,350 @@ +import { useState, useMemo, useEffect, useCallback } from 'react'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAuthor } from '@/hooks/useAuthor'; +import { useAppContext } from '@/hooks/useAppContext'; +import { useToast } from '@/hooks/useToast'; +import { useNWC } from '@/hooks/useNWCContext'; +import type { NWCConnection } from '@/hooks/useNWC'; +import { nip57 } from 'nostr-tools'; +import type { Event } from 'nostr-tools'; +import type { WebLNProvider } from '@webbtc/webln-types'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useNostr } from '@nostrify/react'; +import type { NostrEvent } from '@nostrify/nostrify'; + +export function useZaps( + target: Event | Event[], + webln: WebLNProvider | null, + _nwcConnection: NWCConnection | null, + onZapSuccess?: () => void +) { + const { nostr } = useNostr(); + const { toast } = useToast(); + const { user } = useCurrentUser(); + const { config } = useAppContext(); + const queryClient = useQueryClient(); + + // Handle the case where an empty array is passed (from ZapButton when external data is provided) + const actualTarget = Array.isArray(target) ? (target.length > 0 ? target[0] : null) : target; + + const author = useAuthor(actualTarget?.pubkey); + const { sendPayment, getActiveConnection } = useNWC(); + const [isZapping, setIsZapping] = useState(false); + const [invoice, setInvoice] = useState<string | null>(null); + + // Cleanup state when component unmounts + useEffect(() => { + return () => { + setIsZapping(false); + setInvoice(null); + }; + }, []); + + const { data: zapEvents, ...query } = useQuery<NostrEvent[], Error>({ + queryKey: ['zaps', actualTarget?.id], + staleTime: 30000, // 30 seconds + refetchInterval: (query) => { + // Only refetch if the query is currently being observed (component is mounted) + return query.getObserversCount() > 0 ? 60000 : false; + }, + queryFn: async (c) => { + if (!actualTarget) return []; + + const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]); + + // Query for zap receipts for this specific event + if (actualTarget.kind >= 30000 && actualTarget.kind < 40000) { + // Addressable event + const identifier = actualTarget.tags.find((t) => t[0] === 'd')?.[1] || ''; + const events = await nostr.query([{ + kinds: [9735], + '#a': [`${actualTarget.kind}:${actualTarget.pubkey}:${identifier}`], + }], { signal }); + return events; + } else { + // Regular event + const events = await nostr.query([{ + kinds: [9735], + '#e': [actualTarget.id], + }], { signal }); + return events; + } + }, + enabled: !!actualTarget?.id, + }); + + // Process zap events into simple counts and totals + const { zapCount, totalSats, zaps } = useMemo(() => { + if (!zapEvents || !Array.isArray(zapEvents) || !actualTarget) { + return { zapCount: 0, totalSats: 0, zaps: [] }; + } + + let count = 0; + let sats = 0; + + zapEvents.forEach(zap => { + count++; + + // Try multiple methods to extract the amount: + + // Method 1: amount tag (from zap request, sometimes copied to receipt) + const amountTag = zap.tags.find(([name]) => name === 'amount')?.[1]; + if (amountTag) { + const millisats = parseInt(amountTag); + sats += Math.floor(millisats / 1000); + return; + } + + // Method 2: Extract from bolt11 invoice + const bolt11Tag = zap.tags.find(([name]) => name === 'bolt11')?.[1]; + if (bolt11Tag) { + try { + const invoiceSats = nip57.getSatoshisAmountFromBolt11(bolt11Tag); + sats += invoiceSats; + return; + } catch (error) { + console.warn('Failed to parse bolt11 amount:', error); + } + } + + // Method 3: Parse from description (zap request JSON) + const descriptionTag = zap.tags.find(([name]) => name === 'description')?.[1]; + if (descriptionTag) { + try { + const zapRequest = JSON.parse(descriptionTag); + const requestAmountTag = zapRequest.tags?.find(([name]: string[]) => name === 'amount')?.[1]; + if (requestAmountTag) { + const millisats = parseInt(requestAmountTag); + sats += Math.floor(millisats / 1000); + return; + } + } catch (error) { + console.warn('Failed to parse description JSON:', error); + } + } + + console.warn('Could not extract amount from zap receipt:', zap.id); + }); + + + return { zapCount: count, totalSats: sats, zaps: zapEvents }; + }, [zapEvents, actualTarget]); + + const zap = async (amount: number, comment: string) => { + if (amount <= 0) { + return; + } + + setIsZapping(true); + setInvoice(null); // Clear any previous invoice at the start + + if (!user) { + toast({ + title: 'Login required', + description: 'You must be logged in to send a zap.', + variant: 'destructive', + }); + setIsZapping(false); + return; + } + + if (!actualTarget) { + toast({ + title: 'Event not found', + description: 'Could not find the event to zap.', + variant: 'destructive', + }); + setIsZapping(false); + return; + } + + try { + if (!author.data || !author.data?.metadata || !author.data?.event ) { + toast({ + title: 'Author not found', + description: 'Could not find the author of this item.', + variant: 'destructive', + }); + setIsZapping(false); + return; + } + + const { lud06, lud16 } = author.data.metadata; + if (!lud06 && !lud16) { + toast({ + title: 'Lightning address not found', + description: 'The author does not have a lightning address configured.', + variant: 'destructive', + }); + setIsZapping(false); + return; + } + + // Get zap endpoint using the old reliable method + const zapEndpoint = await nip57.getZapEndpoint(author.data.event); + if (!zapEndpoint) { + toast({ + title: 'Zap endpoint not found', + description: 'Could not find a zap endpoint for the author.', + variant: 'destructive', + }); + setIsZapping(false); + return; + } + + // Create zap request - use appropriate event format based on kind + // For addressable events (30000-39999), pass the object to get 'a' tag + // For all other events, pass the ID string to get 'e' tag + const event = (actualTarget.kind >= 30000 && actualTarget.kind < 40000) + ? actualTarget + : actualTarget.id; + + const zapAmount = amount * 1000; // convert to millisats + + const zapRequest = nip57.makeZapRequest({ + profile: actualTarget.pubkey, + event: event, + amount: zapAmount, + relays: config.relayMetadata.relays.map(r => r.url), + comment + }); + + // Sign the zap request (but don't publish to relays - only send to LNURL endpoint) + if (!user.signer) { + throw new Error('No signer available'); + } + const signedZapRequest = await user.signer.signEvent(zapRequest); + + try { + const res = await fetch(`${zapEndpoint}?amount=${zapAmount}&nostr=${encodeURI(JSON.stringify(signedZapRequest))}`); + const responseData = await res.json(); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${responseData.reason || 'Unknown error'}`); + } + + const newInvoice = responseData.pr; + if (!newInvoice || typeof newInvoice !== 'string') { + throw new Error('Lightning service did not return a valid invoice'); + } + + // Get the current active NWC connection dynamically + const currentNWCConnection = getActiveConnection(); + + // Try NWC first if available and properly connected + if (currentNWCConnection && currentNWCConnection.connectionString && currentNWCConnection.isConnected) { + try { + await sendPayment(currentNWCConnection, newInvoice); + + // Clear states immediately on success + setIsZapping(false); + setInvoice(null); + + toast({ + title: 'Zap successful!', + description: `You sent ${amount} sats via NWC to the author.`, + }); + + // Invalidate zap queries to refresh counts + queryClient.invalidateQueries({ queryKey: ['zaps'] }); + + // Close dialog last to ensure clean state + onZapSuccess?.(); + return; + } catch (nwcError) { + console.error('NWC payment failed, falling back:', nwcError); + + // Show specific NWC error to user for debugging + const errorMessage = nwcError instanceof Error ? nwcError.message : 'Unknown NWC error'; + toast({ + title: 'NWC payment failed', + description: `${errorMessage}. Falling back to other payment methods...`, + variant: 'destructive', + }); + } + } + + if (webln) { // Try WebLN next + try { + // For native WebLN, we may need to enable it first + let webLnProvider = webln; + if (webln.enable && typeof webln.enable === 'function') { + const enabledProvider = await webln.enable(); + // Some implementations return the provider, others return void + // Cast to WebLNProvider to handle both cases + const provider = enabledProvider as WebLNProvider | undefined; + if (provider) { + webLnProvider = provider; + } + } + + await webLnProvider.sendPayment(newInvoice); + + // Clear states immediately on success + setIsZapping(false); + setInvoice(null); + + toast({ + title: 'Zap successful!', + description: `You sent ${amount} sats to the author.`, + }); + + // Invalidate zap queries to refresh counts + queryClient.invalidateQueries({ queryKey: ['zaps'] }); + + // Close dialog last to ensure clean state + onZapSuccess?.(); + } catch (weblnError) { + console.error('WebLN payment failed, falling back:', weblnError); + + // Show specific WebLN error to user for debugging + const errorMessage = weblnError instanceof Error ? weblnError.message : 'Unknown WebLN error'; + toast({ + title: 'WebLN payment failed', + description: `${errorMessage}. Falling back to other payment methods...`, + variant: 'destructive', + }); + + setInvoice(newInvoice); + setIsZapping(false); + } + } else { // Default - show QR code and manual Lightning URI + setInvoice(newInvoice); + setIsZapping(false); + } + } catch (err) { + console.error('Zap error:', err); + toast({ + title: 'Zap failed', + description: (err as Error).message, + variant: 'destructive', + }); + setIsZapping(false); + } + } catch (err) { + console.error('Zap error:', err); + toast({ + title: 'Zap failed', + description: (err as Error).message, + variant: 'destructive', + }); + setIsZapping(false); + } + }; + + const resetInvoice = useCallback(() => { + setInvoice(null); + }, []); + + return { + zaps, + zapCount, + totalSats, + ...query, + zap, + isZapping, + invoice, + setInvoice, + resetInvoice, + }; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..33fdf9d --- /dev/null +++ b/src/index.css @@ -0,0 +1,101 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + + --sidebar-background: 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%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/src/lib/dmConstants.ts b/src/lib/dmConstants.ts new file mode 100644 index 0000000..1794579 --- /dev/null +++ b/src/lib/dmConstants.ts @@ -0,0 +1,86 @@ +import type { NostrEvent } from '@nostrify/nostrify'; + +// ============================================================================ +// Message Protocol Types +// ============================================================================ + +export const MESSAGE_PROTOCOL = { + NIP04: 'nip04', + NIP17: 'nip17', + UNKNOWN: 'unknown', +} as const; + +export type MessageProtocol = typeof MESSAGE_PROTOCOL[keyof typeof MESSAGE_PROTOCOL]; + +// ============================================================================ +// Protocol Mode (for user selection) +// ============================================================================ + +export const PROTOCOL_MODE = { + NIP04_ONLY: 'nip04_only', + NIP17_ONLY: 'nip17_only', + NIP04_OR_NIP17: 'nip04_or_nip17', +} as const; + +export type ProtocolMode = typeof PROTOCOL_MODE[keyof typeof PROTOCOL_MODE]; + +// ============================================================================ +// Loading Phases +// ============================================================================ + +export const LOADING_PHASES = { + IDLE: 'idle', + CACHE: 'cache', + RELAYS: 'relays', + SUBSCRIPTIONS: 'subscriptions', + READY: 'ready', +} as const; + +export type LoadingPhase = typeof LOADING_PHASES[keyof typeof LOADING_PHASES]; + +// ============================================================================ +// Protocol Configuration +// ============================================================================ + +export const PROTOCOL_CONFIG = { + [MESSAGE_PROTOCOL.NIP04]: { + label: 'NIP-04', + description: 'Legacy DMs', + kind: 4, + }, + [MESSAGE_PROTOCOL.NIP17]: { + label: 'NIP-17', + description: 'Private DMs', + kind: 1059, + }, + [MESSAGE_PROTOCOL.UNKNOWN]: { + label: 'Unknown', + description: 'Unknown protocol', + kind: 0, + }, +} as const; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Get the message protocol from an event kind + */ +export function getMessageProtocol(event: NostrEvent): MessageProtocol { + switch (event.kind) { + case 4: + return MESSAGE_PROTOCOL.NIP04; + case 1059: + return MESSAGE_PROTOCOL.NIP17; + default: + return MESSAGE_PROTOCOL.UNKNOWN; + } +} + +/** + * Check if a protocol is valid for sending messages + */ +export function isValidSendProtocol(protocol: MessageProtocol): boolean { + return protocol === MESSAGE_PROTOCOL.NIP04 || protocol === MESSAGE_PROTOCOL.NIP17; +} diff --git a/src/lib/dmMessageStore.ts b/src/lib/dmMessageStore.ts new file mode 100644 index 0000000..40a7a45 --- /dev/null +++ b/src/lib/dmMessageStore.ts @@ -0,0 +1,117 @@ +import { openDB, type IDBPDatabase } from 'idb'; +import type { NostrEvent } from '@nostrify/nostrify'; + +// ============================================================================ +// IndexedDB Schema +// ============================================================================ + +// Use domain-based naming to avoid conflicts between apps on same domain +const getDBName = () => { + // Use hostname for unique DB per app (e.g., 'nostr-dm-store-localhost', 'nostr-dm-store-myapp.com') + const hostname = typeof window !== 'undefined' ? window.location.hostname : 'default'; + return `nostr-dm-store-${hostname}`; +}; +const DB_NAME = getDBName(); +const DB_VERSION = 1; +const STORE_NAME = 'messages'; + +interface StoredParticipant { + messages: NostrEvent[]; + lastActivity: number; + hasNIP4: boolean; + hasNIP17: boolean; +} + +export interface MessageStore { + participants: Record<string, StoredParticipant>; + lastSync: { + nip4: number | null; + nip17: number | null; + }; +} + +// ============================================================================ +// Database Operations +// ============================================================================ + +/** + * Open the IndexedDB database + */ +async function openDatabase(): Promise<IDBPDatabase> { + return openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + // Create the messages store if it doesn't exist + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }, + }); +} + +/** + * Write messages to IndexedDB for a specific user + * Messages are stored in their original encrypted form (kind 4 or kind 13) + */ +export async function writeMessagesToDB( + userPubkey: string, + messageStore: MessageStore +): Promise<void> { + try { + const db = await openDatabase(); + + // Store messages in their original encrypted form (no NIP-44 wrapper needed) + // Each message content is already encrypted by the sender + await db.put(STORE_NAME, messageStore, userPubkey); + } catch (error) { + console.error('[MessageStore] ❌ Error writing to IndexedDB:', error); + throw error; + } +} + +/** + * Read messages from IndexedDB for a specific user + * Messages are stored in their original encrypted form (kind 4 or kind 13) + */ +export async function readMessagesFromDB( + userPubkey: string +): Promise<MessageStore | undefined> { + try { + const db = await openDatabase(); + const data = await db.get(STORE_NAME, userPubkey); + + if (!data) { + return undefined; + } + + return data as MessageStore; + } catch (error) { + console.error('[MessageStore] Error reading from IndexedDB:', error); + throw error; + } +} + +/** + * Delete messages from IndexedDB for a specific user + */ +export async function deleteMessagesFromDB(userPubkey: string): Promise<void> { + try { + const db = await openDatabase(); + await db.delete(STORE_NAME, userPubkey); + } catch (error) { + console.error('[MessageStore] Error deleting from IndexedDB:', error); + throw error; + } +} + +/** + * Clear all messages from IndexedDB + */ +export async function clearAllMessages(): Promise<void> { + try { + const db = await openDatabase(); + await db.clear(STORE_NAME); + } catch (error) { + console.error('[MessageStore] Error clearing IndexedDB:', error); + throw error; + } +} diff --git a/src/lib/dmUtils.ts b/src/lib/dmUtils.ts new file mode 100644 index 0000000..8b5ba92 --- /dev/null +++ b/src/lib/dmUtils.ts @@ -0,0 +1,98 @@ +import type { NostrEvent } from '@nostrify/nostrify'; + +/** + * Validate that an event is a proper DM event + */ +export function validateDMEvent(event: NostrEvent): boolean { + // Must be kind 4 (NIP-04 DM) + if (event.kind !== 4) return false; + + // Must have a 'p' tag + const hasRecipient = event.tags?.some(([name]) => name === 'p'); + if (!hasRecipient) return false; + + // Must have content (even if encrypted) + if (!event.content) return false; + + return true; +} + +/** + * Get the recipient pubkey from a DM event + */ +export function getRecipientPubkey(event: NostrEvent): string | undefined { + return event.tags?.find(([name]) => name === 'p')?.[1]; +} + +/** + * Get the conversation partner pubkey from a DM event + * (the other person in the conversation, not the current user) + */ +export function getConversationPartner(event: NostrEvent, userPubkey: string): string | undefined { + const isFromUser = event.pubkey === userPubkey; + + if (isFromUser) { + // If we sent it, the partner is the recipient + return getRecipientPubkey(event); + } else { + // If they sent it, the partner is the author + return event.pubkey; + } +} + +/** + * Format timestamp for display (matches Signal/WhatsApp/Telegram pattern) + * Today: Show time (e.g., "2:45 PM") + * Yesterday: "Yesterday" + * This week: Day name (e.g., "Mon") + * This year: Month and day (e.g., "Jan 15") + * Older: Full date (e.g., "Jan 15, 2024") + */ +export function formatConversationTime(timestamp: number): string { + const date = new Date(timestamp * 1000); + const now = new Date(); + + // Start of today (midnight) + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + // Start of yesterday + const yesterdayStart = new Date(todayStart); + yesterdayStart.setDate(yesterdayStart.getDate() - 1); + + // Start of this week (assuming week starts on Sunday, adjust if needed) + const weekStart = new Date(todayStart); + weekStart.setDate(weekStart.getDate() - weekStart.getDay()); + + if (date >= todayStart) { + // Today: Show time (e.g., "2:45 PM") + return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); + } else if (date >= yesterdayStart) { + // Yesterday + return 'Yesterday'; + } else if (date >= weekStart) { + // This week: Show day name (e.g., "Monday") + return date.toLocaleDateString(undefined, { weekday: 'short' }); + } else if (date.getFullYear() === now.getFullYear()) { + // This year: Show month and day (e.g., "Jan 15") + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + } else { + // Older: Show full date (e.g., "Jan 15, 2024") + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + } +} + +/** + * Format timestamp as full date and time for tooltips + * e.g., "Mon, Jan 15, 2024, 2:45 PM" + */ +export function formatFullDateTime(timestamp: number): string { + const date = new Date(timestamp * 1000); + return date.toLocaleString(undefined, { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit' + }); +} diff --git a/src/lib/genUserName.test.ts b/src/lib/genUserName.test.ts new file mode 100644 index 0000000..b92b52a --- /dev/null +++ b/src/lib/genUserName.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { genUserName } from './genUserName'; + +describe('genUserName', () => { + it('generates a deterministic name from a seed', () => { + const seed = 'test-seed-123'; + const name1 = genUserName(seed); + const name2 = genUserName(seed); + + expect(name1).toEqual('Brave Whale'); + expect(name1).toEqual(name2); + }); + + it('generates different names for different seeds', () => { + const name1 = genUserName('seed1'); + const name2 = genUserName('seed2'); + const name3 = genUserName('seed3'); + + // While it's theoretically possible for different seeds to generate the same name, + // it's very unlikely with our word lists + expect(name1).not.toBe(name2); + expect(name2).not.toBe(name3); + expect(name1).not.toBe(name3); + }); + + it('handles typical Nostr pubkey format', () => { + // Typical hex pubkey (64 characters) + const pubkey = 'e4690a13290739da123aa17d553851dec4cdd0e9d89aa18de3741c446caf8761'; + const name = genUserName(pubkey); + + expect(name).toEqual('Gentle Hawk'); + }); +}); \ No newline at end of file diff --git a/src/lib/genUserName.ts b/src/lib/genUserName.ts new file mode 100644 index 0000000..0ac13d9 --- /dev/null +++ b/src/lib/genUserName.ts @@ -0,0 +1,29 @@ +/** Generate a deterministic user display name based on a string seed. */ +export function genUserName(seed: string): string { + // Use a simple hash of the pubkey to generate consistent adjective + noun combinations + const adjectives = [ + 'Swift', 'Bright', 'Calm', 'Bold', 'Wise', 'Kind', 'Quick', 'Brave', + 'Cool', 'Sharp', 'Clear', 'Strong', 'Smart', 'Fast', 'Keen', 'Pure', + 'Noble', 'Gentle', 'Fierce', 'Steady', 'Clever', 'Proud', 'Silent', 'Wild' + ]; + + const nouns = [ + 'Fox', 'Eagle', 'Wolf', 'Bear', 'Lion', 'Tiger', 'Hawk', 'Owl', + 'Deer', 'Raven', 'Falcon', 'Lynx', 'Otter', 'Whale', 'Shark', 'Dolphin', + 'Phoenix', 'Dragon', 'Panther', 'Jaguar', 'Cheetah', 'Leopard', 'Puma', 'Cobra' + ]; + + // Create a simple hash from the pubkey + let hash = 0; + for (let i = 0; i < seed.length; i++) { + const char = seed.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + + // Use absolute value to ensure positive index + const adjIndex = Math.abs(hash) % adjectives.length; + const nounIndex = Math.abs(hash >> 8) % nouns.length; + + return [adjectives[adjIndex], nouns[nounIndex]].join(' '); +} \ No newline at end of file diff --git a/src/lib/polyfills.ts b/src/lib/polyfills.ts new file mode 100644 index 0000000..17e7e56 --- /dev/null +++ b/src/lib/polyfills.ts @@ -0,0 +1,90 @@ +import Buffer from 'buffer'; + +/** + * Polyfill for Buffer in browser environment + * + * Many Node.js libraries like isomorphic-git and bitcoinjs-lib expect Buffer to be globally available. + * This polyfill makes the buffer package's Buffer available globally. + */ +if (!globalThis.Buffer) { + globalThis.Buffer = Buffer.Buffer; +} + +/** + * Polyfill for AbortSignal.any() + * + * AbortSignal.any() creates an AbortSignal that will be aborted when any of the + * provided signals are aborted. This is useful for combining multiple abort signals. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static + */ + +// Check if AbortSignal.any is already available +if (!AbortSignal.any) { + AbortSignal.any = function(signals: AbortSignal[]): AbortSignal { + // If no signals provided, return a signal that never aborts + if (signals.length === 0) { + return new AbortController().signal; + } + + // If only one signal, return it directly for efficiency + if (signals.length === 1) { + return signals[0]; + } + + // Check if any signal is already aborted + for (const signal of signals) { + if (signal.aborted) { + // Create an already-aborted signal with the same reason + const controller = new AbortController(); + controller.abort(signal.reason); + return controller.signal; + } + } + + // Create a new controller for the combined signal + const controller = new AbortController(); + + // Function to abort the combined signal + const onAbort = (event: Event) => { + const target = event.target as AbortSignal; + controller.abort(target.reason); + }; + + // Listen for abort events on all input signals + for (const signal of signals) { + signal.addEventListener('abort', onAbort, { once: true }); + } + + // Clean up listeners when the combined signal is aborted + controller.signal.addEventListener('abort', () => { + for (const signal of signals) { + signal.removeEventListener('abort', onAbort); + } + }, { once: true }); + + return controller.signal; + }; +} + +/** + * Polyfill for AbortSignal.timeout() + * + * AbortSignal.timeout() creates an AbortSignal that will be aborted after a + * specified number of milliseconds. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static + */ + +// Check if AbortSignal.timeout is already available +if (!AbortSignal.timeout) { + AbortSignal.timeout = function(milliseconds: number): AbortSignal { + const controller = new AbortController(); + + setTimeout(() => { + controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError')); + }, milliseconds); + + return controller.signal; + }; +} \ No newline at end of file diff --git a/lib/utils.ts b/src/lib/utils.ts similarity index 100% rename from lib/utils.ts rename to src/lib/utils.ts diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..e263904 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,17 @@ +import { createRoot } from 'react-dom/client'; + +// Import polyfills first +import './lib/polyfills.ts'; + +import { ErrorBoundary } from '@/components/ErrorBoundary'; +import App from './App.tsx'; +import './index.css'; + +// FIXME: a custom font should be used. Eg: +// import '@fontsource-variable/<font-name>'; + +createRoot(document.getElementById("root")!).render( + <ErrorBoundary> + <App /> + </ErrorBoundary> +); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx new file mode 100644 index 0000000..4427de5 --- /dev/null +++ b/src/pages/Index.tsx @@ -0,0 +1,25 @@ +import { useSeoMeta } from '@unhead/react'; + +// FIXME: Update this page (the content is just a fallback if you fail to update the page) + +const Index = () => { + useSeoMeta({ + title: 'Welcome to Your Blank App', + description: 'A modern Nostr client application built with React, TailwindCSS, and Nostrify.', + }); + + return ( + <div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900"> + <div className="text-center"> + <h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-gray-100"> + Welcome to Your Blank App + </h1> + <p className="text-xl text-gray-600 dark:text-gray-400"> + Start building your amazing project here! + </p> + </div> + </div> + ); +}; + +export default Index; diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx new file mode 100644 index 0000000..121cd08 --- /dev/null +++ b/src/pages/Messages.tsx @@ -0,0 +1,24 @@ +import { useSeoMeta } from '@unhead/react'; +import { DMMessagingInterface } from '@/components/dm/DMMessagingInterface'; + +const Messages = () => { + useSeoMeta({ + title: 'Messages', + description: 'Private encrypted messaging on Nostr', + }); + + return ( + <div className="min-h-screen bg-background"> + <div className="container mx-auto p-4 h-screen flex flex-col"> + {/* Header */} + <div className="flex items-center justify-between mb-4"> + <h1 className="text-2xl font-semibold">Messages</h1> + </div> + + <DMMessagingInterface className="flex-1" /> + </div> + </div> + ); +}; + +export default Messages; diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx new file mode 100644 index 0000000..5aa4439 --- /dev/null +++ b/src/pages/NIP19Page.tsx @@ -0,0 +1,42 @@ +import { nip19 } from 'nostr-tools'; +import { useParams } from 'react-router-dom'; +import NotFound from './NotFound'; + +export function NIP19Page() { + const { nip19: identifier } = useParams<{ nip19: string }>(); + + if (!identifier) { + return <NotFound />; + } + + let decoded; + try { + decoded = nip19.decode(identifier); + } catch { + return <NotFound />; + } + + const { type } = decoded; + + switch (type) { + case 'npub': + case 'nprofile': + // AI agent should implement profile view here + return <div>Profile placeholder</div>; + + case 'note': + // AI agent should implement note view here + return <div>Note placeholder</div>; + + case 'nevent': + // AI agent should implement event view here + return <div>Event placeholder</div>; + + case 'naddr': + // AI agent should implement addressable event view here + return <div>Addressable event placeholder</div>; + + default: + return <NotFound />; + } +} \ No newline at end of file diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx new file mode 100644 index 0000000..85e429d --- /dev/null +++ b/src/pages/NotFound.tsx @@ -0,0 +1,33 @@ +import { useSeoMeta } from "@unhead/react"; +import { useLocation } from "react-router-dom"; +import { useEffect } from "react"; + +const NotFound = () => { + const location = useLocation(); + + useSeoMeta({ + title: "404 - Page Not Found", + description: "The page you are looking for could not be found. Return to the home page to continue browsing.", + }); + + useEffect(() => { + console.error( + "404 Error: User attempted to access non-existent route:", + location.pathname + ); + }, [location.pathname]); + + return ( + <div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900"> + <div className="text-center"> + <h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-gray-100">404</h1> + <p className="text-xl text-gray-600 dark:text-gray-400 mb-4">Oops! Page not found</p> + <a href="/" className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline"> + Return to Home + </a> + </div> + </div> + ); +}; + +export default NotFound; diff --git a/src/test/ErrorBoundary.test.tsx b/src/test/ErrorBoundary.test.tsx new file mode 100644 index 0000000..3f49427 --- /dev/null +++ b/src/test/ErrorBoundary.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; + + + +// Test component that throws an error +const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error('Test error'); + } + return <div>No error</div>; +}; + +describe('ErrorBoundary', () => { + it('renders children when no error occurs', () => { + render( + <ErrorBoundary> + <div>Test content</div> + </ErrorBoundary> + ); + + expect(screen.getByText('Test content')).toBeInTheDocument(); + }); + + it('catches and displays error when child throws', () => { + // Suppress console.error for this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + <ErrorBoundary> + <ThrowError shouldThrow={true} /> + </ErrorBoundary> + ); + + expect(screen.getByText('Something went wrong')).toBeInTheDocument(); + expect(screen.getByText('An unexpected error occurred. The error has been reported.')).toBeInTheDocument(); + + consoleSpy.mockRestore(); + }); + + + + it('uses custom fallback when provided', () => { + const customFallback = <div>Custom error message</div>; + + // Suppress console.error for this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + <ErrorBoundary fallback={customFallback}> + <ThrowError shouldThrow={true} /> + </ErrorBoundary> + ); + + expect(screen.getByText('Custom error message')).toBeInTheDocument(); + + consoleSpy.mockRestore(); + }); +}); \ No newline at end of file diff --git a/src/test/TestApp.tsx b/src/test/TestApp.tsx new file mode 100644 index 0000000..3b2d4b9 --- /dev/null +++ b/src/test/TestApp.tsx @@ -0,0 +1,53 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createHead, UnheadProvider } from '@unhead/react/client'; +import { BrowserRouter } from 'react-router-dom'; +import { NostrLoginProvider } from '@nostrify/react/login'; +import NostrProvider from '@/components/NostrProvider'; +import { AppProvider } from '@/components/AppProvider'; +import { NWCProvider } from '@/contexts/NWCContext'; +import { AppConfig } from '@/contexts/AppContext'; + +interface TestAppProps { + children: React.ReactNode; +} + +export function TestApp({ children }: TestAppProps) { + const head = createHead(); + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + const defaultConfig: AppConfig = { + theme: 'light', + relayMetadata: { + relays: [ + { url: 'wss://relay.nostr.band', read: true, write: true }, + ], + updatedAt: 0, + }, + }; + + return ( + <UnheadProvider head={head}> + <AppProvider storageKey='test-app-config' defaultConfig={defaultConfig}> + <QueryClientProvider client={queryClient}> + <NostrLoginProvider storageKey='test-login'> + <NostrProvider> + <NWCProvider> + <BrowserRouter> + {children} + </BrowserRouter> + </NWCProvider> + </NostrProvider> + </NostrLoginProvider> + </QueryClientProvider> + </AppProvider> + </UnheadProvider> + ); +} + +export default TestApp; \ No newline at end of file diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..fb9808b --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,40 @@ +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; + +// Mock window.matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// Mock window.scrollTo +Object.defineProperty(window, 'scrollTo', { + writable: true, + value: vi.fn(), +}); + +// Mock IntersectionObserver +global.IntersectionObserver = vi.fn().mockImplementation((_callback) => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + root: null, + rootMargin: '', + thresholds: [], +})); + +// Mock ResizeObserver +global.ResizeObserver = vi.fn().mockImplementation((_callback) => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// <reference types="vite/client" /> diff --git a/stack.json b/stack.json new file mode 100644 index 0000000..816d30f --- /dev/null +++ b/stack.json @@ -0,0 +1,19 @@ +{ + "name": "mkstack", + "title": "MKStack", + "description": "Build Nostr apps in minutes with this React framework", + "uri": "git+https://gitlab.com/soapbox-pub/mkstack.git", + "image": "https://gitlab.com/-/project/69048882/uploads/0a9e3a19f2901d5c772952c1de10ce4f/image.png", + "commands": [ + "npm", + "npx" + ], + "tech": [ + "react", + "typescript", + "tailwind", + "shadcn-ui", + "nostrify", + "vite" + ] +} \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index 3e088fc..8ff2536 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,95 +1,97 @@ -import type { Config } from "tailwindcss" +import type { Config } from "tailwindcss"; +import tailwindcssAnimate from "tailwindcss-animate"; -const config = { - darkMode: ["class"], - content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx}', +export default { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", ], - prefix: "", - theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px' - } - }, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - keyframes: { - 'accordion-down': { - from: { - height: '0' - }, - to: { - height: 'var(--radix-accordion-content-height)' - } - }, - 'accordion-up': { - from: { - height: 'var(--radix-accordion-content-height)' - }, - to: { - height: '0' - } - } - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out' - } - } - }, - plugins: [require("tailwindcss-animate")], -} satisfies Config - -export default config \ No newline at end of file + prefix: "", + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + } + } + }, + plugins: [tailwindcssAnimate], +} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json index dcb55b0..a7596da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,31 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", + + /* Bundler mode */ "moduleResolution": "bundler", - "resolveJsonModule": true, + "allowImportingTsExtensions": true, "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": false, + "strictNullChecks": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitAny": false, + "noFallthroughCasesInSwitch": false, + + "baseUrl": ".", "paths": { - "@/*": ["./*"] + "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/page.tsx.orig"], - "exclude": ["node_modules"] + "include": ["src"] } diff --git a/types/nav.ts b/types/nav.ts deleted file mode 100644 index 49061ee..0000000 --- a/types/nav.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface NavItem { - title: string - href?: string - disabled?: boolean - external?: boolean - } \ No newline at end of file diff --git a/utils/bunkerUtils.ts b/utils/bunkerUtils.ts deleted file mode 100644 index 9c0c433..0000000 --- a/utils/bunkerUtils.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { SimplePool } from 'nostr-tools'; -import { BunkerSigner, parseBunkerInput } from 'nostr-tools/nip46'; - -/** - * Converts a hex string to a Uint8Array. - * @param hex The hex string to convert. - * @returns Uint8Array representation of the hex string. - */ -function hexToBytes(hex: string): Uint8Array { - if (hex.length % 2 !== 0) throw new Error('Invalid hex string'); - const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.substr(i, 2), 16); - } - return bytes; -} - -/** - * Creates a BunkerSigner instance from the stored bunker connection information - * @returns A Promise that resolves to a BunkerSigner instance or null if not using bunker login - */ -export async function getBunkerSigner(): Promise<{ - signer: BunkerSigner; - pool: SimplePool; -} | null> { - // Check if user is logged in with bunker - const loginType = localStorage.getItem("loginType"); - if (loginType !== "bunker") return null; - - // Get the stored bunker connection info - const localKeyHex = localStorage.getItem("bunkerLocalKey"); - const bunkerUrl = localStorage.getItem("bunkerUrl"); - - if (!localKeyHex || !bunkerUrl) return null; - - try { - // Convert hex to Uint8Array for the local key - const localKey = hexToBytes(localKeyHex); - - // Parse the bunker URL - const bunkerPointer = await parseBunkerInput(bunkerUrl); - if (!bunkerPointer) throw new Error('Invalid bunker URL'); - - // Create pool and bunker signer - const pool = new SimplePool(); - const signer = new BunkerSigner(localKey, bunkerPointer, { pool }); - - // Connect to the bunker - await signer.connect(); - - return { signer, pool }; - } catch (error) { - console.error('Error creating bunker signer:', error); - return null; - } -} - -/** - * Signs an event using the appropriate method based on login type - * @param event The unsigned event to sign - * @returns A Promise that resolves to the signed event or null if signing failed - */ -export async function signEventWithBunker(event: any): Promise<any | null> { - const loginType = localStorage.getItem("loginType"); - - if (loginType === "bunker") { - const bunkerConnection = await getBunkerSigner(); - if (!bunkerConnection) return null; - - try { - const { signer, pool } = bunkerConnection; - - // Sign the event using the bunker - const signedEvent = await signer.signEvent(event); - - // Clean up - await signer.close(); - pool.close([]); - - return signedEvent; - } catch (error) { - console.error('Error signing with bunker:', error); - return null; - } - } - - return null; -} \ No newline at end of file diff --git a/utils/nip65Utils.ts b/utils/nip65Utils.ts deleted file mode 100644 index e314dd2..0000000 --- a/utils/nip65Utils.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { SimplePool, Filter, Event } from 'nostr-tools'; - -// Interface for NIP-65 relay with read/write permissions -export interface Nip65Relay { - url: string; - read: boolean; - write: boolean; -} - -// Interface for relay configuration -export interface RelayConfig { - inbox: string[]; // Read relays - outbox: string[]; // Write relays - all: string[]; // All relays (for backward compatibility) -} - -/** - * Fetches NIP-65 relay list metadata for a specific user - * @param pubkey User's public key - * @param relays Relays to query for NIP-65 events - * @returns Object with parsed relay permissions - */ -export async function fetchNip65Relays(pubkey: string, relays: string[]): Promise<Nip65Relay[]> { - // Create a pool for temporary use - const pool = new SimplePool(); - - try { - // Define filter for NIP-65 events (kind:10002) - const filter: Filter = { - kinds: [10002], - authors: [pubkey], - limit: 1, // We only need the most recent one - }; - - // Fetch the event (pool.get returns a single event or undefined) - const latestEvent = await pool.get(relays, filter); - - if (!latestEvent) { - return []; - } - - // Parse the relay tags - return parseNip65Event(latestEvent); - } catch (error) { - console.error('Error fetching NIP-65 relays:', error); - return []; - } finally { - // Close the pool to clean up connections - pool.close(relays); - } -} - -/** - * Parses a NIP-65 event and extracts relay information - * @param event NIP-65 event (kind:10002) - * @returns Array of relays with read/write permissions - */ -export function parseNip65Event(event: Event): Nip65Relay[] { - if (event.kind !== 10002) { - return []; - } - - const relays: Nip65Relay[] = []; - - // Process each 'r' tag - for (const tag of event.tags) { - if (tag[0] === 'r' && tag[1]) { - const url = tag[1]; - const permission = tag[2]?.toLowerCase(); - - // Default is both read and write if no permission specified - relays.push({ - url, - read: permission ? permission.includes('read') : true, - write: permission ? permission.includes('write') : true - }); - } - } - - return relays; -} - -/** - * Gets the current relay configuration from localStorage - * @returns RelayConfig object with inbox, outbox, and all relays - */ -export function getRelayConfig(): RelayConfig { - // Check if we're on the client side - if (typeof window === 'undefined') { - // Return default config for server-side rendering - return { - inbox: ["wss://relay.nostr.band", "wss://relay.damus.io", "wss://nos.lol"], - outbox: ["wss://relay.nostr.band", "wss://relay.damus.io", "wss://nos.lol"], - all: ["wss://relay.nostr.band", "wss://relay.damus.io", "wss://nos.lol"] - }; - } - - try { - const customRelays = JSON.parse(localStorage.getItem("customRelays") || "[]"); - const nip65Relays = JSON.parse(localStorage.getItem("nip65Relays") || "[]"); - - // Default relays - updated with more reliable options - const defaultRelays = [ - "wss://relay.nostr.band", - "wss://relay.damus.io", - "wss://nos.lol", - "wss://freelay.sovbit.host" - ]; - - // Combine all relays - const allRelays = Array.from(new Set([...defaultRelays, ...customRelays, ...nip65Relays])); - - // Filter out localhost relays to prevent connection errors - const filteredRelays = allRelays.filter(relay => { - try { - const url = new URL(relay); - return !url.hostname.includes('localhost') && !url.hostname.includes('127.0.0.1'); - } catch { - // If URL parsing fails, keep the relay - return true; - } - }); - - // Only log once per session to reduce console spam - if (!(window as any).__relayConfigLogged) { - console.log("Relay configuration:", { - customRelays, - nip65Relays, - defaultRelays, - allRelays, - filteredRelays - }); - (window as any).__relayConfigLogged = true; - } - - // For now, use all relays for both inbox and outbox - // In the future, this could be more sophisticated based on NIP-65 permissions - return { - inbox: filteredRelays, - outbox: filteredRelays, - all: filteredRelays - }; - } catch (error) { - console.error('Error getting relay config:', error); - // Fallback to basic relays - const fallbackRelays = [ - "wss://relay.nostr.band", - "wss://relay.damus.io", - "wss://nos.lol", - ]; - return { - inbox: fallbackRelays, - outbox: fallbackRelays, - all: fallbackRelays - }; - } -} - -/** - * Merges NIP-65 relays with existing custom relays and stores in localStorage - * @param nip65Relays NIP-65 relays to merge - */ -export function mergeAndStoreRelays(nip65Relays: Nip65Relay[]): string[] { - try { - // Get existing custom relays - const existingRelays = JSON.parse(localStorage.getItem("customRelays") || "[]"); - - // Extract URLs from NIP-65 relays (we'll add all relays for now, both read and write) - const nip65RelayUrls = nip65Relays.map(relay => relay.url); - - // Store NIP-65 relays separately for future use - localStorage.setItem("nip65Relays", JSON.stringify(nip65RelayUrls)); - - // Merge existing and NIP-65 relays, removing duplicates - const mergedRelays = Array.from(new Set([...existingRelays, ...nip65RelayUrls])); - - // Store updated list - localStorage.setItem("customRelays", JSON.stringify(mergedRelays)); - - return mergedRelays; - } catch (error) { - console.error('Error merging relays:', error); - return []; - } -} - -/** - * Gets the appropriate relays for reading events - * @param targetPubkey Optional pubkey to get their read relays (for NIP-65) - * @returns Array of relay URLs to use for reading - */ -export function getReadRelays(targetPubkey?: string): string[] { - const config = getRelayConfig(); - - // If we have a target pubkey, we could fetch their NIP-65 relays - // For now, use the current user's inbox - return config.inbox; -} - -/** - * Gets the appropriate relays for publishing events - * @param authorPubkey The pubkey of the event author - * @returns Array of relay URLs to use for publishing - */ -export function getWriteRelays(authorPubkey?: string): string[] { - const config = getRelayConfig(); - - // For now, use the current user's outbox - // In the future, this could include the author's write relays from NIP-65 - return config.outbox; -} \ No newline at end of file diff --git a/utils/publishUtils.ts b/utils/publishUtils.ts deleted file mode 100644 index 5c62831..0000000 --- a/utils/publishUtils.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Event as NostrEvent, SimplePool } from 'nostr-tools'; -import { getWriteRelays } from './nip65Utils'; - -/** - * Publishes an event to the appropriate outbox relays - * @param event The signed event to publish - * @param authorPubkey The pubkey of the event author (for determining write relays) - * @returns Promise that resolves when the event has been published to all relays - */ -export async function publishToOutbox(event: NostrEvent, authorPubkey?: string): Promise<void> { - console.log("publishToOutbox called with event:", event.id) - console.log("Event kind:", event.kind) - console.log("Event content length:", event.content?.length || 0) - console.log("Event tags:", event.tags) - console.log("Author pubkey:", authorPubkey) - - const writeRelays = getWriteRelays(authorPubkey); - console.log("Write relays:", writeRelays) - - if (writeRelays.length === 0) { - console.error("No write relays configured") - throw new Error('No write relays configured. Please add some relays in the relay settings.'); - } - - const pool = new SimplePool(); - - try { - console.log("Publishing to relays:", writeRelays) - - // Publish to all write relays with timeout - const publishPromises = writeRelays.map(async (relay) => { - try { - console.log(`Publishing to relay: ${relay}`) - await pool.publish([relay], event); - console.log(`Successfully published to ${relay}`) - return { relay, success: true }; - } catch (error) { - console.error(`Failed to publish to ${relay}:`, error) - return { relay, success: false, error }; - } - }); - - const results = await Promise.allSettled(publishPromises); - console.log("Publish results:", results) - - // Check if at least one relay succeeded - const successfulPublishes = results.filter(result => - result.status === 'fulfilled' && result.value.success - ); - - if (successfulPublishes.length === 0) { - console.error("Failed to publish to any relay") - throw new Error('Failed to publish to any relay. Please check your relay configuration.'); - } - - console.log(`Successfully published to ${successfulPublishes.length} out of ${writeRelays.length} relays`) - } catch (error) { - console.error("Error in publishToOutbox:", error) - throw error; - } finally { - // Close the pool to clean up connections - pool.close(writeRelays); - } -} - -/** - * Publishes an event to all configured relays (legacy function for backward compatibility) - * @param event The signed event to publish - * @returns Promise that resolves when the event has been published to all relays - */ -export async function publishToAllRelays(event: NostrEvent): Promise<void> { - const { getRelayConfig } = await import('./nip65Utils'); - const config = getRelayConfig(); - - if (config.all.length === 0) { - throw new Error('No relays configured'); - } - - const pool = new SimplePool(); - - try { - // Publish to all relays - await pool.publish(config.all, event); - } finally { - // Close the pool to clean up connections - pool.close(config.all); - } -} diff --git a/utils/relayHooks.ts b/utils/relayHooks.ts deleted file mode 100644 index 26e88bc..0000000 --- a/utils/relayHooks.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useMemo } from 'react'; -import { getRelayConfig, getReadRelays, getWriteRelays } from './nip65Utils'; -import { useState, useEffect } from 'react'; - -/** - * Hook to get the appropriate relays for reading events - * @param targetPubkey Optional pubkey to get their read relays (for NIP-65) - * @returns Array of relay URLs to use for reading - */ -export function useReadRelays(targetPubkey?: string): string[] { - return useMemo(() => { - return getReadRelays(targetPubkey); - }, [targetPubkey]); -} - -/** - * Hook to get the appropriate relays for publishing events - * @param authorPubkey The pubkey of the event author - * @returns Array of relay URLs to use for publishing - */ -export function useWriteRelays(authorPubkey?: string): string[] { - return useMemo(() => { - return getWriteRelays(authorPubkey); - }, [authorPubkey]); -} - -/** - * Hook to get all relays (for backward compatibility) - * @returns Array of all relay URLs - */ -export function useAllRelays(): string[] { - return useMemo(() => { - const config = getRelayConfig(); - return config.all; - }, []); -} - -/** - * Hook to get the current user's pubkey from localStorage - * @returns The current user's pubkey or null if not logged in - */ -export function useCurrentUserPubkey(): string | null { - const [pubkey, setPubkey] = useState<string | null>(null); - const [isClient, setIsClient] = useState(false); - - useEffect(() => { - setIsClient(true); - const storedPubkey = localStorage.getItem('pubkey'); - setPubkey(storedPubkey); - }, []); - - return isClient ? pubkey : null; -} diff --git a/utils/textUtils.tsx b/utils/textUtils.tsx deleted file mode 100644 index 99b45fe..0000000 --- a/utils/textUtils.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { ReactNode } from 'react'; -import Link from 'next/link'; - -/** - * Renders text content with hyperlinked hashtags - * @param content The text content that may contain hashtags - * @param eventTags The tags array from a Nostr event - * @returns An array of text and link elements - */ -export function renderTextWithLinkedTags(content: string, eventTags: string[][]): ReactNode[] { - if (!content) return []; - - // Extract all hashtags from the event tags - const eventHashtags = eventTags - .filter((tag) => tag[0] === "t") - .map((tag) => tag[1].toLowerCase()); - - // Find hashtags in the content with regex - const hashtagRegex = /#(\w+)/g; - let lastIndex = 0; - const result: ReactNode[] = []; - let match; - - while ((match = hashtagRegex.exec(content)) !== null) { - const hashtag = match[1].toLowerCase(); - const fullHashtag = match[0]; // #hashtag - const matchIndex = match.index; - - // Add text before the hashtag - if (matchIndex > lastIndex) { - result.push(content.substring(lastIndex, matchIndex)); - } - - // Check if this hashtag exists in the event tags - if (eventHashtags.includes(hashtag)) { - // Create a link for matching hashtags - result.push( - <Link href={`/tag/${hashtag}`} key={`${hashtag}-${matchIndex}`} className="text-blue-500 hover:underline"> - {fullHashtag} - </Link> - ); - } else { - // Add the hashtag without a link if it's not in the tags - result.push(fullHashtag); - } - - lastIndex = matchIndex + fullHashtag.length; - } - - // Add any remaining text - if (lastIndex < content.length) { - result.push(content.substring(lastIndex)); - } - - return result; -} diff --git a/utils/utils.ts b/utils/utils.ts deleted file mode 100644 index 9ac5b7e..0000000 --- a/utils/utils.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Event as NostrEvent, finalizeEvent} from "nostr-tools"; -import { hexToBytes } from "@noble/hashes/utils" -import { signEventWithBunker } from "./bunkerUtils"; - -// Check if the event has nsfw or sexy tags -export function hasNsfwContent(tags: string[][]): boolean { - return tags.some(tag => - (tag[0] === 't' && (tag[1]?.toLowerCase() === 'nsfw' || tag[1]?.toLowerCase() === 'sexy')) || - (tag[0] === 'content-warning') - ); -} - -export function getImageUrl(tags: string[][]): string { - const imetaTags = tags.filter(tag => tag[0] === 'imeta'); - - // First, check for 'image' fields (thumbnails for videos) - for (const imetaTag of imetaTags) { - const imageItem = imetaTag.find(item => item.startsWith('image ')); - if (imageItem) { - return imageItem.split(' ')[1]; - } - } - - // Then, check for 'url' fields that point to images - for (const imetaTag of imetaTags) { - const urlItem = imetaTag.find(item => item.startsWith('url ')); - const mimeItem = imetaTag.find(item => item.startsWith('m ')); - - if (urlItem) { - const url = urlItem.split(' ')[1]; - // If this imeta tag has an image mime type, use it - if (mimeItem && mimeItem.startsWith('m image/')) { - return url; - } - // If the URL looks like an image, use it - if (url.match(/\.(jpg|jpeg|png|webp|gif|apng|avif)$/i)) { - return url; - } - } - } - - // Fallback: return the first URL found - const firstImetaTag = imetaTags[0]; - if (firstImetaTag) { - const urlItem = firstImetaTag.find(item => item.startsWith('url ')); - if (urlItem) { - return urlItem.split(' ')[1]; - } - } - - return ''; -} - -export function getThumbnailUrl(tags: string[][]): string { - const imetaTag = tags.find(tag => tag[0] === 'imeta'); - if (imetaTag) { - const imageItem = imetaTag.find(item => item.startsWith('image ')); - if (imageItem) { - return imageItem.split(' ')[1]; - } - } - return ''; -} - -export function extractDimensions(event: NostrEvent): { width: number; height: number } { - const imetaTag = event.tags.find(tag => tag[0] === 'imeta'); - if (imetaTag) { - const dimInfo = imetaTag.find(item => item.startsWith('dim ')); - if (dimInfo) { - const [width, height] = dimInfo.split(' ')[1].split('x').map(Number); - return { width, height }; - } - } - return { width: 500, height: 300 }; // Default dimensions if not found -} - -export async function signEvent(loginType: string | null, event: NostrEvent): Promise<NostrEvent | null> { - console.log("signEvent called with loginType:", loginType) - console.log("Event to sign:", { kind: event.kind, content: event.content?.substring(0, 100) + "..." }) - - // Sign event - let eventSigned: NostrEvent = { ...event, sig: '' }; - if (loginType === 'extension') { - try { - console.log("Signing with extension...") - eventSigned = await window.nostr.signEvent(event); - console.log("Extension signing successful:", eventSigned.id) - } catch (error) { - console.error("Extension signing failed:", error) - throw error - } - } else if (loginType === 'amber') { - // TODO: Sign event with amber - alert('Signing with Amber is not implemented yet, sorry!'); - return null; - } else if (loginType === 'bunker') { - // Sign with bunker (NIP-46) - try { - console.log("Signing with bunker...") - const signedWithBunker = await signEventWithBunker(event); - if (signedWithBunker) { - console.log("Bunker signing successful:", signedWithBunker.id) - return signedWithBunker; - } else { - console.error("Bunker signing returned null") - alert('Failed to sign with bunker. Please check your connection and try again.'); - return null; - } - } catch (error) { - console.error("Bunker signing failed:", error) - alert(`Failed to sign with bunker: ${error instanceof Error ? error.message : 'Unknown error'}`); - return null; - } - } else if (loginType === 'raw_nsec') { - if (typeof window !== 'undefined') { - try { - console.log("Signing with raw nsec...") - let nsecStr = null; - nsecStr = window.localStorage.getItem('nsec'); - if (nsecStr != null) { - eventSigned = finalizeEvent(event, hexToBytes(nsecStr)); - console.log("Raw nsec signing successful:", eventSigned.id) - } else { - console.error("No nsec found in localStorage") - throw new Error("No private key found") - } - } catch (error) { - console.error("Raw nsec signing failed:", error) - throw error - } - } - } else { - console.error("Unknown login type:", loginType) - throw new Error(`Unknown login type: ${loginType}`) - } - - console.log("Final signed event:", eventSigned); - return eventSigned; -} - -// Create proxied image URL -export const getProxiedImageUrl = (url: string, width: number, height: number) => { - if (!url.startsWith("http")) return url; - try { - // Encode the URL to be used in the proxy - const encodedUrl = encodeURIComponent(url); - const imgproxyEnv = process.env.NEXT_PUBLIC_IMGPROXY_URL; - const imgproxyUrl = new URL(imgproxyEnv || "https://imgproxy.example.com"); - return `${imgproxyUrl}_/resize:fit:${width}:${height}/plain/${encodedUrl}`; - } catch (error) { - console.error("Error creating proxied image URL:", error); - return url; - } -} - - - -// Blacklist annoying pubkeys - -export const blacklistPubkeys = new Set([ - "0403c86a1bb4cfbc34c8a493fbd1f0d158d42dd06d03eaa3720882a066d3a378", - "7444faae22d4d4939c815819dca3c4822c209758bf86afc66365db5f79f67ddb", - "3ffac3a6c859eaaa8cdddb2c7002a6e10b33efeb92d025b14ead6f8a2d656657", - "5943c88f3c60cd9edb125a668e2911ad419fc04e94549ed96a721901dd958372", -]); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..650ac55 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,31 @@ +import path from "node:path"; + +import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from "vitest/config"; + +// https://vitejs.dev/config/ +export default defineConfig(() => ({ + server: { + host: "::", + port: 8080, + }, + plugins: [ + react(), + ], + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + onConsoleLog(log) { + return !log.includes("React Router Future Flag Warning"); + }, + env: { + DEBUG_PRINT_LIMIT: '0', // Suppress DOM output that exceeds AI context windows + }, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +})); \ No newline at end of file