2025-04-16 22:33:21 -05:00
# Project Overview
2025-04-17 14:26:35 -05:00
This project is a Nostr client application built with React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify.
2025-04-16 22:33:21 -05:00
## Technology Stack
2025-04-17 14:26:35 -05:00
- **React 18.x**: Stable version of React with hooks, concurrent rendering, and improved performance
2025-04-16 22:33:21 -05:00
- **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
2025-04-17 11:44:57 -05:00
## 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.
2025-04-16 22:33:21 -05:00
## Nostr Protocol Integration
2025-04-17 11:23:46 -05:00
This project comes with custom hooks for querying and publishing events on the Nostr network.
2025-04-16 22:33:21 -05:00
2025-04-17 11:23:46 -05:00
### The `useNostr` Hook
2025-04-16 22:33:21 -05:00
2025-04-17 11:23:46 -05:00
The `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively.
2025-04-16 22:33:21 -05:00
```typescript
2025-04-17 11:23:46 -05:00
import { useNostr } from '@nostrify/react ';
function useCustomHook() {
const { nostr } = useNostr();
// ...
}
2025-04-16 22:33:21 -05:00
```
2025-04-17 11:23:46 -05:00
### 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.
2025-04-16 22:33:21 -05:00
```typescript
import { useNostr } from '@nostrify/react ';
import { useQuery } from '@tanstack/query ';
function usePosts() {
const { nostr } = useNostr();
return useQuery({
queryKey: ['posts'],
2025-04-23 15:13:17 -05:00
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
const events = await nostr.query([{ kinds: [1], limit: 20 }], { signal });
2025-04-16 22:33:21 -05:00
return events; // these events could be transformed into another format
},
});
}
```
2025-04-17 11:23:46 -05:00
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.
2025-05-09 16:10:33 -05:00
### 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;
}
```
2025-04-17 11:23:46 -05:00
### 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.
2025-04-17 18:46:08 -05:00
### Nostr Login
2025-04-18 15:41:35 -05:00
To enable login with Nostr, simply use the `LoginArea` component already included in this project.
2025-04-17 18:46:08 -05:00
```tsx
2025-04-18 15:41:35 -05:00
import { LoginArea } from "@/components/auth/LoginArea ";
2025-04-17 21:15:30 -05:00
2025-04-17 18:46:08 -05:00
function MyComponent() {
return (
2025-04-17 21:15:30 -05:00
< div >
2025-04-17 23:27:15 -05:00
{/* other components ... */}
2025-04-18 15:41:35 -05:00
< LoginArea / >
2025-04-17 21:15:30 -05:00
< / div >
2025-04-17 18:46:08 -05:00
);
}
```
2025-04-18 15:41:35 -05:00
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.
2025-04-17 21:15:30 -05:00
2025-05-10 16:51:31 -05:00
## `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
2025-05-14 12:00:02 -05:00
The base Nostr protocol uses hex string identifiers when filtering by event IDs and pubkeys. Nostr filters only accept hex strings.
2025-05-10 16:51:31 -05:00
```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);
2025-05-14 12:00:02 -05:00
// Optional: guard certain types (depending on the use-case)
2025-05-10 16:51:31 -05:00
if (decoded.type !== 'naddr') {
2025-05-14 12:00:02 -05:00
throw new Error('Unsupported Nostr identifier');
2025-05-10 16:51:31 -05:00
}
// 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 }
);
```
2025-05-14 15:56:53 -05:00
## 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.
2025-05-14 17:08:07 -05:00
## Uploading Files
Use the `useUploadFile` hook to upload files.
```tsx
import { useUploadFile } from "@/hooks/useUploadFile ";
function MyComponent() {
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const handleUpload = async (file: File) => {
try {
// The first tuple in the array contains the URL
const [[_, url]] = await uploadFile(file);
// ...use the url
} catch (error) {
// ...handle errors
}
};
// ...rest of component
}
```
2025-04-16 22:33:21 -05:00
## 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
2025-04-18 12:44:37 -05:00
Whenever you modify code, you should test your changes after you're finished by running:
2025-04-17 18:49:05 -05:00
2025-04-18 12:44:37 -05:00
```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.