Derek Ross on Nostr: # Project Overview This project is a Nostr client application built with React 18.x, ...
# 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
- **TanStack Query**: For data fetching, caching, and state management
- **TypeScript**: For type-safe JavaScript development
## Project Structure
- `/src/components/`: UI components including NostrProvider for Nostr integration
- `/src/hooks/`: Custom hooks including `useNostr` and `useNostrQuery`
- `/src/pages/`: Page components used by React Router
- `/src/lib/`: Utility functions and shared logic
- `/public/`: Static assets
## 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
- **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
- **Sonner**: Toast notification manager
- **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.
## Nostr Protocol Integration
This project comes with custom hooks for querying and publishing events on the Nostr network.
### 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();
// ...
}
```
### 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
},
});
}
```
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.
### 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 { NostrEvent, NostrMetadata } from '@nostrify/nostrify';
import { useAuthor } from '@/hooks/useAuthor';
function Post({ event }: { event: NostrEvent }) {
const author = useAuthor(event.pubkey);
const metadata: NostrMetadata | undefined = author.data?.metadata;
const displayName = metadata?.name || event.pubkey.slice(0, 8);
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.
```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 />
</div>
);
}
```
The `LoginArea` component displays a "Log in" button when the user is logged out, and changes to an account switcher once the user is logged in. It handles all the login-related UI and interactions internally, including displaying login dialogs and switching between accounts.
## `npub`, `naddr`, and other Nostr addresses
Nostr defines a set identifiers in NIP-19. Their prefixes:
- `npub`: public keys
- `nsec`: private keys
- `note`: note ids
- `nprofile`: a nostr profile
- `nevent`: a nostr event
- `naddr`: a nostr replaceable event coordinate
- `nrelay`: a nostr relay (deprecated)
NIP-19 identifiers include a prefix, the number "1", then a base32-encoded data string.
### Use in Filters
The base Nostr protocol uses hex string identifiers for filtering by event IDs, pubkeys, and signatures. 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
if (decoded.type !== 'naddr') {
throw new Error('Invalid stack ID');
}
// 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 }
);
```
## 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 multiple Nostr relays for network redundancy
## Build & Deployment
- Build for production: `npm run build`
- Development build: `npm run build:dev`
## Testing Your Changes
Whenever you modify code, you should test your changes after you're finished by running:
```bash
npm run ci
```
This command will typecheck the code and attempt to build it.
Your task is not considered finished until this test passes without errors.
Published at
2025-05-15 15:03:02Event JSON
{
"id": "a6445b0c3034e48911a417152d6672afe7bf89b1d2dc720d6a00e761c574cce3",
"pubkey": "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24",
"created_at": 1747321382,
"kind": 1,
"tags": [
[
"client",
"Damus Notedeck"
],
[
"e",
"39cadf37df86f03c9c6eedb61fa7d3025a9e6efed51da315236f73e3d721f584",
"",
"root"
],
[
"e",
"8b3c66db445b3dda840e853aea87f47d478b27809be43c0dc3839ca96bf17e42",
"",
"reply"
],
[
"p",
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"
],
[
"p",
"0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd"
],
[
"p",
"40b9c85fffeafc1cadf8c30a4e5c88660ff6e4971a0dc723d5ab674b5e61b451"
]
],
"content": "# Project Overview\n\nThis project is a Nostr client application built with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify.\n\n## Technology Stack\n\n- **React 18.x**: Stable version of React with hooks, concurrent rendering, and improved performance\n- **TailwindCSS 3.x**: Utility-first CSS framework for styling\n- **Vite**: Fast build tool and development server\n- **shadcn/ui**: Unstyled, accessible UI components built with Radix UI and Tailwind\n- **Nostrify**: Nostr protocol framework for Deno and web\n- **React Router**: For client-side routing\n- **TanStack Query**: For data fetching, caching, and state management\n- **TypeScript**: For type-safe JavaScript development\n\n## Project Structure\n\n- `/src/components/`: UI components including NostrProvider for Nostr integration\n- `/src/hooks/`: Custom hooks including `useNostr` and `useNostrQuery`\n- `/src/pages/`: Page components used by React Router\n- `/src/lib/`: Utility functions and shared logic\n- `/public/`: Static assets\n\n## UI Components\n\nThe 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:\n\n- **Accordion**: Vertically collapsing content panels\n- **Alert**: Displays important messages to users\n- **AlertDialog**: Modal dialog for critical actions requiring confirmation\n- **AspectRatio**: Maintains consistent width-to-height ratio\n- **Avatar**: User profile pictures with fallback support\n- **Badge**: Small status descriptors for UI elements\n- **Breadcrumb**: Navigation aid showing current location in hierarchy\n- **Button**: Customizable button with multiple variants and sizes\n- **Calendar**: Date picker component \n- **Card**: Container with header, content, and footer sections\n- **Carousel**: Slideshow for cycling through elements\n- **Chart**: Data visualization component\n- **Checkbox**: Selectable input element\n- **Collapsible**: Toggle for showing/hiding content\n- **Command**: Command palette for keyboard-first interfaces\n- **ContextMenu**: Right-click menu component\n- **Dialog**: Modal window overlay\n- **Drawer**: Side-sliding panel\n- **DropdownMenu**: Menu that appears from a trigger element\n- **Form**: Form validation and submission handling\n- **HoverCard**: Card that appears when hovering over an element\n- **InputOTP**: One-time password input field\n- **Input**: Text input field\n- **Label**: Accessible form labels\n- **Menubar**: Horizontal menu with dropdowns\n- **NavigationMenu**: Accessible navigation component\n- **Pagination**: Controls for navigating between pages\n- **Popover**: Floating content triggered by a button\n- **Progress**: Progress indicator\n- **RadioGroup**: Group of radio inputs\n- **Resizable**: Resizable panels and interfaces\n- **ScrollArea**: Scrollable container with custom scrollbars\n- **Select**: Dropdown selection component\n- **Separator**: Visual divider between content\n- **Sheet**: Side-anchored dialog component\n- **Sidebar**: Navigation sidebar component\n- **Skeleton**: Loading placeholder\n- **Slider**: Input for selecting a value from a range\n- **Sonner**: Toast notification manager\n- **Switch**: Toggle switch control\n- **Table**: Data table with headers and rows\n- **Tabs**: Tabbed interface component\n- **Textarea**: Multi-line text input\n- **Toast**: Toast notification component\n- **ToggleGroup**: Group of toggle buttons\n- **Toggle**: Two-state button\n- **Tooltip**: Informational text that appears on hover\n\nThese 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.\n\n## Nostr Protocol Integration\n\nThis project comes with custom hooks for querying and publishing events on the Nostr network.\n\n### The `useNostr` Hook\n\nThe `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively.\n\n```typescript\nimport { useNostr } from '@nostrify/react';\n\nfunction useCustomHook() {\n const { nostr } = useNostr();\n\n // ...\n}\n```\n\n### Query Nostr Data with `useNostr` and Tanstack Query\n\nWhen querying Nostr, the best practice is to create custom hooks that combine `useNostr` and `useQuery` to get the required data.\n\n```typescript\nimport { useNostr } from '@nostrify/react';\nimport { useQuery } from '@tanstack/query';\n\nfunction usePosts() {\n const { nostr } = useNostr();\n\n return useQuery({\n queryKey: ['posts'],\n queryFn: async (c) =\u003e {\n const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);\n const events = await nostr.query([{ kinds: [1], limit: 20 }], { signal });\n return events; // these events could be transformed into another format\n },\n });\n}\n```\n\nThe data may be transformed into a more appropriate format if needed, and multiple calls to `nostr.query()` may be made in a single queryFn.\n\n### The `useAuthor` Hook\n\nTo display profile data for a user by their Nostr pubkey (such as an event author), use the `useAuthor` hook.\n\n```tsx\nimport { NostrEvent, NostrMetadata } from '@nostrify/nostrify';\nimport { useAuthor } from '@/hooks/useAuthor';\n\nfunction Post({ event }: { event: NostrEvent }) {\n const author = useAuthor(event.pubkey);\n const metadata: NostrMetadata | undefined = author.data?.metadata;\n\n const displayName = metadata?.name || event.pubkey.slice(0, 8);\n const profileImage = metadata?.picture;\n\n // ...render elements with this data\n}\n```\n\n#### `NostrMetadata` type\n\n```ts\n/** Kind 0 metadata. */\ninterface NostrMetadata {\n /** A short description of the user. */\n about?: string;\n /** A URL to a wide (~1024x768) picture to be optionally displayed in the background of a profile screen. */\n banner?: string;\n /** A boolean to clarify that the content is entirely or partially the result of automation, such as with chatbots or newsfeeds. */\n bot?: boolean;\n /** An alternative, bigger name with richer characters than `name`. `name` should always be set regardless of the presence of `display_name` in the metadata. */\n display_name?: string;\n /** A bech32 lightning address according to NIP-57 and LNURL specifications. */\n lud06?: string;\n /** An email-like lightning address according to NIP-57 and LNURL specifications. */\n lud16?: string;\n /** A short name to be displayed for the user. */\n name?: string;\n /** An email-like Nostr address according to NIP-05. */\n nip05?: string;\n /** A URL to the user's avatar. */\n picture?: string;\n /** A web URL related in any way to the event author. */\n website?: string;\n}\n```\n\n### The `useNostrPublish` Hook\n\nTo publish events, use the `useNostrPublish` hook in this project.\n\n```tsx\nimport { useState } from 'react';\n\nimport { useCurrentUser } from \"@/hooks/useCurrentUser\";\nimport { useNostrPublish } from '@/hooks/useNostrPublish';\n\nexport function MyComponent() {\n const [ data, setData] = useState\u003cRecord\u003cstring, string\u003e\u003e({});\n\n const { user } = useCurrentUser();\n const { mutate: createEvent } = useNostrPublish();\n\n const handleSubmit = () =\u003e {\n createEvent({ kind: 1, content: data.content });\n };\n\n if (!user) {\n return \u003cspan\u003eYou must be logged in to use this form.\u003c/span\u003e;\n }\n\n return (\n \u003cform onSubmit={handleSubmit} disabled={!user}\u003e\n {/* ...some input fields */}\n \u003c/form\u003e\n );\n}\n```\n\nThe `useCurrentUser` hook should be used to ensure that the user is logged in before they are able to publish Nostr events.\n\n### Nostr Login\n\nTo enable login with Nostr, simply use the `LoginArea` component already included in this project.\n\n```tsx\nimport { LoginArea } from \"@/components/auth/LoginArea\";\n\nfunction MyComponent() {\n return (\n \u003cdiv\u003e\n {/* other components ... */}\n\n \u003cLoginArea /\u003e\n \u003c/div\u003e\n );\n}\n```\n\nThe `LoginArea` component displays a \"Log in\" button when the user is logged out, and changes to an account switcher once the user is logged in. It handles all the login-related UI and interactions internally, including displaying login dialogs and switching between accounts.\n\n## `npub`, `naddr`, and other Nostr addresses\n\nNostr defines a set identifiers in NIP-19. Their prefixes:\n\n- `npub`: public keys\n- `nsec`: private keys\n- `note`: note ids\n- `nprofile`: a nostr profile\n- `nevent`: a nostr event\n- `naddr`: a nostr replaceable event coordinate\n- `nrelay`: a nostr relay (deprecated)\n\nNIP-19 identifiers include a prefix, the number \"1\", then a base32-encoded data string.\n\n### Use in Filters\n\nThe base Nostr protocol uses hex string identifiers for filtering by event IDs, pubkeys, and signatures. Nostr filters only accept hex strings.\n\n```ts\n// ❌ Wrong: naddr is not decoded\nconst events = await nostr.query(\n [{ ids: [naddr] }],\n { signal }\n);\n```\n\nCorrected example:\n\n```ts\n// Import nip19 from nostr-tools\nimport { nip19 } from 'nostr-tools';\n\n// Decode a NIP-19 identifier\nconst decoded = nip19.decode(value);\n\n// Optional: guard certain types\nif (decoded.type !== 'naddr') {\n throw new Error('Invalid stack ID');\n}\n\n// Get the addr object\nconst naddr = decoded.data;\n\n// ✅ Correct: naddr is expanded into the correct filter\nconst events = await nostr.query(\n [{\n kinds: [naddr.kind],\n authors: [naddr.pubkey],\n '#d': [naddr.identifier],\n }],\n { signal }\n);\n```\n\n## Development Practices\n\n- Uses React Query for data fetching and caching\n- Follows shadcn/ui component patterns\n- Implements Path Aliases with `@/` prefix for cleaner imports\n- Uses Vite for fast development and production builds\n- Component-based architecture with React hooks\n- Default connection to multiple Nostr relays for network redundancy\n\n## Build \u0026 Deployment\n\n- Build for production: `npm run build`\n- Development build: `npm run build:dev`\n\n## Testing Your Changes\n\nWhenever you modify code, you should test your changes after you're finished by running:\n\n```bash\nnpm run ci\n```\n\nThis command will typecheck the code and attempt to build it.\n\nYour task is not considered finished until this test passes without errors.",
"sig": "7fa61e35935d3097704af0dabd69c653fcf8615e3e08592a8934a34bc2cfc37bd4169d2462107e00b3876c3083588172726606027800760d0ea330e60444ff2b"
}