Xây dựng dapp trên cardano từ con số không
About Lesson

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.

Yêu Cầu Chuẩn Bị

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:

  1. Trang chủ (/marketplace): Grid NFT.
  2. Trang chi tiết (/nft/[unit]): Chi tiết NFT với tùy chọn mua/bán.
  3. 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

  1. 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.
  2. 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.

  1. 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.
  2. 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.
  3. 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.

  1. Cài đặt phiên bản ổn định (1.8.14):

    npm install @meshsdk/core@1.8.14
     
  2. 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;
     
  3. 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>
      );
    }
     
  4. 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

  1. Cài Prettier và ESLint:

    npm install --save-dev prettier eslint eslint-config-next
     
  2. Tạo file .prettierrc:

    {
      "singleQuote": true,
      "semi": false,
      "trailingComma": "es5"
    }
     
  3. Tạo file .prettierignore:

    node_modules
    .next
    build
    
     
  4. Cập nhật package.json với script:

    "scripts": {
      "format": "prettier --write ."
    }
     
  5. 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.).

  6. 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.

  1. 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.

  1. 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.

  1. 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.

Tài Liệu Tham Khảo

Kết Luận

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

Đề bài

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.

Yêu cầu

  • 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
  1. Chạy lệnh khởi tạo project Next.js.

  2. Cài đặt shadcn/ui:

    npx shadcn-ui@latest init
     
  3. Thêm file utils/cn.ts:

    import { clsx } from "clsx";
    import { twMerge } from "tailwind-merge";
    
    export function cn(...inputs: any[]) {
      return twMerge(clsx(inputs));
    }
     
  4. Khởi động server và kiểm tra giao diện mặc định.

Đáp án

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)

Đề bài

Tạo store quản lý trạng thái ví người dùng bằng Zustand.

Yêu cầu

  • Cài đặt Zustand bằng npm install zustand.
  • Tạo file hooks/useWallet.ts.
  • Lưu trữ các giá trị: walletNameaddressbrowserWallet.
  • Cung cấp hàm connect() và disconnect() để điều khiển trạng thái.
Cách giải
  1. Import create từ Zustand.
  2. Tạo store và định nghĩa các biến cần thiết.
  3. Kết nối ví thông qua window.cardano[walletName].enable().

Đáp án

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

Đề bài

Tạo component WalletConnectButton để hiển thị trạng thái ví và tùy chọn kết nối.

Yêu cầu

  • 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
  1. Lấy dữ liệu ví từ store Zustand.
  2. Hiển thị điều kiện theo trạng thái kết nối.
  3. Thêm event onClick để gọi connect() hoặc disconnect().

Đáp án

"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

Đề bài

Xây dựng trang chủ / hiển thị danh sách NFT mẫu.

Yêu cầ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
  1. Tạo component NFTCard trong components/nft/.
  2. Dữ liệu có thể là một mảng tĩnh trong data/nfts.ts.
  3. Render danh sách NFT trên trang app/page.tsx.

Đáp án

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

Đề bài

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.

Yêu cầu

  • 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
  1. Sử dụng router động [unit].tsx để lấy dữ liệu NFT theo ID.
  2. Lấy dữ liệu ví hiện tại từ store useWallet.
  3. So sánh owner và address để xác định quyền hiển thị.

Đáp án

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>
  );
}