diff --git a/src/App.tsx b/src/App.tsx
index 72e40f2..425b039 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,46 +12,49 @@ import Footer from './components/Footer/Footer';
import Topbar from './components/Navbar/Topbar';
import { Bounce, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
+import { WebSocketProvider } from './providers/WebSocketProvider';
function App() {
return (
- <>
- {/* Navbar */}
-
+
+ <>
+ {/* Navbar */}
+
- {/* Route configuration */}
-
-
- } />
- } />
- } />
- } />
- }
- />
- {/* TODO: collection/wallet routes */}
-
-
-
+ {/* Route configuration */}
+
+
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ {/* TODO: collection/wallet routes */}
+
+
+
-
- >
+
+ >
+
diff --git a/src/Components/CreateAuction/CreateAuctionForm.tsx b/src/Components/CreateAuction/CreateAuctionForm.tsx
index f1ea970..f00728c 100644
--- a/src/Components/CreateAuction/CreateAuctionForm.tsx
+++ b/src/Components/CreateAuction/CreateAuctionForm.tsx
@@ -104,7 +104,7 @@ const CreateAuctionForm = ({ className }: CreateAuctionFormProps) => {
auctionLot: [auctionLot],
biddingStart:
Number(auctionFormValidated.data.biddingStart) < Date.now()
- ? Date.now().toString()
+ ? (Date.now() + 30000).toString()
: auctionFormValidated.data.biddingStart,
},
additionalAuctionLotOrefs:
diff --git a/src/components/AuctionCard/AuctionCard.tsx b/src/components/AuctionCard/AuctionCard.tsx
index 0b93c2f..d8754db 100644
--- a/src/components/AuctionCard/AuctionCard.tsx
+++ b/src/components/AuctionCard/AuctionCard.tsx
@@ -32,10 +32,9 @@ function AuctionCard({ auctionInfo }: AuctionCardProps) {
const { name: walletName } = useWallet();
const { data: standingBidState, isLoading: isLoadingStandingBidState } =
useStandingBidState(walletName as WalletApp, auctionInfo);
- const standingBidStatePrice = standingBidState?.value || '';
+
let formattedPrice = '';
- // Unexpected response from queryStandingBidState right now, this will be changed
- if (contractOutputResultSchema.safeParse(standingBidStatePrice).success) {
+ if (contractOutputResultSchema.safeParse(standingBidState).success) {
const standingBidValue = standingBidState?.value as StandingBidState;
formattedPrice = formatLovelaceToAda(standingBidValue?.price);
}
diff --git a/src/components/AuctionDetail/AuctionDetail.tsx b/src/components/AuctionDetail/AuctionDetail.tsx
new file mode 100644
index 0000000..0c6cc62
--- /dev/null
+++ b/src/components/AuctionDetail/AuctionDetail.tsx
@@ -0,0 +1,91 @@
+import { useActiveAuctions } from 'src/hooks/api/auctions';
+import { getUrlParams } from 'src/utils/getUrlParams';
+import IpfsImage from '../IpfsImage/IpfsImage';
+import { useWallet } from '@meshsdk/react';
+import { WalletApp } from 'hydra-auction-offchain';
+import { getIsSeller } from 'src/utils/auctionState';
+import { useWalletAddress } from 'src/hooks/api/user';
+import AuctionDetailSeller from './AuctionDetailSeller';
+import AuctionDetailBidder from './AuctionDetailBidder';
+import { getAuctionAssetUnit } from 'src/utils/auction';
+import AuctionSubDetail from './AuctionSubDetail';
+import { useAssetMetadata } from 'src/hooks/api/assets';
+import { useCleanupAuction } from 'src/hooks/api/cleanup';
+import { Button } from '../shadcn/Button';
+import { removeLocalStorageItem } from 'src/utils/localStorage';
+
+const MOCK_NFT_TITLE = 'My NFT';
+
+// TODO: Claims and cleanup components are always showing, we will implement the conditions when the APIs are ready
+export default function AuctionDetail() {
+ // TODO: Display some badge if the user is already a bidder, and for the state of the auction
+
+ // Use url params to get the auctionId
+ const urlParams = getUrlParams();
+ const auctionId = urlParams.get('auctionId') || '';
+ const { name: walletName, wallet, connected } = useWallet();
+ const walletApp: WalletApp = walletName as WalletApp;
+ const { data: auctions, isLoading, isError } = useActiveAuctions(walletApp);
+ const { data: walletAddress } = useWalletAddress(wallet, connected);
+
+ // With auctionId we find the auction details from the queryAuctions cache
+ const auctionInfo = auctions?.find(
+ (auction) => auction.auctionId === auctionId
+ );
+ const assetUnit = getAuctionAssetUnit(auctionInfo);
+
+ const cleanupAuction = useCleanupAuction(walletApp);
+
+ const { data: assetMetadata } = useAssetMetadata(assetUnit);
+
+ if (isLoading) return
Loading...
;
+ if (isError) return Error getting auction...
;
+ if (!auctionInfo) return Error finding auction...
;
+ if (!walletAddress || !walletName) return Wallet not valid
;
+
+ // Identifying if we are the seller or a bidder of this auction
+ const isSeller = getIsSeller(walletAddress, auctionInfo);
+ console.log({ auctionInfo });
+
+ console.log({ isSeller });
+ const handleCleanupAuction = () => {
+ cleanupAuction.mutate(auctionInfo);
+ };
+
+ return (
+
+
+
+
+
+
+ {assetMetadata?.name ? assetMetadata.name : MOCK_NFT_TITLE}
+
+ {/* NOTE: If you want to make testing easier, just show both bidder and seller auction details without any conditions */}
+ {isSeller ? (
+
+ ) : (
+
+ )}
+
+
+ Cleanup
+
+
+
+
+
+ );
+}
diff --git a/src/components/AuctionDetail/BiddingView.tsx b/src/components/AuctionDetail/BiddingView.tsx
index 11dc762..d068632 100644
--- a/src/components/AuctionDetail/BiddingView.tsx
+++ b/src/components/AuctionDetail/BiddingView.tsx
@@ -27,9 +27,8 @@ export default function BiddingView({
auctionInfo
);
- const standingBidStatePrice = standingBidState?.value || '';
let formattedPrice = '';
- if (contractOutputResultSchema.safeParse(standingBidStatePrice)) {
+ if (contractOutputResultSchema.safeParse(standingBidState).success) {
const standingBidValue = standingBidState?.value as StandingBidState;
formattedPrice = formatLovelaceToAda(standingBidValue?.price);
}
diff --git a/src/components/AuctionList/AuctionList.tsx b/src/components/AuctionList/AuctionList.tsx
new file mode 100644
index 0000000..931809b
--- /dev/null
+++ b/src/components/AuctionList/AuctionList.tsx
@@ -0,0 +1,143 @@
+import { useQueryClient } from '@tanstack/react-query';
+import { useActiveAuctions } from '../../hooks/api/auctions';
+import AuctionCard from '../AuctionCard/AuctionCard';
+import { useWallet } from '@meshsdk/react';
+import { AuctionInfo, WalletApp } from 'hydra-auction-offchain';
+
+import { useEffect, useState } from 'react';
+import {
+ METADATA_QUERY_KEY,
+ getAndStoreAssetMetadata,
+} from 'src/hooks/api/assets';
+import { getLocalStorageItem } from 'src/utils/localStorage';
+import { getAuctionAssetUnit } from 'src/utils/auction';
+import { useWalletAddress } from 'src/hooks/api/user';
+import {
+ AuctionListSortState,
+ auctionListFilterOptions,
+} from 'src/utils/auctionState';
+import { DropDown } from '../DropDown/DropDown';
+
+export default function AuctionList() {
+ const { name: walletName, wallet, connected } = useWallet();
+ const { data: walletAddress } = useWalletAddress(wallet, connected);
+ const walletApp: WalletApp = walletName as WalletApp;
+ const { data: auctions } = useActiveAuctions(walletApp);
+
+ const queryClient = useQueryClient();
+
+ const localMetadata = getLocalStorageItem('metadata');
+
+ console.log({ walletApp });
+ console.log({ auctions });
+ console.log({ localMetadata });
+
+ const [auctionsWithImage, setAuctionsWithImage] = useState<
+ AuctionInfo[] | null
+ >([]);
+
+ const [filteredAuctions, setFilteredAuctions] = useState<
+ AuctionInfo[] | undefined
+ >([]);
+
+ const [activeFilter, setActiveFilter] = useState(
+ AuctionListSortState.ALL
+ );
+
+ const fetchAndFilterAuctionsByImage = async (auctions: AuctionInfo[]) => {
+ const filteredAuctions = await Promise.all(
+ auctions.map(async (auction) => {
+ const assetUnit = getAuctionAssetUnit(auction);
+
+ await queryClient.prefetchQuery({
+ queryKey: [METADATA_QUERY_KEY, assetUnit],
+ queryFn: async () => await getAndStoreAssetMetadata(assetUnit),
+ });
+
+ const nftHasImage: any = queryClient.getQueryData([
+ METADATA_QUERY_KEY,
+ assetUnit,
+ ]);
+
+ return nftHasImage?.image !== undefined ? auction : null;
+ })
+ );
+
+ // Auto sorted to most recently started auctions
+ return filteredAuctions
+ .filter(Boolean)
+ ?.sort(
+ (a: AuctionInfo | null, b: AuctionInfo | null) =>
+ Number(b?.auctionTerms.biddingStart) -
+ Number(a?.auctionTerms.biddingStart)
+ );
+ };
+
+ useEffect(() => {
+ async function getAuctionsWithImage(auctions: AuctionInfo[]) {
+ const filteredAuctionsByImage = await fetchAndFilterAuctionsByImage(
+ auctions
+ );
+ setAuctionsWithImage(filteredAuctionsByImage as AuctionInfo[]);
+ }
+ if (auctions) {
+ getAuctionsWithImage(auctions);
+ }
+ }, [auctions]);
+
+ useEffect(() => {
+ if (auctionsWithImage) {
+ switch (activeFilter) {
+ case AuctionListSortState.ALL:
+ setFilteredAuctions(auctionsWithImage);
+ break;
+ case AuctionListSortState.SELLER:
+ setFilteredAuctions(
+ auctionsWithImage?.filter(
+ (auction) => auction.auctionTerms.sellerAddress === walletAddress
+ )
+ );
+ break;
+ case AuctionListSortState.NOT_SELLER:
+ setFilteredAuctions(
+ auctionsWithImage?.filter(
+ (auction) => auction.auctionTerms.sellerAddress !== walletAddress
+ )
+ );
+ break;
+ default:
+ setFilteredAuctions(auctionsWithImage);
+ }
+ }
+ }, [activeFilter, auctions, auctionsWithImage]);
+
+ return (
+ <>
+
+
Query Auctions
+
+
+
+
+
{filteredAuctions?.length || 0} Auctions
+
{
+ setActiveFilter(auctionListFilterOptions[index].accessor);
+ }}
+ options={auctionListFilterOptions}
+ title="Filter Auctions"
+ />
+
+
+
+ {filteredAuctions?.map((auctionInfo, index) => (
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/components/CreateAuction/AuctionLot.tsx b/src/components/CreateAuction/AuctionLot.tsx
new file mode 100644
index 0000000..ba5c252
--- /dev/null
+++ b/src/components/CreateAuction/AuctionLot.tsx
@@ -0,0 +1,58 @@
+import { ValueEntry } from 'hydra-auction-offchain';
+import { useEffect, useState } from 'react';
+import { StringInput } from '../Inputs/StringInput';
+import { NumberInput } from '../Inputs/NumberInput';
+
+export const MOCK_AUCTION_LOT: ValueEntry = {
+ cs: 'c0f8644a01a6bf5db02f4afe30d604975e63dd274f1098a1738e561d',
+ tn: '4d6f6e614c697361',
+ quantity: '1',
+};
+
+type AuctionLotProps = {
+ onChangeAuctionLot?: (auctionLot: ValueEntry) => void;
+};
+
+export const AuctionLot = ({ onChangeAuctionLot }: AuctionLotProps) => {
+ const [auctionLot, setAuctionLot] = useState(MOCK_AUCTION_LOT);
+
+ useEffect(() => {
+ onChangeAuctionLot && onChangeAuctionLot(auctionLot);
+ }, [auctionLot]);
+
+ const handleAuctionLotInputChange = (inputId: string, value: any) => {
+ setAuctionLot({
+ ...auctionLot,
+ [inputId]: inputId === 'quantity' ? value.toString() : value,
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/CreateAuction/AuctionLotList.tsx b/src/components/CreateAuction/AuctionLotList.tsx
new file mode 100644
index 0000000..d3f6cea
--- /dev/null
+++ b/src/components/CreateAuction/AuctionLotList.tsx
@@ -0,0 +1,69 @@
+import { ValueEntry } from 'hydra-auction-offchain';
+import { AuctionLot, MOCK_AUCTION_LOT } from './AuctionLot';
+import { useEffect, useState } from 'react';
+
+type AuctionLotListProps = {
+ onChangeAuctionLotList?: (auctionLots: ValueEntry[]) => void;
+};
+
+export const AuctionLotList = ({
+ onChangeAuctionLotList,
+}: AuctionLotListProps) => {
+ const [auctionLots, setAuctionLots] = useState([
+ MOCK_AUCTION_LOT,
+ ]);
+
+ const handleAddAuctionLot = (e: React.MouseEvent) => {
+ e.preventDefault();
+ setAuctionLots([...auctionLots, MOCK_AUCTION_LOT]);
+ };
+
+ const handleRemoveAuctionLot = (
+ e: React.MouseEvent,
+ index: number
+ ) => {
+ e.preventDefault();
+ setAuctionLots(auctionLots.filter((_, lotIndex) => lotIndex !== index));
+ };
+
+ const handleChangeAuctionLot = (index: number, auctionLot: ValueEntry) => {
+ setAuctionLots(
+ auctionLots.map((lot, lotIndex) =>
+ lotIndex === index ? auctionLot : lot
+ )
+ );
+ };
+
+ useEffect(() => {
+ onChangeAuctionLotList && onChangeAuctionLotList(auctionLots);
+ }, [auctionLots, onChangeAuctionLotList]);
+
+ return (
+
+ {auctionLots.map((_, index) => (
+
+
+
Auction Lot {index + 1}
+
handleRemoveAuctionLot(e, index)}
+ >
+ X
+
+
+
+ handleChangeAuctionLot(index, auctionLot)
+ }
+ />
+
+ ))}
+
+ +
+
+
+ );
+};
diff --git a/src/components/CreateAuction/CreateAuction.tsx b/src/components/CreateAuction/CreateAuction.tsx
new file mode 100644
index 0000000..3ace042
--- /dev/null
+++ b/src/components/CreateAuction/CreateAuction.tsx
@@ -0,0 +1,33 @@
+import { getUrlParams } from 'src/utils/getUrlParams';
+import CreateAuctionTabs from '../CreateAuctionTabs/CreateAuctionTabs';
+import { useAssets } from '@meshsdk/react';
+import CurrentListing from './CurrentListing';
+
+export default function CreateAuction() {
+ // Use url params to get the assetUnit
+ const urlParams = getUrlParams();
+ const assetUnit = urlParams.get('assetUnit');
+ const assetName = urlParams.get('assetName');
+
+ // For now we are going to use the assetUnit to get the asset from the queryAssets cache inside the form.
+ // This way we dont need to prop drill the asset across the tabs
+ const assets = useAssets();
+ const assetToList = assets?.find((asset) => asset.unit === assetUnit);
+
+ if (!assetUnit || !assetName || !assetToList)
+ return Error finding asset...
;
+ return (
+ <>
+ List an NFT
+
+
+ >
+ );
+}
diff --git a/src/components/CreateAuction/CreateAuctionForm.tsx b/src/components/CreateAuction/CreateAuctionForm.tsx
new file mode 100644
index 0000000..f00728c
--- /dev/null
+++ b/src/components/CreateAuction/CreateAuctionForm.tsx
@@ -0,0 +1,220 @@
+import React, { useEffect, useState } from 'react';
+import { NumberInput } from '../Inputs/NumberInput';
+import { DateTimeInput } from '../Inputs/DateInput';
+import { DropDown } from '../DropDown/DropDown';
+import { AuctionTermsInput, WalletApp } from 'hydra-auction-offchain';
+import { generateMockAnnounceAuctionParams } from 'src/mocks/announceAuction.mock';
+import { getUrlParams } from 'src/utils/getUrlParams';
+import { useExtendedAssets } from 'src/hooks/api/assets';
+import { useAnnounceAuction } from 'src/hooks/api/announceAuction';
+import { useWallet } from '@meshsdk/react';
+import { useDelegates } from 'src/hooks/api/delegates';
+import { auctionTermsInputSchema } from 'src/schemas/auctionTermsSchema';
+import { removePolicyIdFromAssetUnit } from 'src/utils/formatting';
+import { toast } from 'react-toastify';
+import { ONE_DAY_MS, formatDate } from 'src/utils/date';
+
+type CreateAuctionFormProps = {
+ className?: string;
+};
+const CreateAuctionForm = ({ className }: CreateAuctionFormProps) => {
+ const { data: delegateGroup } = useDelegates();
+ const urlParams = getUrlParams();
+ const assetUnit = urlParams.get('assetUnit');
+
+ const mockAnnounceAuctionParams = generateMockAnnounceAuctionParams();
+ const [auctionFormData, setAuctionFormData] = useState(
+ mockAnnounceAuctionParams.auctionTerms
+ );
+
+ const { name: walletApp } = useWallet();
+ const { data: assets, isError } = useExtendedAssets(walletApp as WalletApp);
+ const { mutate: announceAuction, isPending: isAnnounceAuctionPending } =
+ useAnnounceAuction(walletApp);
+
+ // Auto set the cleanup to two days after purchase deadline every time purchase deadline is set
+ useEffect(() => {
+ if (auctionFormData.purchaseDeadline) {
+ handleAuctionInputChange(
+ 'cleanup',
+ String(Number(auctionFormData.purchaseDeadline) + ONE_DAY_MS * 2)
+ );
+ }
+ }, [auctionFormData.purchaseDeadline]);
+
+ if (isError) {
+ return null;
+ }
+ const assetToList = assets?.find((asset) => asset.unit === assetUnit);
+ if (!assetToList) {
+ console.log('No asset found for this asset unit');
+ toast.error('No asset found for this asset unit');
+
+ return null;
+ }
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const auctionFormValidated = auctionTermsInputSchema
+ .refine((data) => data.biddingEnd > data.biddingStart, {
+ message: 'Bidding end must be after bidding start',
+ })
+ .refine((data) => data.purchaseDeadline > data.biddingEnd, {
+ message: 'Purchase deadline must be after bidding end',
+ })
+ .refine((data) => data.cleanup > data.purchaseDeadline, {
+ message: 'Cleanup must be after purchase deadline',
+ })
+ .refine((data) => Number(data.minBidIncrement) > 0, {
+ message: 'New bids must be larger than the standing bid',
+ })
+ .refine((data) => Number(data.auctionFeePerDelegate) > 2000000, {
+ message:
+ 'The auction fee for each delegate must contain the min 2 ADA for the utxos that will be sent to the delegates during fee distribution',
+ })
+ .refine(
+ (data) =>
+ Number(data.startingBid) >
+ Number(data.auctionFeePerDelegate) * data.delegates.length,
+ {
+ message: 'Starting bid must be greater than the total auction fees',
+ }
+ )
+ .refine((data) => data.delegates.length > 0, {
+ message: 'Must have at least one delegate',
+ })
+ .safeParse(auctionFormData);
+
+ if (!auctionFormValidated.success) {
+ toast.error(
+ `Error creating auction: ${auctionFormValidated.error.issues[0].message}`
+ );
+ console.log(auctionFormValidated.error);
+ } else {
+ // Using nft from the url to announce the auction
+ const auctionLot = {
+ cs: assetToList.policyId,
+ tn: removePolicyIdFromAssetUnit(assetToList.unit),
+ quantity: assetToList.quantity,
+ };
+
+ const params = {
+ auctionTerms: {
+ ...auctionFormValidated.data,
+ auctionLot: [auctionLot],
+ biddingStart:
+ Number(auctionFormValidated.data.biddingStart) < Date.now()
+ ? (Date.now() + 30000).toString()
+ : auctionFormValidated.data.biddingStart,
+ },
+ additionalAuctionLotOrefs:
+ mockAnnounceAuctionParams.additionalAuctionLotOrefs, // Empty array for now but can be implemented
+ };
+ console.log({ announceAuctionParams: params });
+ announceAuction(params);
+ }
+ };
+
+ const handleAuctionInputChange = (inputId: string, value: any) => {
+ console.log({ inputId });
+ setAuctionFormData({
+ ...auctionFormData,
+ [inputId]: value,
+ });
+ };
+
+ // To allow for multiple auction lots once it is supported
+ // const handleAuctionLotsChange = (auctionLots: ValueEntry[]) => {
+ // auctionFormData.current = {
+ // ...auctionFormData.current,
+ // auctionTerms: {
+ // ...auctionFormData.current.auctionTerms,
+ // auctionLot: auctionLots,
+ // },
+ // };
+ // };
+
+ return (
+
+ );
+};
+export default CreateAuctionForm;
diff --git a/src/components/CreateAuction/CreateAuctionList.tsx b/src/components/CreateAuction/CreateAuctionList.tsx
new file mode 100644
index 0000000..3a52dbc
--- /dev/null
+++ b/src/components/CreateAuction/CreateAuctionList.tsx
@@ -0,0 +1,13 @@
+import WalletNfts from '../WalletNfts/WalletNfts';
+
+export default function CreateAuctionList() {
+ return (
+ <>
+
+
My Nfts
+
+
+
+ >
+ );
+}
diff --git a/src/components/CreateAuction/CurrentListing.tsx b/src/components/CreateAuction/CurrentListing.tsx
new file mode 100644
index 0000000..f159ce1
--- /dev/null
+++ b/src/components/CreateAuction/CurrentListing.tsx
@@ -0,0 +1,19 @@
+import IpfsImage from '../IpfsImage/IpfsImage';
+
+type CurrentListingProps = {
+ name: string;
+ assetUnit: string;
+};
+
+const CurrentListing = ({ name, assetUnit }: CurrentListingProps) => {
+ return (
+
+
+ Current Listing:
+
+
+
{name}
+
+ );
+};
+export default CurrentListing;
diff --git a/src/components/CreateAuctionTabs/CreateAuctionTabs.tsx b/src/components/CreateAuctionTabs/CreateAuctionTabs.tsx
new file mode 100644
index 0000000..58808e6
--- /dev/null
+++ b/src/components/CreateAuctionTabs/CreateAuctionTabs.tsx
@@ -0,0 +1,82 @@
+import { Asset } from '@meshsdk/core';
+import { useState } from 'react';
+import NavPiece from './NavPiece';
+import SelectTab from './SelectTab';
+import CreateAuctionForm from '../CreateAuction/CreateAuctionForm';
+import { Button } from '../shadcn/Button';
+
+type CreateAuctionTabsProps = {
+ assetToList: Asset | undefined;
+};
+
+const ANNOUNCE_AUCTION_TABS = [
+ {
+ label: 'Select NFT',
+ key: 'select',
+ },
+ {
+ label: 'Auction Details',
+ key: 'details',
+ },
+];
+
+type CreateAuctionNavProps = {
+ activeTab: number;
+ setActiveTab: (tab: number) => void;
+};
+const CreateAuctionNav = ({
+ activeTab,
+ setActiveTab,
+}: CreateAuctionNavProps) => {
+ return (
+
+ {ANNOUNCE_AUCTION_TABS.map((tab, index) => {
+ return (
+
+ setActiveTab(
+ ANNOUNCE_AUCTION_TABS.findIndex(
+ (auctionTab) => auctionTab.key === tab.key
+ )
+ )
+ }
+ isLastIndex={index === ANNOUNCE_AUCTION_TABS.length - 1}
+ />
+ );
+ })}
+
+ );
+};
+
+const TabSwitch = ({ tab }: { tab: string }) => {
+ if (tab === 'select') {
+ return ;
+ }
+ return ;
+};
+
+export default function AnnounceTabs({ assetToList }: CreateAuctionTabsProps) {
+ const [activeTab, setActiveTab] = useState(0);
+
+ const handleNext = () => {
+ activeTab < ANNOUNCE_AUCTION_TABS.length - 1 && setActiveTab(activeTab + 1);
+ };
+ return (
+
+
+
+
+
+ {activeTab !== ANNOUNCE_AUCTION_TABS.length - 1 && (
+
+ Next
+
+ )}
+ {/* */}
+
+
+ );
+}
diff --git a/src/components/CreateAuctionTabs/NavPiece.tsx b/src/components/CreateAuctionTabs/NavPiece.tsx
new file mode 100644
index 0000000..a2966a5
--- /dev/null
+++ b/src/components/CreateAuctionTabs/NavPiece.tsx
@@ -0,0 +1,39 @@
+import { ChevronRightIcon } from '@heroicons/react/24/solid';
+import clsx from 'clsx';
+import React from 'react';
+
+type NavPieceProps = {
+ label: string;
+ isActive: boolean;
+ onClick: () => void;
+ isLastIndex: boolean;
+};
+const NavPiece = ({
+ label,
+ isActive,
+ onClick,
+ isLastIndex = false,
+}: NavPieceProps) => {
+ return (
+
+
+ {label}
+
+
+
+
+
+ );
+};
+export default NavPiece;
diff --git a/src/components/CreateAuctionTabs/SelectTab.tsx b/src/components/CreateAuctionTabs/SelectTab.tsx
new file mode 100644
index 0000000..7a0d3a4
--- /dev/null
+++ b/src/components/CreateAuctionTabs/SelectTab.tsx
@@ -0,0 +1,52 @@
+import { useExtendedAssets } from 'src/hooks/api/assets';
+import { getUrlParams } from 'src/utils/getUrlParams';
+import { DropDown } from '../DropDown/DropDown';
+import { AssetExtended } from '@meshsdk/core';
+import { useWallet } from '@meshsdk/react';
+import { WalletApp } from 'hydra-auction-offchain';
+import { useMemo } from 'react';
+import { removeSpecialCharsAssetName } from 'src/utils/formatting';
+
+const SelectTab = () => {
+ const { name: walletApp } = useWallet();
+ const { data: assets, isError } = useExtendedAssets(walletApp as WalletApp);
+
+ const urlParams = getUrlParams();
+ const assetUnitToList = urlParams.get('assetUnit');
+ const assetIndex = useMemo(
+ () => assets?.findIndex((asset) => asset.unit === assetUnitToList),
+ [assetUnitToList, assets]
+ );
+
+ if (isError) {
+ return null;
+ }
+
+ const onChange = (index: number) => {
+ // redirect to the current page, but pass the new assetUnit
+ const asset: AssetExtended | undefined = assets?.[index];
+ window.location.href = `/create-auction?assetUnit=${asset?.unit}&assetName=${asset?.assetName}`;
+ };
+
+ return (
+
+
List most recently minted
+
Or choose
+
+
NFT
+
{
+ return {
+ label: removeSpecialCharsAssetName(asset.assetName),
+ accessor: asset.unit || '',
+ };
+ })}
+ title={'Select NFT'}
+ defaultIndex={assetIndex || 0}
+ onChange={onChange}
+ className="mt-4"
+ />
+
+ );
+};
+export default SelectTab;
diff --git a/src/components/CustomLink/CustomLink.tsx b/src/components/CustomLink/CustomLink.tsx
new file mode 100644
index 0000000..1e24090
--- /dev/null
+++ b/src/components/CustomLink/CustomLink.tsx
@@ -0,0 +1,14 @@
+import { Link } from 'react-router-dom';
+
+type CustomLinkProps = {
+ href: string;
+ label: string;
+};
+
+export default function CustomLink({ href, label }: CustomLinkProps) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/src/components/CustomWallet/CustomWallet.tsx b/src/components/CustomWallet/CustomWallet.tsx
new file mode 100644
index 0000000..ea0bad0
--- /dev/null
+++ b/src/components/CustomWallet/CustomWallet.tsx
@@ -0,0 +1,41 @@
+import { CardanoWallet, useWallet } from '@meshsdk/react';
+import { useEffect } from 'react';
+import { setLocalStorageItem } from 'src/utils/localStorage';
+
+type CustomWalletProps = {
+ isDark: boolean;
+};
+
+export default function CustomWallet(
+ { isDark }: CustomWalletProps = { isDark: false }
+) {
+ const { connect, name: walletName, connected, wallet } = useWallet();
+
+ useEffect(() => {
+ const lastConnectedWallet = localStorage.getItem('lastConnectedWallet');
+
+ async function connectWallet() {
+ if (lastConnectedWallet && !connected) {
+ try {
+ await connect(JSON.parse(lastConnectedWallet).name);
+ } catch (err) {
+ console.log(err);
+ }
+ }
+ }
+ connectWallet();
+ }, []);
+
+ const storeConnection = () => {
+ setLocalStorageItem('lastConnectedWallet', {
+ name: walletName,
+ timestamp: Date.now(),
+ });
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/DropDown/DropDown.tsx b/src/components/DropDown/DropDown.tsx
new file mode 100644
index 0000000..7bc8e53
--- /dev/null
+++ b/src/components/DropDown/DropDown.tsx
@@ -0,0 +1,78 @@
+import { useEffect, useMemo, useState } from 'react';
+import { OutsideAlerter } from '../OutsideAlerter/OutsideAlerter';
+import clsx from 'clsx';
+
+type DropDownItem = {
+ label: string;
+ accessor: string;
+};
+
+type DropDownProps = {
+ options?: DropDownItem[];
+ defaultIndex?: number;
+ title: string;
+ onChange?: (index: number) => void;
+ className?: string;
+};
+
+export const DropDown = ({
+ options,
+ title,
+ defaultIndex,
+ onChange,
+ className,
+}: DropDownProps) => {
+ const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0);
+ const [show, setShow] = useState(false);
+
+ useEffect(() => {
+ setActiveIndex(defaultIndex ?? 0);
+ }, [defaultIndex]);
+
+ const activeTitle = useMemo(
+ () => options?.at(activeIndex)?.label ?? title,
+ [activeIndex, options, title]
+ );
+
+ const handleClick = () => {
+ setShow(!show);
+ };
+
+ const handleItemClick = (index: number) => {
+ setActiveIndex(index);
+ setShow(false);
+ onChange?.(index);
+ };
+
+ return (
+ setShow(false)}>
+
+
+ {activeTitle}
+
+ {show && (
+
+ {options?.length &&
+ options.map((option, index) => {
+ return (
+
handleItemClick(index)}
+ >
+ {option.label}
+
+ );
+ })}
+
+ )}
+
+
+ );
+};
diff --git a/src/components/EnterAuction/EnterAuction.tsx b/src/components/EnterAuction/EnterAuction.tsx
new file mode 100644
index 0000000..9380ded
--- /dev/null
+++ b/src/components/EnterAuction/EnterAuction.tsx
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+import { enterAuctionFormSchema } from 'src/schemas/enterAuctionFormSchema';
+import { NumberInput } from '../Inputs/NumberInput';
+import {
+ AuctionInfo,
+ EnterAuctionContractParams,
+ WalletApp,
+} from 'hydra-auction-offchain';
+import { useEnterAuction } from 'src/hooks/api/enterAuction';
+import { useWallet } from '@meshsdk/react';
+import { Button } from '../shadcn/Button';
+import { useWalletAddress } from 'src/hooks/api/user';
+
+type EnterAuctionFormProps = {
+ auction: AuctionInfo;
+};
+
+// TODO: we are setting the default deposit to minimum deposit, they can enter an alternative if they wish
+export const EnterAuctionForm = ({ auction }: EnterAuctionFormProps) => {
+ const { name: walletApp, wallet, connected } = useWallet();
+ const { data: walletAddress } = useWalletAddress(wallet, connected);
+ const { mutate: enterAuction, isPending: isEnterAuctionPending } =
+ useEnterAuction(walletApp as WalletApp);
+
+ const [enterAuctionFormData, setEnterAuctionFormData] =
+ useState({
+ auctionInfo: auction,
+ depositAmount: auction.auctionTerms.minDepositAmount,
+ });
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const auctionForm = enterAuctionFormSchema
+ .refine(
+ (data) => {
+ if (data.depositAmount === null) return true;
+ return (
+ Number(data.depositAmount) >=
+ Number(auction.auctionTerms.minDepositAmount)
+ );
+ },
+ {
+ message: `Deposit amount must be at least the minimum required desposit of ${auction?.auctionTerms.minDepositAmount} ADA.`,
+ }
+ )
+ .safeParse(enterAuctionFormData);
+
+ if (!auctionForm.success) {
+ console.log(auctionForm.error);
+ } else {
+ if (walletAddress) {
+ enterAuction({
+ enterAuctionParams: auctionForm.data as EnterAuctionContractParams,
+ walletAddress,
+ });
+ }
+ // TODO: should show pop up message that tells bidder that the seller will now authorize them and they will be notified
+ }
+ };
+
+ const handleInputChange = (inputId: string, value: string | number) => {
+ setEnterAuctionFormData({
+ ...enterAuctionFormData,
+ [inputId]: String(value),
+ });
+ };
+ return (
+
+
+
+ );
+};
+
+export default function EnterAuction({ auction }: EnterAuctionFormProps) {
+ const [show, setShow] = useState(false);
+
+ return (
+
+ setShow(!show)} className="w-full mb-2">
+ Enter Auction
+
+ {show && }
+
+ );
+}
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 0000000..aa5c300
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,49 @@
+import { StringInput } from '../Inputs/StringInput';
+
+export default function Footer() {
+ return (
+
+
+
+
+ Join the Hydra Auction community
+
+
+
+
+
+ Subscribe
+
+
+
Get the latest updates
+
+
+
+
+ );
+}
diff --git a/src/components/ImageWrapper/ImageWrapper.tsx b/src/components/ImageWrapper/ImageWrapper.tsx
new file mode 100644
index 0000000..6bd9337
--- /dev/null
+++ b/src/components/ImageWrapper/ImageWrapper.tsx
@@ -0,0 +1,24 @@
+type ImageWrapperProps = React.HTMLProps & {
+ src: string;
+ alt: string;
+ small?: boolean;
+};
+
+const LARGE_IMAGE_WIDTH = 500;
+const SMALL_IMAGE_WIDTH = 246;
+
+export default function ImageWrapper({
+ src,
+ alt,
+ className,
+ small = false,
+}: ImageWrapperProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Inputs/DateInput.tsx b/src/components/Inputs/DateInput.tsx
new file mode 100644
index 0000000..6ebc109
--- /dev/null
+++ b/src/components/Inputs/DateInput.tsx
@@ -0,0 +1,41 @@
+import { useState } from 'react';
+import { formatDate } from 'src/utils/date';
+
+type DateTimeInputProps = {
+ label: string;
+ inputId: string;
+ placeholder?: string;
+ inputValue?: string;
+ onChange?: (inputId: string, value: string) => void;
+};
+
+export const DateTimeInput = ({
+ label,
+ inputId,
+ onChange,
+ placeholder,
+ inputValue,
+}: DateTimeInputProps) => {
+ const [value, setValue] = useState(inputValue || '');
+ return (
+
+
+ {label}
+
+
+
{
+ const formattedDate = formatDate(new Date(e.target.value));
+ onChange &&
+ onChange(inputId, new Date(formattedDate).getTime().toString());
+ !inputValue && setValue(formattedDate);
+ }}
+ id={inputId}
+ className="border-none p-1 m-0 mb-1 bg-gray-100"
+ type="datetime-local"
+ placeholder={placeholder}
+ value={inputValue ? inputValue : value}
+ />
+
+ );
+};
diff --git a/src/components/Inputs/NumberInput.tsx b/src/components/Inputs/NumberInput.tsx
new file mode 100644
index 0000000..f45b5fa
--- /dev/null
+++ b/src/components/Inputs/NumberInput.tsx
@@ -0,0 +1,36 @@
+type NumberInputProps = {
+ label: string;
+ inputId: string;
+ placeholder?: string;
+ onChange?: (inputId: string, value: number) => void;
+ value?: number | string;
+ disabled?: boolean;
+};
+
+// TODO: Add currency symbol
+export const NumberInput = ({
+ label,
+ inputId,
+ onChange,
+ placeholder,
+ value,
+ disabled,
+}: NumberInputProps) => {
+ return (
+
+
+ {label}
+
+
+
onChange && onChange(inputId, Number(e.target.value))}
+ id={inputId}
+ className="border-none p-1 m-0 mb-1 bg-gray-100"
+ placeholder={placeholder ? placeholder : ''}
+ type="number"
+ value={value}
+ />
+
+ );
+};
diff --git a/src/components/Inputs/StringInput.tsx b/src/components/Inputs/StringInput.tsx
new file mode 100644
index 0000000..a22277a
--- /dev/null
+++ b/src/components/Inputs/StringInput.tsx
@@ -0,0 +1,36 @@
+import clsx from 'clsx';
+
+type StringInputProps = {
+ label: string;
+ inputId: string;
+ placeholder?: string;
+ onChange?: (inputId: string, value: string) => void;
+ className?: string;
+ inputClassName?: string;
+};
+export const StringInput = ({
+ label,
+ inputId,
+ onChange,
+ placeholder,
+ className,
+ inputClassName,
+}: StringInputProps) => {
+ return (
+
+
+ {label}
+
+
onChange && onChange(inputId, e.target.value)}
+ id={inputId}
+ className={clsx(
+ 'border-none px-1 m-0 focus-visible:ring-0 focus-visible:outline-none',
+ inputClassName
+ )}
+ placeholder={placeholder ? placeholder : ''}
+ type="text"
+ />
+
+ );
+};
diff --git a/src/components/IpfsImage/IpfsImage.tsx b/src/components/IpfsImage/IpfsImage.tsx
new file mode 100644
index 0000000..f3c054c
--- /dev/null
+++ b/src/components/IpfsImage/IpfsImage.tsx
@@ -0,0 +1,26 @@
+import ImageWrapper from '../ImageWrapper/ImageWrapper';
+import { useAssetMetadata } from 'src/hooks/api/assets';
+
+type IpfsImageProps = {
+ small?: boolean;
+ className?: string;
+ assetUnit: string;
+};
+
+export default function IpfsImage({
+ small = false,
+ className,
+ assetUnit,
+}: IpfsImageProps) {
+ const { data: assetMetadata, isLoading } = useAssetMetadata(assetUnit);
+ if (isLoading) return Loading ...
;
+
+ return (
+
+ );
+}
diff --git a/src/components/Navbar/Topbar.tsx b/src/components/Navbar/Topbar.tsx
new file mode 100644
index 0000000..0f566a7
--- /dev/null
+++ b/src/components/Navbar/Topbar.tsx
@@ -0,0 +1,70 @@
+import { Link } from 'react-router-dom';
+import CustomWallet from '../CustomWallet/CustomWallet';
+import { StringInput } from '../Inputs/StringInput';
+
+import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
+
+export default function Topbar() {
+ return (
+
+
+
+
+
+
+ Explore
+
+
+
+
+ Create
+
+
+ {/*
+
+ Announce Auction Form
+
+ */}
+ {/*
+
+ Enter Auction Form
+
+
+
+
+ Place Bid Form
+
+ */}
+
+
+ {/* */}
+
+
+
+
+
+ );
+}
diff --git a/src/components/OutsideAlerter/OutsideAlerter.tsx b/src/components/OutsideAlerter/OutsideAlerter.tsx
new file mode 100644
index 0000000..488f9a6
--- /dev/null
+++ b/src/components/OutsideAlerter/OutsideAlerter.tsx
@@ -0,0 +1,31 @@
+import React, { useRef, useEffect, ReactNode } from 'react';
+
+interface OutsideAlerterProps {
+ children: ReactNode;
+ onOutsideClick: () => void;
+}
+
+function useOutsideAlerter(
+ ref: React.RefObject,
+ clickEvent: () => void
+) {
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ clickEvent();
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [ref, clickEvent]);
+}
+
+export const OutsideAlerter: React.FC = (props) => {
+ const wrapperRef = useRef(null);
+ useOutsideAlerter(wrapperRef, props.onOutsideClick);
+
+ return {props.children}
;
+};
diff --git a/src/components/PlaceBid/PlaceBid.tsx b/src/components/PlaceBid/PlaceBid.tsx
new file mode 100644
index 0000000..2092fdf
--- /dev/null
+++ b/src/components/PlaceBid/PlaceBid.tsx
@@ -0,0 +1,73 @@
+import React, { useRef } from 'react';
+import { PlaceBidFormT } from 'src/schemas/placeBidFormSchema';
+
+import { NumberInput } from '../Inputs/NumberInput';
+import { usePlaceBid } from '../../hooks/api/placeBid';
+import { AuctionInfo } from 'hydra-auction-offchain';
+import { WalletApp } from 'hydra-auction-offchain';
+
+type PlaceBidFormProps = {
+ auctionInfo: AuctionInfo;
+ sellerSignature: string;
+ standingBid: string;
+ walletApp: WalletApp;
+};
+
+export const PlaceBidForm = ({
+ auctionInfo,
+ sellerSignature,
+ standingBid,
+ walletApp,
+}: PlaceBidFormProps) => {
+ console.log({ sellerSignature });
+ const { mutate: placeBidMutation, isPending: isPlaceBidPending } =
+ usePlaceBid(auctionInfo, sellerSignature, walletApp);
+
+ const placeBidFormData = useRef({
+ bidAmount: 0,
+ });
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ // To uncomment once we can use standing bid to safe check the input
+ // console.log({ placeBidFormData });
+ // const placeBidForm = placeBidFormSchema
+ // .refine((data) => data.bidAmount > Number(standingBid), {
+ // message: 'Bid must be higher than the current bid',
+ // })
+ // .safeParse(placeBidFormData.current);
+
+ // if (!placeBidForm.success) {
+ // // TODO: Show error to client
+ // console.log(placeBidForm.error);
+ // } else {
+ // console.log('MUTATING PLACE BID');
+ // placeBid.mutate(String(placeBidForm.data.bidAmount));
+ // }
+
+ const placeBidResponse = placeBidMutation(
+ String(placeBidFormData.current.bidAmount)
+ );
+ console.log({ placeBidResponse });
+ };
+ return (
+
+ );
+};
diff --git a/src/components/Time/AuctionStateRemaining.tsx b/src/components/Time/AuctionStateRemaining.tsx
new file mode 100644
index 0000000..493ba69
--- /dev/null
+++ b/src/components/Time/AuctionStateRemaining.tsx
@@ -0,0 +1,99 @@
+import { AuctionTerms } from 'hydra-auction-offchain';
+import { TimeRemaining } from './TimeRemaining';
+import { useEffect, useState } from 'react';
+
+type AuctionStateRemainingProps = {
+ biddingEnd: string;
+ biddingStart: string;
+ purchaseDeadline: string;
+ cleanup: string;
+ size?: string;
+};
+
+const TimeRemainingCard = ({
+ endDate,
+ label,
+ size = 'small',
+}: {
+ endDate: number;
+ label: string;
+ size?: string;
+}) => {
+ return size === 'small' ? (
+
+ ) : (
+
+ );
+};
+
+export default function AuctionStateRemaining({
+ biddingEnd,
+ biddingStart,
+ purchaseDeadline,
+ cleanup,
+ size = 'small',
+}: AuctionStateRemainingProps) {
+ const purchaseDeadlineDate = Number(purchaseDeadline);
+ const biddingStartDate = Number(biddingStart);
+ const biddingEndDate = Number(biddingEnd);
+ const cleanupDate = Number(cleanup);
+ const [now, setNowTime] = useState(Date.now());
+
+ useEffect(() => {
+ const timerInterval = setInterval(() => {
+ setNowTime(Date.now());
+ }, 1000);
+
+ return () => clearInterval(timerInterval);
+ }, []);
+
+ if (now > cleanupDate) {
+ return (
+
+ Expired
+
+ );
+ } else if (now > purchaseDeadlineDate) {
+ return (
+
+ );
+ } else if (now > biddingEndDate) {
+ return (
+
+ );
+ } else if (now > biddingStartDate) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+}
diff --git a/src/components/Time/TimeRemaining.tsx b/src/components/Time/TimeRemaining.tsx
new file mode 100644
index 0000000..b141c88
--- /dev/null
+++ b/src/components/Time/TimeRemaining.tsx
@@ -0,0 +1,137 @@
+import { useEffect, useState } from 'react';
+
+// Component to display the time remaining for an auction
+// TODO: add color coding to warn auction is ending soon
+
+type TimeRemainingSlotProps = {
+ value: string;
+ label: string;
+ size?: string;
+};
+
+const TimeRemainingSlot = ({
+ value,
+ label,
+ size = 'small',
+}: TimeRemainingSlotProps) => {
+ return (
+
+
+ {value}
+
+
{label}
+
+ );
+};
+// TODO: FIX when a state changes, it just shows expired and the last state it needs to update the text and show new countdown
+export const TimeRemaining = ({
+ endDate,
+ size = 'small',
+}: {
+ endDate: number;
+ size?: string;
+}) => {
+ const [timeRemaining, setTimeRemaining] = useState(calculateTimeRemaining());
+
+ function calculateTimeRemaining() {
+ const currentTime = Date.now();
+ const timeDiff = endDate - currentTime;
+ const expired = timeDiff < 0;
+
+ const secsInMin = 60;
+ const secsInHour = secsInMin * 60;
+ const secsInDay = secsInHour * 24;
+
+ const totalSeconds = Math.floor(timeDiff / 1000);
+ const days = Math.floor(totalSeconds / secsInDay);
+ const hours = Math.floor((totalSeconds % secsInDay) / secsInHour);
+ const minutes = Math.floor((totalSeconds % secsInHour) / secsInMin);
+ const seconds = totalSeconds % secsInMin;
+
+ return { days, hours, minutes, seconds, expired };
+ }
+
+ useEffect(() => {
+ const timerInterval = setInterval(() => {
+ setTimeRemaining(calculateTimeRemaining());
+ }, 1000);
+
+ return () => clearInterval(timerInterval);
+ }, [endDate]);
+
+ if (size === 'large') {
+ if (timeRemaining.expired)
+ return -
;
+ return timeRemaining.hours > 0 ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ );
+ } else {
+ if (timeRemaining.expired) return -
;
+ return timeRemaining.hours > 0 ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ );
+ }
+};
diff --git a/src/components/WalletNfts/WalletNfts.tsx b/src/components/WalletNfts/WalletNfts.tsx
new file mode 100644
index 0000000..d14c5ef
--- /dev/null
+++ b/src/components/WalletNfts/WalletNfts.tsx
@@ -0,0 +1,54 @@
+import { useExtendedAssets } from 'src/hooks/api/assets';
+import IpfsImage from '../IpfsImage/IpfsImage';
+import { useWallet } from '@meshsdk/react';
+import { WalletApp } from 'hydra-auction-offchain';
+import { removeSpecialCharsAssetName } from 'src/utils/formatting';
+
+type WalletNftCardProps = {
+ assetImageSrc?: string;
+ assetName: string;
+ assetUnit: string;
+};
+
+// TODO: combine WalletNftCard and AuctionCard
+const WalletNftCard = ({ assetName, assetUnit }: WalletNftCardProps) => {
+ return (
+
+
+
+
+ {assetName}
+
+ );
+};
+
+export default function WalletNfts() {
+ const { name: walletApp } = useWallet();
+
+ const { data: assets, isError } = useExtendedAssets(walletApp as WalletApp);
+ console.log({ walletApp, assets, isError });
+
+ if (isError) return Error getting assets...
;
+ return assets && assets?.length > 0 ? (
+
+ {assets?.map((asset, index) => (
+
+
+
+ ))}
+
+ ) : (
+
+ No NFTs - try minting one to list an auction
+
+ );
+}
diff --git a/src/components/layout.tsx b/src/components/layout.tsx
new file mode 100644
index 0000000..056f1d8
--- /dev/null
+++ b/src/components/layout.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+export default function Layout({ children }: React.PropsWithChildren) {
+ return (
+ {children}
+ );
+}
diff --git a/src/contexts/WebSocketContext.ts b/src/contexts/WebSocketContext.ts
new file mode 100644
index 0000000..f037454
--- /dev/null
+++ b/src/contexts/WebSocketContext.ts
@@ -0,0 +1,24 @@
+import { createContext } from 'vm';
+
+export enum WebSocketReadyState {
+ CONNECTING = 0,
+ OPEN = 1,
+ CLOSING = 2,
+ CLOSED = 3,
+}
+
+type WebSocketContextType = {
+ wsUrl: string;
+ wss: WebSocket | null;
+ readyState: WebSocketReadyState;
+ setWsUrl: (url: string) => void;
+};
+
+export const WEBSOCKET_DEFAULT_STATE: WebSocketContextType = {
+ wsUrl: '',
+ wss: null,
+ readyState: WebSocketReadyState.CLOSED,
+ setWsUrl: () => {},
+};
+
+export const WebSocketContext = createContext(WEBSOCKET_DEFAULT_STATE);
diff --git a/src/hooks/api/auctions.ts b/src/hooks/api/auctions.ts
index d21db1e..0b92299 100644
--- a/src/hooks/api/auctions.ts
+++ b/src/hooks/api/auctions.ts
@@ -1,4 +1,5 @@
import {
+ AuctionFilters,
AuctionInfo,
DiscoverSellerSigContractParams,
discoverSellerSignature,
@@ -19,14 +20,15 @@ export const QUERY_AUCTIONS_QUERY_KEY = 'query-auctions';
export const AUCTIONS_ENTERED_QUERY_KEY = 'auctions-entered';
export const AUCTIONS_AUTHORIZED_QUERY_KEY = 'auctions-authorized';
-export const useActiveAuctions = (walletApp?: WalletApp) => {
+export const useActiveAuctions = (
+ walletApp?: WalletApp,
+ auctionFilters?: AuctionFilters
+) => {
const activeAuctions = useQuery({
queryKey: [QUERY_AUCTIONS_QUERY_KEY, walletApp],
queryFn: async () => {
if (walletApp) {
- return await queryAuctions(walletApp);
- } else {
- return [];
+ return await queryAuctions(walletApp, auctionFilters);
}
},
refetchInterval: 10000,
diff --git a/src/hooks/websocket.ts b/src/hooks/websocket.ts
new file mode 100644
index 0000000..d333d68
--- /dev/null
+++ b/src/hooks/websocket.ts
@@ -0,0 +1,8 @@
+import { useContext } from 'react';
+import { WebSocketContext } from 'src/providers/WebSocketProvider';
+
+export const useSocket = () => {
+ const socket = useContext(WebSocketContext);
+
+ return socket;
+};
diff --git a/src/providers/WebSocketProvider.tsx b/src/providers/WebSocketProvider.tsx
new file mode 100644
index 0000000..a0819c2
--- /dev/null
+++ b/src/providers/WebSocketProvider.tsx
@@ -0,0 +1,72 @@
+import { PropsWithChildren, useState, useEffect, useRef } from 'react';
+import {
+ WEBSOCKET_DEFAULT_STATE,
+ WebSocketContext,
+ WebSocketReadyState,
+} from 'src/contexts/WebSocketContext';
+
+// for now we will use one wss and re create it on every url change
+// if we want to keep multiple open we could use a map or object of wss and identify them by the auctionInfo.delegateInfo.wsServers
+// would need a max number of wss to keep open
+// if the requested one is not found we create new one and discard the oldest
+
+export const WebSocketProvider = ({ children }: PropsWithChildren) => {
+ const ws = useRef(null);
+ const [wsUrl, setWsUrl] = useState(WEBSOCKET_DEFAULT_STATE.wsUrl);
+ const prevWsUrl = useRef(null);
+ const [readyState, setReadyState] = useState(
+ WEBSOCKET_DEFAULT_STATE.readyState
+ );
+
+ useEffect(() => {
+ /* WS initialization and cleanup */
+ if (!wsUrl) return;
+ if (ws.current) {
+ if (prevWsUrl.current !== wsUrl) {
+ ws.current.close();
+ setReadyState(WebSocketReadyState.CLOSING);
+ ws.current = new WebSocket(wsUrl);
+ setReadyState(WebSocketReadyState.CONNECTING);
+
+ prevWsUrl.current = wsUrl;
+ }
+ } else {
+ ws.current = new WebSocket(wsUrl);
+ setReadyState(WebSocketReadyState.CONNECTING);
+
+ prevWsUrl.current = wsUrl;
+ }
+
+ if (ws.current) {
+ ws.current.onopen = () => {
+ console.log('WS open');
+ setReadyState(WebSocketReadyState.OPEN);
+ };
+ ws.current.onclose = () => {
+ console.log('WS close');
+ setReadyState(WebSocketReadyState.CLOSED);
+ };
+ ws.current.onmessage = (message: MessageEvent) => {
+ // const { type, ...data } = JSON.parse(message.data);
+ console.log('WS message', message.data);
+ };
+ }
+
+ return () => {
+ ws.current?.close();
+ };
+ }, [wsUrl, ws]);
+
+ const value = {
+ wsUrl,
+ wss: ws.current,
+ readyState,
+ setWsUrl,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/tsconfig.json b/tsconfig.json
index ce793ea..bee4229 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "target": "es6",
+ "target": "ES6",
"lib": [
"dom",
"dom.iterable",