Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions nft-marketplace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Tutorial application: Build an NFT marketplace

This folder contains the starter files and completed applications for each part of the tutorial [Build an NFT marketplace](https://docs.tezos.com/tutorials/build-an-nft-marketplace).
14 changes: 14 additions & 0 deletions nft-marketplace/frontend-starter/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/nodeSpecific.ts"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
54 changes: 54 additions & 0 deletions nft-marketplace/frontend-starter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "nft-marketplace",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "if test -f .env; then sed -i \"s/\\(VITE_CONTRACT_ADDRESS *= *\\).*/\\1$(jq -r 'last(.tasks[]).output[0].address' ../.taq/testing-state.json)/\" .env ; else jq -r '\"VITE_CONTRACT_ADDRESS=\" + last(.tasks[]).output[0].address' ../.taq/testing-state.json > .env ; fi && vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@airgap/beacon-sdk": "4.6.3",
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.1",
"@mui/icons-material": "7.3.6",
"@mui/material": "7.3.6",
"@taquito/beacon-wallet": "23.0.3",
"@taquito/taquito": "23.0.3",
"@taquito/tzip12": "23.0.3",
"@tzkt/sdk-api": "^2.2.1",
"formik": "2.4.9",
"notistack": "3.0.2",
"react": "19.2.1",
"react-dom": "19.2.1",
"react-router-dom": "7.10.1",
"react-swipeable-views": "0.14.1",
"yup": "1.7.1"
},
"devDependencies": {
"@types/react-swipeable-views": "0.13.6",
"@airgap/beacon-types": "4.6.3",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@typescript-eslint/eslint-plugin": "8.49.0",
"@typescript-eslint/parser": "8.49.0",
"@vitejs/plugin-react-swc": "4.2.2",
"assert": "^2.1.0",
"buffer": "^6.0.3",
"crypto-browserify": "3.12.1",
"eslint": "9.39.1",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-react-refresh": "0.4.24",
"https-browserify": "^1.0.0",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"typescript": "5.9.3",
"url": "0.11.4",
"vite": "7.2.7"
}
}
Binary file not shown.
Binary file added nft-marketplace/frontend-starter/public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions nft-marketplace/frontend-starter/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions nft-marketplace/frontend-starter/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.App {
text-align: center;
background-color: #282c34;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}

.App-logo {
height: 10vh;
pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
.App-logo {
}
}

.App-header {
max-height: 10vh;
width: 100%;
background-color: white;
}

.App-body {
height: 90vh;
}

.App-link {
color: #61dafb;
}

@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
146 changes: 146 additions & 0 deletions nft-marketplace/frontend-starter/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { NetworkType } from "@airgap/beacon-dapp";
import { BeaconWallet } from "@taquito/beacon-wallet";
import { TezosToolkit } from "@taquito/taquito";
import { TokenMetadata, tzip12, Tzip12Module } from "@taquito/tzip12";
import * as api from "@tzkt/sdk-api";
import { BigNumber } from "bignumber.js";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import "./App.css";
import { NftWalletType, Storage } from "./nft.types";
import Paperbase from "./Paperbase";
export type TZIP21TokenMetadata = TokenMetadata & {
artifactUri?: string; //A URI (as defined in the JSON Schema Specification) to the asset.
displayUri?: string; //A URI (as defined in the JSON Schema Specification) to an image of the asset.
thumbnailUri?: string; //A URI (as defined in the JSON Schema Specification) to an image of the asset for wallets and client applications to have a scaled down image to present to end-users.
description?: string; //General notes, abstracts, or summaries about the contents of an asset.
minter?: string; //The tz address responsible for minting the asset.
creators?: string[]; //The primary person, people, or organization(s) responsible for creating the intellectual content of the asset.
isBooleanAmount?: boolean; //Describes whether an account can have an amount of exactly 0 or 1. (The purpose of this field is for wallets to determine whether or not to display balance information and an amount field when transferring.)
};

export type UserContextType = {
storage: Storage | null;
userAddress: string;
userBalance: number;
setUserAddress: Dispatch<SetStateAction<string>>;
Tezos: TezosToolkit;
setUserBalance: Dispatch<SetStateAction<number>>;
wallet: BeaconWallet;
nftContractAddress: string;
nftContract: NftWalletType | null;
setNftContract: Dispatch<SetStateAction<NftWalletType | null>>;
nftContractTokenMetadataMap: Map<string, TZIP21TokenMetadata>;
setnftContractTokenMetadataMap: Dispatch<
SetStateAction<Map<string, TZIP21TokenMetadata>>
>;
refreshUserContextOnPageReload: () => Promise<void>;
};

export let UserContext = React.createContext<UserContextType | null>(null);
const nftContractAddress = import.meta.env.VITE_CONTRACT_ADDRESS;

function App() {
api.defaults.baseUrl = "https://api.shadownet.tzkt.io";

const [storage, setStorage] = useState<Storage | null>(null);
const [userAddress, setUserAddress] = useState<string>("");
const [userBalance, setUserBalance] = useState<number>(0);
const [nftContract, setNftContract] = useState<NftWalletType | null>(null);
const [nftContractTokenMetadataMap, setnftContractTokenMetadataMap] = useState<
Map<string, TZIP21TokenMetadata>
>(new Map());

const [Tezos, _] = useState<TezosToolkit>(
new TezosToolkit(import.meta.env.VITE_TEZOS_NODE)
);
const [wallet, __] = useState<BeaconWallet>(
new BeaconWallet({
name: "Training",
network: {
name: "Shadownet",
rpcUrl: import.meta.env.VITE_TEZOS_NODE,
type: NetworkType.CUSTOM,
}
})
);

const refreshUserContextOnPageReload = async () => {
console.log("refreshUserContext");
//CONTRACT
try {
let c = await Tezos.contract.at(nftContractAddress, tzip12);
console.log("nftContractAddress", nftContractAddress);

let nftContract: NftWalletType = await Tezos.wallet.at<NftWalletType>(
nftContractAddress
);
const storage = (await nftContract.storage()) as Storage;

const token_metadataBigMapId = (
storage.token_metadata as unknown as { id: BigNumber }
).id.toNumber();

const token_ids = await api.bigMapsGetKeys(token_metadataBigMapId, {
micheline: "Json",
active: true,
});

await Promise.all(
token_ids.map(async (token_id: api.BigMapKey) => {
let tokenMetadata: TZIP21TokenMetadata = (await c
.tzip12()
.getTokenMetadata(token_id.key)) as TZIP21TokenMetadata;
nftContractTokenMetadataMap.set(token_id.key, tokenMetadata);
})
);
setnftContractTokenMetadataMap(new Map(nftContractTokenMetadataMap)); //new Map to force refresh
setNftContract(nftContract);
setStorage(storage);
} catch (error) {
console.log("error refreshing nft contract: ", error);
}

//USER
const activeAccount = await wallet.client.getActiveAccount();
if (activeAccount) {
setUserAddress(activeAccount.address);
const balance = await Tezos.tz.getBalance(activeAccount.address);
setUserBalance(balance.toNumber());
}

console.log("refreshUserContext ended.");
};

useEffect(() => {
Tezos.setWalletProvider(wallet);
Tezos.addExtension(new Tzip12Module());
}, [wallet]);

useEffect(() => {
refreshUserContextOnPageReload();
}, []);

return (
<UserContext.Provider
value={{
userAddress,
userBalance,
setUserAddress,
Tezos,
setUserBalance,
wallet,
nftContractAddress,
nftContract,
nftContractTokenMetadataMap,
setnftContractTokenMetadataMap,
setNftContract,
storage,
refreshUserContextOnPageReload,
}}
>
<Paperbase />
</UserContext.Provider>
);
}

export default App;
51 changes: 51 additions & 0 deletions nft-marketplace/frontend-starter/src/ConnectWallet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Wallet } from "@mui/icons-material";
import { Button } from "@mui/material";
import { BeaconWallet } from "@taquito/beacon-wallet";
import { TezosToolkit } from "@taquito/taquito";
import { Dispatch, SetStateAction } from "react";
import { useNavigate } from "react-router-dom";
import { TZIP21TokenMetadata } from "./App";
import { PagesPaths } from "./Navigator";

type ButtonProps = {
Tezos: TezosToolkit;
setUserAddress: Dispatch<SetStateAction<string>>;
setUserBalance: Dispatch<SetStateAction<number>>;
wallet: BeaconWallet;
nftContractTokenMetadataMap: Map<string, TZIP21TokenMetadata>;
};

const ConnectButton = ({
Tezos,
setUserAddress,
setUserBalance,
wallet,
nftContractTokenMetadataMap,
}: ButtonProps): JSX.Element => {
const navigate = useNavigate();

const connectWallet = async (): Promise<void> => {
try {
await wallet.requestPermissions();
// gets user's address
const userAddress = await wallet.getPKH();
const balance = await Tezos.tz.getBalance(userAddress);
setUserBalance(balance.toNumber());
setUserAddress(userAddress);
if (nftContractTokenMetadataMap && nftContractTokenMetadataMap.size > 0)
navigate(PagesPaths.CATALOG);
else navigate(PagesPaths.MINT);
} catch (error) {
console.log(error);
}
};

return (
<Button sx={{ p: 1 }} onClick={connectWallet}>
<Wallet />
&nbsp; Connect wallet
</Button>
);
};

export default ConnectButton;
44 changes: 44 additions & 0 deletions nft-marketplace/frontend-starter/src/DisconnectWallet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Logout } from "@mui/icons-material";
import { Button, ButtonGroup, Tooltip } from "@mui/material";
import { BeaconWallet } from "@taquito/beacon-wallet";
import { Dispatch, SetStateAction } from "react";
import { useNavigate } from "react-router-dom";
import { PagesPaths } from "./Navigator";
interface ButtonProps {
userAddress: string;
userBalance: number;
wallet: BeaconWallet;
setUserAddress: Dispatch<SetStateAction<string>>;
setUserBalance: Dispatch<SetStateAction<number>>;
}

const DisconnectButton = ({
userAddress,
userBalance,
wallet,
setUserAddress,
setUserBalance,
}: ButtonProps): JSX.Element => {
const navigate = useNavigate();

const disconnectWallet = async (): Promise<void> => {
setUserAddress("");
setUserBalance(0);
console.log("disconnecting wallet");
await wallet.clearActiveAccount();
navigate(PagesPaths.CATALOG);
};

return (
<ButtonGroup>
<Tooltip title={userBalance / 1000000 + " Tz"}>
<Button disableRipple>{userAddress}</Button>
</Tooltip>
<Button sx={{ p: 1 }}>
<Logout onClick={disconnectWallet} />
</Button>
</ButtonGroup>
);
};

export default DisconnectButton;
Loading