Skip to content
Open
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
194 changes: 149 additions & 45 deletions frontend-lectures/lesson3/openriver/src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,160 @@
import React from 'react'
import { useReadContract } from 'wagmi'
import React, { useState } from 'react';
import { useAccount, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { openriverAbi, openriverAddress } from '../contracts';
import { NFTItemData, truncateAddress } from '../utils/nft';
import { ListModal, TransferModal, ApproveModal } from './NFTActionModals';

const Card = ({ tokenId }: { tokenId: bigint }) => {
const { data: onchainNFT } = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: 'tokenURI',
args: [tokenId],
})

const normalizedImageSrc = React.useMemo(() => {
if (typeof onchainNFT !== 'string' || !onchainNFT) {
return null
}
interface CardProps {
nft: NFTItemData;
onRefresh: () => void;
}

if (onchainNFT.startsWith('ipfs://')) {
return `https://ipfs.io/ipfs/${onchainNFT.replace('ipfs://', '')}`
}
export const Card: React.FC<CardProps> = ({ nft, onRefresh }) => {
const { address } = useAccount();

const [showList, setShowList] = useState(false);
const [showTransfer, setShowTransfer] = useState(false);
const [showApprove, setShowApprove] = useState(false);

return onchainNFT
}, [onchainNFT])
const { writeContract: buyContract, data: buyHash, isPending: isBuyPending } = useWriteContract();
const { isLoading: isBuyConfirming, isSuccess: isBuySuccess } = useWaitForTransactionReceipt({ hash: buyHash });

const { writeContract: delistContract, data: delistHash, isPending: isDelistPending } = useWriteContract();
const { isLoading: isDelistConfirming, isSuccess: isDelistSuccess } = useWaitForTransactionReceipt({ hash: delistHash });

React.useEffect(() => {
if (isBuySuccess || isDelistSuccess) {
onRefresh();
}
}, [isBuySuccess, isDelistSuccess, onRefresh]);

const isOwner = Boolean(address && nft.owner.toLowerCase() === address.toLowerCase());

const {data: marketData} = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "marketplace",
args: [BigInt(tokenId)]
})
const handleQuickBuy = () => {
buyContract({
abi: openriverAbi,
address: openriverAddress,
functionName: 'purchase',
args: [nft.tokenId],
value: nft.price,
});
};

console.log(marketData?.[0])
const handleQuickDelist = () => {
delistContract({
abi: openriverAbi,
address: openriverAddress,
functionName: 'removeFromMarketplace',
args: [nft.tokenId],
});
};

return (
<div className="w-full max-w-[300px] rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<div className="flex h-[180px] items-center justify-center overflow-hidden rounded-md bg-slate-100">
{normalizedImageSrc ? (
<img
alt="This one na NFT"
src={normalizedImageSrc}
className="h-full w-full object-cover"
/>
) : (
<p className="text-sm text-slate-500">NFT image unavailable</p>
)}
</div>
<div className="flex justify-between py-5">
<h2 className="text-3xl">#29</h2>
<h2 className="text-3xl font-bold">200 ETH</h2>
<>
<div className="simple-card p-4 flex flex-col justify-between">
<div>
<div className="relative aspect-square rounded-lg overflow-hidden mb-3 bg-slate-900 border border-slate-800">
<img
src={nft.imageSrc}
alt={nft.name}
className="w-full h-full object-cover"
/>
<div className="absolute top-2 left-2">
{nft.isListed ? (
<span className="px-2 py-1 rounded text-xs font-bold bg-green-950 text-green-400 border border-green-800">
For Sale
</span>
) : (
<span className="px-2 py-1 rounded text-xs font-semibold bg-slate-800 text-slate-300">
Not Listed
</span>
)}
</div>
<div className="absolute top-2 right-2">
<span className="px-2 py-1 rounded text-xs font-mono font-bold bg-slate-900 text-slate-200">
#{nft.tokenId.toString()}
</span>
</div>
</div>

<div className="space-y-1 mb-3">
<h3 className="font-bold text-base text-white truncate">
{nft.name}
</h3>
<div className="flex justify-between items-center text-xs text-slate-400">
<span>Owner:</span>
<span className="font-mono text-slate-200">
{isOwner ? 'You' : truncateAddress(nft.owner, 4)}
</span>
</div>
</div>
</div>

<div className="pt-3 border-t border-slate-800 space-y-3">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-400 uppercase font-semibold">
{nft.isListed ? 'Price' : 'Status'}
</span>
<span className="text-sm font-bold text-sky-400">
{nft.isListed ? `${nft.priceFormatted} ETH` : 'Not Listed'}
</span>
</div>

<div>
{isOwner ? (
nft.isListed ? (
<button
onClick={handleQuickDelist}
disabled={isDelistPending || isDelistConfirming}
className="w-full btn-danger py-2 rounded text-xs font-semibold"
>
{isDelistPending || isDelistConfirming ? 'Removing...' : 'Remove Listing'}
</button>
) : (
<button
onClick={() => setShowList(true)}
className="w-full btn-primary py-2 rounded text-xs font-semibold"
>
Put Up For Sale
</button>
)
) : nft.isListed ? (
<button
onClick={handleQuickBuy}
disabled={isBuyPending || isBuyConfirming}
className="w-full btn-primary py-2 rounded text-xs font-semibold"
>
{isBuyPending || isBuyConfirming ? 'Buying...' : `Buy Now (${nft.priceFormatted} ETH)`}
</button>
) : (
<div className="text-center py-2 text-xs text-slate-400">
Not Available
</div>
)}
</div>
</div>
</div>
</div>
)
}

export default Card
<ListModal
isOpen={showList}
onClose={() => setShowList(false)}
nft={nft}
onSuccess={onRefresh}
/>
<TransferModal
isOpen={showTransfer}
onClose={() => setShowTransfer(false)}
nft={nft}
onSuccess={onRefresh}
/>
<ApproveModal
isOpen={showApprove}
onClose={() => setShowApprove(false)}
nft={nft}
onSuccess={onRefresh}
/>
</>
);
};

export default Card;
126 changes: 109 additions & 17 deletions frontend-lectures/lesson3/openriver/src/components/Cards.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,117 @@
import { useReadContract } from "wagmi";
import Card from "./Card";
import { openriverAbi, openriverAddress } from "../contracts";
import React, { useState, useMemo } from 'react';
import Card from './Card';
import { useNFTs } from '../hooks/useNFTs';
import { useAccount } from 'wagmi';

export const Cards = ({ nftNum }: { nftNum?: bigint }) => {

const {data: tokenIds} = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "tokenIds",
})
export const Cards = ({ initialFilter }: { initialFilter?: 'all' | 'listed' | 'my' }) => {
const { nfts, loading, refresh } = useNFTs();
const { address } = useAccount();

const [activeTab, setActiveTab] = useState<'all' | 'listed' | 'my'>(initialFilter || 'all');
const [searchTerm, setSearchTerm] = useState('');

const filteredNFTs = useMemo(() => {
return nfts.filter((nft) => {
if (activeTab === 'listed' && !nft.isListed) return false;
if (activeTab === 'my') {
if (!address) return false;
if (nft.owner.toLowerCase() !== address.toLowerCase()) return false;
}

if (searchTerm.trim() !== '') {
const query = searchTerm.toLowerCase();
const matchesName = nft.name.toLowerCase().includes(query);
const matchesTokenId = nft.tokenId.toString().includes(query);
const matchesOwner = nft.owner.toLowerCase().includes(query);
if (!matchesName && !matchesTokenId && !matchesOwner) return false;
}

return true;
});
}, [nfts, activeTab, searchTerm, address]);


return (
<div className="flex gap-10 flex-wrap w-360 mt-5 m-auto ">
{Array.from({ length: Number(nftNum) }, (_, i) => i + 1).map((index) => (
<Card key={index} tokenId={BigInt(index)} />
))}
<div className="space-y-6">
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 simple-card p-4">
<div className="flex gap-2 bg-slate-900 p-1 rounded-lg border border-slate-800">
<button
onClick={() => setActiveTab('all')}
className={`px-4 py-2 rounded-md text-xs font-semibold transition ${
activeTab === 'all'
? 'bg-sky-600 text-white'
: 'text-slate-400 hover:text-white'
}`}
>
All NFTs ({nfts.length})
</button>
<button
onClick={() => setActiveTab('listed')}
className={`px-4 py-2 rounded-md text-xs font-semibold transition ${
activeTab === 'listed'
? 'bg-sky-600 text-white'
: 'text-slate-400 hover:text-white'
}`}
>
For Sale ({nfts.filter((n) => n.isListed).length})
</button>
<button
onClick={() => setActiveTab('my')}
className={`px-4 py-2 rounded-md text-xs font-semibold transition ${
activeTab === 'my'
? 'bg-sky-600 text-white'
: 'text-slate-400 hover:text-white'
}`}
>
My NFTs ({nfts.filter((n) => address && n.owner.toLowerCase() === address.toLowerCase()).length})
</button>
</div>

<div className="flex items-center gap-2 w-full sm:w-auto">
<input
type="text"
placeholder="Search by name or ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="simple-input px-3 py-2 rounded-lg text-xs w-full sm:w-64"
/>
<button
onClick={refresh}
title="Reload NFTs"
className="btn-secondary px-3 py-2 text-xs font-semibold"
>
Refresh 🔄
</button>
</div>
</div>

{loading && (
<div className="simple-card p-8 text-center text-slate-400 text-sm">
Loading NFTs... Please wait.
</div>
)}

{!loading && filteredNFTs.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{filteredNFTs.map((nft) => (
<Card key={nft.tokenId.toString()} nft={nft} onRefresh={refresh} />
))}
</div>
)}

{!loading && filteredNFTs.length === 0 && (
<div className="simple-card p-8 text-center space-y-2">
<h3 className="text-base font-bold text-white">No NFTs Found</h3>
<p className="text-xs text-slate-400">
{activeTab === 'my'
? address
? 'You do not own any NFTs in this store yet. Try minting one!'
: 'Please connect your wallet to view your owned NFTs.'
: activeTab === 'listed'
? 'There are currently no items put up for sale.'
: 'No items match your search.'}
</p>
</div>
)}
</div>
)
}
);
};
Loading