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
63 changes: 50 additions & 13 deletions app/positions/components/PositionsContent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use client';

import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Name } from '@coinbase/onchainkit/identity';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { FaHistory, FaGift, FaPlus } from 'react-icons/fa';
import { FaHistory, FaGift, FaPlus, FaCircle } from 'react-icons/fa';
import { useAccount } from 'wagmi';
import { Avatar } from '@/components/Avatar/Avatar';
import Header from '@/components/layout/header/Header';
import EmptyScreen from '@/components/Status/EmptyScreen';
import LoadingScreen from '@/components/Status/LoadingScreen';
Expand All @@ -19,6 +22,12 @@ export default function Positions() {
const [selectedPosition, setSelectedPosition] = useState<MarketPosition | null>(null);

const { account } = useParams<{ account: string }>();
const { address, isConnected } = useAccount();

const isOwner = useMemo(() => {
if (!account) return false;
return account === address;
}, [account, address]);

const { loading, isRefetching, data: marketPositions, refetch } = useUserPositions(account);

Expand All @@ -28,8 +37,32 @@ export default function Positions() {
<div className="flex flex-col justify-between font-zen">
<Header />
<div className="container gap-8" style={{ padding: '0 5%' }}>
<div className="mb-4 flex items-center">
<h1 className="font-zen text-2xl">Portfolio</h1>
</div>
<div className="flex flex-col items-center justify-between pb-4 sm:flex-row">
<h1 className="py-8 font-zen"> Portfolio </h1>
<div className="flex items-start gap-4">
<div className="relative overflow-hidden rounded">
<Avatar address={account as `0x${string}`} size={36} rounded={false} />
{isConnected && account === address && (
<div className="absolute bottom-0 right-0 h-4 w-full bg-gradient-to-r from-green-500/20 to-green-500/40 backdrop-blur-sm">
<div className="absolute bottom-1 right-1">
<FaCircle size={8} className="text-green-500" />
</div>
</div>
)}
</div>
<div className="flex flex-col">
<Name
address={account as `0x${string}`}
className={`rounded p-2 font-monospace text-sm ${
isConnected && account === address
? 'bg-green-500/10 text-green-500'
: 'bg-hovered'
}`}
/>
</div>
</div>
<div className="flex gap-4">
<Link href={`/history/${account}`} className="no-underline">
<button
Expand All @@ -51,16 +84,19 @@ export default function Positions() {
Rewards
</button>
</Link>
<Link href="/positions/onboarding" className="no-underline">
<button
aria-label="Create a new position"
type="button"
className="bg-monarch-orange hover:bg-monarch-orange/90 flex items-center gap-2 rounded p-2 font-zen text-sm text-white shadow-sm transition-all duration-200 ease-in-out hover:shadow-md"
>
<FaPlus size={14} />
New Position
</button>
</Link>
{isOwner && (
<Link href="/positions/onboarding" className="no-underline">
<button
aria-label="Create a new position"
type="button"
className="bg-monarch-orange hover:bg-monarch-orange/90 flex items-center gap-2 rounded p-2 font-zen text-sm text-white shadow-sm transition-all duration-200 ease-in-out hover:shadow-md"
disabled={account !== address}
>
<FaPlus size={14} />
New Position
</button>
</Link>
)}
</div>
</div>

Expand Down Expand Up @@ -102,6 +138,7 @@ export default function Positions() {
) : (
<div className="mt-4">
<PositionsSummaryTable
account={account}
marketPositions={marketPositions}
setShowWithdrawModal={setShowWithdrawModal}
setShowSupplyModal={setShowSupplyModal}
Expand Down
56 changes: 37 additions & 19 deletions app/positions/components/PositionsSummaryTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useMemo, useState, useEffect } from 'react';
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react';
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from '@nextui-org/react';
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
import { IoRefreshOutline, IoChevronDownOutline } from 'react-icons/io5';
import { toast } from 'react-toastify';
import { useAccount } from 'wagmi';
import { TokenIcon } from '@/components/TokenIcon';
import { formatReadable, formatBalance } from '@/utils/balance';
import { getNetworkImg } from '@/utils/networks';
Expand All @@ -25,6 +26,7 @@ export enum EarningsPeriod {
}

type PositionsSummaryTableProps = {
account: string;
marketPositions: MarketPosition[];
setShowWithdrawModal: (show: boolean) => void;
setShowSupplyModal: (show: boolean) => void;
Expand All @@ -40,13 +42,20 @@ export function PositionsSummaryTable({
setSelectedPosition,
refetch,
isRefetching,
account,
}: PositionsSummaryTableProps) {
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const [showRebalanceModal, setShowRebalanceModal] = useState(false);
const [selectedGroupedPosition, setSelectedGroupedPosition] = useState<GroupedPosition | null>(
null,
);
const [earningsPeriod, setEarningsPeriod] = useState<EarningsPeriod>(EarningsPeriod.Day);
const { address } = useAccount();

const isOwner = useMemo(() => {
if (!account) return false;
return account === address;
}, [marketPositions, address]);

const getEarningsForPeriod = (position: MarketPosition) => {
if (!position.earned) return '0';
Expand Down Expand Up @@ -218,32 +227,34 @@ export function PositionsSummaryTable({
<div className="mb-4 flex items-center justify-end gap-2">
<Dropdown>
<DropdownTrigger>
<button
type="button"
aria-label="Select earnings period"
className="bg-surface-dark flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-secondary transition-colors hover:bg-orange-500 hover:text-white"
<Button
variant="light"
size="sm"
startContent={<IoChevronDownOutline className="h-4 w-4" />}
className="font-zen text-secondary opacity-80 transition-all duration-200 ease-in-out hover:opacity-100"
>
{earningsPeriod}
<IoChevronDownOutline className="h-4 w-4" />
</button>
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Time period selection"
aria-label="Earnings period selection"
className="bg-surface rounded p-2"
onAction={(key) => setEarningsPeriod(key as EarningsPeriod)}
>
{Object.entries(periodLabels).map(([period, label]) => (
<DropdownItem key={period}>{label}</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<button
type="button"
className="bg-surface-dark flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-secondary transition-colors hover:bg-orange-500 hover:text-white"
<Button
variant="light"
size="sm"
startContent={<IoRefreshOutline className="h-4 w-4" />}
onClick={handleManualRefresh}
className="font-zen text-secondary opacity-80 transition-all duration-200 ease-in-out hover:opacity-100"
>
<IoRefreshOutline className="h-4 w-4" />
Refresh
</button>
</Button>
</div>
<div className="bg-surface overflow-hidden rounded">
<table className="responsive w-full min-w-[640px] font-zen">
Expand Down Expand Up @@ -351,18 +362,25 @@ export function PositionsSummaryTable({
/>
</div>
</td>
<td data-label="Actions" className="px-4 py-3 text-right text-primary">
<div className="flex space-x-2">
<button
type="button"
className="bg-hovered rounded-sm bg-opacity-50 p-2 text-xs duration-300 ease-in-out hover:bg-primary"
<td data-label="Actions" className="justify-center px-4 py-3">
<div className="flex items-center justify-center">
<Button
variant="light"
size="sm"
className={`bg-opacity-50 p-2 text-xs duration-300 ease-in-out ${
isOwner ? 'hover:bg-primary' : 'cursor-not-allowed opacity-50'
}`}
onClick={() => {
if (!isOwner) {
toast.error('You can only rebalance your own positions');
return;
}
setSelectedGroupedPosition(groupedPosition);
setShowRebalanceModal(true);
}}
>
Rebalance
</button>
</Button>
</div>
</td>
</tr>
Expand Down
2 changes: 0 additions & 2 deletions app/positions/components/RebalanceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,6 @@ export function RebalanceModal({
[chainId, groupedPosition.chainId],
);

console.log('needSwitchChain', needSwitchChain);

const handleExecuteRebalance = useCallback(async () => {
if (needSwitchChain) {
try {
Expand Down
2 changes: 1 addition & 1 deletion app/positions/components/onboarding/AssetSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function AssetSelection() {
role="button"
key={`${token.symbol}-${token.network}`}
onClick={() => handleTokenSelect(token)}
className="group relative flex items-start gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-all duration-300 hover:border-primary hover:shadow-lg dark:border-gray-700 dark:bg-gray-800/50 dark:hover:bg-gray-800"
className="group relative flex items-start gap-4 rounded border border-gray-200 bg-white p-4 text-left transition-all duration-300 hover:border-primary hover:shadow-lg dark:border-gray-700 dark:bg-gray-800/50 dark:hover:bg-gray-800"
whileHover={{ scale: 1.02 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
>
Expand Down
27 changes: 26 additions & 1 deletion app/positions/components/onboarding/SetupPositions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { Button, Slider } from '@nextui-org/react';
import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify';
import { formatUnits, parseUnits } from 'viem';
import { useChainId, useSwitchChain } from 'wagmi';
import OracleVendorBadge from '@/components/OracleVendorBadge';
import { SupplyProcessModal } from '@/components/SupplyProcessModal';
import { useLocalStorage } from '@/hooks/useLocalStorage';
Expand All @@ -16,6 +18,7 @@ import { useOnboarding } from './OnboardingContext';

export function SetupPositions() {
const router = useRouter();
const chainId = useChainId();
const { selectedToken, selectedMarkets } = useOnboarding();
const { balances } = useUserBalances();
const [useEth] = useLocalStorage('useEth', false);
Expand All @@ -27,6 +30,13 @@ export function SetupPositions() {
const [error, setError] = useState<string | null>(null);
const [isSupplying, setIsSupplying] = useState(false);

const needSwitchChain = useMemo(
() => chainId !== selectedToken?.network,
[chainId, selectedToken],
);

const { switchChain } = useSwitchChain();

// Redirect if no token selected
useEffect(() => {
if (!selectedToken) {
Expand Down Expand Up @@ -222,7 +232,22 @@ export function SetupPositions() {
} = useMultiMarketSupply(selectedToken!, supplies, useEth, usePermit2Setting);

const handleSupply = async () => {
if (isSupplying) return;
if (isSupplying) {
toast.info('Supplying in progress');
return;
}

if (needSwitchChain && selectedToken) {
try {
switchChain({ chainId: selectedToken.network });
toast.info('Network changed, please click again to execute');
return;
} catch (switchError) {
console.error('Failed to switch network:', switchError);
toast.error('Failed to switch network');
return;
}
}
setIsSupplying(true);

try {
Expand Down
2 changes: 1 addition & 1 deletion app/settings/faq/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function FAQPage() {
<h1 className="mb-8 text-3xl font-bold">Frequently Asked Questions</h1>
<div className="space-y-6">
{faqs.map((faq, index) => (
<div key={index} className="bg-surface rounded-lg p-6">
<div key={index} className="bg-surface rounded p-6">
<h3 className="mb-2 text-lg font-semibold">{faq.question}</h3>
<p className="text-secondary">{faq.answer}</p>
</div>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { MORPHO } from '@/utils/morpho';
type AvatarProps = {
address: Address;
size?: number;
rounded?: boolean;
};

export function Avatar({ address, size = 30 }: AvatarProps) {
export function Avatar({ address, size = 30, rounded = true }: AvatarProps) {
const [useEffigy, setUseEffigy] = useState(true);
const effigyUrl = `https://effigy.im/a/${address}.svg`;
const dicebearUrl = `https://api.dicebear.com/7.x/pixel-art/png?seed=${address}`;
Expand All @@ -34,7 +35,7 @@ export function Avatar({ address, size = 30 }: AvatarProps) {
alt={`Avatar for ${address}`}
width={size}
height={size}
style={{ borderRadius: '50%' }}
style={{ borderRadius: rounded ? '50%' : '5px' }}
onError={() => setUseEffigy(false)}
/>
</div>
Expand Down
2 changes: 0 additions & 2 deletions src/components/supplyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element
tokenSymbol: market.loanAsset.symbol,
});

console.log('isApproved', isApproved);

const needSwitchChain = useMemo(
() => chainId !== market.morphoBlue.chain.id,
[chainId, market.morphoBlue.chain.id],
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/useRebalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: ()
const shares = groupedPosition.markets.find(
(m) => m.market.uniqueKey === actions[0].fromMarket.uniqueKey,
)?.supplyShares;

console.log('shares', shares);

if (isWithdrawMax && shares === undefined) {
throw new Error('No share found for max withdraw');
}
Expand Down