Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 28 additions & 65 deletions src/components/shared/account-actions-popover.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use client';

import { useState, useCallback, type ReactNode } from 'react';
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
import { motion, AnimatePresence } from 'framer-motion';
import { useCallback, type ReactNode } from 'react';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { LuCopy, LuUser } from 'react-icons/lu';
import { SiEthereum } from 'react-icons/si';
import { useStyledToast } from '@/hooks/useStyledToast';
Expand All @@ -16,93 +15,57 @@ type AccountActionsPopoverProps = {
};

/**
* Minimal popover showing account actions:
* Dropdown menu showing account actions:
* - Copy address
* - View account (positions page)
* - View on Etherscan
*/
export function AccountActionsPopover({ address, children }: AccountActionsPopoverProps) {
const [isOpen, setIsOpen] = useState(false);
const toast = useStyledToast();

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(address);
toast.success('Address copied', `${address.slice(0, 6)}...${address.slice(-4)}`);
setIsOpen(false);
} catch (error) {
console.error('Failed to copy address', error);
}
}, [address, toast]);

const handleViewAccount = useCallback(() => {
window.location.href = `/positions/${address}`;
setIsOpen(false);
}, [address]);

const handleViewExplorer = useCallback(() => {
const explorerUrl = getExplorerURL(address, SupportedNetworks.Mainnet);
window.open(explorerUrl, '_blank', 'noopener,noreferrer');
setIsOpen(false);
}, [address]);

return (
<Popover
open={isOpen}
onOpenChange={setIsOpen}
>
<PopoverTrigger>
<div className="cursor-pointer outline-none focus:outline-none">{children}</div>
</PopoverTrigger>
<PopoverContent className="p-0 bg-surface shadow-lg border-none">
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -10 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
className="flex min-w-[180px] flex-col rounded-sm bg-surface font-zen"
>
{/* Copy Address */}
<motion.button
type="button"
onClick={() => void handleCopy()}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-secondary transition-colors hover:bg-hovered hover:text-primary"
whileHover={{ x: 2 }}
whileTap={{ scale: 0.98 }}
>
<LuCopy className="h-4 w-4" />
<span>Copy Address</span>
</motion.button>

{/* View Account */}
<motion.button
type="button"
onClick={handleViewAccount}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-secondary transition-colors hover:bg-hovered hover:text-primary"
whileHover={{ x: 2 }}
whileTap={{ scale: 0.98 }}
>
<LuUser className="h-4 w-4" />
<span>View Account</span>
</motion.button>

{/* View on Explorer */}
<motion.button
type="button"
onClick={handleViewExplorer}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-secondary transition-colors hover:bg-hovered hover:text-primary"
whileHover={{ x: 2 }}
whileTap={{ scale: 0.98 }}
>
<SiEthereum className="h-4 w-4" />
<span>View on Explorer</span>
</motion.button>
</motion.div>
)}
</AnimatePresence>
</PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="cursor-pointer">{children}</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => void handleCopy()}
startContent={<LuCopy className="h-4 w-4" />}
>
Copy Address
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleViewAccount}
startContent={<LuUser className="h-4 w-4" />}
>
View Account
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleViewExplorer}
startContent={<SiEthereum className="h-4 w-4" />}
>
View on Explorer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
64 changes: 64 additions & 0 deletions src/features/markets/components/market-id-actions-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';

import { useCallback, type ReactNode } from 'react';
import { useRouter } from 'next/navigation';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { LuCopy } from 'react-icons/lu';
import { GoGraph } from 'react-icons/go';
import { useStyledToast } from '@/hooks/useStyledToast';

type MarketIdActionsPopoverProps = {
marketId: string;
chainId: number;
children: ReactNode;
};

/**
* Dropdown menu showing market ID actions:
* - Copy ID
* - View Market
*/
export function MarketIdActionsPopover({ marketId, chainId, children }: MarketIdActionsPopoverProps) {
const toast = useStyledToast();
const router = useRouter();

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(marketId);
toast.success('Market ID copied', `${marketId.slice(0, 10)}...${marketId.slice(-6)}`);
} catch (error) {
console.error('Failed to copy market ID', error);
}
}, [marketId, toast]);

const handleViewMarket = useCallback(() => {
router.push(`/market/${chainId}/${marketId}`);
}, [chainId, marketId, router]);

return (
<div
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="cursor-pointer">{children}</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => void handleCopy()}
startContent={<LuCopy className="h-4 w-4" />}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleViewMarket}
startContent={<GoGraph className="h-4 w-4" />}
>
View Market
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
30 changes: 18 additions & 12 deletions src/features/markets/components/market-id-badge.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Link from 'next/link';
'use client';

import Image from 'next/image';
import { getNetworkImg } from '@/utils/networks';
import { MarketIdActionsPopover } from './market-id-actions-popover';

type MarketIdBadgeProps = {
marketId: string;
Expand All @@ -13,7 +15,7 @@ export function MarketIdBadge({ marketId, chainId, showNetworkIcon = false, show
const displayId = marketId.slice(2, 8);
const chainImg = getNetworkImg(chainId);

return (
const badge = (
<div className="flex items-center gap-1.5">
{showNetworkIcon && chainImg && (
<Image
Expand All @@ -23,16 +25,20 @@ export function MarketIdBadge({ marketId, chainId, showNetworkIcon = false, show
height={15}
/>
)}
{showLink ? (
<Link
className="group flex items-center justify-center no-underline hover:underline"
href={`/market/${chainId}/${marketId}`}
>
<span className="rounded bg-gray-100 px-1 py-0.5 text-xs font-monospace opacity-70 dark:bg-gray-800">{displayId}</span>
</Link>
) : (
<span className="rounded bg-gray-100 px-1 py-0.5 text-xs font-monospace opacity-70 dark:bg-gray-800">{displayId}</span>
)}
<span className="rounded bg-gray-100 px-1 py-0.5 font-monospace text-xs opacity-70 dark:bg-gray-800">{displayId}</span>
</div>
);

if (showLink) {
return (
<MarketIdActionsPopover
marketId={marketId}
chainId={chainId}
>
{badge}
</MarketIdActionsPopover>
);
}

return badge;
}