From 3ccf45f3c5e2f6efca4b4535d2feac4cf24e7af1 Mon Sep 17 00:00:00 2001 From: Amira Nasri Date: Thu, 16 Oct 2025 11:49:48 +0100 Subject: [PATCH 1/2] feat: convert auction-tutorial to TypeScript, migrate to useNear, and replace context --- frontends/01-frontend/next-env.d.ts | 6 + frontends/01-frontend/package.json | 22 +- .../src/components/{Bid.jsx => Bid.tsx} | 46 ++-- .../01-frontend/src/components/LastBid.jsx | 15 -- .../01-frontend/src/components/LastBid.tsx | 23 ++ .../01-frontend/src/components/Navigation.jsx | 37 --- .../01-frontend/src/components/Navigation.tsx | 44 ++++ .../components/Skeletons/Skeleton.module.css | 41 ++-- .../src/components/Skeletons/SkeletonBid.tsx} | 3 + .../components/Skeletons/SkeletonTimer.tsx} | 8 +- .../01-frontend/src/components/Timer.jsx | 77 ------ .../01-frontend/src/components/Timer.tsx | 89 +++++++ frontends/01-frontend/src/config.js | 2 - frontends/01-frontend/src/config.ts | 24 ++ frontends/01-frontend/src/context.js | 13 - frontends/01-frontend/src/global.d.ts | 6 + frontends/01-frontend/src/hooks/useNear.tsx | 119 +++++++++ frontends/01-frontend/src/pages/_app.js | 27 --- frontends/01-frontend/src/pages/_app.tsx | 20 ++ .../src/pages/api/getBidHistory.js | 42 ---- .../src/pages/api/getBidHistory.ts | 61 +++++ frontends/01-frontend/src/pages/index.js | 106 -------- frontends/01-frontend/src/pages/index.tsx | 139 +++++++++++ frontends/01-frontend/src/wallets/near.js | 142 ----------- frontends/01-frontend/tsconfig.json | 35 +++ frontends/03-frontend/next-env.d.ts | 6 + frontends/03-frontend/package.json | 18 +- .../{AuctionItem.jsx => AuctionItem.tsx} | 0 frontends/03-frontend/src/components/Bid.jsx | 59 ----- frontends/03-frontend/src/components/Bid.tsx | 86 +++++++ .../03-frontend/src/components/LastBid.jsx | 16 -- .../03-frontend/src/components/LastBid.tsx | 35 +++ .../03-frontend/src/components/Navigation.jsx | 37 --- .../03-frontend/src/components/Navigation.tsx | 50 ++++ .../components/Skeletons/Skeleton.module.css | 41 ++-- ...uctionItem.jsx => SkeletonAuctionItem.tsx} | 0 .../src/components/Skeletons/SkeletonBid.tsx} | 0 .../components/Skeletons/SkeletonTimer.tsx} | 8 +- .../03-frontend/src/components/Timer.jsx | 77 ------ .../03-frontend/src/components/Timer.tsx | 90 +++++++ frontends/03-frontend/src/config.js | 2 - frontends/03-frontend/src/config.ts | 24 ++ frontends/03-frontend/src/context.js | 13 - frontends/03-frontend/src/global.d.ts | 6 + frontends/03-frontend/src/hooks/useNear.tsx | 119 +++++++++ frontends/03-frontend/src/pages/_app.js | 27 --- frontends/03-frontend/src/pages/_app.tsx | 16 ++ .../src/pages/api/getBidHistory.js | 44 ---- .../src/pages/api/getBidHistory.ts | 81 +++++++ frontends/03-frontend/src/pages/index.js | 139 ----------- frontends/03-frontend/src/pages/index.tsx | 227 ++++++++++++++++++ frontends/03-frontend/src/wallets/near.js | 142 ----------- frontends/03-frontend/tsconfig.json | 35 +++ 53 files changed, 1439 insertions(+), 1106 deletions(-) create mode 100644 frontends/01-frontend/next-env.d.ts rename frontends/01-frontend/src/components/{Bid.jsx => Bid.tsx} (55%) delete mode 100644 frontends/01-frontend/src/components/LastBid.jsx create mode 100644 frontends/01-frontend/src/components/LastBid.tsx delete mode 100644 frontends/01-frontend/src/components/Navigation.jsx create mode 100644 frontends/01-frontend/src/components/Navigation.tsx rename frontends/{03-frontend/src/components/Skeletons/SkeletonBid.jsx => 01-frontend/src/components/Skeletons/SkeletonBid.tsx} (79%) rename frontends/{03-frontend/src/components/Skeletons/SkeletonTimer.jsx => 01-frontend/src/components/Skeletons/SkeletonTimer.tsx} (77%) delete mode 100644 frontends/01-frontend/src/components/Timer.jsx create mode 100644 frontends/01-frontend/src/components/Timer.tsx delete mode 100644 frontends/01-frontend/src/config.js create mode 100644 frontends/01-frontend/src/config.ts delete mode 100644 frontends/01-frontend/src/context.js create mode 100644 frontends/01-frontend/src/global.d.ts create mode 100644 frontends/01-frontend/src/hooks/useNear.tsx delete mode 100644 frontends/01-frontend/src/pages/_app.js create mode 100644 frontends/01-frontend/src/pages/_app.tsx delete mode 100644 frontends/01-frontend/src/pages/api/getBidHistory.js create mode 100644 frontends/01-frontend/src/pages/api/getBidHistory.ts delete mode 100644 frontends/01-frontend/src/pages/index.js create mode 100644 frontends/01-frontend/src/pages/index.tsx delete mode 100644 frontends/01-frontend/src/wallets/near.js create mode 100644 frontends/01-frontend/tsconfig.json create mode 100644 frontends/03-frontend/next-env.d.ts rename frontends/03-frontend/src/components/{AuctionItem.jsx => AuctionItem.tsx} (100%) delete mode 100644 frontends/03-frontend/src/components/Bid.jsx create mode 100644 frontends/03-frontend/src/components/Bid.tsx delete mode 100644 frontends/03-frontend/src/components/LastBid.jsx create mode 100644 frontends/03-frontend/src/components/LastBid.tsx delete mode 100644 frontends/03-frontend/src/components/Navigation.jsx create mode 100644 frontends/03-frontend/src/components/Navigation.tsx rename frontends/03-frontend/src/components/Skeletons/{SkeletonAuctionItem.jsx => SkeletonAuctionItem.tsx} (100%) rename frontends/{01-frontend/src/components/Skeletons/SkeletonBid.jsx => 03-frontend/src/components/Skeletons/SkeletonBid.tsx} (100%) rename frontends/{01-frontend/src/components/Skeletons/SkeletonTimer.jsx => 03-frontend/src/components/Skeletons/SkeletonTimer.tsx} (77%) delete mode 100644 frontends/03-frontend/src/components/Timer.jsx create mode 100644 frontends/03-frontend/src/components/Timer.tsx delete mode 100644 frontends/03-frontend/src/config.js create mode 100644 frontends/03-frontend/src/config.ts delete mode 100644 frontends/03-frontend/src/context.js create mode 100644 frontends/03-frontend/src/global.d.ts create mode 100644 frontends/03-frontend/src/hooks/useNear.tsx delete mode 100644 frontends/03-frontend/src/pages/_app.js create mode 100644 frontends/03-frontend/src/pages/_app.tsx delete mode 100644 frontends/03-frontend/src/pages/api/getBidHistory.js create mode 100644 frontends/03-frontend/src/pages/api/getBidHistory.ts delete mode 100644 frontends/03-frontend/src/pages/index.js create mode 100644 frontends/03-frontend/src/pages/index.tsx delete mode 100644 frontends/03-frontend/src/wallets/near.js create mode 100644 frontends/03-frontend/tsconfig.json diff --git a/frontends/01-frontend/next-env.d.ts b/frontends/01-frontend/next-env.d.ts new file mode 100644 index 00000000..254b73c1 --- /dev/null +++ b/frontends/01-frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontends/01-frontend/package.json b/frontends/01-frontend/package.json index 394b15bf..02a69de2 100644 --- a/frontends/01-frontend/package.json +++ b/frontends/01-frontend/package.json @@ -11,21 +11,27 @@ "start": "next start", "lint": "next lint" }, - "dependencies": { - "@near-wallet-selector/core": "^8.9.11", - "@near-wallet-selector/here-wallet": "^8.9.11", - "@near-wallet-selector/modal-ui": "^8.9.11", - "@near-wallet-selector/my-near-wallet": "^8.9.11", + "dependencies": { + "@hot-labs/near-connect": "^0.6.2", + "@near-js/crypto": "^2.3.1", + "@near-js/providers": "^2.3.1", + "@near-js/transactions": "^2.3.1", + "@near-js/utils": "^2.3.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@walletconnect/sign-client": "^2.21.9", "bootstrap": "^5", "bootstrap-icons": "^1.11.3", - "near-api-js": "^4.0.3", - "next": "14.2.3", + "near-api-js": "^6.3.0", + "next": "^15", "react": "^18", "react-dom": "^18", "react-toastify": "^10.0.5" }, "devDependencies": { + "@types/node": "24.7.2", "eslint": "^8", - "eslint-config-next": "14.2.3" + "eslint-config-next": "14.2.3", + "typescript": "^5.9.3" } } diff --git a/frontends/01-frontend/src/components/Bid.jsx b/frontends/01-frontend/src/components/Bid.tsx similarity index 55% rename from frontends/01-frontend/src/components/Bid.jsx rename to frontends/01-frontend/src/components/Bid.tsx index 20493b8c..a7f8ab2d 100644 --- a/frontends/01-frontend/src/components/Bid.jsx +++ b/frontends/01-frontend/src/components/Bid.tsx @@ -1,34 +1,45 @@ -import { useContext, useEffect, useState } from 'react'; -import { NearContext } from '@/context'; +import { useEffect, useState } from 'react'; import styles from './Bid.module.css'; import { toast } from 'react-toastify'; +import { useNear } from '@/hooks/useNear'; -const Bid = ({pastBids, lastBid, action}) => { - const [amount, setAmount] = useState(lastBid + 1); - const { signedAccountId } = useContext(NearContext); - const nearMultiplier = Math.pow(10, 24) +interface BidProps { + pastBids: [string, number][] | string | null; + lastBid: number; + action: (amount: number) => Promise; +} + +const Bid = ({ pastBids, lastBid, action }: BidProps) => { + const [amount, setAmount] = useState(lastBid + 1); + const { signedAccountId } = useNear(); + const nearMultiplier = Math.pow(10, 24); const handleBid = async () => { if (signedAccountId) { - await action(amount); - toast("you have made a successful bid"); + try { + await action(amount); + console.log('Bid placed successfully'); + toast.success('You have made a successful bid'); + } catch (err) { + toast.error('Failed to place bid'); + } } else { - toast("Please sign in to make a bid"); + toast.info('Please sign in to make a bid'); } - } + }; useEffect(() => { setAmount(lastBid + 1); - } - , [lastBid]); + }, [lastBid]); return (

History

+ {typeof pastBids === 'string' ? (

{pastBids}

) : pastBids === null ? ( -

Loading...

+

Loading...

) : pastBids.length === 0 ? (

No bids have been placed yet

) : ( @@ -41,20 +52,21 @@ const Bid = ({pastBids, lastBid, action}) => { ))} )} +
setAmount(e.target.value)} + onChange={(e) => setAmount(Number(e.target.value))} className={styles.inputField} - /> + />
); -} +}; -export default Bid; \ No newline at end of file +export default Bid; diff --git a/frontends/01-frontend/src/components/LastBid.jsx b/frontends/01-frontend/src/components/LastBid.jsx deleted file mode 100644 index eaa2b8ac..00000000 --- a/frontends/01-frontend/src/components/LastBid.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import styles from './LastBid.module.css'; - -const LastBid = ({lastBid, highestBidder, lastUpdate}) => { - return ( -
-
- The last bid was {lastBid} $NEAR -
- Made by {highestBidder} - Refresh page in {lastUpdate} -
- ) -} - -export default LastBid \ No newline at end of file diff --git a/frontends/01-frontend/src/components/LastBid.tsx b/frontends/01-frontend/src/components/LastBid.tsx new file mode 100644 index 00000000..33c2696e --- /dev/null +++ b/frontends/01-frontend/src/components/LastBid.tsx @@ -0,0 +1,23 @@ +import styles from './LastBid.module.css'; + +interface LastBidProps { + lastBid: number; + highestBidder: string; + lastUpdate: number; +} + +const LastBid = ({ lastBid, highestBidder, lastUpdate }: LastBidProps) => { + return ( +
+
+ + The last bid was {lastBid} $NEAR + +
+ Made by {highestBidder} + Refresh page in {lastUpdate} +
+ ); +} + +export default LastBid; diff --git a/frontends/01-frontend/src/components/Navigation.jsx b/frontends/01-frontend/src/components/Navigation.jsx deleted file mode 100644 index 6849b8b6..00000000 --- a/frontends/01-frontend/src/components/Navigation.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { useEffect, useState, useContext } from 'react'; - -import { NearContext } from '@/context'; -import NearLogo from '/public/near-logo.svg'; - -export const Navigation = () => { - const { signedAccountId, wallet } = useContext(NearContext); - const [action, setAction] = useState(() => { }); - const [label, setLabel] = useState('Loading...'); - - useEffect(() => { - if (!wallet) return; - - if (signedAccountId) { - setAction(() => wallet.signOut); - setLabel(`Logout ${signedAccountId}`); - } else { - setAction(() => wallet.signIn); - setLabel('Login'); - } - }, [signedAccountId, wallet]); - - return ( - - ); -}; \ No newline at end of file diff --git a/frontends/01-frontend/src/components/Navigation.tsx b/frontends/01-frontend/src/components/Navigation.tsx new file mode 100644 index 00000000..8f77f5ca --- /dev/null +++ b/frontends/01-frontend/src/components/Navigation.tsx @@ -0,0 +1,44 @@ +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { useNear } from "@/hooks/useNear"; + +import NearLogo from "../../public/near-logo.svg"; + +export const Navigation = () => { + const [action, setAction] = useState<() => void>(() => () => {}); + const [label, setLabel] = useState("Loading..."); + const { signedAccountId, signIn, signOut } = useNear(); + + useEffect(() => { + if (signedAccountId) { + setAction(() => signOut); + setLabel(`Logout ${signedAccountId}`); + } else { + setAction(() => signIn); + setLabel("Login"); + } + }, [signedAccountId, signIn, signOut]); + + return ( + + ); +}; \ No newline at end of file diff --git a/frontends/01-frontend/src/components/Skeletons/Skeleton.module.css b/frontends/01-frontend/src/components/Skeletons/Skeleton.module.css index 3bead234..615cf035 100644 --- a/frontends/01-frontend/src/components/Skeletons/Skeleton.module.css +++ b/frontends/01-frontend/src/components/Skeletons/Skeleton.module.css @@ -1,3 +1,4 @@ +/* Container for card or skeleton sections */ .container { border: 1px solid #ddd; border-radius: 8px; @@ -6,19 +7,12 @@ flex-direction: column; width: 300px; margin-bottom: 2rem; -} - -.imageSection {.container, .historyContainer { - border: 1px solid #ddd; - border-radius: 8px; - overflow: hidden; - display: flex; - flex-direction: column; - width: 300px; - margin-bottom: 2rem; background-color: #f2f2f2; + padding: 1rem; + position: relative; } +/* Image section */ .imageSection { position: relative; padding: 1rem; @@ -31,6 +25,7 @@ border-radius: 8px; } +/* Description section */ .description { padding: 1rem; text-align: center; @@ -49,6 +44,7 @@ margin: 0 auto; } +/* Price section */ .priceSection { background-color: #f7f7f7; padding: 1rem; @@ -69,9 +65,11 @@ border-radius: 4px; } +/* Timer styling */ .timer { display: flex; gap: 1rem; + justify-content: space-around; margin-bottom: 2rem; } @@ -85,7 +83,6 @@ font-weight: bold; } - .skeletonTime { height: 40px; width: 40px; @@ -93,6 +90,7 @@ border-radius: 4px; } +/* Bid list items */ .bidItem { display: flex; justify-content: space-between; @@ -103,19 +101,13 @@ .bidItem:last-child { border-bottom: none; } - position: relative; -} +/* Image and stats for card */ .imageSection img { width: 100%; height: auto; } -.description { - padding: 1rem; - text-align: center; -} - .stats { margin-top: 1rem; } @@ -125,12 +117,7 @@ color: #555; } -.priceSection { - background-color: #f7f7f7; - padding: 1rem; - text-align: center; -} - +/* Current price and bid button */ .currentPrice { font-size: 1.5rem; color: #333; @@ -145,3 +132,9 @@ cursor: pointer; border-radius: 4px; } + +.bidMessage { + font-size: 0.9rem; + color: #777; + margin-bottom: 0.5rem; +} \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Skeletons/SkeletonBid.jsx b/frontends/01-frontend/src/components/Skeletons/SkeletonBid.tsx similarity index 79% rename from frontends/03-frontend/src/components/Skeletons/SkeletonBid.jsx rename to frontends/01-frontend/src/components/Skeletons/SkeletonBid.tsx index d86194ba..8577e87e 100644 --- a/frontends/03-frontend/src/components/Skeletons/SkeletonBid.jsx +++ b/frontends/01-frontend/src/components/Skeletons/SkeletonBid.tsx @@ -4,6 +4,9 @@ const SkeletonBid = () => { return (

History

+ {/* Placeholder message under the title */} +

Please log in to make a bid

+
    {Array.from({ length: 5 }).map((_, index) => (
  • diff --git a/frontends/03-frontend/src/components/Skeletons/SkeletonTimer.jsx b/frontends/01-frontend/src/components/Skeletons/SkeletonTimer.tsx similarity index 77% rename from frontends/03-frontend/src/components/Skeletons/SkeletonTimer.jsx rename to frontends/01-frontend/src/components/Skeletons/SkeletonTimer.tsx index 9f3391c8..3a39a8a6 100644 --- a/frontends/03-frontend/src/components/Skeletons/SkeletonTimer.jsx +++ b/frontends/01-frontend/src/components/Skeletons/SkeletonTimer.tsx @@ -2,7 +2,7 @@ import styles from './Skeleton.module.css'; const SkeletonTimer = () => { return ( -
    +
    99 Days
    @@ -16,9 +16,7 @@ const SkeletonTimer = () => { 99 Seconds
    - - ); -} +}; -export default SkeletonTimer; \ No newline at end of file +export default SkeletonTimer; diff --git a/frontends/01-frontend/src/components/Timer.jsx b/frontends/01-frontend/src/components/Timer.jsx deleted file mode 100644 index 4e69066b..00000000 --- a/frontends/01-frontend/src/components/Timer.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useEffect, useState } from 'react'; -import styles from './Timer.module.css'; -import { toast } from 'react-toastify'; - -const Timer = ({ endTime, claimed, action }) => { - const claim = async() =>{ - await action(); - toast("Congratulations!!") - }; - - const [time, setTime] = useState((Number(endTime) / 10 ** 6) - Date.now()); - useEffect(() => { - const timer = setInterval(() => { - setTime((prevTime) => { - const newTime = prevTime - 1000; - if (newTime <= 0) { - clearInterval(timer); - return 0; - } - return newTime; - }); - }, 1000); - - return () => clearInterval(timer); - }, []); - - const formatTime = (time) => { - const allSeconds = Math.floor(time / 1000); - const days = Math.floor(allSeconds / (3600 * 24)); - const hours = Math.floor((allSeconds % (3600 * 24)) / 3600); - const minutes = Math.floor((allSeconds % 3600) / 60); - const seconds = allSeconds % 60; - - return { allSeconds, days, hours, minutes, seconds }; - }; - - const { allSeconds, days, hours, minutes, seconds } = formatTime(time); - - const showCounter = !claimed && allSeconds > 0 - const showActionButton = !claimed && allSeconds <=0 - return ( - <> - {claimed &&
    -

    Auction has been claimed!

    -
    } - {showCounter && ( -
    -

    Time Remaining:

    -
    -
    - {String(days).padStart(2, '0')} Days -
    -
    - {String(hours).padStart(2, '0')} Hours -
    -
    - {String(minutes).padStart(2, '0')} Minutes -
    -
    - {String(seconds).padStart(2, '0')} Seconds -
    -
    -
    - )} - {showActionButton && -
    - -
    - } - - - ); -}; - -export default Timer; \ No newline at end of file diff --git a/frontends/01-frontend/src/components/Timer.tsx b/frontends/01-frontend/src/components/Timer.tsx new file mode 100644 index 00000000..07a3d137 --- /dev/null +++ b/frontends/01-frontend/src/components/Timer.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from 'react'; +import styles from './Timer.module.css'; +import { toast } from 'react-toastify'; + +interface TimerProps { + endTime: string | number; + claimed: boolean; + action: () => Promise; +} + +const Timer = ({ endTime, claimed, action }: TimerProps) => { + const claim = async () => { + await action(); + toast("Congratulations!!"); + }; + + const initialTime = (Number(endTime) / 10 ** 6) - Date.now(); + const [time, setTime] = useState(initialTime > 0 ? initialTime : 0); + + useEffect(() => { + if (time <= 0) return; + + const timer = setInterval(() => { + setTime((prevTime) => { + const newTime = prevTime - 1000; + if (newTime <= 0) { + clearInterval(timer); + return 0; + } + return newTime; + }); + }, 1000); + + return () => clearInterval(timer); + }, [time]); + + const formatTime = (timeMs: number) => { + const allSeconds = Math.floor(timeMs / 1000); + const days = Math.floor(allSeconds / (3600 * 24)); + const hours = Math.floor((allSeconds % (3600 * 24)) / 3600); + const minutes = Math.floor((allSeconds % 3600) / 60); + const seconds = allSeconds % 60; + + return { allSeconds, days, hours, minutes, seconds }; + }; + + const { allSeconds, days, hours, minutes, seconds } = formatTime(time); + + const showCounter = !claimed && allSeconds > 0; + const showActionButton = !claimed && allSeconds <= 0; + + return ( + <> + {claimed && ( +
    +

    Auction has been claimed!

    +
    + )} + {showCounter && ( +
    +

    Time Remaining:

    +
    +
    + {String(days).padStart(2, '0')} Days +
    +
    + {String(hours).padStart(2, '0')} Hours +
    +
    + {String(minutes).padStart(2, '0')} Minutes +
    +
    + {String(seconds).padStart(2, '0')} Seconds +
    +
    +
    + )} + {showActionButton && ( +
    + +
    + )} + + ); +}; + +export default Timer; diff --git a/frontends/01-frontend/src/config.js b/frontends/01-frontend/src/config.js deleted file mode 100644 index 3885a2e0..00000000 --- a/frontends/01-frontend/src/config.js +++ /dev/null @@ -1,2 +0,0 @@ -export const AUCTION_CONTRACT = "basic-auction-example.testnet"; // Replace with your contract name -export const NetworkId = "testnet"; \ No newline at end of file diff --git a/frontends/01-frontend/src/config.ts b/frontends/01-frontend/src/config.ts new file mode 100644 index 00000000..0650301d --- /dev/null +++ b/frontends/01-frontend/src/config.ts @@ -0,0 +1,24 @@ +const contractPerNetwork = { + testnet: '889d3efcab5e7bd444ca6196557a059acd41e44fb3b7155ad5d22385198ea162', // <- your deployed contract account +}; + +export const NetworkId = 'testnet'; +export const AUCTION_CONTRACT = contractPerNetwork[NetworkId]; + +// Chains for EVM Wallets +const evmWalletChains = { + mainnet: { + chainId: 397, + name: 'Near Mainnet', + explorer: 'https://eth-explorer.near.org', + rpc: 'https://eth-rpc.mainnet.near.org', + }, + testnet: { + chainId: 398, + name: 'Near Testnet', + explorer: 'https://eth-explorer-testnet.near.org', + rpc: 'https://eth-rpc.testnet.near.org', + }, +}; + +export const EVMWalletChain = evmWalletChains[NetworkId]; diff --git a/frontends/01-frontend/src/context.js b/frontends/01-frontend/src/context.js deleted file mode 100644 index 74223096..00000000 --- a/frontends/01-frontend/src/context.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createContext } from 'react'; - -/** - * @typedef NearContext - * @property {import('./wallets/near').Wallet} wallet Current wallet - * @property {string} signedAccountId The AccountId of the signed user - */ - -/** @type {import ('react').Context} */ -export const NearContext = createContext({ - wallet: undefined, - signedAccountId: '' -}); \ No newline at end of file diff --git a/frontends/01-frontend/src/global.d.ts b/frontends/01-frontend/src/global.d.ts new file mode 100644 index 00000000..14c22bb0 --- /dev/null +++ b/frontends/01-frontend/src/global.d.ts @@ -0,0 +1,6 @@ +declare module "*.png" { + const value: string; + export default value; + +} +declare module "*.module.css"; diff --git a/frontends/01-frontend/src/hooks/useNear.tsx b/frontends/01-frontend/src/hooks/useNear.tsx new file mode 100644 index 00000000..f200a815 --- /dev/null +++ b/frontends/01-frontend/src/hooks/useNear.tsx @@ -0,0 +1,119 @@ +import { useCallback, useEffect, useState } from "react"; +import { JsonRpcProvider } from "@near-js/providers"; +import { NearConnector, NearWallet } from "@hot-labs/near-connect"; + +interface ConnectedWallet { + wallet: NearWallet; + accounts: { accountId: string }[]; +} + +interface FunctionCallParams { + contractId: string; + method: string; + args?: Record; + gas?: string; + deposit?: string; +} + +interface ViewFunctionParams { + contractId: string; + method: string; + args?: Record; +} + +let connector: NearConnector | undefined; +const provider = new JsonRpcProvider({ url: "https://test.rpc.fastnear.com" }); + +if (typeof window !== "undefined") { + connector = new NearConnector({ network: "testnet" }); +} + +export function useNear() { + const [wallet, setWallet] = useState(undefined); + const [signedAccountId, setSignedAccountId] = useState(""); + const [loading, setLoading] = useState(true); + + const signIn = useCallback(async () => { + if (!connector) return; + const connectedWallet = await connector.connect(); + console.log("Connected wallet", connectedWallet); + }, []); + + const signOut = useCallback(async () => { + if (!wallet || !connector) return; + await connector.disconnect(wallet); + console.log("Disconnected wallet"); + setWallet(undefined); + setSignedAccountId(""); + }, [wallet]); + + useEffect(() => { + if (!connector) return; + + async function reload() { + try { + const { wallet, accounts } = (await connector!.getConnectedWallet()) as ConnectedWallet; + setWallet(wallet); + setSignedAccountId(accounts[0]?.accountId || ""); + } catch { + setWallet(undefined); + setSignedAccountId(""); + } finally { + setLoading(false); + } + } + + const onSignOut = () => { + setWallet(undefined); + setSignedAccountId(""); + }; + + const onSignIn = async (payload: { wallet: NearWallet }) => { + console.log("Signed in with payload", payload); + setWallet(payload.wallet); + const accountId = await payload.wallet.getAddress(); + setSignedAccountId(accountId); + }; + + connector.on("wallet:signOut", onSignOut); + connector.on("wallet:signIn", onSignIn); + + reload(); + + return () => { + connector?.off("wallet:signOut", onSignOut); + connector?.off("wallet:signIn", onSignIn); + }; + }, []); + + const viewFunction = useCallback(async ({ contractId, method, args = {} }: ViewFunctionParams) => { + return provider.callFunction(contractId, method, args); + }, []); + + const callFunction = useCallback( + async ({ contractId, method, args = {}, gas = "30000000000000", deposit = "0" }: FunctionCallParams) => { + if (!wallet) throw new Error("Wallet not connected"); + return wallet.signAndSendTransaction({ + receiverId: contractId, + actions: [ + { + type: "FunctionCall", + params: { methodName: method, args, gas, deposit }, + }, + ], + }); + }, + [wallet] + ); + + return { + signedAccountId, + wallet, + signIn, + signOut, + loading, + viewFunction, + callFunction, + provider, + }; +} \ No newline at end of file diff --git a/frontends/01-frontend/src/pages/_app.js b/frontends/01-frontend/src/pages/_app.js deleted file mode 100644 index 9d66ea65..00000000 --- a/frontends/01-frontend/src/pages/_app.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from 'react'; - -import '@/styles/globals.css'; -import { NearContext } from '@/context'; -import { Navigation } from '@/components/Navigation'; - -import { Wallet } from '@/wallets/near'; -import { NetworkId } from '@/config'; - -import { ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; - -const wallet = new Wallet({ networkId: NetworkId }); - -export default function MyApp({ Component, pageProps }) { - const [signedAccountId, setSignedAccountId] = useState(''); - - useEffect(() => { wallet.startUp(setSignedAccountId) }, []); - - return ( - - - - - - ); -} diff --git a/frontends/01-frontend/src/pages/_app.tsx b/frontends/01-frontend/src/pages/_app.tsx new file mode 100644 index 00000000..e4da27f3 --- /dev/null +++ b/frontends/01-frontend/src/pages/_app.tsx @@ -0,0 +1,20 @@ +import '@/styles/globals.css'; +import { Navigation } from '@/components/Navigation'; + +import type { AppProps } from "next/app"; + +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + + +export default function App({ Component, pageProps }: AppProps) { + + + return ( + <> + + + + + ); +} diff --git a/frontends/01-frontend/src/pages/api/getBidHistory.js b/frontends/01-frontend/src/pages/api/getBidHistory.js deleted file mode 100644 index 61171667..00000000 --- a/frontends/01-frontend/src/pages/api/getBidHistory.js +++ /dev/null @@ -1,42 +0,0 @@ -export default async function handler(req, res) { - try { - if (!process.env.API_KEY) { - return res.status(500).json({ error: "API key not provided" }); - } - // Get all bid transactions - const { contractId } = req.query; - const bidsRes = await fetch(`https://api-testnet.nearblocks.io/v1/account/${contractId}/txns?method=bid&page=1&per_page=25&order=desc`, { - headers: { - 'Accept': '*/*', - 'Authorization': `Bearer ${process.env.API_KEY}` - } - }); - - const bidsJson = await bidsRes.json(); - - const txns = bidsJson.txns; - let pastBids = []; - - // Loop through all bids and add valid bids to the pastBids array until 5 are found - for (let i = 0; i < txns.length; i++) { - const txn = txns[i]; - - if (txn.receipt_outcome.status) { - let amount = txn.actions[0].deposit; - let account = txn.predecessor_account_id - - if (pastBids.length < 5) { - pastBids.push([account, amount]); - } else { - break; - } - } - } - - // Respond with the past bids - return res.status(200).json({ pastBids }); - } catch (error) { - return res.status(500).json({ error: "Failed to fetch past bids" }); - } - } - \ No newline at end of file diff --git a/frontends/01-frontend/src/pages/api/getBidHistory.ts b/frontends/01-frontend/src/pages/api/getBidHistory.ts new file mode 100644 index 00000000..8d2a8716 --- /dev/null +++ b/frontends/01-frontend/src/pages/api/getBidHistory.ts @@ -0,0 +1,61 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +interface BidResponse { + pastBids: [string, string][]; +} + +interface ErrorResponse { + error: string; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + try { + if (!process.env.API_KEY) { + res.status(500).json({ error: "API key not provided" }); + return; + } + + const { contractId } = req.query; + + if (!contractId || typeof contractId !== "string") { + res.status(400).json({ error: "Missing or invalid contractId" }); + return; + } + + const bidsRes = await fetch( + `https://api-testnet.nearblocks.io/v1/account/${contractId}/txns?method=bid&page=1&per_page=25&order=desc`, + { + headers: { + Accept: "*/*", + Authorization: `Bearer ${process.env.API_KEY}`, + }, + } + ); + + const bidsJson = await bidsRes.json(); + const txns = bidsJson.txns || []; + + const pastBids: [string, string][] = []; + + for (let i = 0; i < txns.length; i++) { + const txn = txns[i]; + + if (txn.receipt_outcome?.status && txn.actions?.[0]?.deposit) { + const amount: string = txn.actions[0].deposit; + const account: string = txn.predecessor_account_id; + + pastBids.push([account, amount]); + + if (pastBids.length >= 5) break; + } + } + + res.status(200).json({ pastBids }); + } catch (error) { + console.error("Error fetching past bids:", error); + res.status(500).json({ error: "Failed to fetch past bids" }); + } +} diff --git a/frontends/01-frontend/src/pages/index.js b/frontends/01-frontend/src/pages/index.js deleted file mode 100644 index da0c9718..00000000 --- a/frontends/01-frontend/src/pages/index.js +++ /dev/null @@ -1,106 +0,0 @@ -import styles from '@/styles/app.module.css'; -import Timer from '@/components/Timer'; -import Bid from '@/components/Bid'; -import { useContext, useEffect, useState } from 'react'; -import SkeletonTimer from '@/components/Skeletons/SkeletonTimer'; -import SkeletonBid from '@/components/Skeletons/SkeletonBid'; -import { NearContext } from '@/context'; -import { AUCTION_CONTRACT } from '@/config'; -import LastBid from '@/components/LastBid'; - -export default function Home() { - const [highestBid, setHighestBid] = useState(null) - const [highestBidder, setHighestBidder] = useState(null) - const [claimed, setClaimed] = useState(false) - const [auctionEndTime, setAuctionEndTime] = useState(null) - const [secondsRemaining, setSecondsRemaining] = useState(20) - const [pastBids, setPastBids] = useState(null) - const nearMultiplier = Math.pow(10, 24) - - const { wallet } = useContext(NearContext); - - useEffect(() => { - const getInfo = async () => { - const highestBidData = await wallet.viewMethod({ - contractId: AUCTION_CONTRACT, - method: "get_highest_bid", - }); - setHighestBid(highestBidData.bid / nearMultiplier) - setHighestBidder(highestBidData.bidder) - - const claimedData = await wallet.viewMethod({ - contractId: AUCTION_CONTRACT, - method: "get_claimed", - }); - setClaimed(claimedData) - - const auctionEndTimeData = await wallet.viewMethod({ - contractId: AUCTION_CONTRACT, - method: "get_auction_end_time", - }); - setAuctionEndTime(auctionEndTimeData) - } - getInfo(); - - fetchPastBids(); - - const intervalId = setInterval(() => { - getInfo(); - setSecondsRemaining(20); - }, 20000); - - const countdownIntervalId = setInterval(() => { - setSecondsRemaining(prev => (prev === 1 ? 20 : prev - 1)); - }, 1000); - - - return () => { - clearInterval(intervalId); - clearInterval(countdownIntervalId); - }; - }, []); - - const bid = async (amount) => { - let real_amount = amount * nearMultiplier - let response = await wallet.callMethod({ - contractId: AUCTION_CONTRACT, - method: "bid", - deposit: real_amount, - args: {}, - gas:"300000000000000" - }) - return response - } - - const claim = async () => { - let response = await wallet.callMethod({ - contractId: AUCTION_CONTRACT, - method: "claim", - gas:"300000000000000" - }) - return response - } - - const fetchPastBids = async () => { - const response = await fetch(`/api/getBidHistory?contractId=${AUCTION_CONTRACT}`); - const data = await response.json(); - if (data.error) { - setPastBids(data.error); - } else { - setPastBids(data.pastBids); - } - } - - return ( -
    -
    - {!highestBid ? : } -
    -
    - {!auctionEndTime ? : } - {!highestBidder ? : } -
    -
    - - ); -} \ No newline at end of file diff --git a/frontends/01-frontend/src/pages/index.tsx b/frontends/01-frontend/src/pages/index.tsx new file mode 100644 index 00000000..aa765485 --- /dev/null +++ b/frontends/01-frontend/src/pages/index.tsx @@ -0,0 +1,139 @@ + +import { useEffect, useState } from 'react'; + +import styles from '@/styles/app.module.css'; + +import Timer from '@/components/Timer'; +import Bid from '@/components/Bid'; +import SkeletonTimer from '@/components/Skeletons/SkeletonTimer'; +import SkeletonBid from '@/components/Skeletons/SkeletonBid'; +import LastBid from '@/components/LastBid'; + +import { useNear } from '@/hooks/useNear'; +import { AUCTION_CONTRACT } from '@/config'; + +interface HighestBidData { + bid: number; + bidder: string; +} + +export default function Home() { + const [highestBid, setHighestBid] = useState(0); + const [highestBidder, setHighestBidder] = useState(''); + const [claimed, setClaimed] = useState(false); + const [auctionEndTime, setAuctionEndTime] = useState(0); + const [secondsRemaining, setSecondsRemaining] = useState(20); + const [pastBids, setPastBids] = useState<[]>([]); + const nearMultiplier = 1e24; + + const { signedAccountId, viewFunction, callFunction } = useNear(); + + useEffect(() => { + if (!signedAccountId) return; + + const getInfo = async () => { + try { + const highestBidData = (await viewFunction({ + contractId: AUCTION_CONTRACT, + method: 'get_highest_bid', + })) as HighestBidData | null; + + if (highestBidData) { + setHighestBid(highestBidData.bid / nearMultiplier); + setHighestBidder(highestBidData.bidder); + } + + const claimedData = (await viewFunction({ + contractId: AUCTION_CONTRACT, + method: 'get_claimed', + })) as boolean; + setClaimed(claimedData); + + const auctionEndTimeData = (await viewFunction({ + contractId: AUCTION_CONTRACT, + method: 'get_auction_end_time', + })) as number; + setAuctionEndTime(auctionEndTimeData); + } catch (err) { + console.error('Error fetching auction info:', err); + } + }; + + const fetchPastBids = async () => { + try { + const response = await fetch(`/api/getBidHistory?contractId=${AUCTION_CONTRACT}`); + const data = await response.json(); + setPastBids(data?.pastBids || []); + } catch (err) { + console.error('Error fetching past bids:', err); + setPastBids([]); + } + }; + + getInfo(); + fetchPastBids(); + + const intervalId = setInterval(() => { + getInfo(); + setSecondsRemaining(20); + }, 20000); + + const countdownIntervalId = setInterval(() => { + setSecondsRemaining((prev) => (prev === 1 ? 20 : prev - 1)); + }, 1000); + + return () => { + clearInterval(intervalId); + clearInterval(countdownIntervalId); + }; + }, [signedAccountId, viewFunction]); + + const placeBid = async (amount: number) => { + const realAmount = amount * nearMultiplier; + return await callFunction({ + contractId: AUCTION_CONTRACT, + method: 'bid', + args: {}, + deposit: realAmount.toString(), + gas: '300000000000000', + }); + }; + + const claimAuction = async () => { + return await callFunction({ + contractId: AUCTION_CONTRACT, + method: 'claim', + args: {}, + gas: '300000000000000', + }); + }; + + return ( +
    +
    + {!highestBid ? ( + + ) : ( + + )} +
    + +
    + {!auctionEndTime ? ( + + ) : ( + + )} + {!highestBidder ? ( + + ) : ( + + )} +
    +
    + ); +} diff --git a/frontends/01-frontend/src/wallets/near.js b/frontends/01-frontend/src/wallets/near.js deleted file mode 100644 index 5c700af5..00000000 --- a/frontends/01-frontend/src/wallets/near.js +++ /dev/null @@ -1,142 +0,0 @@ -// near api js -import { providers } from 'near-api-js'; - -// wallet selector -import { distinctUntilChanged, map } from 'rxjs'; -import '@near-wallet-selector/modal-ui/styles.css'; -import { setupModal } from '@near-wallet-selector/modal-ui'; -import { setupWalletSelector } from '@near-wallet-selector/core'; -import { setupHereWallet } from '@near-wallet-selector/here-wallet'; -import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; - -const THIRTY_TGAS = '30000000000000'; -const NO_DEPOSIT = '0'; - -export class Wallet { - /** - * @constructor - * @param {Object} options - the options for the wallet - * @param {string} options.networkId - the network id to connect to - * @param {string} options.createAccessKeyFor - the contract to create an access key for - * @example - * const wallet = new Wallet({ networkId: 'testnet', createAccessKeyFor: 'contractId' }); - * wallet.startUp((signedAccountId) => console.log(signedAccountId)); - */ - constructor({ networkId = 'testnet', createAccessKeyFor = undefined }) { - this.createAccessKeyFor = createAccessKeyFor; - this.networkId = networkId; - } - - /** - * To be called when the website loads - * @param {Function} accountChangeHook - a function that is called when the user signs in or out# - * @returns {Promise} - the accountId of the signed-in user - */ - startUp = async (accountChangeHook) => { - this.selector = setupWalletSelector({ - network: this.networkId, - modules: [setupMyNearWallet(), setupHereWallet()] - }); - - const walletSelector = await this.selector; - const isSignedIn = walletSelector.isSignedIn(); - const accountId = isSignedIn ? walletSelector.store.getState().accounts[0].accountId : ''; - - walletSelector.store.observable - .pipe( - map(state => state.accounts), - distinctUntilChanged() - ) - .subscribe(accounts => { - const signedAccount = accounts.find((account) => account.active)?.accountId; - accountChangeHook(signedAccount); - }); - - return accountId; - }; - - /** - * Displays a modal to login the user - */ - signIn = async () => { - const modal = setupModal(await this.selector, { contractId: this.createAccessKeyFor }); - modal.show(); - }; - - /** - * Logout the user - */ - signOut = async () => { - const selectedWallet = await (await this.selector).wallet(); - selectedWallet.signOut(); - }; - - /** - * Makes a read-only call to a contract - * @param {Object} options - the options for the call - * @param {string} options.contractId - the contract's account id - * @param {string} options.method - the method to call - * @param {Object} options.args - the arguments to pass to the method - * @returns {Promise} - the result of the method call - */ - viewMethod = async ({ contractId, method, args = {} }) => { - const url = `https://rpc.${this.networkId}.near.org`; - const provider = new providers.JsonRpcProvider({ url }); - - let res = await provider.query({ - request_type: 'call_function', - account_id: contractId, - method_name: method, - args_base64: Buffer.from(JSON.stringify(args)).toString('base64'), - finality: 'optimistic', - }); - return JSON.parse(Buffer.from(res.result).toString()); - }; - - - /** - * Makes a call to a contract - * @param {Object} options - the options for the call - * @param {string} options.contractId - the contract's account id - * @param {string} options.method - the method to call - * @param {Object} options.args - the arguments to pass to the method - * @param {string} options.gas - the amount of gas to use - * @param {string} options.deposit - the amount of yoctoNEAR to deposit - * @returns {Promise} - the resulting transaction - */ - callMethod = async ({ contractId, method, args = {}, gas = THIRTY_TGAS, deposit = NO_DEPOSIT }) => { - // Sign a transaction with the "FunctionCall" action - const selectedWallet = await (await this.selector).wallet(); - const outcome = await selectedWallet.signAndSendTransaction({ - receiverId: contractId, - actions: [ - { - type: 'FunctionCall', - params: { - methodName: method, - args, - gas, - deposit, - }, - }, - ], - }); - - return providers.getTransactionLastResult(outcome); - }; - - /** - * Retrieves transaction result from the network - * @param {string} txhash - the transaction hash - * @returns {Promise} - the result of the transaction - */ - getTransactionResult = async (txhash) => { - const walletSelector = await this.selector; - const { network } = walletSelector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - // Retrieve transaction result from the network - const transaction = await provider.txStatus(txhash, 'unnused'); - return providers.getTransactionLastResult(transaction); - }; -} \ No newline at end of file diff --git a/frontends/01-frontend/tsconfig.json b/frontends/01-frontend/tsconfig.json new file mode 100644 index 00000000..e68f335b --- /dev/null +++ b/frontends/01-frontend/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + "jsx": "preserve", + "strict": true, + "moduleResolution": "node", + "skipLibCheck": true, + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "global.d.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/frontends/03-frontend/next-env.d.ts b/frontends/03-frontend/next-env.d.ts new file mode 100644 index 00000000..254b73c1 --- /dev/null +++ b/frontends/03-frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontends/03-frontend/package.json b/frontends/03-frontend/package.json index 394b15bf..f5e71459 100644 --- a/frontends/03-frontend/package.json +++ b/frontends/03-frontend/package.json @@ -12,20 +12,26 @@ "lint": "next lint" }, "dependencies": { - "@near-wallet-selector/core": "^8.9.11", - "@near-wallet-selector/here-wallet": "^8.9.11", - "@near-wallet-selector/modal-ui": "^8.9.11", - "@near-wallet-selector/my-near-wallet": "^8.9.11", + "@hot-labs/near-connect": "^0.6.2", + "@near-js/crypto": "^2.3.1", + "@near-js/providers": "^2.3.1", + "@near-js/transactions": "^2.3.1", + "@near-js/utils": "^2.3.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@walletconnect/sign-client": "^2.21.9", "bootstrap": "^5", "bootstrap-icons": "^1.11.3", "near-api-js": "^4.0.3", - "next": "14.2.3", + "next": "^15", "react": "^18", "react-dom": "^18", "react-toastify": "^10.0.5" }, "devDependencies": { + "@types/node": "^24.7.2", "eslint": "^8", - "eslint-config-next": "14.2.3" + "eslint-config-next": "14.2.3", + "typescript": "^5.9.3" } } diff --git a/frontends/03-frontend/src/components/AuctionItem.jsx b/frontends/03-frontend/src/components/AuctionItem.tsx similarity index 100% rename from frontends/03-frontend/src/components/AuctionItem.jsx rename to frontends/03-frontend/src/components/AuctionItem.tsx diff --git a/frontends/03-frontend/src/components/Bid.jsx b/frontends/03-frontend/src/components/Bid.jsx deleted file mode 100644 index 04b53c9d..00000000 --- a/frontends/03-frontend/src/components/Bid.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useContext, useEffect, useState } from 'react'; -import { NearContext } from '@/context'; -import styles from './Bid.module.css'; -import { toast } from 'react-toastify'; - -const Bid = ({ pastBids, ftName, ftImg, lastBidDisplay, ftDecimals, action}) => { - const [amount, setAmount] = useState(lastBidDisplay + 1); - const { signedAccountId } = useContext(NearContext); - - const handleBid = async () => { - if (signedAccountId) { - await action(amount); - toast("you have made a successful bid"); - } else { - toast("Please sign in to make a bid"); - } - } - - useEffect(() => { - setAmount(lastBidDisplay + 1); - } - , [lastBidDisplay]); - - return ( -
    -

    History

    - {typeof pastBids === 'string' ? ( -

    {pastBids}

    - ) : pastBids === null ? ( -

    Loading...

    - ) : pastBids.length === 0 ? ( -

    No bids have been placed yet

    - ) : ( -
      - {pastBids?.map((bid, index) => ( -
    • - {bid[1] / Math.pow(10, ftDecimals)} {ftName} - {bid[0]} -
    • - ))} -
    - )} -
    - setAmount(e.target.value)} - className={styles.inputField} - /> - -
    -
    - ); -} - -export default Bid; \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Bid.tsx b/frontends/03-frontend/src/components/Bid.tsx new file mode 100644 index 00000000..107c166d --- /dev/null +++ b/frontends/03-frontend/src/components/Bid.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState, ChangeEvent } from "react"; +import { toast } from "react-toastify"; +import { useNear } from "@/hooks/useNear"; +import styles from "./Bid.module.css"; + +interface BidProps { + pastBids: [string, number][] | string | null; + ftName: string; + ftImg: string; + lastBidDisplay: number; + ftDecimals: number; + action: (amount: number) => Promise; +} + +export default function Bid({ + pastBids, + ftName, + ftImg, + lastBidDisplay, + ftDecimals, + action, +}: BidProps) { + const [amount, setAmount] = useState(lastBidDisplay + 1); + const { signedAccountId } = useNear(); + + const handleBid = async () => { + if (signedAccountId) { + try { + await action(amount); + toast.success("You have made a successful bid!"); + } catch (error) { + console.error("Error placing bid:", error); + toast.error("Failed to place bid. Please try again."); + } + } else { + toast.info("Please sign in to make a bid."); + } + }; + + useEffect(() => { + setAmount(lastBidDisplay + 1); + }, [lastBidDisplay]); + + const handleAmountChange = (e: ChangeEvent) => { + setAmount(Number(e.target.value)); + }; + + return ( +
    +

    History

    + + {typeof pastBids === "string" ? ( +

    {pastBids}

    + ) : pastBids === null ? ( +

    Loading...

    + ) : pastBids.length === 0 ? ( +

    No bids have been placed yet

    + ) : ( +
      + {pastBids.map((bid, index) => ( +
    • + + {bid[1] / Math.pow(10, ftDecimals)} {ftName} + + {bid[0]} +
    • + ))} +
    + )} + +
    + + +
    +
    + ); +} diff --git a/frontends/03-frontend/src/components/LastBid.jsx b/frontends/03-frontend/src/components/LastBid.jsx deleted file mode 100644 index 6844dd28..00000000 --- a/frontends/03-frontend/src/components/LastBid.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import styles from './LastBid.module.css'; - -const LastBid = ({lastBid, lastUpdate, ftName, ftImg, lastBidDisplay }) => { - return ( -
    -
    - The last bid was {lastBidDisplay} {ftName} - {ftName} -
    - Made by {lastBid.bidder} - Refresh page in {lastUpdate} -
    - ) -} - -export default LastBid \ No newline at end of file diff --git a/frontends/03-frontend/src/components/LastBid.tsx b/frontends/03-frontend/src/components/LastBid.tsx new file mode 100644 index 00000000..e52abc48 --- /dev/null +++ b/frontends/03-frontend/src/components/LastBid.tsx @@ -0,0 +1,35 @@ +import styles from "./LastBid.module.css"; + +interface LastBidData { + bidder: string; +} + +interface LastBidProps { + lastBid: LastBidData; + lastUpdate: number; + ftName: string; + ftImg: string; + lastBidDisplay: number; +} + +export default function LastBid({ + lastBid, + lastUpdate, + ftName, + ftImg, + lastBidDisplay, +}: LastBidProps) { + return ( +
    +
    + + The last bid was {lastBidDisplay} {ftName} + + {ftName} +
    + + Made by {lastBid.bidder || "Unknown"} + Refresh page in {lastUpdate}s +
    + ); +} diff --git a/frontends/03-frontend/src/components/Navigation.jsx b/frontends/03-frontend/src/components/Navigation.jsx deleted file mode 100644 index 6849b8b6..00000000 --- a/frontends/03-frontend/src/components/Navigation.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { useEffect, useState, useContext } from 'react'; - -import { NearContext } from '@/context'; -import NearLogo from '/public/near-logo.svg'; - -export const Navigation = () => { - const { signedAccountId, wallet } = useContext(NearContext); - const [action, setAction] = useState(() => { }); - const [label, setLabel] = useState('Loading...'); - - useEffect(() => { - if (!wallet) return; - - if (signedAccountId) { - setAction(() => wallet.signOut); - setLabel(`Logout ${signedAccountId}`); - } else { - setAction(() => wallet.signIn); - setLabel('Login'); - } - }, [signedAccountId, wallet]); - - return ( - - ); -}; \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Navigation.tsx b/frontends/03-frontend/src/components/Navigation.tsx new file mode 100644 index 00000000..6d6d4a27 --- /dev/null +++ b/frontends/03-frontend/src/components/Navigation.tsx @@ -0,0 +1,50 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useState } from 'react'; +import { useNear } from '@/hooks/useNear'; + +export const Navigation = () => { + const { signedAccountId, signIn, signOut } = useNear(); + const [action, setAction] = useState<(() => void | Promise) | null>(null); + const [label, setLabel] = useState('Loading...'); + + useEffect(() => { + if (!signedAccountId) { + setAction(() => signIn); + setLabel('Login'); + return; + } + + setAction(() => signOut); + setLabel(`Logout ${signedAccountId}`); + }, [signedAccountId, signIn, signOut]); + + return ( + + ); +}; diff --git a/frontends/03-frontend/src/components/Skeletons/Skeleton.module.css b/frontends/03-frontend/src/components/Skeletons/Skeleton.module.css index 3bead234..615cf035 100644 --- a/frontends/03-frontend/src/components/Skeletons/Skeleton.module.css +++ b/frontends/03-frontend/src/components/Skeletons/Skeleton.module.css @@ -1,3 +1,4 @@ +/* Container for card or skeleton sections */ .container { border: 1px solid #ddd; border-radius: 8px; @@ -6,19 +7,12 @@ flex-direction: column; width: 300px; margin-bottom: 2rem; -} - -.imageSection {.container, .historyContainer { - border: 1px solid #ddd; - border-radius: 8px; - overflow: hidden; - display: flex; - flex-direction: column; - width: 300px; - margin-bottom: 2rem; background-color: #f2f2f2; + padding: 1rem; + position: relative; } +/* Image section */ .imageSection { position: relative; padding: 1rem; @@ -31,6 +25,7 @@ border-radius: 8px; } +/* Description section */ .description { padding: 1rem; text-align: center; @@ -49,6 +44,7 @@ margin: 0 auto; } +/* Price section */ .priceSection { background-color: #f7f7f7; padding: 1rem; @@ -69,9 +65,11 @@ border-radius: 4px; } +/* Timer styling */ .timer { display: flex; gap: 1rem; + justify-content: space-around; margin-bottom: 2rem; } @@ -85,7 +83,6 @@ font-weight: bold; } - .skeletonTime { height: 40px; width: 40px; @@ -93,6 +90,7 @@ border-radius: 4px; } +/* Bid list items */ .bidItem { display: flex; justify-content: space-between; @@ -103,19 +101,13 @@ .bidItem:last-child { border-bottom: none; } - position: relative; -} +/* Image and stats for card */ .imageSection img { width: 100%; height: auto; } -.description { - padding: 1rem; - text-align: center; -} - .stats { margin-top: 1rem; } @@ -125,12 +117,7 @@ color: #555; } -.priceSection { - background-color: #f7f7f7; - padding: 1rem; - text-align: center; -} - +/* Current price and bid button */ .currentPrice { font-size: 1.5rem; color: #333; @@ -145,3 +132,9 @@ cursor: pointer; border-radius: 4px; } + +.bidMessage { + font-size: 0.9rem; + color: #777; + margin-bottom: 0.5rem; +} \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Skeletons/SkeletonAuctionItem.jsx b/frontends/03-frontend/src/components/Skeletons/SkeletonAuctionItem.tsx similarity index 100% rename from frontends/03-frontend/src/components/Skeletons/SkeletonAuctionItem.jsx rename to frontends/03-frontend/src/components/Skeletons/SkeletonAuctionItem.tsx diff --git a/frontends/01-frontend/src/components/Skeletons/SkeletonBid.jsx b/frontends/03-frontend/src/components/Skeletons/SkeletonBid.tsx similarity index 100% rename from frontends/01-frontend/src/components/Skeletons/SkeletonBid.jsx rename to frontends/03-frontend/src/components/Skeletons/SkeletonBid.tsx diff --git a/frontends/01-frontend/src/components/Skeletons/SkeletonTimer.jsx b/frontends/03-frontend/src/components/Skeletons/SkeletonTimer.tsx similarity index 77% rename from frontends/01-frontend/src/components/Skeletons/SkeletonTimer.jsx rename to frontends/03-frontend/src/components/Skeletons/SkeletonTimer.tsx index 9f3391c8..3a39a8a6 100644 --- a/frontends/01-frontend/src/components/Skeletons/SkeletonTimer.jsx +++ b/frontends/03-frontend/src/components/Skeletons/SkeletonTimer.tsx @@ -2,7 +2,7 @@ import styles from './Skeleton.module.css'; const SkeletonTimer = () => { return ( -
    +
    99 Days
    @@ -16,9 +16,7 @@ const SkeletonTimer = () => { 99 Seconds
    - - ); -} +}; -export default SkeletonTimer; \ No newline at end of file +export default SkeletonTimer; diff --git a/frontends/03-frontend/src/components/Timer.jsx b/frontends/03-frontend/src/components/Timer.jsx deleted file mode 100644 index 4e69066b..00000000 --- a/frontends/03-frontend/src/components/Timer.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useEffect, useState } from 'react'; -import styles from './Timer.module.css'; -import { toast } from 'react-toastify'; - -const Timer = ({ endTime, claimed, action }) => { - const claim = async() =>{ - await action(); - toast("Congratulations!!") - }; - - const [time, setTime] = useState((Number(endTime) / 10 ** 6) - Date.now()); - useEffect(() => { - const timer = setInterval(() => { - setTime((prevTime) => { - const newTime = prevTime - 1000; - if (newTime <= 0) { - clearInterval(timer); - return 0; - } - return newTime; - }); - }, 1000); - - return () => clearInterval(timer); - }, []); - - const formatTime = (time) => { - const allSeconds = Math.floor(time / 1000); - const days = Math.floor(allSeconds / (3600 * 24)); - const hours = Math.floor((allSeconds % (3600 * 24)) / 3600); - const minutes = Math.floor((allSeconds % 3600) / 60); - const seconds = allSeconds % 60; - - return { allSeconds, days, hours, minutes, seconds }; - }; - - const { allSeconds, days, hours, minutes, seconds } = formatTime(time); - - const showCounter = !claimed && allSeconds > 0 - const showActionButton = !claimed && allSeconds <=0 - return ( - <> - {claimed &&
    -

    Auction has been claimed!

    -
    } - {showCounter && ( -
    -

    Time Remaining:

    -
    -
    - {String(days).padStart(2, '0')} Days -
    -
    - {String(hours).padStart(2, '0')} Hours -
    -
    - {String(minutes).padStart(2, '0')} Minutes -
    -
    - {String(seconds).padStart(2, '0')} Seconds -
    -
    -
    - )} - {showActionButton && -
    - -
    - } - - - ); -}; - -export default Timer; \ No newline at end of file diff --git a/frontends/03-frontend/src/components/Timer.tsx b/frontends/03-frontend/src/components/Timer.tsx new file mode 100644 index 00000000..89373641 --- /dev/null +++ b/frontends/03-frontend/src/components/Timer.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import styles from "./Timer.module.css"; +import { toast } from "react-toastify"; + +interface TimerProps { + endTime: number; + claimed: boolean; + action: () => Promise; +} + +export default function Timer({ endTime, claimed, action }: TimerProps) { + const [time, setTime] = useState(Number(endTime) / 10 ** 6 - Date.now()); + + const claim = async () => { + try { + await action(); + toast.success("🎉 Congratulations! You claimed the auction!"); + } catch (error) { + console.error("Error claiming auction:", error); + toast.error("Failed to claim auction. Please try again."); + } + }; + + useEffect(() => { + const timer = setInterval(() => { + setTime((prevTime) => { + const newTime = prevTime - 1000; + if (newTime <= 0) { + clearInterval(timer); + return 0; + } + return newTime; + }); + }, 1000); + + return () => clearInterval(timer); + }, []); + + const formatTime = (time: number) => { + const allSeconds = Math.max(Math.floor(time / 1000), 0); + const days = Math.floor(allSeconds / (3600 * 24)); + const hours = Math.floor((allSeconds % (3600 * 24)) / 3600); + const minutes = Math.floor((allSeconds % 3600) / 60); + const seconds = allSeconds % 60; + return { allSeconds, days, hours, minutes, seconds }; + }; + + const { allSeconds, days, hours, minutes, seconds } = formatTime(time); + + const showCounter = !claimed && allSeconds > 0; + const showActionButton = !claimed && allSeconds <= 0; + + return ( + <> + {claimed && ( +
    +

    Auction has been claimed!

    +
    + )} + + {showCounter && ( +
    +

    Time Remaining:

    +
    +
    + {String(days).padStart(2, "0")} Days +
    +
    + {String(hours).padStart(2, "0")} Hours +
    +
    + {String(minutes).padStart(2, "0")} Minutes +
    +
    + {String(seconds).padStart(2, "0")} Seconds +
    +
    +
    + )} + + {showActionButton && ( +
    + +
    + )} + + ); +} diff --git a/frontends/03-frontend/src/config.js b/frontends/03-frontend/src/config.js deleted file mode 100644 index a6a384c2..00000000 --- a/frontends/03-frontend/src/config.js +++ /dev/null @@ -1,2 +0,0 @@ -export const AUCTION_CONTRACT = "auction-example.testnet"; // Replace with your contract name -export const NetworkId = "testnet"; \ No newline at end of file diff --git a/frontends/03-frontend/src/config.ts b/frontends/03-frontend/src/config.ts new file mode 100644 index 00000000..c25a75da --- /dev/null +++ b/frontends/03-frontend/src/config.ts @@ -0,0 +1,24 @@ +const contractPerNetwork = { + testnet: 'auction-example.testnet', // <- your deployed contract account +}; + +export const NetworkId = 'testnet'; +export const AUCTION_CONTRACT = contractPerNetwork[NetworkId]; + +// Chains for EVM Wallets +const evmWalletChains = { + mainnet: { + chainId: 397, + name: 'Near Mainnet', + explorer: 'https://eth-explorer.near.org', + rpc: 'https://eth-rpc.mainnet.near.org', + }, + testnet: { + chainId: 398, + name: 'Near Testnet', + explorer: 'https://eth-explorer-testnet.near.org', + rpc: 'https://eth-rpc.testnet.near.org', + }, +}; + +export const EVMWalletChain = evmWalletChains[NetworkId]; diff --git a/frontends/03-frontend/src/context.js b/frontends/03-frontend/src/context.js deleted file mode 100644 index 74223096..00000000 --- a/frontends/03-frontend/src/context.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createContext } from 'react'; - -/** - * @typedef NearContext - * @property {import('./wallets/near').Wallet} wallet Current wallet - * @property {string} signedAccountId The AccountId of the signed user - */ - -/** @type {import ('react').Context} */ -export const NearContext = createContext({ - wallet: undefined, - signedAccountId: '' -}); \ No newline at end of file diff --git a/frontends/03-frontend/src/global.d.ts b/frontends/03-frontend/src/global.d.ts new file mode 100644 index 00000000..14c22bb0 --- /dev/null +++ b/frontends/03-frontend/src/global.d.ts @@ -0,0 +1,6 @@ +declare module "*.png" { + const value: string; + export default value; + +} +declare module "*.module.css"; diff --git a/frontends/03-frontend/src/hooks/useNear.tsx b/frontends/03-frontend/src/hooks/useNear.tsx new file mode 100644 index 00000000..f200a815 --- /dev/null +++ b/frontends/03-frontend/src/hooks/useNear.tsx @@ -0,0 +1,119 @@ +import { useCallback, useEffect, useState } from "react"; +import { JsonRpcProvider } from "@near-js/providers"; +import { NearConnector, NearWallet } from "@hot-labs/near-connect"; + +interface ConnectedWallet { + wallet: NearWallet; + accounts: { accountId: string }[]; +} + +interface FunctionCallParams { + contractId: string; + method: string; + args?: Record; + gas?: string; + deposit?: string; +} + +interface ViewFunctionParams { + contractId: string; + method: string; + args?: Record; +} + +let connector: NearConnector | undefined; +const provider = new JsonRpcProvider({ url: "https://test.rpc.fastnear.com" }); + +if (typeof window !== "undefined") { + connector = new NearConnector({ network: "testnet" }); +} + +export function useNear() { + const [wallet, setWallet] = useState(undefined); + const [signedAccountId, setSignedAccountId] = useState(""); + const [loading, setLoading] = useState(true); + + const signIn = useCallback(async () => { + if (!connector) return; + const connectedWallet = await connector.connect(); + console.log("Connected wallet", connectedWallet); + }, []); + + const signOut = useCallback(async () => { + if (!wallet || !connector) return; + await connector.disconnect(wallet); + console.log("Disconnected wallet"); + setWallet(undefined); + setSignedAccountId(""); + }, [wallet]); + + useEffect(() => { + if (!connector) return; + + async function reload() { + try { + const { wallet, accounts } = (await connector!.getConnectedWallet()) as ConnectedWallet; + setWallet(wallet); + setSignedAccountId(accounts[0]?.accountId || ""); + } catch { + setWallet(undefined); + setSignedAccountId(""); + } finally { + setLoading(false); + } + } + + const onSignOut = () => { + setWallet(undefined); + setSignedAccountId(""); + }; + + const onSignIn = async (payload: { wallet: NearWallet }) => { + console.log("Signed in with payload", payload); + setWallet(payload.wallet); + const accountId = await payload.wallet.getAddress(); + setSignedAccountId(accountId); + }; + + connector.on("wallet:signOut", onSignOut); + connector.on("wallet:signIn", onSignIn); + + reload(); + + return () => { + connector?.off("wallet:signOut", onSignOut); + connector?.off("wallet:signIn", onSignIn); + }; + }, []); + + const viewFunction = useCallback(async ({ contractId, method, args = {} }: ViewFunctionParams) => { + return provider.callFunction(contractId, method, args); + }, []); + + const callFunction = useCallback( + async ({ contractId, method, args = {}, gas = "30000000000000", deposit = "0" }: FunctionCallParams) => { + if (!wallet) throw new Error("Wallet not connected"); + return wallet.signAndSendTransaction({ + receiverId: contractId, + actions: [ + { + type: "FunctionCall", + params: { methodName: method, args, gas, deposit }, + }, + ], + }); + }, + [wallet] + ); + + return { + signedAccountId, + wallet, + signIn, + signOut, + loading, + viewFunction, + callFunction, + provider, + }; +} \ No newline at end of file diff --git a/frontends/03-frontend/src/pages/_app.js b/frontends/03-frontend/src/pages/_app.js deleted file mode 100644 index 9d66ea65..00000000 --- a/frontends/03-frontend/src/pages/_app.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from 'react'; - -import '@/styles/globals.css'; -import { NearContext } from '@/context'; -import { Navigation } from '@/components/Navigation'; - -import { Wallet } from '@/wallets/near'; -import { NetworkId } from '@/config'; - -import { ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; - -const wallet = new Wallet({ networkId: NetworkId }); - -export default function MyApp({ Component, pageProps }) { - const [signedAccountId, setSignedAccountId] = useState(''); - - useEffect(() => { wallet.startUp(setSignedAccountId) }, []); - - return ( - - - - - - ); -} diff --git a/frontends/03-frontend/src/pages/_app.tsx b/frontends/03-frontend/src/pages/_app.tsx new file mode 100644 index 00000000..ab263c02 --- /dev/null +++ b/frontends/03-frontend/src/pages/_app.tsx @@ -0,0 +1,16 @@ +import '@/styles/globals.css'; +import 'react-toastify/dist/ReactToastify.css'; + +import { Navigation } from '@/components/Navigation'; +import { ToastContainer } from 'react-toastify'; +import type { AppProps } from 'next/app'; + +export default function MyApp({ Component, pageProps }: AppProps) { + return ( + <> + + + + + ); +} diff --git a/frontends/03-frontend/src/pages/api/getBidHistory.js b/frontends/03-frontend/src/pages/api/getBidHistory.js deleted file mode 100644 index a7298019..00000000 --- a/frontends/03-frontend/src/pages/api/getBidHistory.js +++ /dev/null @@ -1,44 +0,0 @@ -export default async function handler(req, res) { - try { - if (!process.env.API_KEY) { - return res.status(500).json({ error: "API key not provided" }); - } - // Get all bid transactions - const { contractId, ftId } = req.query; - const bidsRes = await fetch(`https://api-testnet.nearblocks.io/v1/account/${contractId}/txns?from=${ftId}&method=ft_on_transfer&page=1&per_page=25&order=desc`, { - headers: { - 'Accept': '*/*', - 'Authorization': `Bearer ${process.env.API_KEY}` - } - }); - - const bidsJson = await bidsRes.json(); - - const txns = bidsJson.txns; - let pastBids = []; - - // Loop through all bids and add valid bids to the pastBids array until 5 are found - for (let i = 0; i < txns.length; i++) { - const txn = txns[i]; - - if (txn.receipt_outcome.status) { - let args = txn.actions[0].args; - let parsedArgs = JSON.parse(args); - let amount = Number(parsedArgs.amount); - let account = parsedArgs.sender_id; - - if (pastBids.length < 5) { - pastBids.push([account, amount]); - } else { - break; - } - } - } - - // Respond with the past bids - return res.status(200).json({ pastBids }); - } catch (error) { - return res.status(500).json({ error: "Failed to fetch past bids" }); - } - } - \ No newline at end of file diff --git a/frontends/03-frontend/src/pages/api/getBidHistory.ts b/frontends/03-frontend/src/pages/api/getBidHistory.ts new file mode 100644 index 00000000..01bfc44e --- /dev/null +++ b/frontends/03-frontend/src/pages/api/getBidHistory.ts @@ -0,0 +1,81 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + + +interface Txn { + receipt_outcome: { + status: any; + }; + actions: { + args: string; + }[]; +} + +interface BidsResponse { + txns: Txn[]; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { + try { + if (!process.env.API_KEY) { + res.status(500).json({ error: "API key not provided" }); + return; + } + + const { contractId, ftId } = req.query; + + if (!contractId || !ftId || typeof contractId !== "string" || typeof ftId !== "string") { + res.status(400).json({ error: "Missing or invalid query parameters" }); + return; + } + + + const bidsRes = await fetch( + `https://api-testnet.nearblocks.io/v1/account/${contractId}/txns?from=${ftId}&method=ft_on_transfer&page=1&per_page=25&order=desc`, + { + headers: { + Accept: "*/*", + Authorization: `Bearer ${process.env.API_KEYs}`, + }, + } + ); + + if (!bidsRes.ok) { + throw new Error(`Failed to fetch transactions: ${bidsRes.statusText}`); + } + + const bidsJson: BidsResponse = await bidsRes.json(); + const txns = bidsJson.txns; + + const pastBids: [string, number][] = []; + + for (let i = 0; i < txns.length; i++) { + const txn = txns[i]; + + if (txn.receipt_outcome.status) { + try { + const args = txn.actions[0]?.args; + const parsedArgs = JSON.parse(args); + const amount = Number(parsedArgs.amount); + const account = parsedArgs.sender_id; + + if (pastBids.length < 5) { + pastBids.push([account, amount]); + } else { + break; + } + } catch { + // skip invalid JSON + continue; + } + } + } + + res.status(200).json({ pastBids }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to fetch past bids" }); + } +} diff --git a/frontends/03-frontend/src/pages/index.js b/frontends/03-frontend/src/pages/index.js deleted file mode 100644 index 345154a5..00000000 --- a/frontends/03-frontend/src/pages/index.js +++ /dev/null @@ -1,139 +0,0 @@ -import styles from '@/styles/app.module.css'; -import AuctionItem from '@/components/AuctionItem'; -import Timer from '@/components/Timer'; -import Bid from '@/components/Bid'; -import { useContext, useEffect, useState } from 'react'; -import SkeletonAuctionItem from '@/components/Skeletons/SkeletonAuctionItem'; -import SkeletonTimer from '@/components/Skeletons/SkeletonTimer'; -import SkeletonBid from '@/components/Skeletons/SkeletonBid'; -import { NearContext } from '@/context'; -import { AUCTION_CONTRACT } from '@/config'; -import LastBid from '@/components/LastBid'; - -export default function Home() { - const [auctionInfo, setAuctionInfo] = useState(null) - const [nftInfo, setNftInfo] = useState(null) - const [secondsRemaining, setSecondsRemaining] = useState(20) - const [ftContract, setFtContract] = useState("") - const [ftName, setFtName] = useState("") - const [ftImg, setFtImg] = useState("") - const [ftDecimals, setFtDecimals] = useState(0) - const [lastBidDisplay, setLastBidDisplay] = useState(0) - const [validAuction, setValidAuction] = useState("Invalid Auction") - const [pastBids, setPastBids] = useState(null) - - const { wallet } = useContext(NearContext); - - useEffect(() => { - const getInfo = async () => { - const data = await wallet.viewMethod({ - contractId: AUCTION_CONTRACT, - method: "get_auction_info", - }); - setAuctionInfo(data) - } - getInfo(); - - if (ftContract) { - fetchPastBids(); - } - - const intervalId = setInterval(() => { - getInfo(); - setSecondsRemaining(20); - }, 20000); - - const countdownIntervalId = setInterval(() => { - setSecondsRemaining(prev => (prev === 1 ? 20 : prev - 1)); - }, 1000); - - - return () => { - clearInterval(intervalId); - clearInterval(countdownIntervalId); - }; - }, []); - - useEffect(() => { - const getNftInfo = async () => { - const data = await wallet.viewMethod({ - contractId: auctionInfo.nft_contract, - method: "nft_token", - args: { token_id: auctionInfo.token_id } - }); - setNftInfo(data) - if (data.owner_id == AUCTION_CONTRACT) { - setValidAuction("Valid Auction") - } - } - if (auctionInfo) { - getNftInfo(); - } - - }, [auctionInfo]); - - useEffect(() => { - const getFtInfo = async () => { - const ftInfo = await wallet.viewMethod({ - contractId: auctionInfo.ft_contract, - method: "ft_metadata", - }); - setFtContract(auctionInfo.ft_contract) - setFtName(ftInfo.symbol) - setFtImg(ftInfo.icon) - setFtDecimals(ftInfo.decimals) - let bidAmount = auctionInfo.highest_bid.bid / Math.pow(10, ftInfo.decimals) - setLastBidDisplay(bidAmount) - - fetchPastBids(); - } - if (auctionInfo) { - getFtInfo(); - } - }, [auctionInfo]); - - const bid = async (amount) => { - let real_amount = amount * Math.pow(10, ftDecimals) - let response = await wallet.callMethod({ - contractId: auctionInfo.ft_contract, - method: "ft_transfer_call", - deposit: 1, - args: { "receiver_id": AUCTION_CONTRACT, "amount": String(real_amount), "msg": "" }, - gas:"300000000000000" - }) - return response - } - - const claim = async () => { - let response = await wallet.callMethod({ - contractId: AUCTION_CONTRACT, - method: "claim", - gas:"300000000000000" - }) - return response - } - - const fetchPastBids = async () => { - const response = await fetch(`/api/getBidHistory?contractId=${AUCTION_CONTRACT}&ftId=${ftContract}`); - const data = await response.json(); - if (data.error) { - setPastBids(data.error); - } else { - setPastBids(data.pastBids); - } - } - - return ( -
    -
    - {!auctionInfo ? : } - {!auctionInfo ? : } -
    -
    - {!auctionInfo ? : } - {!auctionInfo ? : } -
    -
    - - ); -} \ No newline at end of file diff --git a/frontends/03-frontend/src/pages/index.tsx b/frontends/03-frontend/src/pages/index.tsx new file mode 100644 index 00000000..79957dc0 --- /dev/null +++ b/frontends/03-frontend/src/pages/index.tsx @@ -0,0 +1,227 @@ +import { useEffect, useState } from 'react'; +import styles from '@/styles/app.module.css'; + +import AuctionItem from '@/components/AuctionItem'; +import Timer from '@/components/Timer'; +import Bid from '@/components/Bid'; +import LastBid from '@/components/LastBid'; + +import SkeletonAuctionItem from '@/components/Skeletons/SkeletonAuctionItem'; +import SkeletonTimer from '@/components/Skeletons/SkeletonTimer'; +import SkeletonBid from '@/components/Skeletons/SkeletonBid'; + +import { useNear } from '@/hooks/useNear'; +import { AUCTION_CONTRACT } from '@/config'; + +// Interfaces +interface BidInfo { + bidder: string; + bid: number; +} + +interface AuctionInfo { + highest_bid: { bidder: string; bid: number }; + auction_end_time: number | string; + claimed: boolean; + ft_contract: string; + nft_contract: string; + token_id: string; +} + +interface NftToken { + owner_id: string; + metadata: Record; +} + +interface FtMetadata { + symbol: string; + icon: string; + decimals: number; +} + +export default function Home() { + const [auctionInfo, setAuctionInfo] = useState(null); + const [nftInfo, setNftInfo] = useState(null); + const [secondsRemaining, setSecondsRemaining] = useState(20); + const [ftContract, setFtContract] = useState(''); + const [ftName, setFtName] = useState(''); + const [ftImg, setFtImg] = useState(''); + const [ftDecimals, setFtDecimals] = useState(0); + const [lastBidDisplay, setLastBidDisplay] = useState(0); + const [validAuction, setValidAuction] = useState('Invalid Auction'); + const [pastBids, setPastBids] = useState(null); + + const { wallet, viewFunction, callFunction } = useNear(); + + // Fetch auction info every 20s + useEffect(() => { + if (!wallet) return; + + const getInfo = async () => { + try { + const data = (await viewFunction({ + contractId: AUCTION_CONTRACT, + method: 'get_auction_info', + })) as AuctionInfo; + + setAuctionInfo(data); + } catch (err) { + console.error('Error fetching auction info:', err); + } + }; + + getInfo(); + const intervalId = setInterval(getInfo, 20000); + + const countdownIntervalId = setInterval(() => { + setSecondsRemaining((prev) => (prev === 1 ? 20 : prev - 1)); + }, 1000); + + return () => { + clearInterval(intervalId); + clearInterval(countdownIntervalId); + }; + }, [wallet]); + + // Fetch NFT info + useEffect(() => { + if (!auctionInfo) return; + + const getNftInfo = async () => { + try { + const data = (await viewFunction({ + contractId: auctionInfo.nft_contract, + method: 'nft_token', + args: { token_id: auctionInfo.token_id }, + })) as NftToken; + + setNftInfo(data); + + if (data.owner_id === AUCTION_CONTRACT) { + setValidAuction('Valid Auction'); + } + } catch (err) { + console.error('Error fetching NFT info:', err); + } + }; + + getNftInfo(); + }, [auctionInfo, viewFunction]); + + // Fetch FT info + useEffect(() => { + if (!auctionInfo) return; + + const getFtInfo = async () => { + try { + const ftInfo = (await viewFunction({ + contractId: auctionInfo.ft_contract, + method: 'ft_metadata', + })) as FtMetadata; + + setFtContract(auctionInfo.ft_contract); + setFtName(ftInfo.symbol); + setFtImg(ftInfo.icon); + setFtDecimals(ftInfo.decimals); + + const bidAmount = auctionInfo.highest_bid.bid / 10 ** ftInfo.decimals; + setLastBidDisplay(bidAmount); + + fetchPastBids(auctionInfo.ft_contract); + } catch (err) { + console.error('Error fetching FT info:', err); + } + }; + + getFtInfo(); + }, [auctionInfo, viewFunction]); + + // Fetch past bids and convert to [string, number][] + const fetchPastBids = async (ftId: string) => { + try { + const response = await fetch(`/api/getBidHistory?contractId=${AUCTION_CONTRACT}&ftId=${ftId}`); + const data = await response.json(); + + if (data.error) { + setPastBids(data.error); + } else { + const converted: [string, number][] = data.pastBids.map((b: BidInfo) => [b.bidder, b.bid]); + setPastBids(converted); + } + } catch (err) { + console.error('Error fetching past bids:', err); + } + }; + + // Place a bid + const bid = async (amount: number) => { + if (!auctionInfo) return; + const realAmount = amount * 10 ** ftDecimals; + + const response = await callFunction({ + contractId: auctionInfo.ft_contract, + method: 'ft_transfer_call', + deposit: '1', + args: { receiver_id: AUCTION_CONTRACT, amount: String(realAmount), msg: '' }, + gas: '300000000000000', + }); + + return response; + }; + + // Claim auction + const claim = async () => { + if (!auctionInfo) return; + + const response = await callFunction({ + contractId: AUCTION_CONTRACT, + method: 'claim', + gas: '300000000000000', + }); + + return response; + }; + + return ( +
    +
    + {!auctionInfo ? ( + + ) : ( + + )} + {!auctionInfo ? ( + + ) : ( + + )} +
    + +
    + {!auctionInfo ? ( + + ) : ( + + )} + {!auctionInfo ? ( + + ) : ( + + )} +
    +
    + ); +} diff --git a/frontends/03-frontend/src/wallets/near.js b/frontends/03-frontend/src/wallets/near.js deleted file mode 100644 index 5c700af5..00000000 --- a/frontends/03-frontend/src/wallets/near.js +++ /dev/null @@ -1,142 +0,0 @@ -// near api js -import { providers } from 'near-api-js'; - -// wallet selector -import { distinctUntilChanged, map } from 'rxjs'; -import '@near-wallet-selector/modal-ui/styles.css'; -import { setupModal } from '@near-wallet-selector/modal-ui'; -import { setupWalletSelector } from '@near-wallet-selector/core'; -import { setupHereWallet } from '@near-wallet-selector/here-wallet'; -import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; - -const THIRTY_TGAS = '30000000000000'; -const NO_DEPOSIT = '0'; - -export class Wallet { - /** - * @constructor - * @param {Object} options - the options for the wallet - * @param {string} options.networkId - the network id to connect to - * @param {string} options.createAccessKeyFor - the contract to create an access key for - * @example - * const wallet = new Wallet({ networkId: 'testnet', createAccessKeyFor: 'contractId' }); - * wallet.startUp((signedAccountId) => console.log(signedAccountId)); - */ - constructor({ networkId = 'testnet', createAccessKeyFor = undefined }) { - this.createAccessKeyFor = createAccessKeyFor; - this.networkId = networkId; - } - - /** - * To be called when the website loads - * @param {Function} accountChangeHook - a function that is called when the user signs in or out# - * @returns {Promise} - the accountId of the signed-in user - */ - startUp = async (accountChangeHook) => { - this.selector = setupWalletSelector({ - network: this.networkId, - modules: [setupMyNearWallet(), setupHereWallet()] - }); - - const walletSelector = await this.selector; - const isSignedIn = walletSelector.isSignedIn(); - const accountId = isSignedIn ? walletSelector.store.getState().accounts[0].accountId : ''; - - walletSelector.store.observable - .pipe( - map(state => state.accounts), - distinctUntilChanged() - ) - .subscribe(accounts => { - const signedAccount = accounts.find((account) => account.active)?.accountId; - accountChangeHook(signedAccount); - }); - - return accountId; - }; - - /** - * Displays a modal to login the user - */ - signIn = async () => { - const modal = setupModal(await this.selector, { contractId: this.createAccessKeyFor }); - modal.show(); - }; - - /** - * Logout the user - */ - signOut = async () => { - const selectedWallet = await (await this.selector).wallet(); - selectedWallet.signOut(); - }; - - /** - * Makes a read-only call to a contract - * @param {Object} options - the options for the call - * @param {string} options.contractId - the contract's account id - * @param {string} options.method - the method to call - * @param {Object} options.args - the arguments to pass to the method - * @returns {Promise} - the result of the method call - */ - viewMethod = async ({ contractId, method, args = {} }) => { - const url = `https://rpc.${this.networkId}.near.org`; - const provider = new providers.JsonRpcProvider({ url }); - - let res = await provider.query({ - request_type: 'call_function', - account_id: contractId, - method_name: method, - args_base64: Buffer.from(JSON.stringify(args)).toString('base64'), - finality: 'optimistic', - }); - return JSON.parse(Buffer.from(res.result).toString()); - }; - - - /** - * Makes a call to a contract - * @param {Object} options - the options for the call - * @param {string} options.contractId - the contract's account id - * @param {string} options.method - the method to call - * @param {Object} options.args - the arguments to pass to the method - * @param {string} options.gas - the amount of gas to use - * @param {string} options.deposit - the amount of yoctoNEAR to deposit - * @returns {Promise} - the resulting transaction - */ - callMethod = async ({ contractId, method, args = {}, gas = THIRTY_TGAS, deposit = NO_DEPOSIT }) => { - // Sign a transaction with the "FunctionCall" action - const selectedWallet = await (await this.selector).wallet(); - const outcome = await selectedWallet.signAndSendTransaction({ - receiverId: contractId, - actions: [ - { - type: 'FunctionCall', - params: { - methodName: method, - args, - gas, - deposit, - }, - }, - ], - }); - - return providers.getTransactionLastResult(outcome); - }; - - /** - * Retrieves transaction result from the network - * @param {string} txhash - the transaction hash - * @returns {Promise} - the result of the transaction - */ - getTransactionResult = async (txhash) => { - const walletSelector = await this.selector; - const { network } = walletSelector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - // Retrieve transaction result from the network - const transaction = await provider.txStatus(txhash, 'unnused'); - return providers.getTransactionLastResult(transaction); - }; -} \ No newline at end of file diff --git a/frontends/03-frontend/tsconfig.json b/frontends/03-frontend/tsconfig.json new file mode 100644 index 00000000..e68f335b --- /dev/null +++ b/frontends/03-frontend/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + "jsx": "preserve", + "strict": true, + "moduleResolution": "node", + "skipLibCheck": true, + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "global.d.ts" + ], + "exclude": [ + "node_modules" + ] +} From 89178fda92e1239b4096e902c66f1b2caf0695a3 Mon Sep 17 00:00:00 2001 From: Amira Nasri Date: Thu, 16 Oct 2025 11:50:47 +0100 Subject: [PATCH 2/2] fix config.ts --- frontends/01-frontend/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontends/01-frontend/src/config.ts b/frontends/01-frontend/src/config.ts index 0650301d..c25a75da 100644 --- a/frontends/01-frontend/src/config.ts +++ b/frontends/01-frontend/src/config.ts @@ -1,5 +1,5 @@ const contractPerNetwork = { - testnet: '889d3efcab5e7bd444ca6196557a059acd41e44fb3b7155ad5d22385198ea162', // <- your deployed contract account + testnet: 'auction-example.testnet', // <- your deployed contract account }; export const NetworkId = 'testnet';