Skip to main content

Build Components

Now let’s build the UI components that use our hooks.

1. USDC Section

Create src/components/usdc-section.tsx:
import { useCurrentAccount } from "@mysten/dapp-kit-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useUsdcBalance } from "@/hooks/use-usdc-balance";
import { useMintUsdc } from "@/hooks/use-mint-usdc";
import { formatUSDC, USDC_MINT_AMOUNT } from "@/lib/contracts";
import { Loader2, Plus } from "lucide-react";

export function UsdcSection() {
  const account = useCurrentAccount();
  const { data } = useUsdcBalance();
  const { mint, isPending } = useMintUsdc();

  if (!account) return null;

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <img
            src="https://cryptologos.cc/logos/usd-coin-usdc-logo.png"
            alt="USDC"
            className="w-5 h-5"
          />
          USDC
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-3">
        <div className="flex justify-between items-center p-3 bg-muted rounded-lg">
          <span className="text-sm text-muted-foreground">Balance</span>
          <span className="text-lg font-bold">
            {formatUSDC(data?.balance ?? 0n)}
          </span>
        </div>

        <Button onClick={mint} disabled={isPending} className="w-full">
          {isPending ? (
            <Loader2 className="w-4 h-4 animate-spin" />
          ) : (
            <>
              <Plus className="w-4 h-4 mr-1" />
              Mint {USDC_MINT_AMOUNT}
            </>
          )}
        </Button>
      </CardContent>
    </Card>
  );
}

2. NFT Mint Section

Create src/components/nft-mint-section.tsx:
import { useCurrentAccount } from "@mysten/dapp-kit-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useUsdcBalance } from "@/hooks/use-usdc-balance";
import { useMintNft } from "@/hooks/use-mint-nft";
import { formatUSDC, NFT_MINT_PRICE } from "@/lib/contracts";
import { Sparkles, Loader2 } from "lucide-react";

export function NftMintSection() {
  const account = useCurrentAccount();
  const { data: usdcData } = useUsdcBalance();
  const { mint, isPending } = useMintNft();

  if (!account) return null;

  const hasEnoughBalance = (usdcData?.balance ?? 0n) >= NFT_MINT_PRICE;

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <Sparkles className="w-4 h-4" />
          Mint NFT
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-3">
        <div className="relative rounded-lg overflow-hidden">
          <img
            src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQh1cpBR8d2D2P55Z2kQzlr6uGuhRfWzEnoVQ&s"
            alt="PSIL NFT"
            className="w-full aspect-square object-cover"
          />
          <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
          <div className="absolute bottom-0 p-3 text-white">
            <p className="font-bold">Pria Solo Itu Lagi</p>
            <p className="text-xs opacity-80">SAYA AKAN PULANG KE SOLO</p>
          </div>
        </div>

        <div className="flex justify-between items-center p-3 bg-muted rounded-lg">
          <span className="text-sm text-muted-foreground">Price</span>
          <span className="font-bold">{formatUSDC(NFT_MINT_PRICE)} USDC</span>
        </div>

        {!hasEnoughBalance && (
          <p className="text-sm text-destructive">Insufficient USDC balance</p>
        )}

        <Button
          onClick={mint}
          disabled={isPending || !hasEnoughBalance}
          className="w-full"
        >
          {isPending ? (
            <>
              <Loader2 className="w-4 h-4 mr-1 animate-spin" />
              Minting...
            </>
          ) : (
            <>
              <Sparkles className="w-4 h-4 mr-1" />
              Mint
            </>
          )}
        </Button>
      </CardContent>
    </Card>
  );
}
Create src/components/nft-gallery.tsx:
import { useCurrentAccount } from "@mysten/dapp-kit-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useOwnedNfts } from "@/hooks/use-owned-nfts";
import { ImageIcon } from "lucide-react";

export function NftGallery() {
  const account = useCurrentAccount();
  const { data: nfts } = useOwnedNfts();

  if (!account) return null;

  const count = nfts?.length ?? 0;

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <ImageIcon className="w-4 h-4" />
          Your NFTs
          {count > 0 && (
            <span className="ml-auto text-sm font-normal text-muted-foreground">
              {count}
            </span>
          )}
        </CardTitle>
      </CardHeader>
      <CardContent>
        {count > 0 ? (
          <div className="grid grid-cols-2 gap-2">
            {nfts!.map((nft) => (
              <div
                key={nft.id}
                className="relative rounded-lg overflow-hidden border"
              >
                <img
                  src={nft.url}
                  alt={nft.name}
                  className="w-full aspect-square object-cover"
                />
                <span className="absolute top-1 right-1 bg-black/60 text-white text-xs px-1.5 py-0.5 rounded">
                  #{nft.edition}
                </span>
              </div>
            ))}
          </div>
        ) : (
          <p className="text-center py-6 text-sm text-muted-foreground">
            No NFTs yet
          </p>
        )}
      </CardContent>
    </Card>
  );
}

4. Update App

Update src/app.tsx to use all components:
import { ConnectButton, useCurrentAccount } from "@mysten/dapp-kit-react";
import { UsdcSection } from "./components/usdc-section";
import { NftMintSection } from "./components/nft-mint-section";
import { NftGallery } from "./components/nft-gallery";

export function App() {
  const account = useCurrentAccount();

  return (
    <div className="min-h-screen bg-background bg-grid">
      <header className="sticky top-0 z-50 border-b bg-background/80 backdrop-blur">
        <div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
          <h1 className="font-bold">Sui Workshop</h1>
          <ConnectButton />
        </div>
      </header>

      <main className="max-w-4xl mx-auto px-4 py-6">
        {account ? (
          <div className="grid gap-4 md:grid-cols-2">
            <div className="space-y-4">
              <UsdcSection />
              <NftGallery />
            </div>
            <NftMintSection />
          </div>
        ) : (
          <div className="text-center py-20">
            <h2 className="text-xl font-bold mb-2">Welcome</h2>
            <p className="text-muted-foreground mb-4">Connect wallet to start</p>
            <ConnectButton />
          </div>
        )}
      </main>
    </div>
  );
}

Install Lucide Icons

npm install lucide-react

Final Project Structure

src/
├── app.tsx
├── main.tsx
├── index.css
├── components/
│   ├── providers.tsx
│   ├── usdc-section.tsx
│   ├── nft-mint-section.tsx
│   ├── nft-gallery.tsx
│   └── ui/
├── hooks/
│   ├── use-usdc-balance.ts
│   ├── use-mint-usdc.ts
│   ├── use-owned-nfts.ts
│   └── use-mint-nft.ts
└── lib/
    ├── contracts.ts
    ├── dapp-kit.ts
    └── utils.ts

Test the App

  1. Run npm run dev
  2. Connect your wallet
  3. Mint some USDC tokens
  4. Mint an NFT with USDC
  5. See your NFT in the gallery
Congratulations! You’ve built a complete NFT minting dApp on Sui!