logo

使用React和Dyte构建实时拍卖平台

255
2023年11月16日
学习如何使用React和Dyte构建一个实时拍卖平台,以便积极参与实时拍卖,与拍卖师互动,并进行实时出价。

在当今的数字时代,随着实时在线市场和拍卖的出现,买卖艺术已经取得了显著的进步。实时拍卖是通过数字平台实现的互动竞标过程。参与者在规定的时间内对商品或服务进行实时竞标,拍卖结束时最高出价者获得被拍卖的物品或服务。

我们希望通过Dyte帮助人们创建引人入胜的实时体验,而实时拍卖恰好就在市场上。通过将技术与实时竞标相结合,在本文中,我们将深入探讨基本要素,并带领您了解在锤子落下之前如何构建和运行自己的实时拍卖平台的应用流程。

让我们在竞标开始之前发现待售的产品!

为什么要构建实时拍卖平台?

  • 便捷: 随时随地参与实时拍卖。
  • 实时竞标: 体验与其他爱好者实时竞标的刺激。
  • 丰富的物品范围: 发现各种物品,从收藏品到艺术品等等。
  • 无缝集成: 实时拍卖应用通过Dyte可靠的音视频会议和可定制的用户界面为用户提供流畅且不间断的拍卖体验。

开始之前

  • 需要基本的React.js知识来构建此应用程序。
  • 请确保您的计算机上安装了Node.js。我们将使用它来运行我们的应用程序。
  • 最后,您需要从开发者门户获取API密钥和组织ID,以便在调用Dyte的REST API时进行身份验证。

构建实时竞标平台

安装

您需要安装Dyte的React UI Kit和Core包才能开始。您可以使用npm或Yarn来执行此操作。

npm install @dytesdk/react-ui-kit @dytesdk/react-web-core

入门

我们将首先从开发者门户获取组织ID和API密钥。然后,我们将创建一个帐户并转到API密钥页面。

请确保您不要在任何地方上传您的API密钥。

API Keys page

接下来,我们将使用以下Rest API创建一个会议。以下是一个示例响应。

{
  "success": true,
  "data": {
    "id": "497f6eca-6276-4993-bfeb-53cbbbbaxxxx",
    "name": "string",
    "picture": "<http://example.com>",
    "custom_participant_id": "string",
    "preset_name": "string",
    "created_at": "2019-08-24T14:15:22Z",
    "updated_at": "2019-08-24T14:15:22Z",
    "token": "string"
  }
}

我们使用载荷中的ID使用Add Participants API生成身份验证令牌。此身份验证令牌用于初始化Dyte客户端。让我们开始设置项目。

构建您的自定义用户界面

创建一个文件 src/App.tsx。我们将使用DyteMeeting钩子来初始化一个新的Dyte会议。DyteProvider用于将会议对象传递给此应用程序中所有子组件。我们还将设置加入和离开房间的事件监听器。

import { useEffect, useState } from 'react';
import { DyteProvider, useDyteClient } from '@dytesdk/react-web-core';
import { LoadingScreen } from './pages';
import { Meeting, SetupScreen } from './pages';

function App() {
  const [meeting, initMeeting] = useDyteClient();
  const [roomJoined, setRoomJoined] = useState<boolean>(false);

  useEffect(() => {
    const searchParams = new URL(window.location.href).searchParams;
    const authToken = searchParams.get('authToken');

    if (!authToken) {
      alert(
        "An authToken wasn't passed, please pass an authToken in the URL query to join a meeting."
      );
      return;
    }

    initMeeting({
      authToken,
      defaults: {
        audio: false,
        video: false,
      },
    });
  }, []);

  useEffect(() => {
    if (!meeting) return;

    const roomJoinedListener = () => {
      setRoomJoined(true);
    };
    const roomLeftListener = () => {
      setRoomJoined(false);
    };
    meeting.self.on('roomJoined', roomJoinedListener);
    meeting.self.on('roomLeft', roomLeftListener);

    return () => {
      meeting.self.removeListener('roomJoined', roomJoinedListener);
      meeting.self.removeListener('roomLeft', roomLeftListener);
    }

  }, [meeting])

  return (
    <DyteProvider value={meeting} fallback={<LoadingScreen />}>
      {
        !roomJoined ? <SetupScreen /> : <Meeting />
      }
    </DyteProvider>
  )
}

现在,我们将构建一个设置屏幕;这是用户将看到的第一个页面。

创建一个文件 src/pages/setupScreen/setupScreen.tsx。我们将从此页面更新用户的显示名称并加入Dyte会议。

import { useEffect, useState } from 'react'
import './setupScreen.css'
import { useDyteMeeting } from '@dytesdk/react-web-core';
import {
  DyteAudioVisualizer,
  DyteAvatar,
  DyteCameraToggle,
  DyteMicToggle,
  DyteNameTag,
  DyteParticipantTile,
} from '@dytesdk/react-ui-kit';

const SetupScreen = () => {
  const { meeting } = useDyteMeeting();
  const [isHost, setIsHost] = useState<boolean>(false);
  const [name, setName] = useState<string>('');

  useEffect(() => {
    if (!meeting) return;
    const preset = meeting.self.presetName;
    const name = meeting.self.name;
    setName(name);

    if (preset.includes('host')) {
      setIsHost(true);
    }
  }, [meeting])

  const joinMeeting = () => {
    meeting?.self.setName(name);
    meeting.joinRoom();
  }

  return (
    <div className='setup-screen'>
      <div className="setup-media">
        <div className="video-container">
          <DyteParticipantTile meeting={meeting} participant={meeting.self}>
            <DyteAvatar size="md" participant={meeting.self}/>
            <DyteNameTag meeting={meeting} participant={meeting.self}>
              <DyteAudioVisualizer size='sm' slot="start" participant={meeting.self} />
            </DyteNameTag>
            <div className='setup-media-controls'>
              <DyteMicToggle size="sm" meeting={meeting}/>
              <DyteCameraToggle size="sm" meeting={meeting}/>
            </div>
          </DyteParticipantTile>
        </div>
      </div>
      <div className="setup-information">
        <div className="setup-content">
          <h2>Welcome! {name}</h2>
          <p>{isHost ? 'You are joining as a Host' : 'You are joining as a bidder'}</p>
          <input disabled={!meeting.self.permissions.canEditDisplayName ?? false} className='setup-name' value={name} onChange={(e) => {
            setName(e.target.value)
          }} />
          <button className='setup-join' onClick={joinMeeting}>
            Join Meeting
          </button>
        </div>
      </div>
    </div>
  )
}

export default SetupScreen

现在,我们已经完成了基本设置,让我们构建实时拍卖平台。

创建一个文件 src/pages/meeting/Meeting.tsx

我们的实时拍卖应用将实现以下功能:

  • 为主持人提供开始/停止拍卖的选项。
  • 为主持人提供在不同拍卖产品之间导航的选项。
  • 允许用户为每个产品进行实时竞标。
  • 向所有用户显示最高出价。
import { useEffect, useState } from 'react'
import './meeting.css'
import {
  DyteCameraToggle,
  DyteChatToggle,
  DyteGrid,
  DyteHeader,
  DyteLeaveButton,
  DyteMicToggle,
  DyteNotifications,
  DyteParticipantsAudio,
  DyteSidebar,
  sendNotification,
} from '@dytesdk/react-ui-kit'
import { useDyteMeeting } from '@dytesdk/react-web-core';
import { AuctionControlBar, Icon } from '../../components';
import { bidItems } from '../../constants';

interface Bid {
  bid: number;
  user: string;
}

const Meeting = () => {
  const { meeting } = useDyteMeeting();

  const [item, setItem] = useState(0);
  const [isHost, setIsHost] = useState<boolean>(false);
  const [showPopup, setShowPopup] = useState<boolean>(true);
  const [auctionStarted, setAuctionStarted] = useState<boolean>(false);
  const [activeSidebar, setActiveSidebar] = useState<boolean>(false);
  const [highestBid, setHighestBid] = useState<Bid>({ bid: 100, user: 'default' });

  const handlePrev = () => {
    if (item - 1 < 0) return;
    setItem(item - 1)
    meeting.participants.broadcastMessage('item-changed', { item: item - 1 })
  }
  const handleNext = () => {
    if ( item + 1 >= bidItems.length) return;
    setItem(item + 1)
    meeting.participants.broadcastMessage('item-changed', { item: item + 1 })
  }

  useEffect(() => {
    setHighestBid({
      bid: bidItems[item].startingBid,
      user: 'default'
    })
  }, [item])

  useEffect(() => {
    if (!meeting) return;

    const preset = meeting.self.presetName;
    if (preset.includes('host')) {
      setIsHost(true);
    }

    const handleBroadcastedMessage = ({ type, payload }: { type: string, payload: any }) => {
      switch(type) {
        case 'auction-toggle': {
          setAuctionStarted(payload.started);
          break;
        }
        case 'item-changed': {
          setItem(payload.item);
          break;
        }
        case 'new-bid': {
          sendNotification({
            id: 'new-bid',
            message: `${payload.user} just made a bid of $ ${payload.bid}!`,
            duration: 2000,
          })
          if (parseFloat(payload.bid) > highestBid.bid) setHighestBid(payload)
          break;
        }
        default:
          break;
      }
    }
    meeting.participants.on('broadcastedMessage', handleBroadcastedMessage);

    const handleDyteStateUpdate = ({detail}: any) => {
        if (detail.activeSidebar) {
         setActiveSidebar(true);
        } else {
          setActiveSidebar(false);
        }
    }

    document.body.addEventListener('dyteStateUpdate', handleDyteStateUpdate);

    return () => {
      document.body.removeEventListener('dyteStateUpdate', handleDyteStateUpdate);
      meeting.participants.removeListener('broadcastedMessage', handleBroadcastedMessage);
    }
  }, [meeting])

  useEffect(() => {
    const participantJoinedListener = () => {
      if (!auctionStarted) return;
      setTimeout(() => {
        meeting.participants.broadcastMessage('auction-toggle', {
          started: auctionStarted
        })
      }, 500)
    
    }
    meeting.participants.joined.on('participantJoined', participantJoinedListener);
    return () => {
      meeting.participants.joined.removeListener('participantJoined', participantJoinedListener);
    }
  }, [meeting, auctionStarted])

  const toggleAuction = () => {
    if (!isHost) return;
    meeting.participants.broadcastMessage('auction-toggle', {
      started: !auctionStarted
    })
    if (!auctionStarted) {
      meeting.self.pin();
    } else {
      meeting.self.unpin();
    }
    setAuctionStarted(!auctionStarted);
  }

  return (
    <div className='meeting-container'>
      <DyteParticipantsAudio meeting={meeting} />
      <DyteNotifications meeting={meeting} />

      <DyteHeader meeting={meeting} size='lg'>
        <div className="meeting-header">
          {
            auctionStarted && (
              <div className="show-auction-popup" onClick={() => setShowPopup(() => !showPopup)}>
                <Icon size='sm' icon={showPopup ? 'close' : 'next'} />
              </div>
            )
          }
        </div>
      </DyteHeader>

      <div className='meeting-grid'>
        {
          auctionStarted && (
            <div className={`auction-container ${!showPopup ? 'hide-auction-popup' : ''}`}>
              <img className='auction-img' src={bidItems[item].link} />
              <div className='auction-desc'>
                {bidItems[item].description}
              </div>
              <AuctionControlBar
                item={item}
                highestBid={highestBid}
                handleNext={handleNext}
                handlePrev={handlePrev}
                isHost={isHost}
              />
          </div>
          )
        }
        <DyteGrid layout='column' meeting={meeting} style={{ height: '100%' }}/>
        {activeSidebar && <DyteSidebar meeting={meeting} />}
      </div>

      <div className='meeting-controlbar'>
        <DyteMicToggle size='md' meeting={meeting} />
        <DyteCameraToggle size='md'  meeting={meeting} />
        <DyteLeaveButton size='md' />
        <DyteChatToggle size='md' meeting={meeting} />
        {
          isHost && (
            <button className='auction-toggle-button' onClick={toggleAuction}>
              <Icon size='lg' icon='auction' />
              {auctionStarted ? 'Stop' : 'Start'} Auction
            </button>
          )
        }
      </div>
    </div>
  )
}

export default Meeting

原文输出!

本文链接:https://www.iokks.com/art/be9e1c6a4d25
本博客所有文章除特别声明外,均采用CC BY 4.0 CN协议 许可协议。转载请注明出处!