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:
- **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.
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 Nostr events associated with it yet. Typically NIPs define custom event kinds.
Before deciding upon a kind number for a custom event, use `nostr__read_nips_index` to see what kinds are currently in use across all NIPs. The kind number determines the event's behavior and storage characteristics:
For more detailed information about event structure and behavior, read the `event` protocol page using `nostr__read_protocol`.
**Important**: Whenever new Nostr event kinds are created, if they are not already defined by an existing NIP, the `NIP.md` file in the project must be created or updated to document the custom event structure. Whenever the structure of one of these custom events changes, `NIP.md` must be updated accordingly.
The `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively.
/** 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. */
The `LoginArea` component handles all the login-related UI and interactions, including displaying login dialogs and switching between accounts. It should not be wrapped in any conditional logic.
`LoginArea` displays a "Log in" button 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.
For URL routing, use NIP-19 identifiers as path parameters (e.g., `/:nip19`) to create secure, universal links to Nostr events. Decode the identifier and render the appropriate component based on the type:
- Regular events: Use `/nevent1...` paths
- Replaceable/addressable events: Use `/naddr1...` paths
Always use `naddr` identifiers for addressable events instead of just the `d` tag value, as `naddr` contains the author pubkey needed to create secure filters. This prevents security issues where malicious actors could publish events with the same `d` tag to override content.
```ts
// Secure routing with naddr
const decoded = nip19.decode(params.nip19);
if (decoded.type === 'naddr' && decoded.data.kind === 30024) {
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.
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.
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:
The project includes an `AppProvider` that manages global application state including theme and relay configuration. The default configuration includes:
```typescript
const defaultConfig: AppConfig = {
theme: "light",
relayUrl: "wss://relay.nostr.band",
};
```
Preset relays are available including Ditto, Nostr.Band, Damus, and Primal. The app uses local storage to persist user preferences.
## Routing
The project uses React Router with a centralized routing configuration in `AppRouter.tsx`. To add new routes:
When no content is found (empty search results, no data available, etc.), display a minimalist empty state with the `RelaySelector` component. This allows users to easily switch relays to discover content from different sources.
```tsx
import { RelaySelector } from '@/components/RelaySelector';
import { Card, CardContent } from '@/components/ui/card';
// Empty state example
<divclassName="col-span-full">
<CardclassName="border-dashed">
<CardContentclassName="py-12 px-8 text-center">
<divclassName="max-w-sm mx-auto space-y-6">
<pclassName="text-muted-foreground">
No results found. Try another relay?
</p>
<RelaySelectorclassName="w-full"/>
</div>
</CardContent>
</Card>
</div>
```
**Key principles for empty states:**
- Keep text minimal and clear
- Use dashed borders to indicate empty/placeholder state
- Include the RelaySelector to provide actionable next steps
- Center content with generous whitespace (`py-12 px-8`)
- Constrain width with `max-w-sm mx-auto` for focused layout
- **Color schemes**: Incorporate the user's color preferences when specified, and choose an appropriate scheme that matches the application's purpose and aesthetic
**Important for AI Assistants**: Only create tests when the user is experiencing a specific problem or explicitly requests tests. Do not proactively write tests for new features or components unless the user is having issues that require testing to diagnose or resolve.
The project includes a `TestApp` component that provides all necessary context providers for testing. Wrap components with this component to provide required context providers: