feat: Add CardOptionsDropdown component for sharing and viewing raw (#118)

* feat: Add CardOptionsDropdown component for sharing and viewing raw event data in NoteCard and KIND20Card

* Update components/CardOptionsDropdown.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update components/CardOptionsDropdown.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: Import useMemo in CardOptionsDropdown for optimized rendering

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
mroxso
2025-05-24 21:19:28 +02:00
committed by GitHub
parent 77dfb01a34
commit 24f0c0f9cd
3 changed files with 157 additions and 17 deletions

View File

@@ -0,0 +1,148 @@
import React, { useMemo } from 'react';
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Textarea } from "./ui/textarea";
import { DotsVerticalIcon, CodeIcon, Share1Icon } from "@radix-ui/react-icons";
import { Input } from "./ui/input";
import { useRef, useState } from 'react';
import { useToast } from "./ui/use-toast";
import { Event as NostrEvent, nip19 } from "nostr-tools";
interface CardOptionsDropdownProps {
event: NostrEvent;
}
export default function CardOptionsDropdown({ event }: CardOptionsDropdownProps) {
const jsonEvent = useMemo(() => JSON.stringify(event, null, 2), [event]);
const inputRef = useRef(null);
const inputRefID = useRef(null);
const { toast } = useToast();
const [dropdownOpen, setDropdownOpen] = useState(false);
const [shareDrawerOpen, setShareDrawerOpen] = useState(false);
const [rawDrawerOpen, setRawDrawerOpen] = useState(false);
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
toast({
description: 'URL copied to clipboard',
title: 'Copied'
});
} catch (err) {
toast({
description: 'Error copying URL to clipboard',
title: 'Error',
variant: 'destructive'
});
}
};
const handleCopyNoteId = async () => {
try {
await navigator.clipboard.writeText(nip19.noteEncode(event.id));
toast({
description: 'Note ID copied to clipboard',
title: 'Copied'
});
} catch (err) {
toast({
description: 'Error copying Note ID to clipboard',
title: 'Error',
variant: 'destructive'
});
}
};
return (
<>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="ml-auto" aria-label="Options">
<DotsVerticalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Share option */}
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false);
setTimeout(() => setShareDrawerOpen(true), 100);
}}
>
<Share1Icon className="mr-2 h-4 w-4" />
Share
</DropdownMenuItem>
{/* View Raw option */}
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false);
setTimeout(() => setRawDrawerOpen(true), 100);
}}
>
<CodeIcon className="mr-2 h-4 w-4" />
View Raw
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Share Drawer */}
<Drawer open={shareDrawerOpen} onOpenChange={setShareDrawerOpen}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Share this Note</DrawerTitle>
<DrawerDescription>Share this Note with others.</DrawerDescription>
</DrawerHeader>
<div className="px-4">
<div className="flex items-center mb-4">
<Input ref={inputRef} value={window.location.href} readOnly className="mr-2" />
<Button variant="outline" onClick={handleCopyLink}>Copy Link</Button>
</div>
<div className="flex items-center mb-4">
<Input ref={inputRefID} value={nip19.noteEncode(event.id)} readOnly className="mr-2" />
<Button variant="outline" onClick={handleCopyNoteId}>Copy Note ID</Button>
</div>
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
{/* Raw Event Drawer */}
<Drawer open={rawDrawerOpen} onOpenChange={setRawDrawerOpen}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Raw Event</DrawerTitle>
<DrawerDescription>This shows the raw event data.</DrawerDescription>
</DrawerHeader>
<div className="px-4 pb-4">
<Textarea rows={20} readOnly value={jsonEvent} />
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</>
);
}

View File

@@ -7,13 +7,12 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel"
import ReactionButton from "@/components/ReactionButton"
import { Avatar, AvatarImage } from "@/components/ui/avatar"
import ViewRawButton from "@/components/ViewRawButton"
import ViewNoteButton from "./ViewNoteButton"
import Link from "next/link"
import ViewCopyButton from "./ViewCopyButton"
import type { Event as NostrEvent } from "nostr-tools"
import ZapButton from "./ZapButton"
import Image from "next/image"
import CardOptionsDropdown from "./CardOptionsDropdown"
import { renderTextWithLinkedTags } from "@/utils/textUtils"
// Function to extract all images from a kind 20 event's imeta tags
@@ -103,8 +102,8 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
<>
<div key={event.id}>
<Card className="my-4">
<CardHeader>
<CardTitle>
<CardHeader className="flex flex-row items-center">
<CardTitle className="flex-1">
<Link href={hrefProfile} style={{ textDecoration: "none" }}>
<TooltipProvider>
<Tooltip>
@@ -125,6 +124,7 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
</TooltipProvider>
</Link>
</CardTitle>
<CardOptionsDropdown event={event} />
</CardHeader>
<CardContent className="p-0">
<div className="w-full">
@@ -177,10 +177,6 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
<ZapButton event={event} />
{showViewNoteCardButton && <ViewNoteButton event={event} />}
</div>
<div className="flex space-x-2">
<ViewCopyButton event={event} />
<ViewRawButton event={event} />
</div>
</div>
</div>
</CardContent>

View File

@@ -25,12 +25,11 @@ import {
} from "@/components/ui/carousel"
import ReactionButton from '@/components/ReactionButton';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import ViewRawButton from '@/components/ViewRawButton';
import ViewNoteButton from './ViewNoteButton';
import Link from 'next/link';
import ViewCopyButton from './ViewCopyButton';
import { Event as NostrEvent } from "nostr-tools";
import ZapButton from './ZapButton';
import CardOptionsDropdown from './CardOptionsDropdown';
import { renderTextWithLinkedTags } from '@/utils/textUtils';
interface NoteCardProps {
@@ -60,8 +59,8 @@ const NoteCard: React.FC<NoteCardProps> = ({ pubkey, text, eventId, tags, event,
return (
<>
<Card>
<CardHeader>
<CardTitle>
<CardHeader className="flex flex-row items-center">
<CardTitle className="flex-1">
<Link href={hrefProfile} style={{ textDecoration: 'none' }}>
<TooltipProvider>
<Tooltip>
@@ -80,6 +79,7 @@ const NoteCard: React.FC<NoteCardProps> = ({ pubkey, text, eventId, tags, event,
</TooltipProvider>
</Link>
</CardTitle>
<CardOptionsDropdown event={event} />
</CardHeader>
<CardContent>
<div className='py-4'>
@@ -147,16 +147,12 @@ const NoteCard: React.FC<NoteCardProps> = ({ pubkey, text, eventId, tags, event,
</div>
</div>
<hr />
<div className='py-4 space-x-4 flex justify-between items-start'>
<div className='py-4 space-x-4 flex'>
<div className='flex space-x-4'>
<ReactionButton event={event} />
<ZapButton event={event} />
{showViewNoteCardButton && <ViewNoteButton event={event} />}
</div>
<div className='flex space-x-2'>
<ViewCopyButton event={event} />
<ViewRawButton event={event} />
</div>
</div>
</CardContent>
<CardFooter>