add nostr-react
This commit is contained in:
159
.github/prompts/nostr-react.md
vendored
Normal file
159
.github/prompts/nostr-react.md
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
<p align="center">
|
||||
<b>nostr-react</b>
|
||||
</p>
|
||||
<p align="center">
|
||||
React Hooks for Nostr ✨
|
||||
</p>
|
||||
|
||||
## 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>
|
||||
);
|
||||
}
|
||||
```
|
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import NostrClientWrapper from "@/components/nostr-client-wrapper";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -28,14 +29,16 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
<NostrClientWrapper>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</NostrClientWrapper>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
116
app/page.tsx
116
app/page.tsx
@@ -1,103 +1,23 @@
|
||||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import { dateToUnix, useNostrEvents } from "nostr-react";
|
||||
import { useRef } from "react";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
const now = useRef(new Date()); // Make sure current time isn't re-rendered
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
20
components/nostr-client-wrapper.tsx
Normal file
20
components/nostr-client-wrapper.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { NostrProvider } from "nostr-react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const relayUrls = [
|
||||
"wss://relay.nostr.band"
|
||||
];
|
||||
|
||||
export default function NostrClientWrapper({
|
||||
children
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<NostrProvider relayUrls={relayUrls} debug={true}>
|
||||
{children}
|
||||
</NostrProvider>
|
||||
);
|
||||
}
|
171
package-lock.json
generated
171
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "15.3.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"nostr-react": "^0.7.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
@@ -917,6 +918,39 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"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/@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"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1012,6 +1046,45 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"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",
|
||||
"@noble/hashes": "~1.3.1",
|
||||
"@scure/base": "~1.1.0"
|
||||
},
|
||||
"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",
|
||||
"@scure/base": "~1.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -4123,6 +4196,64 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jotai": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/jotai/-/jotai-1.13.1.tgz",
|
||||
"integrity": "sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw==",
|
||||
"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",
|
||||
@@ -4767,6 +4898,44 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-react": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-react/-/nostr-react-0.7.0.tgz",
|
||||
"integrity": "sha512-m8fX+eaor+Xq7/DcxsHYS5IsNj4cyTYiIgm6lNqZeNR5mV8Yb4JlzOdw53WCJztgwDUrBx8/gPK3F88uIY1B6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jotai": "^1.12.1",
|
||||
"nostr-tools": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "Unlicense",
|
||||
"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/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5980,7 +6149,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
@@ -15,6 +15,7 @@
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "15.3.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"nostr-react": "^0.7.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
|
Reference in New Issue
Block a user