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
731 changes: 731 additions & 0 deletions frontend-lectures/lesson3/openriver/EXPLICACION_ES5.md

Large diffs are not rendered by default.

158 changes: 44 additions & 114 deletions frontend-lectures/lesson3/openriver/package-lock.json

Large diffs are not rendered by default.

25 changes: 23 additions & 2 deletions frontend-lectures/lesson3/openriver/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
import React from 'react'

const Button = () => {
type ButtonProps = {
label: string;
onClick?: () => void;
type?: 'button' | 'submit';
variant?: 'primary' | 'danger';
disabled?: boolean;
}

const Button = ({ label, onClick, type = 'button', variant = 'primary', disabled = false }: ButtonProps) => {
const base = 'w-52 py-2 px-4 rounded text-white font-semibold transition-opacity';
const variants = {
primary: 'bg-blue-500 hover:bg-blue-600',
danger: 'bg-red-500 hover:bg-red-600',
};

return (
<div>Button</div>
<button
type={type}
onClick={onClick}
disabled={disabled}
className={`${base} ${variants[variant]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{label}
</button>
)
}

Expand Down
64 changes: 44 additions & 20 deletions frontend-lectures/lesson3/openriver/src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,65 @@
import React from 'react'
import { useReadContract } from 'wagmi'
import { formatEther } from 'viem'
import { openriverAbi, openriverAddress } from '../contracts';

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

const normalizedImageSrc = React.useMemo(() => {
if (typeof onchainNFT !== 'string' || !onchainNFT) {
return null
return null;
}

if (onchainNFT.startsWith('ipfs://')) {
return `https://ipfs.io/ipfs/${onchainNFT.replace('ipfs://', '')}`
return `https://ipfs.io/ipfs/${onchainNFT.replace('ipfs://', '')}`;
}
return onchainNFT;
}, [onchainNFT]);

// wagmi returns the marketplace struct as a typed object with named fields
const { data: marketData } = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: 'marketplace',
args: [tokenId],
});

return onchainNFT
}, [onchainNFT])
// Debug: log what we're getting
React.useEffect(() => {
console.log(`Token ${tokenId.toString()} marketplace data:`, marketData);
}, [marketData, tokenId]);

// Handle the data - it could be an array or an object depending on wagmi version
const isListed = (() => {
if (!marketData) return false;
if (Array.isArray(marketData)) return marketData[0];
return (marketData as any).listing ?? false;
})();

const {data: marketData} = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "marketplace",
args: [BigInt(tokenId)]
})
const priceWei = (() => {
if (!marketData) return BigInt(0);
if (Array.isArray(marketData)) return marketData[1] as bigint;
return (marketData as any).price ?? BigInt(0);
})();

const priceEth = formatEther(priceWei);

console.log(marketData?.[0])
// Hide non-listed NFTs if showOnlyListed is true
if (showOnlyListed && !isListed) {
return null;
}

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"
alt={`NFT #${tokenId}`}
src={normalizedImageSrc}
className="h-full w-full object-cover"
/>
Expand All @@ -46,11 +68,13 @@ const Card = ({ tokenId }: { tokenId: bigint }) => {
)}
</div>
<div className="flex justify-between py-5">
<h2 className="text-3xl">#29</h2>
<h2 className="text-3xl font-bold">200 ETH</h2>
<h2 className="text-3xl">#{tokenId.toString()}</h2>
<h2 className="text-3xl font-bold">
{isListed ? `${priceEth} ETH` : 'Not listed'}
</h2>
</div>
</div>
)
}
);
};

export default Card
export default Card;
27 changes: 14 additions & 13 deletions frontend-lectures/lesson3/openriver/src/components/Cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ import Card from "./Card";
import { openriverAbi, openriverAddress } from "../contracts";

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

const { data: tokenIdsRaw } = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "tokenIds",
})


});

const tokenIds = tokenIdsRaw as bigint | undefined;

// Use the prop if provided (e.g. from MyNFT page), otherwise fall back to
// the value fetched directly from the contract (used on the home page).
const count = nftNum ?? tokenIds;


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>
)
return (
<div className="flex gap-10 flex-wrap w-360 mt-5 m-auto">
{Array.from({ length: Number(count ?? 0) }, (_, i) => i + 1).map((index) => (
<Card key={index} tokenId={BigInt(index)} />
))}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const Header = () => {
<div className="font-extrabold text-4xl text-blue-700">Open River</div>
<div className="flex gap-5">
<Link href={"/"}>Home</Link>
<Link href={"/dashboard"}>Dashboard</Link>
<Link href={"/mint"}>Minting</Link>
<Link href={"/list"}>Listing</Link>
<Link href={"/myNFT"}>MyNFTs</Link>

</div>
<div className="cta"><ConnectButton/></div>
</div>
Expand Down
77 changes: 50 additions & 27 deletions frontend-lectures/lesson3/openriver/src/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import { useWriteContract } from "wagmi"
import { useReadContract, useAccount } from "wagmi";
import { openriverAbi, openriverAddress } from "../../contracts";
import { useState } from "react";

import Link from "next/link";
import Button from "../../components/Button";

const Dashboard = () => {
const { writeContract: mintNFT } = useWriteContract();
const [tokenUrI, setTokenUrI] = useState("");
const [royalty, setRoyalty] = useState(0);

const handleMintNFT = async () => {
mintNFT(
{
abi: openriverAbi,
address: openriverAddress,
functionName: "newItem",
args: [
tokenUrI,
royalty
]
}
)
const { address, isConnected } = useAccount();

const { data: totalSupplyRaw } = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "tokenIds",
});

const totalSupply = totalSupplyRaw as bigint | undefined;

const { data: ownedCount } = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
}) as { data?: bigint };

if (!isConnected) {
return (
<div className="flex flex-col items-center justify-center mt-32 gap-4">
<p className="text-xl text-slate-600">Connect your wallet to view your dashboard.</p>
</div>
);
}

return (
<div>
<div className="">
<input type="text" placeholder="Token URI" onChange={(e) => setTokenUrI(e.target.value)} />
<input type="number" placeholder="Royalty" onChange={(e) => setRoyalty(Number(e.target.value))} />
<button onClick={handleMintNFT}>Mint NFT</button>
<div className="max-w-3xl mx-auto mt-16 px-6">
<h1 className="text-3xl font-extrabold text-blue-700 mb-8">Dashboard</h1>

<div className="grid grid-cols-2 gap-6 mb-10">
<div className="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
<p className="text-sm text-slate-500 mb-1">Total NFTs minted</p>
<p className="text-4xl font-bold text-blue-700">{totalSupply?.toString() ?? '—'}</p>
</div>
<div className="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
<p className="text-sm text-slate-500 mb-1">NFTs you own</p>
<p className="text-4xl font-bold text-blue-700">{ownedCount?.toString() ?? '—'}</p>
</div>
</div>

<div className="flex gap-4">
<Link href="/mint"><Button label="Mint an NFT" /></Link>
<Link href="/list"><Button label="List an NFT" variant="danger" /></Link>
<Link href="/marketplace"><Button label="Marketplace" /></Link>
<Link href="/myNFT"><Button label="My NFTs" /></Link>
</div>
</div>
)
}
);
};

export default Dashboard
export default Dashboard;
53 changes: 31 additions & 22 deletions frontend-lectures/lesson3/openriver/src/pages/list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
import { useState } from 'react'
import { useWriteContract } from 'wagmi';
import { openriverAbi, openriverAddress } from '../../contracts';
import Button from '../../components/Button';

const index = () => {
const { writeContract: mintNFT } = useWriteContract();
const index = () => {
const { writeContract: listNFT } = useWriteContract();
const [tokenId, setTokenId] = useState(0);
const [price, setPrice] = useState(0);

const handleMintNFT = async () => {
mintNFT(
{
abi: openriverAbi,
address: openriverAddress,
functionName: "listOnMarketplace",
args: [
tokenId,
price
]
}
)
}
const handleListNFT = () => {
listNFT({
abi: openriverAbi,
address: openriverAddress,
functionName: "listOnMarketplace",
args: [
BigInt(tokenId), // contract expects uint256
BigInt(price), // contract expects uint256
],
});
};

return (
<div className='mt-20'>
<div className="flex flex-col gap-5 items-center justify-center">
<input className='w-125 p-5' type="text" placeholder="Token Id" onChange={(e) => setTokenId(Number(e.target.value))} />
<input className='w-125 p-5' type="number" placeholder="Price" onChange={(e) => setPrice(Number(e.target.value))} />
<button className='w-52 bg-blue-500 text-white py-2 px-4 rounded' onClick={handleMintNFT}>List NFT</button>
<input
className='w-125 p-5'
type="number"
placeholder="Token Id"
onChange={(e) => setTokenId(Number(e.target.value))}
/>
<input
className='w-125 p-5'
type="number"
placeholder="Price (in Wei)"
onChange={(e) => setPrice(Number(e.target.value))}
/>
<Button label="List NFT" onClick={handleListNFT} />
</div>
</div>
)
}

);
};

export default index
export default index;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextPage } from "next";
import { useReadContract } from "wagmi";
import { openriverAbi, openriverAddress } from "../../contracts";
import Card from "../../components/Card";

const Marketplace: NextPage = () => {
const { data: totalSupplyRaw } = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "tokenIds",
});

const totalSupply = totalSupplyRaw as bigint | undefined;

return (
<div className="mt-16 px-6">
<h1 className="text-3xl font-extrabold text-blue-700 mb-8">Marketplace</h1>

<div className="flex gap-10 flex-wrap w-full">
{Array.from({ length: Number(totalSupply ?? 0) }, (_, i) => i + 1).map((index) => (
<div key={index} className="flex-shrink-0">
<Card tokenId={BigInt(index)} showOnlyListed={true} />
</div>
))}
</div>

{totalSupply === BigInt(0) && (
<div className="text-center mt-12">
<p className="text-xl text-slate-500">No NFTs listed yet</p>
</div>
)}
</div>
);
};

export default Marketplace;
Loading