Skip to content

Commit

Permalink
Merge pull request #16 from ostyjs/widgets
Browse files Browse the repository at this point in the history
Widgets
  • Loading branch information
sepehr-safari authored Dec 28, 2024
2 parents 4761d2d + 7a820ff commit f46149b
Show file tree
Hide file tree
Showing 43 changed files with 1,650 additions and 5 deletions.
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ features:
- title: UI Components
details: A comprehensive design system with reusable UI components.
- title: Widgets
details: Pre-built widgets like Zap Modal, Login Form, etc.
details: Pre-built widgets like Zap Modal, Login Form, User Profile, Notes, and more.
- title: Templates
details: Ready-to-use templates for React. (Vue, and Svelte coming soon)
details: Ready-to-use templates for different social media platforms.
- title: Core Libraries
details: Efficient websockets, caching, routing, and state management solutions.
- title: Best Practices
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-osty",
"version": "0.4.4",
"version": "0.5.0",
"type": "module",
"license": "MIT",
"author": "Sepehr Safari",
Expand Down
5 changes: 3 additions & 2 deletions templates/react-shadcn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"input-otp": "^1.4.1",
"lucide-react": "^0.462.0",
"next-themes": "^0.4.3",
"nostr-hooks": "^4.1.9",
"nostr-tools": "^2.10.3",
"nostr-hooks": "^4.2.6",
"nostr-tools": "^2.10.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
Expand All @@ -61,6 +61,7 @@
"sonner": "^1.7.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"vaul": "^1.1.1",
"zod": "^3.23.8",
"zustand": "^5.0.1"
Expand Down
61 changes: 61 additions & 0 deletions templates/react-shadcn/src/features/new-note-widget/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useActiveUser, useNdk, useRealtimeProfile } from 'nostr-hooks';
import { useCallback, useState } from 'react';

import { useToast } from '@/shared/components/ui/use-toast';

export const useNewNoteWidget = ({
replyingToEvent,
}: {
replyingToEvent?: NDKEvent | undefined;
}) => {
const [content, setContent] = useState<string>('');

const { activeUser } = useActiveUser();
const { profile } = useRealtimeProfile(activeUser?.pubkey);

const { ndk } = useNdk();

const { toast } = useToast();

const post = useCallback(() => {
if (!ndk || !ndk.signer) {
return;
}

const e = new NDKEvent(ndk);
e.kind = 1;
e.content = content;

if (replyingToEvent) {
const rootTag = replyingToEvent.tags.find((tag) => tag.length > 3 && tag[3] === 'root');

if (rootTag) {
e.tags.push(['e', rootTag[1], '', 'root']);
e.tags.push(['e', replyingToEvent.id, '', 'reply']);
} else {
e.tags.push(['e', replyingToEvent.id, '', 'root']);
}
}

e.publish()
.then((relaySet) => {
if (relaySet.size === 0) {
toast({
title: 'Error',
description: 'Failed to post note',
variant: 'destructive',
});
}
})
.catch((_) => {
toast({
title: 'Error',
description: 'Failed to post note',
variant: 'destructive',
});
});
}, [ndk, content, replyingToEvent, toast]);

return { content, setContent, post, profile };
};
36 changes: 36 additions & 0 deletions templates/react-shadcn/src/features/new-note-widget/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';

import { Avatar, AvatarFallback, AvatarImage } from '@/shared/components/ui/avatar';
import { Button } from '@/shared/components/ui/button';
import { Textarea } from '@/shared/components/ui/textarea';

import { useNewNoteWidget } from './hooks';

export const NewNoteWidget = ({ replyingToEvent }: { replyingToEvent?: NDKEvent | undefined }) => {
const { content, post, setContent, profile } = useNewNoteWidget({ replyingToEvent });

return (
<>
<div className="p-2 bg-muted flex flex-col gap-2">
<div className="flex gap-2">
<Avatar>
<AvatarImage src={profile?.image} alt={profile?.name} />
<AvatarFallback className="bg-muted" />
</Avatar>

<Textarea
className="bg-background"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</div>

<div className="w-full flex gap-2 justify-end">
<Button className="px-8" onClick={post}>
Post
</Button>
</div>
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './note-bookmark-btn';
export * from './note-comment-btn';
export * from './note-content';
export * from './note-footer';
export * from './note-header';
export * from './note-like-btn';
export * from './note-parent-preview';
export * from './note-repost-btn';
export * from './note-zap-btn';
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useActiveUser, useNdk, useSubscription } from 'nostr-hooks';
import { useCallback, useEffect, useMemo } from 'react';

export const useNoteBookmarkBtn = (event: NDKEvent) => {
const { ndk } = useNdk();

const { myBookmarkList } = useMyBookmarkList();

const isBookmarkedByMe = useMemo(
() => myBookmarkList?.getMatchingTags('e')?.some((e) => e.length > 1 && e[1] === event.id),
[myBookmarkList, event],
);

const bookmark = useCallback(() => {
if (!ndk) {
return;
}

if (isBookmarkedByMe) {
return;
}

const e = new NDKEvent(ndk);
e.kind = NDKKind.BookmarkList;
e.tags = [...(myBookmarkList?.tags || [])];
e.tags.push(['e', event.id]);
e.publish();
}, [ndk, event, isBookmarkedByMe, myBookmarkList]);

const unbookmark = useCallback(() => {
if (!ndk) {
return;
}

if (!isBookmarkedByMe) {
return;
}

const e = new NDKEvent(ndk);
e.kind = NDKKind.BookmarkList;
e.tags = myBookmarkList?.tags.filter((t) => t[1] !== event.id) || [];
e.publish();
}, [ndk, event, isBookmarkedByMe, myBookmarkList]);

return { isBookmarkedByMe, bookmark, unbookmark };
};

const useMyBookmarkList = () => {
const { activeUser } = useActiveUser();

const subId = activeUser ? `my-bookmark-list` : undefined;

const { createSubscription, events } = useSubscription(subId);

const myBookmarkList = useMemo(
() => (events && events.length > 0 ? events[events.length - 1] : undefined),
[events],
);

useEffect(() => {
activeUser &&
createSubscription({
filters: [{ kinds: [NDKKind.BookmarkList], authors: [activeUser.pubkey], limit: 1 }],
});
}, [createSubscription]);

return { myBookmarkList };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { BookmarkIcon } from 'lucide-react';

import { Button } from '@/shared/components/ui/button';

import { cn } from '@/shared/utils';

import { useNoteBookmarkBtn } from './hooks';

export const NoteBookmarkBtn = ({ event }: { event: NDKEvent }) => {
const { isBookmarkedByMe, bookmark, unbookmark } = useNoteBookmarkBtn(event);

return (
<>
<Button
variant="link"
size="icon"
className={cn(isBookmarkedByMe ? 'text-green-600' : 'opacity-50 hover:opacity-100')}
onClick={isBookmarkedByMe ? unbookmark : bookmark}
>
<BookmarkIcon size={18} />
</Button>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MessageSquareIcon } from 'lucide-react';

import { Button } from '@/shared/components/ui/button';

export const NoteCommentBtn = ({ onClick }: { onClick: () => void }) => {
return (
<>
<Button variant="link" size="icon" className="opacity-50 hover:opacity-100" onClick={onClick}>
<MessageSquareIcon size={18} />
</Button>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';

export const NoteContent = ({ event }: { event: NDKEvent }) => {
return (
<div className="pb-2">
<p className="[overflow-wrap:anywhere]">{event.content}</p>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useState } from 'react';

import { NewNoteWidget } from '@/features/new-note-widget';

import { NoteBookmarkBtn, NoteCommentBtn, NoteLikeBtn, NoteRepostBtn, NoteZapBtn } from '..';

export const NoteFooter = ({ event }: { event: NDKEvent }) => {
const [isCommenting, setIsCommenting] = useState(false);

return (
<div className="">
<div className="flex items-center justify-between gap-2">
<NoteCommentBtn onClick={() => setIsCommenting((prev) => !prev)} />

<NoteZapBtn event={event} />

<NoteLikeBtn event={event} />

<NoteRepostBtn event={event} />

<NoteBookmarkBtn event={event} />
</div>

{isCommenting && <NewNoteWidget replyingToEvent={event} />}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useProfile } from 'nostr-hooks';
import { useNavigate } from 'react-router-dom';
import { useCopyToClipboard } from 'usehooks-ts';

export const useNoteHeader = (pubkey: string) => {
const [, copy] = useCopyToClipboard();

const { profile } = useProfile({ pubkey });

const navigate = useNavigate();

return {
profile,
copy,
navigate,
};
};
Loading

0 comments on commit f46149b

Please sign in to comment.