在当今的数字环境中,随着实时在线市场和拍卖的出现,买卖艺术取得了显着的飞跃。现场拍卖是一个实时的交互式竞价过程,通过数字平台进行。参与者在规定的时间范围内对商品或服务进行实时出价,拍卖结束时出价最高的人将赢得被拍卖的物品或服务。
我们希望帮助人们使用 Dyte 创造引人入胜的现场体验,而现场拍卖恰好在市场上。通过将技术与实时竞价相结合,在这篇博客中,我们将深入研究要点,并带您了解在落槌之前构建和运行自己的现场拍卖平台的应用程序流程。
让我们在竞标开始前发现待售产品!
为什么要建立现场拍卖平台?
方便:随时随地参与现场拍卖。
实时竞价:实时体验与其他爱好者竞标的快感。
种类繁多的项目:发现各种物品,从收藏品到艺术品等等。
无缝集成:现场拍卖应用程序通过 Dyte 可靠的音频/视频会议和可定制的 UI 为用户提供流畅、不间断的拍卖体验。
准备工作
构建此应用程序需要具备React.js的基本知识。
请确保您的机器上安装了Node.js。我们将使用它来运行我们的应用程序。
最后,在调用 Dyte 的 REST API 时,您需要开发人员门户中的 API 密钥和组织 ID 来验证自己的身份。
搭建实时竞价平台
安装
您需要安装 Dyte 的 React UI Kit 和 Core 包才能开始使用。您可以使用 npm 或 Yarn 来执行此操作。
npm install @dytesdk/react-ui-kit @dytesdk/react-web-core
开始
我们将首先从开发人员门户获取组织 ID 和 API 密钥。然后,我们将创建一个帐户并导航到 API 密钥页面。
请确保您没有在任何地方上传您的 API 密钥。
接下来,我们将使用以下 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 通过添加参与者 API 生成身份验证令牌。此身份验证令牌用于初始化 Dyte 客户端。让我们开始设置项目。
构建自定义 UI
创建一个文件。我们将使用钩子来初始化一个新的 Dyte 会议。用于将会议对象传递给此应用程序中的所有子组件。我们还将设置用于加入和离开房间的事件侦听器。src/App.tsx DyteMeeting 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>
)
}
现在,我们将构建一个设置屏幕;这是用户将看到的第一页。
创建一个文件。我们将更新用户的显示名称,并从此页面加入 Dyte 会议。src/pages/setupScreen/setupScreen.tsx
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://dyte-live-bidding.vercel.app/!Dyte 去一次,去两次…
您可以在此处查看该项目的完整代码。