format leduc
This commit is contained in:
parent
ee0c08c224
commit
d9aefc102e
|
@ -74,7 +74,7 @@ class DoudizhuGameView extends React.Component {
|
|||
console.log("Cannot find cards in move from player's hand");
|
||||
}
|
||||
gameInfo.considerationTime = this.initConsiderationTime;
|
||||
gameInfo.completedPercent = (this.state.gameInfo.turn + 1) * 100.0 / this.moveHistory.length;
|
||||
gameInfo.completedPercent += 100.0 / this.moveHistory.length;
|
||||
}else {
|
||||
console.log("Mismatched current player index");
|
||||
}
|
||||
|
@ -283,7 +283,7 @@ class DoudizhuGameView extends React.Component {
|
|||
|
||||
render(){
|
||||
let sliderValueText = (value) => {
|
||||
return `${value}°C`;
|
||||
return value;
|
||||
};
|
||||
const gameSpeedMarks = [
|
||||
{
|
||||
|
|
|
@ -4,8 +4,20 @@ import '../assets/gameview.scss';
|
|||
import {LeducHoldemGameBoard} from '../components/GameBoard';
|
||||
import {deepCopy} from "../utils";
|
||||
|
||||
import { Button, Layout } from 'element-react';
|
||||
import { Layout } from 'element-react';
|
||||
import Slider from '@material-ui/core/Slider';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup';
|
||||
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
|
||||
import PauseCircleOutlineRoundedIcon from '@material-ui/icons/PauseCircleOutlineRounded';
|
||||
import ReplayRoundedIcon from '@material-ui/icons/ReplayRounded';
|
||||
import NotInterestedIcon from '@material-ui/icons/NotInterested';
|
||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||
import SkipNextIcon from '@material-ui/icons/SkipNext';
|
||||
import SkipPreviousIcon from '@material-ui/icons/SkipPrevious';
|
||||
|
||||
class LeducHoldemGameView extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -17,8 +29,10 @@ class LeducHoldemGameView extends React.Component {
|
|||
this.gameStateTimeout = null;
|
||||
this.apiUrl = window.g.apiUrl;
|
||||
this.moveHistory = [];
|
||||
|
||||
this.moveHistoryTotalLength = null;
|
||||
this.gameStateHistory = [[],[]];
|
||||
this.initGameState = {
|
||||
gameStatus: "ready", // "ready", "playing", "paused", "over"
|
||||
playerInfo: [],
|
||||
hands: [],
|
||||
latestAction: ["", ""],
|
||||
|
@ -28,142 +42,353 @@ class LeducHoldemGameView extends React.Component {
|
|||
pot: [1, 1],
|
||||
publicCard: "",
|
||||
currentPlayer: null,
|
||||
considerationTime: this.initConsiderationTime
|
||||
considerationTime: this.initConsiderationTime,
|
||||
completedPercent: 0,
|
||||
};
|
||||
this.state = {
|
||||
gameInfo: this.initGameState,
|
||||
gameStateLoop: null,
|
||||
considerationTimeSetting: this.initConsiderationTime
|
||||
gameSpeed: 0
|
||||
}
|
||||
}
|
||||
|
||||
retrieveReplayData(){
|
||||
// for test use
|
||||
const replayId = 0;
|
||||
|
||||
// reset game state
|
||||
this.setState({gameInfo: this.initGameState});
|
||||
|
||||
axios.get(`${this.apiUrl}/replay/leduc_holdem/${replayId}`)
|
||||
.then(res => {
|
||||
res = res.data;
|
||||
this.moveHistory = res.moveHistory;
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
gameInfo.hands = res.initHands;
|
||||
gameInfo.playerInfo = res.playerInfo;
|
||||
gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx;
|
||||
gameInfo.publicCard = res.publicCard;
|
||||
this.setState({gameInfo: gameInfo}, ()=>{
|
||||
this.startReplay();
|
||||
});
|
||||
})
|
||||
.catch(err=>{
|
||||
console.log("err", err);
|
||||
})
|
||||
}
|
||||
|
||||
startReplay(){
|
||||
if(this.gameStateTimeout){
|
||||
window.clearTimeout(this.gameStateTimeout);
|
||||
this.gameStateTimeout = null;
|
||||
generateNewState(){
|
||||
// console.log(this.state.gameInfo.latestAction);
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
const turn = this.state.gameInfo.turn;
|
||||
if(turn >= this.moveHistory[this.state.gameInfo.round].length){
|
||||
if(this.state.gameInfo.round === 0){
|
||||
// todo: if it's the first round, then reveal the public card and start the second round
|
||||
gameInfo.turn = 0;
|
||||
gameInfo.round = 1;
|
||||
gameInfo.latestAction = ["", ""];
|
||||
gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx;
|
||||
gameInfo.considerationTime = this.initConsiderationTime;
|
||||
// gameInfo.completedPercent += 100.0 / this.moveHistoryTotalLength;
|
||||
this.setState({ gameInfo: gameInfo });
|
||||
}else{
|
||||
gameInfo.gameStatus = "over";
|
||||
this.setState({ gameInfo: gameInfo });
|
||||
setTimeout(()=>{
|
||||
// TODO: show winner
|
||||
alert("Game Ends!");
|
||||
}, 200);
|
||||
return gameInfo;
|
||||
}
|
||||
}else{
|
||||
if(gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx){
|
||||
gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move;
|
||||
switch (gameInfo.latestAction[gameInfo.currentPlayer]) {
|
||||
case "Check":
|
||||
break;
|
||||
case "Raise":
|
||||
gameInfo.pot[gameInfo.currentPlayer] += (gameInfo.round+1) * 2;
|
||||
break;
|
||||
case "Call":
|
||||
// the upstream player must have bet more
|
||||
if(gameInfo.pot[(gameInfo.currentPlayer+2-1)%2] > gameInfo.pot[gameInfo.currentPlayer]){
|
||||
gameInfo.pot[gameInfo.currentPlayer] = gameInfo.pot[(gameInfo.currentPlayer+2-1)%2];
|
||||
}else{
|
||||
console.log("Current player choose call but has bet more or equal to the upstream player");
|
||||
}
|
||||
break;
|
||||
case "Fold":
|
||||
// if one player folds, game ends
|
||||
const foldedFound = gameInfo.playerInfo.find(element=>{return element.index === gameInfo.currentPlayer});
|
||||
const foldedId = foldedFound ? foldedFound.id : -1;
|
||||
const winnerFound = gameInfo.playerInfo.find(element=>{return element.index === (gameInfo.currentPlayer+1)%2});
|
||||
const winnerId = winnerFound ? winnerFound.id : -1;
|
||||
gameInfo.gameStatus = "over";
|
||||
this.setState({ gameInfo: gameInfo });
|
||||
setTimeout(()=>{
|
||||
alert(`Player ${foldedId} folded, player ${winnerId} wins!`);
|
||||
}, 200);
|
||||
return gameInfo;
|
||||
default:
|
||||
console.log("Error in player's latest action");
|
||||
}
|
||||
gameInfo.turn++;
|
||||
gameInfo.currentPlayer = (gameInfo.currentPlayer+1)%2;
|
||||
gameInfo.considerationTime = this.initConsiderationTime;
|
||||
gameInfo.completedPercent += 100.0 / this.moveHistoryTotalLength;
|
||||
this.setState({ gameInfo: gameInfo });
|
||||
}else{
|
||||
console.log("Mismatch in current player & move history");
|
||||
}
|
||||
}
|
||||
// loop to update game state
|
||||
this.gameStateTimer();
|
||||
// if current state is new to game state history, push it to the game state history array
|
||||
if(gameInfo.turn === this.gameStateHistory[gameInfo.round].length){
|
||||
this.gameStateHistory[gameInfo.round].push(gameInfo);
|
||||
}else{
|
||||
console.log("inconsistent game state history length and turn number");
|
||||
}
|
||||
return gameInfo;
|
||||
}
|
||||
|
||||
gameStateTimer(){
|
||||
this.gameStateTimeout = setTimeout(()=>{
|
||||
let currentConsiderationTime = this.state.gameInfo.considerationTime;
|
||||
if(currentConsiderationTime > 0) {
|
||||
currentConsiderationTime -= this.considerationTimeDeduction;
|
||||
currentConsiderationTime -= this.considerationTimeDeduction * Math.pow(2, this.state.gameSpeed);
|
||||
currentConsiderationTime = currentConsiderationTime < 0 ? 0 : currentConsiderationTime;
|
||||
if(currentConsiderationTime === 0 && this.state.gameSpeed < 2){
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
gameInfo.toggleFade = "fade-out";
|
||||
this.setState({gameInfo: gameInfo});
|
||||
}
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
gameInfo.considerationTime = currentConsiderationTime;
|
||||
this.setState({gameInfo: gameInfo});
|
||||
this.gameStateTimer();
|
||||
}else{
|
||||
console.log(this.state.gameInfo.latestAction);
|
||||
const turn = this.state.gameInfo.turn;
|
||||
if(turn >= this.moveHistory[this.state.gameInfo.round].length){
|
||||
if(this.state.gameInfo.round === 0){
|
||||
// todo: if it's the first round, then reveal the public card and start the second round
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
gameInfo.turn = 0;
|
||||
gameInfo.round = 1;
|
||||
gameInfo.latestAction = ["", ""];
|
||||
gameInfo.currentPlayer = this.moveHistory[1][0].playerIdx;
|
||||
gameInfo.considerationTime = this.state.considerationTimeSetting;
|
||||
this.setState({gameInfo: gameInfo}, ()=>{
|
||||
this.gameStateTimer();
|
||||
});
|
||||
}else{
|
||||
alert('Game Ends!');
|
||||
}
|
||||
}else{
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
if(gameInfo.currentPlayer === this.moveHistory[gameInfo.round][gameInfo.turn].playerIdx){
|
||||
gameInfo.latestAction[gameInfo.currentPlayer] = this.moveHistory[gameInfo.round][gameInfo.turn].move;
|
||||
switch (gameInfo.latestAction[gameInfo.currentPlayer]) {
|
||||
case "Check":
|
||||
break;
|
||||
case "Raise":
|
||||
gameInfo.pot[gameInfo.currentPlayer] += (gameInfo.round+1) * 2;
|
||||
break;
|
||||
case "Call":
|
||||
// the upstream player must have bet more
|
||||
if(gameInfo.pot[(gameInfo.currentPlayer+2-1)%2] > gameInfo.pot[gameInfo.currentPlayer]){
|
||||
gameInfo.pot[gameInfo.currentPlayer] = gameInfo.pot[(gameInfo.currentPlayer+2-1)%2];
|
||||
}else{
|
||||
console.log("Current player choose call but has bet more or equal to the upstream player");
|
||||
}
|
||||
break;
|
||||
case "Fold":
|
||||
// if one player folds, game ends
|
||||
const foldedFound = gameInfo.playerInfo.find(element=>{return element.index === gameInfo.currentPlayer});
|
||||
const foldedId = foldedFound ? foldedFound.id : -1;
|
||||
const winnerFound = gameInfo.playerInfo.find(element=>{return element.index === (gameInfo.currentPlayer+1)%2});
|
||||
const winnerId = winnerFound ? winnerFound.id : -1;
|
||||
alert(`Player ${foldedId} folded, player ${winnerId} wins!`);
|
||||
return ;
|
||||
default:
|
||||
console.log("Error in player's latest action");
|
||||
}
|
||||
gameInfo.turn++;
|
||||
gameInfo.currentPlayer = (gameInfo.currentPlayer+1)%2;
|
||||
gameInfo.considerationTime = this.state.considerationTimeSetting;
|
||||
this.setState({gameInfo: gameInfo}, ()=>{
|
||||
this.gameStateTimer();
|
||||
});
|
||||
}else{
|
||||
console.log("Mismatch in current player & move history");
|
||||
}
|
||||
let gameInfo = this.generateNewState();
|
||||
if(gameInfo.gameStatus == "over") return;
|
||||
this.gameStateTimer();
|
||||
gameInfo.gameStatus = "playing";
|
||||
if(this.state.gameInfo.toggleFade === "fade-out") {
|
||||
gameInfo.toggleFade = "fade-in";
|
||||
}
|
||||
this.setState({gameInfo: gameInfo}, ()=>{
|
||||
// toggle fade in
|
||||
if(this.state.gameInfo.toggleFade !== ""){
|
||||
setTimeout(()=>{
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
gameInfo.toggleFade = "";
|
||||
this.setState({gameInfo: gameInfo});
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, this.considerationTimeDeduction);
|
||||
}
|
||||
|
||||
startReplay() {
|
||||
// for test use
|
||||
const replayId = 0;
|
||||
|
||||
axios.get(`${this.apiUrl}/replay/leduc_holdem/${replayId}`)
|
||||
.then(res => {
|
||||
res = res.data;
|
||||
// init replay info
|
||||
this.moveHistory = res.moveHistory;
|
||||
this.moveHistoryTotalLength = this.moveHistory.reduce((count, round) => count + round.length, 0);
|
||||
let gameInfo = deepCopy(this.initGameState);
|
||||
gameInfo.gameStatus = "playing";
|
||||
gameInfo.playerInfo = res.playerInfo;
|
||||
gameInfo.hands = res.initHands;
|
||||
gameInfo.currentPlayer = res.moveHistory[0][0].playerIdx;
|
||||
gameInfo.publicCard = res.publicCard;
|
||||
this.gameStateHistory[gameInfo.round].push(gameInfo);
|
||||
this.setState({gameInfo: gameInfo}, ()=>{
|
||||
if(this.gameStateTimeout){
|
||||
window.clearTimeout(this.gameStateTimeout);
|
||||
this.gameStateTimeout = null;
|
||||
}
|
||||
// loop to update game state
|
||||
this.gameStateTimer();
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
pauseReplay(){
|
||||
if(this.gameStateTimeout){
|
||||
window.clearTimeout(this.gameStateTimeout);
|
||||
this.gameStateTimeout = null;
|
||||
}
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
gameInfo.gameStatus = "paused";
|
||||
this.setState({ gameInfo: gameInfo });
|
||||
}
|
||||
|
||||
resumeReplay(){
|
||||
this.gameStateTimer();
|
||||
let gameInfo = deepCopy(this.state.gameInfo);
|
||||
gameInfo.gameStatus = "playing";
|
||||
this.setState({ gameInfo: gameInfo });
|
||||
}
|
||||
|
||||
changeGameSpeed(newVal){
|
||||
this.setState({gameSpeed: newVal});
|
||||
}
|
||||
|
||||
gameStatusButton(status){
|
||||
switch (status) {
|
||||
case "ready":
|
||||
return <Button className={"status-button"} variant={"contained"} startIcon={<PlayArrowRoundedIcon />} color="primary" onClick={()=>{this.startReplay()}}>Start</Button>;
|
||||
case "playing":
|
||||
return <Button className={"status-button"} variant={"contained"} startIcon={<PauseCircleOutlineRoundedIcon />} color="secondary" onClick={()=>{this.pauseReplay()}}>Pause</Button>;
|
||||
case "paused":
|
||||
return <Button className={"status-button"} variant={"contained"} startIcon={<PlayArrowRoundedIcon />} color="primary" onClick={()=>{this.resumeReplay()}}>Resume</Button>;
|
||||
case "over":
|
||||
return <Button className={"status-button"} variant={"contained"} startIcon={<ReplayRoundedIcon />} color="primary" onClick={()=>{this.startReplay()}}>Restart</Button>;
|
||||
default:
|
||||
alert(`undefined game status: ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
go2PrevGameState() {
|
||||
let gameInfo = null;
|
||||
if(this.state.gameInfo.turn === 0 && this.state.gameInfo.round !== 0){
|
||||
let prevRound = this.gameStateHistory[this.state.gameInfo.round-1];
|
||||
gameInfo = deepCopy(prevRound[prevRound.length-1]);
|
||||
}else{
|
||||
gameInfo = deepCopy(this.gameStateHistory[this.state.gameInfo.round][this.state.gameInfo.turn - 1]);
|
||||
}
|
||||
gameInfo.gameStatus = "paused";
|
||||
gameInfo.toggleFade = "";
|
||||
this.setState({gameInfo: gameInfo});
|
||||
}
|
||||
|
||||
go2NextGameState() {
|
||||
let gameInfo = this.generateNewState();
|
||||
if(gameInfo.gameStatus === "over") {
|
||||
this.setState({gameInfo: gameInfo});
|
||||
}else{
|
||||
gameInfo.gameStatus = "paused";
|
||||
gameInfo.toggleFade = "";
|
||||
this.setState({gameInfo: gameInfo});
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
let sliderValueText = (value) => {
|
||||
return value;
|
||||
};
|
||||
const gameSpeedMarks = [
|
||||
{
|
||||
value: -3,
|
||||
label: 'x0.125',
|
||||
},
|
||||
{
|
||||
value: -2,
|
||||
label: 'x0.25',
|
||||
},
|
||||
{
|
||||
value: -1,
|
||||
label: 'x0.5',
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
label: 'x1',
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: 'x2',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: 'x4',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: 'x8',
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{width: "960px", height: "540px"}}>
|
||||
<LeducHoldemGameBoard
|
||||
playerInfo={this.state.gameInfo.playerInfo}
|
||||
hands={this.state.gameInfo.hands}
|
||||
latestAction={this.state.gameInfo.latestAction}
|
||||
mainPlayerId={this.state.gameInfo.mainViewerId}
|
||||
currentPlayer={this.state.gameInfo.currentPlayer}
|
||||
considerationTime={this.state.gameInfo.considerationTime}
|
||||
round={this.state.gameInfo.round}
|
||||
turn={this.state.gameInfo.turn}
|
||||
pot={this.state.gameInfo.pot}
|
||||
publicCard={this.state.gameInfo.publicCard}
|
||||
/>
|
||||
<div className={"leduc-view-container"}>
|
||||
<Layout.Row style={{"height": "540px"}}>
|
||||
<Layout.Col style={{"height": "100%"}} span="17">
|
||||
<div style={{"height": "100%"}}>
|
||||
<Paper className={"leduc-gameboard-paper"} elevation={3}>
|
||||
<LeducHoldemGameBoard
|
||||
playerInfo={this.state.gameInfo.playerInfo}
|
||||
hands={this.state.gameInfo.hands}
|
||||
latestAction={this.state.gameInfo.latestAction}
|
||||
mainPlayerId={this.state.gameInfo.mainViewerId}
|
||||
currentPlayer={this.state.gameInfo.currentPlayer}
|
||||
considerationTime={this.state.gameInfo.considerationTime}
|
||||
round={this.state.gameInfo.round}
|
||||
turn={this.state.gameInfo.turn}
|
||||
pot={this.state.gameInfo.pot}
|
||||
publicCard={this.state.gameInfo.publicCard}
|
||||
/>
|
||||
</Paper>
|
||||
</div>
|
||||
</Layout.Col>
|
||||
<Layout.Col span="7" style={{"height": "100%"}}>
|
||||
<Paper className={"leduc-probability-paper"} elevation={3}>
|
||||
<div className={"probability-player"}>
|
||||
{
|
||||
this.state.gameInfo.playerInfo.length > 0 ?
|
||||
<span>Current Player: {this.state.gameInfo.currentPlayer}</span>
|
||||
:
|
||||
<span>Waiting...</span>
|
||||
}
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={"probability-table"}>
|
||||
<div className={"probability-item"}>
|
||||
{/* {this.computeProbabilityItem(0)} */}
|
||||
</div>
|
||||
<div className={"probability-item"}>
|
||||
{/* {this.computeProbabilityItem(1)} */}
|
||||
</div>
|
||||
<div className={"probability-item"}>
|
||||
{/* {this.computeProbabilityItem(2)} */}
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</Layout.Col>
|
||||
</Layout.Row>
|
||||
<div className="progress-bar">
|
||||
<LinearProgress variant="determinate" value={this.state.gameInfo.completedPercent} />
|
||||
</div>
|
||||
<div className="game-controller">
|
||||
<Layout.Row>
|
||||
<Layout.Col span="24">
|
||||
<Button type="primary" onClick={()=>{this.retrieveReplayData()}}>Start</Button>
|
||||
</Layout.Col>
|
||||
</Layout.Row>
|
||||
<Paper className={"game-controller-paper"} elevation={3}>
|
||||
<Layout.Row style={{"height": "51px"}}>
|
||||
<Layout.Col span="7" style={{"height": "51px", "lineHeight": "48px"}}>
|
||||
<div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={this.state.gameInfo.gameStatus !== "paused" || (this.state.gameInfo.round === 0 && this.state.gameInfo.turn === 0)}
|
||||
onClick={()=>{this.go2PrevGameState()}}
|
||||
>
|
||||
<SkipPreviousIcon />
|
||||
</Button>
|
||||
{ this.gameStatusButton(this.state.gameInfo.gameStatus) }
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={this.state.gameInfo.gameStatus !== "paused"}
|
||||
onClick={()=>{this.go2NextGameState()}}
|
||||
>
|
||||
<SkipNextIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</Layout.Col>
|
||||
<Layout.Col span="1" style={{"height": "100%", "width": "1px"}}>
|
||||
<Divider orientation="vertical" />
|
||||
</Layout.Col>
|
||||
<Layout.Col span="3" style={{"height": "51px", "lineHeight": "51px", "marginLeft": "-1px", "marginRight": "-1px"}}>
|
||||
<div style={{"textAlign": "center"}}>{`Turn: ${this.state.gameInfo.turn}`}</div>
|
||||
</Layout.Col>
|
||||
<Layout.Col span="1" style={{"height": "100%", "width": "1px"}}>
|
||||
<Divider orientation="vertical" />
|
||||
</Layout.Col>
|
||||
<Layout.Col span="14">
|
||||
<div>
|
||||
<label className={"form-label-left"}>Game Speed</label>
|
||||
<div style={{"marginLeft": "100px", "marginRight": "10px"}}>
|
||||
<Slider
|
||||
value={this.state.gameSpeed}
|
||||
getAriaValueText={sliderValueText}
|
||||
onChange={(e, newVal)=>{this.changeGameSpeed(newVal)}}
|
||||
aria-labelledby="discrete-slider-custom"
|
||||
step={1}
|
||||
min={-3}
|
||||
max={3}
|
||||
track={false}
|
||||
valueLabelDisplay="off"
|
||||
marks={gameSpeedMarks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Col>
|
||||
</Layout.Row>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue