Designing the NFT Marketplace User Interface
 
Bài viết này dựa trên nội dung video hướng dẫn thiết kế giao diện người dùng (UI) cho một ứng dụng NFT Marketplace trên blockchain Cardano, sử dụng framework Next.js. Nội dung được thiết kế để giúp người mới bắt đầu (beginner) xây dựng UI với các chức năng cơ bản như kết nối ví, hiển thị danh sách NFT, xem chi tiết NFT, mua bán, cập nhật giá, và quản lý profile. Chúng ta sẽ sử dụng Shadcn UI để tạo các component, MeshJS để kết nối ví, và công cụ như Prettier, ESLint để quản lý code. Bài viết này là phần thứ hai trong chuỗi 6 video về xây dựng dApp NFT Marketplace, tập trung vào thiết kế UI. Các phần sau sẽ bao gồm viết smart contract, test, tích hợp, và triển khai production.
Trước khi bắt đầu, hãy đảm bảo bạn có:
- Node.js và npm: Cài đặt Node.js (khuyến nghị phiên bản 16 trở lên) và npm (hoặc Bun nếu bạn sử dụng Bun để cài đặt nhanh hơn).
 
- Trình Soạn Thảo Mã: Sử dụng Visual Studio Code hoặc tương tự.
 
- Ví Cardano: Thiết lập ví Cardano (như Eternl) với test ADA (tADA) trên mạng thử nghiệm Cardano testnet.
 
- Kiến Thức Cơ Bản: Hiểu biết về JavaScript, React, và các khái niệm blockchain như NFT, giao dịch on-chain.
 
Tổng Quan Về UI NFT Marketplace
 
Dựa trên phân tích ý tưởng từ video trước, UI sẽ hỗ trợ các chức năng cơ bản:
- Kết nối ví: Sử dụng MeshJS để kết nối ví Cardano.
 
- Trang chủ: Hiển thị danh sách NFT đang bán.
 
- Trang chi tiết NFT: Xem thông tin NFT (hình ảnh, tên, giá, metadata, seller), nút mua (Buy Now), cập nhật giá (Update Price), hoặc hủy bán (Delist).
 
- Trang Profile: Hiển thị NFT sở hữu (Owned) và NFT đang bán (Listing), với nút bán (Sale), cập nhật giá, hoặc hủy bán.
 
UI sẽ gồm 3 trang chính:
- Trang chủ (/marketplace): Grid NFT.
 
- Trang chi tiết (/nft/[unit]): Chi tiết NFT với tùy chọn mua/bán.
 
- Trang profile (/profile): Quản lý NFT cá nhân.
 
Sử dụng Shadcn UI để tạo component nhanh (button, form, etc.), và tùy chỉnh để phù hợp với NFT Marketplace.
Các Bước Thiết Kế UI Với Next.js
 
Bước 1: Thiết Lập Dự Án Next.js
 
- 
Truy cập Next.js Documentation và copy lệnh tạo dự án:
npx create-next-app@latest nft-marketplace
 
 
- Nếu dùng Bun: 
bunx create-next-app@latest nft-marketplace. 
- Làm theo hướng dẫn: Chọn tên dự án, sử dụng TypeScript (nếu muốn), App Router.
 
 
- 
Chạy dự án:
cd nft-marketplace
npm run dev
 
 
- Truy cập 
http://localhost:3000 để thấy trang mặc định. 
 
Bước 2: Cài Đặt Shadcn UI
 
Shadcn UI là thư viện component đẹp, dễ tùy chỉnh cho Next.js.
- 
Cài đặt:
npm install @radix-ui/react-slot class-variance-authority
npx shadcn-ui@latest init
 
 
- Nếu dùng Bun: 
bun add @radix-ui/react-slot class-variance-authority và bunx shadcn-ui@latest init. 
- Cấu hình: Chọn Tailwind CSS, và các tùy chọn mặc định.
 
 
- 
Cài thêm component ví dụ như Button:
npx shadcn-ui@latest add button
 
 
- Tạo file 
components/ui/button.tsx với component Button tùy chỉnh. 
 
- 
Cập nhật app/page.tsx để test:
import { Button } from '@/components/ui/button';
export default function Home() {
  return (
    <main>
      <h1>NFT Marketplace</h1>
      <Button>Connect Wallet</Button>
    </main>
  );
}
 
 
 
Bước 3: Cài Đặt MeshJS Và Kết Nối Ví
 
MeshJS dùng để kết nối ví Cardano và xử lý giao dịch.
- 
Cài đặt phiên bản ổn định (1.8.14):
npm install @meshsdk/core@1.8.14
 
 
 
- 
Cập nhật next.config.js để hỗ trợ Webpack (vì MeshJS cần tùy chỉnh):
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  transpilePackages: ['@meshsdk/core'],
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        ...config.resolve.fallback,
        fs: false,
        net: false,
        tls: false,
        crypto: false,
      };
    }
    return config;
  },
};
module.exports = nextConfig;
 
 
 
- 
Tạo component Connect Wallet (components/WalletConnect.tsx):
'use client';
import { useState } from 'react';
import { BrowserWallet } from '@meshsdk/core';
import { Button } from '@/components/ui/button';
export default function WalletConnect({ setWallet }: { setWallet: (wallet: BrowserWallet | null) => void }) {
  const [address, setAddress] = useState('');
  const connect = async () => {
    try {
      const wallet = await BrowserWallet.enable('eternl');
      setWallet(wallet);
      const addresses = await wallet.getUsedAddresses();
      setAddress(addresses[0]);
    } catch (error) {
      console.error('Error connecting wallet:', error);
    }
  };
  return (
    <div>
      {address ? (
        <p>Connected: {address.slice(0, 6)}...{address.slice(-4)}</p>
      ) : (
        <Button onClick={connect}>Connect Wallet</Button>
      )}
    </div>
  );
}
 
 
 
- 
Sử dụng trong app/page.tsx để test kết nối.
 
Bước 4: Cài Đặt Prettier Và ESLint Để Quản Lý Code
 
- 
Cài Prettier và ESLint:
npm install --save-dev prettier eslint eslint-config-next
 
 
 
- 
Tạo file .prettierrc:
{
  "singleQuote": true,
  "semi": false,
  "trailingComma": "es5"
}
 
 
 
- 
Tạo file .prettierignore:
 
- 
Cập nhật package.json với script:
"scripts": {
  "format": "prettier --write ."
}
 
 
 
- 
Chạy npm run format để tự động format code (thay dấu nháy đơn bằng kép, loại bỏ dấu chấm phẩy thừa, etc.).
 
- 
Cài ESLint: Chạy npx eslint --init và chọn cấu hình cho Next.js.
 
Bước 5: Thiết Kế Trang Chủ (Marketplace)
 
Trang chủ hiển thị grid NFT đang bán.
- Cập nhật 
app/page.tsx:
'use client';
import { useState } from 'react';
import WalletConnect from '@/components/WalletConnect';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
export default function Marketplace() {
  const [wallet, setWallet] = useState(null);
  // Giả sử dữ liệu NFT từ query on-chain (sẽ tích hợp sau)
  const nfts = [
    { unit: 'nft1', name: 'NFT 1', price: 100, image: 'https://example.com/nft1.jpg' },
    // Thêm NFT khác
  ];
  return (
    <main>
      <WalletConnect setWallet={setWallet} />
      <h1>NFT Marketplace</h1>
      <div className="grid grid-cols-3 gap-4">
        {nfts.map((nft) => (
          <Card key={nft.unit}>
            <CardHeader>
              <CardTitle>{nft.name}</CardTitle>
            </CardHeader>
            <CardContent>
              <img src={nft.image} alt={nft.name} />
              <p>Price: {nft.price} ADA</p>
            </CardContent>
          </Card>
        ))}
      </div>
    </main>
  );
}
 
 
 
Bước 6: Thiết Kế Trang Chi Tiết NFT (/nft/[unit])
 
Sử dụng dynamic route để xem chi tiết NFT.
- Tạo thư mục 
app/nft/[unit] và file page.tsx:
'use client';
import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
export default function NftDetail() {
  const { unit } = useParams();
  // Giả sử dữ liệu NFT từ query (sẽ tích hợp sau)
  const nft = {
    policyId: 'policy123',
    name: 'NFT Name',
    seller: 'addr_test1...',
    price: 500,
    metadata: { trait1: 'Value1', trait2: 'Value2' },
    image: 'https://example.com/nft.jpg',
  };
  return (
    <main>
      <h1>{nft.name}</h1>
      <img src={nft.image} alt={nft.name} />
      <p>Policy ID: {nft.policyId}</p>
      <p>Seller: {nft.seller.slice(0, 6)}...{nft.seller.slice(-4)}</p>
      <p>Price: {nft.price} ADA</p>
      <Tabs defaultValue="properties">
        <TabsList>
          <TabsTrigger value="properties">Properties</TabsTrigger>
        </TabsList>
        <TabsContent value="properties">
          {Object.entries(nft.metadata).map(([key, value]) => (
            <p key={key}>{key}: {value}</p>
          ))}
        </TabsContent>
      </Tabs>
      {/* Nếu là người mua: */}
      <Button>Buy Now</Button>
      {/* Nếu là người bán: */}
      <Button>Update Price</Button>
      <Button>Delist</Button>
    </main>
  );
}
 
 
 
Bước 7: Thiết Kế Trang Profile (/profile)
 
Trang quản lý NFT sở hữu và đang bán.
- Tạo file 
app/profile/page.tsx:
'use client';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
export default function Profile() {
  // Giả sử dữ liệu từ ví (sẽ tích hợp sau)
  const ownedNfts = [
    { name: 'Owned NFT 1', image: 'https://example.com/owned1.jpg' },
  ];
  const listedNfts = [
    { name: 'Listed NFT 1', price: 1000, image: 'https://example.com/listed1.jpg' },
  ];
  return (
    <main>
      <h1>Profile</h1>
      <Tabs defaultValue="owned">
        <TabsList>
          <TabsTrigger value="owned">Owned</TabsTrigger>
          <TabsTrigger value="listing">Listing</TabsTrigger>
        </TabsList>
        <TabsContent value="owned">
          <div className="grid grid-cols-3 gap-4">
            {ownedNfts.map((nft) => (
              <Card key={nft.name}>
                <CardHeader>
                  <CardTitle>{nft.name}</CardTitle>
                </CardHeader>
                <CardContent>
                  <img src={nft.image} alt={nft.name} />
                  <Button>Sale</Button>
                </CardContent>
              </Card>
            ))}
          </div>
        </TabsContent>
        <TabsContent value="listing">
          <div className="grid grid-cols-3 gap-4">
            {listedNfts.map((nft) => (
              <Card key={nft.name}>
                <CardHeader>
                  <CardTitle>{nft.name}</CardTitle>
                </CardHeader>
                <CardContent>
                  <img src={nft.image} alt={nft.name} />
                  <p>Price: {nft.price} ADA</p>
                  <Button>Update Price</Button>
                  <Button>Delist</Button>
                </CardContent>
              </Card>
            ))}
          </div>
        </TabsContent>
      </Tabs>
    </main>
  );
}
 
 
 
Bước 8: Tùy Chỉnh Và Test UI
 
- Sử dụng AI (như ChatGPT) để generate thêm component nếu cần (ví dụ: form nhập giá cho Sale/Update).
 
- Test: Chạy 
npm run dev, kiểm tra các trang, đảm bảo responsive và không lỗi. 
- Dữ liệu tạm thời: Sử dụng dữ liệu giả; phần sau sẽ query từ blockchain.
 
Bài viết đã hướng dẫn thiết kế UI cho NFT Marketplace trên Cardano với Next.js, Shadcn UI, và MeshJS. Bạn đã có cấu trúc cơ bản với 3 trang chính và các component cần thiết. Các video tiếp theo sẽ viết smart contract để xử lý mua/bán on-chain và tích hợp dữ liệu thực tế từ blockchain. Bạn có thể mở rộng bằng cách thêm bộ lọc, tìm kiếm, hoặc tích hợp IPFS cho hình ảnh NFT.
Bài Tập
📝 Bài tập 1: Khởi tạo dự án Next.js cho NFT Marketplace
 
Khởi tạo dự án Next.js mới cho NFT Marketplace và cài đặt thư viện hỗ trợ giao diện.
- Cài đặt dự án Next.js mới bằng 
npx create-next-app hoặc pnpm create next-app. 
- Cài đặt thư viện shadcn/ui theo hướng dẫn.
 
- Tạo file 
utils/cn.ts chứa hàm tiện ích cn() để nối chuỗi class CSS. 
- Kiểm tra chạy thành công bằng lệnh 
npm run dev hoặc pnpm dev. 
Cách giải
- 
Chạy lệnh khởi tạo project Next.js.
 
- 
Cài đặt shadcn/ui:
npx shadcn-ui@latest init
 
 
 
- 
Thêm file utils/cn.ts:
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: any[]) {
  return twMerge(clsx(inputs));
}
 
 
 
- 
Khởi động server và kiểm tra giao diện mặc định.
 
Sau khi hoàn thành, bạn có thể truy cập http://localhost:3000 để thấy trang mặc định Next.js.
Hệ thống sẵn sàng cho việc phát triển UI marketplace.
 
📝 Bài tập 2: Xây dựng hệ thống quản lý ví (Wallet Store)
 
Tạo store quản lý trạng thái ví người dùng bằng Zustand.
- Cài đặt Zustand bằng 
npm install zustand. 
- Tạo file 
hooks/useWallet.ts. 
- Lưu trữ các giá trị: 
walletName, address, browserWallet. 
- Cung cấp hàm 
connect() và disconnect() để điều khiển trạng thái. 
Cách giải
- Import 
create từ Zustand. 
- Tạo store và định nghĩa các biến cần thiết.
 
- Kết nối ví thông qua 
window.cardano[walletName].enable(). 
import { create } from "zustand";
interface WalletStore {
  walletName: string;
  address: string;
  browserWallet: any;
  connect: (name: string) => Promise<void>;
  disconnect: () => void;
}
export const useWallet = create<WalletStore>((set) => ({
  walletName: "",
  address: "",
  browserWallet: null,
  connect: async (name: string) => {
    try {
      const wallet = await window.cardano[name].enable();
      const addr = await wallet.getChangeAddress();
      set({ walletName: name, browserWallet: wallet, address: addr });
    } catch {
      alert("Không thể kết nối ví");
    }
  },
  disconnect: () => set({ walletName: "", address: "", browserWallet: null }),
}));
 
 
 
📝 Bài tập 3: Tạo nút Connect Wallet
 
Tạo component WalletConnectButton để hiển thị trạng thái ví và tùy chọn kết nối.
- Tạo file 
components/connect/WalletConnectButton.tsx. 
- Nếu chưa kết nối → hiển thị nút “Connect Wallet”.
 
- Nếu đã kết nối → hiển thị địa chỉ ví và nút “Disconnect”.
 
- Sử dụng hook 
useWallet() từ bài tập trước. 
Cách giải
- Lấy dữ liệu ví từ store Zustand.
 
- Hiển thị điều kiện theo trạng thái kết nối.
 
- Thêm event 
onClick để gọi connect() hoặc disconnect(). 
"use client";
import { useWallet } from "@/hooks/useWallet";
export default function WalletConnectButton() {
  const { walletName, address, connect, disconnect } = useWallet();
  return (
    <div className="text-center">
      {!walletName ? (
        <button
          onClick={() => connect("eternl")}
          className="bg-blue-500 text-white px-4 py-2 rounded-md"
        >
          Connect Wallet
        </button>
      ) : (
        <div>
          <p>Đã kết nối: {address.slice(0, 10)}...</p>
          <button
            onClick={disconnect}
            className="bg-red-500 text-white px-4 py-2 mt-2 rounded-md"
          >
            Disconnect
          </button>
        </div>
      )}
    </div>
  );
}
 
 
 
📝 Bài tập 4: Tạo giao diện trang chủ Marketplace
 
Xây dựng trang chủ / hiển thị danh sách NFT mẫu.
- Tạo component 
NFTCard.tsx hiển thị hình ảnh, tên, giá, và nút “Buy Now”. 
- Hiển thị danh sách NFT giả (3–5 NFT).
 
- Tạo layout với tiêu đề “Cardano NFT Marketplace” và nút 
Connect Wallet. 
Cách giải
- Tạo component 
NFTCard trong components/nft/. 
- Dữ liệu có thể là một mảng tĩnh trong 
data/nfts.ts. 
- Render danh sách NFT trên trang 
app/page.tsx. 
import NFTCard from "@/components/nft/NFTCard";
import WalletConnectButton from "@/components/connect/WalletConnectButton";
import { nfts } from "@/data/nfts";
export default function Home() {
  return (
    <div className="p-8">
      <header className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Cardano NFT Marketplace</h1>
        <WalletConnectButton />
      </header>
      <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
        {nfts.map((nft) => (
          <NFTCard key={nft.id} nft={nft} />
        ))}
      </div>
    </div>
  );
}
 
 
 
📝 Bài tập 5: Tạo trang Profile và NFT Detail
 
Xây dựng 2 trang bổ sung:
/nft/[unit].tsx – Hiển thị chi tiết NFT. 
/profile – Hiển thị NFT của người dùng. 
- Trang chi tiết NFT có các nút “Buy Now”, “Update Price”, “Delist”.
 
- Nếu người dùng là chủ sở hữu → hiển thị các nút quản lý.
 
- Trang Profile hiển thị danh sách NFT người dùng đang sở hữu.
 
Cách giải
- Sử dụng router động 
[unit].tsx để lấy dữ liệu NFT theo ID. 
- Lấy dữ liệu ví hiện tại từ store 
useWallet. 
- So sánh 
owner và address để xác định quyền hiển thị. 
import { useWallet } from "@/hooks/useWallet";
export default function NFTDetail({ params }: { params: { unit: string } }) {
  const { address } = useWallet();
  const nft = { id: params.unit, name: "Sample NFT", owner: "addr_test1..." };
  const isOwner = nft.owner === address;
  return (
    <div className="p-8 text-center">
      <h2 className="text-xl font-bold mb-4">{nft.name}</h2>
      {isOwner ? (
        <div>
          <button className="bg-yellow-500 text-white px-4 py-2 m-2 rounded-md">
            Update Price
          </button>
          <button className="bg-red-500 text-white px-4 py-2 m-2 rounded-md">
            Delist
          </button>
        </div>
      ) : (
        <button className="bg-blue-600 text-white px-4 py-2 rounded-md">
          Buy Now
        </button>
      )}
    </div>
  );
}