A Weekend Adventure
Liad YosefÂ
If you build it, they will come
Wakanda Forever
const game = { cards: 5, players: 4 }
if(firebase.ready() === true) {
videoChat.run()
}
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
npx create-react-app splendor
.root {
background: url('./assets/table.png');
...
}
.root {
background: url('./assets/table.png');
...
}
.main {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"game hand hand"
"game players chat";
}
.gameTable {
grid-area: game;
}
.playerHand {
grid-area: hand;
}
.players {
grid-area: players;
}
.chat {
grid-area: chat;
}
function Main() {
return (
<div className={styles.main}>
<Game className={styles.gameTable}></Game>
<Hand className={styles.playerHand}></Hand>
<Players className={styles.players}></Players>
<Chat className={styles.chat}></Chat>
</div>
);
}
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
function Board({ nobles, cardArray, chips }) {
return <>
<Nobles nobles={nobles}/>
<Cards cardArray={cardArray}/>
<Chips chips={chips}/>
</>
}
function Cards({ cardArray }) {
return cardArray.map(({ cardData }) => <Card {...cardData} />);
}
function Card({ image, points, price }) {
const style = { backgroundImage: `url('${image}')` };
return (
<div className={styles.card} style={style}>
<div className={styles.cardPoints}>
{points}/{price}
</div>
</div>
);
}
function Board({ nobles, cardArray, chips }) {
return <>
<Nobles nobles={nobles}/>
<Cards cardArray={cardArray}/>
<Chips chips={chips}/>
</>
}
function Cards({ cardArray }) {
return cardArray.map(({ cardData }) => <Card {...cardData} />);
}
function Card({ image, points, price }) {
const style = { backgroundImage: `url('${image}')` };
return (
<div className={styles.card} style={style}>
<div className={styles.cardPoints}>
{points}/{price}
</div>
</div>
);
}
3
5
2
2
3
4
function Board({ nobles, cardArray, chips }) {
return <>
<Nobles nobles={nobles}/>
<Cards cardArray={cardArray}/>
<Chips chips={chips}/>
</>
}
function Cards({ cardArray }) {
return cardArray.map(({ cardData }) => <Card {...cardData} />);
}
function Card({ image, points, price }) {
const style = { backgroundImage: `url('${image}')` };
return (
<div className={styles.card} style={style}>
<div className={styles.cardPoints}>
{points}/{price}
</div>
</div>
);
}
3
5
2
2
3
4
0
1
0
2
0
4
function Board({ nobles, cardArray, chips }) {
return <>
<Nobles nobles={nobles}/>
<Cards cardArray={cardArray}/>
<Chips chips={chips}/>
</>
}
function Cards({ cardArray }) {
return cardArray.map(({ cardData }) => <Card {...cardData} />);
}
function Card({ image, points, price }) {
const style = { backgroundImage: `url('${image}')` };
return (
<div className={styles.card} style={style}>
<div className={styles.cardPoints}>
{points}/{price}
</div>
</div>
);
}
From where?
npm install --save firebase
import { init as initFirebase } '../firebase-storage';
function Main() {
useEffect(() => {
initFirebase();
}, []);
return <div className={styles.main}>
// our app
</div>
}
firebase
.database()
.ref('players/g1htz3/pic')
.set('https://www.imgbb.com/liadyosef.png');
Writing data
firebase
.database()
.ref('players/g1htz3/pic')
.on('value', snapshot => {
console.log(snapshot.val());
});
Reading data
firebase
.database()
.ref('players/g1htz3/pic')
.set('https://www.imgbb.com/liadyosef.png');
firebase
.database()
.ref()
.child('players/g1htz3/pic')
.set('https://www.imgbb.com/liadyosef.png');
firebase
.database()
.ref()
.child('players/g1htz3')
.update({
pic: 'https://www.imgbb.com/liadyosef.png'
});
firebase
.database()
.ref()
.child('settings/gameDate')
.set(firebase.database.ServerValue.TIMESTAMP);
firebase
.database()
.ref()
.child('hands/g1htz/cards')
.on('value', snapshot => {
console.log(snapshot.value())
});
export function changeValue(key, newValue, type = ChangeMode.SET) {
const ref = firebase.database().ref().child(key);
if(type === ChangeMode.UPDATE) {
return ref.update(newValue);
}
return ref.set(newValue);
}
export function onValue(key, callback) {
const unsubscribe = firebase.database()
.ref()
.child(key)
.on('value', (snapshot) => {
callback(snapshot?.val());
});
return unsubscribe;
}
import { changeValue, onValue } from '../firebase-storage';
// later...
changeValue(`settings/allowedPlayers`, 5);
changeValue(`players/${playerId}/pic`, newProfilePic);
onValue(`hands/${playerId}/chips`, chips => doSomething(chips));
3
5
2
2
3
4
0
1
0
2
0
4
0
1
0
2
0
4
3
5
2
2
3
4
export const Schema = shape({
tableData: shape({
cards: arrayOf([arrayOf([cardID])]),
chips: objectOf({
[chipColor]: int,
}),
}),
playerHands: objectOf({
[playerId]: shape({
cards: arrayOf([cardID]),
chips: objectOf({
[chipColor]: int,
}),
}),
}),
players: objectOf({
[userUUID]: shape({
name: string,
profilePic: string,
}),
}),
gameData: shape({
gameStage: oneOf(['OPEN', 'DONE']),
maxPlayers: int,
currentPlayer: userUUID,
}),
cards: objectOf({
[cardID]: shape({
image: string,
points: int,
gem: oneOf(['WHITE', 'RED', 'GREEN', 'BLUE', 'BROWN']),
price: int,
}),
}),
});
export const Schema = shape({
...
cards: objectOf({
[cardID]: shape({
image: string,
points: int,
gem: oneOf(['WHITE', 'RED', 'GREEN', 'BLUE', 'BROWN']),
price: int,
}),
}),
});
function Card({ cardId }) {
const [cardData, setCardData] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`cards/${cardId}`, card => {
setCardData(card);
})
return unsubscribe;
}, [cardId]);
return cardData ? <div className={styles.card}>
<img src={cardData.image}/>
{cardData.points} / {cardData.price}
</div> : null;
}
function Card({ cardId }) {
const [cardData, setCardData] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`cards/${cardId}`, card => {
setCardData(card);
})
return unsubscribe;
}, [cardId]);
return cardData ? <div className={styles.card}>
<img src={cardData.image}/>
{cardData.points} / {cardData.price}
</div> : null;
}
3
5
2
2
3
4
function Chip({ chipColor }) {
const [chipQuantity, setChipQuantity] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`tableData/chips/${chipColor}`, (chipNum) => {
setChipQuantity(chipNum);
});
return unsubscribe;
}, [chipColor]);
return (
<div className={styles.chip}>
<div className={styles.chipImage} style={{ color: chipColor }}></div>
<div className={styles.chipQuant}>{chipQuantity}</div>
</div>
);
}
function Card({ cardId }) {
const [cardData, setCardData] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`cards/${cardId}`, card => {
setCardData(card);
})
return unsubscribe;
}, [cardId]);
return cardData ? <div className={styles.card}>
<img src={cardData.image}/>
{cardData.points} / {cardData.price}
</div> : null;
}
function Chip({ chipColor }) {
const [chipQuantity, setChipQuantity] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`tableData/chips/${chipColor}`, (chipNum) => {
setChipQuantity(chipNum);
});
return unsubscribe;
}, [chipColor]);
return (
<div className={styles.chip}>
<div className={styles.chipImage} style={{ color: chipColor }}></div>
<div className={styles.chipQuant}>{chipQuantity}</div>
</div>
);
}
function CurrentPlayer() {
const [currentPlayer, setCurrentPlayer] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`gameData/currentPlayer`, (playerId) => {
setCurrentPlayer(playerId);
});
return unsubscribe;
}, []);
return <div className={styles.currentPlayerWrapper}>
`Current player is: ${getPlayerName(playerId)}`
</div>
}
function Card({ cardId }) {
const [cardData, setCardData] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`cards/${cardId}`, card => {
setCardData(card);
})
return unsubscribe;
}, [cardId]);
return cardData ? <div className={styles.card}>
<img src={cardData.image}/>
{cardData.points} / {cardData.price}
</div> : null;
}
function Chip({ chipColor }) {
const [chipQuantity, setChipQuantity] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`tableData/chips/${chipColor}`, (chipNum) => {
setChipQuantity(chipNum);
});
return unsubscribe;
}, [chipColor]);
return (
<div className={styles.chip}>
<div className={styles.chipImage} style={{ color: chipColor }}></div>
<div className={styles.chipQuant}>{chipQuantity}</div>
</div>
);
}
function CurrentPlayer() {
const [currentPlayer, setCurrentPlayer] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`gameData/currentPlayer`, (playerId) => {
setCurrentPlayer(playerId);
});
return unsubscribe;
}, []);
const changePlayer = useCallback((newPlayerId) => {
changeValue(`gameData/currentPlayer`, newPlayerId);
}, [])
return <div className={styles.currentPlayerWrapper}>
`Current player is: ${getPlayerName(playerId)}`
<button onClick={changePlayer}>Change</button>
</div>
}
export function useFirebase(key, initialValue, { type = ChangeMode.SET } = {}) {
const [valueObject, setValueObject] = useState({ value: initialValue, status: PENDING_VALUE });
const [defaultVal] = useState(initialValue);
useEffect(() => {
const unsubscribe = onValue(key, (snapshotVal) => {
let status, val;
if (snapshotVal === null || snapshotVal === undefined) {
status = MISSING_VALUE;
val = defaultVal;
} else {
status = FOUND_VALUE;
val = snapshotVal;
}
setValueObject({ value: val, status });
});
return unsubscribe;
}, [key, defaultVal]);
const changeFirebaseValue = useCallback(
(newValue, writeTo = key) => {
return changeValue(writeTo, newValue, type);
},
[key, type]
);
return [valueObject.value, changeFirebaseValue, valueObject.status];
}
function Card({ cardId }) {
const [cardData, setCardData] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`cards/${cardId}`, card => {
setCardData(card);
})
return unsubscribe;
}, [cardId]);
return cardData ? <div className={styles.card}>
<img src={cardData.image}/>
{cardData.points} / {cardData.price}
</div> : null;
}
function Chip({ chipColor }) {
const [chipQuantity, setChipQuantity] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`tableData/chips/${chipColor}`, (chipNum) => {
setChipQuantity(chipNum);
});
return unsubscribe;
}, [chipColor]);
return (
<div className={styles.chip}>
<div className={styles.chipImage} style={{ color: chipColor }}></div>
<div className={styles.chipQuant}>{chipQuantity}</div>
</div>
);
}
function CurrentPlayer() {
const [currentPlayer, setCurrentPlayer] = useState(null);
useEffect(() => {
const unsubscribe = onValue(`gameData/currentPlayer`, (playerId) => {
setCurrentPlayer(playerId);
});
return unsubscribe;
}, []);
const changePlayer = useCallback((newPlayerId) => {
changeValue(`gameData/currentPlayer`, newPlayerId);
}, [])
return <div className={styles.currentPlayerWrapper}>
`Current player is: ${getPlayerName(playerId)}`
<button onClick={changePlayer}>Change</button>
</div>
}
function Card({ cardId }) {
const [cardData] = useFirebase(`cards/${cardId}`);
return cardData ? <div className={styles.card}>
<img src={cardData.image}/>
{cardData.points} / {cardData.price}
</div> : null;
}
function Chip({ chipColor }) {
const [chipQuantity] = useFirebase(`tableData/chips/${chipColor}`);
return (
<div className={styles.chip}>
<div className={styles.chipImage} style={{ color: chipColor }} />
<div className={styles.chipQuant}>{chipQuantity}</div>
</div>
);
}
function CurrentPlayer() {
const [currentPlayer, changePlayer] = useFirebase(`gameData/currentPlayer`);
return <div className={styles.currentPlayerWrapper}>
`Current player is: ${getPlayerName(playerId)}`
<button onClick={changePlayer}>Change</button>
</div>
}
function Nobles() {
const [nobles] = useFirebase(`tableData/nobles`, []);
return (
<div className={styles.noblesWrapper}>
{nobles.map(nobleId => <Noble id={nobleId} />)}
</div>
);
}
export const Schema = shape({
tableData: shape({
cards: arrayOf([arrayOf([cardID])]),
chips: objectOf({
[chipColor]: int,
}),
nobles: arrayOf([nobleId])
}),
nobles: objectOf({
[nobleId]: shape({
image: string,
points: int
})
}),
...
function usePlayerScore(playerId) {
const [playerData, , status] = useFirebase(`playerHands/${playerId}`, {});
const [cards] = useFirebase('cards', {});
const cardScore = useMemo(
() =>
playerData.cards.reduce((acc, cardId) => {
return acc + cards[cardId]?.points;
}, 0),
[playerData, cards]
);
return [cardScore, null, status];
}
function PlayerView(playerId) {
const [playerScore, , status] = usePlayerScore(playerId);
const [playerName] = usePlayerName(playerId);
return status === PENDING ? null : (
<div className={styles.playerView}>
{playerName}: {playerScore}
</div>
);
}
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
npm install --save react-chat-elements
maxPlayers: int,
currentPlayer: userUUID,
}),
chat: shape({
messages: arrayOf(
shape({
content: string,
timestamp: int,
poster: string,
})
),
}),
cards: objectOf({
[cardID]: shape({
image: string,
points: int,
maxPlayers: int,
currentPlayer: userUUID,
}),
chat: shape({
messages: arrayOf(
shape({
content: string,
timestamp: int,
poster: string,
})
),
}),
cards: objectOf({
[cardID]: shape({
image: string,
points: int,
function Chat() {
const [messages, pushMessage] = useFirebase('chat/messages', [], { type: ChangeMode.PUSH });
const [currentMessage, setCurrentMessage] = useState();
const handleSend = useCallback(() => {
const content = {
content: currentMessage,
timestamp: getServerTimeStamp(),
poster: gameState.userId,
};
pushMessage(content);
}, [currentMessage, pushMessage]);
return (
<>
<MessageList className={styles.messageList} lockable={true} dataSource={messages} />
<Input
onChange={setCurrentMessage}
placeholder='Enter message here...'
rightButtons={<Button color='white' backgroundColor='black' text='Send' onClick={handleSend} />}
/>
</>
);
}
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
npm install --save agora-rtc-sdk
this.rtcClient = AgoraRTC.createClient({ mode: 'rtc', codec: 'h264' });
this.rtcClient.join(token, channelId, uid);
this.localStream = AgoraRTC.createStream({ streamID: this.uid });
this.rtcClient.publish(this.localStream);
this.rtcClient.on('stream-added', (evt) => {
this.rtcClient.subscribe(evt.stream);
});
this.rtcClient.on('stream-subscribed', (evt) => {
evt.stream.play('remote_video_' + evt.stream.getId());
});
export default function Players() {
const [players] = useFirebase('players', {});
return (
<div className={styles.participants}>
<div className={styles.videoGrid}>
<LocalVideo id='local'/>
{Object.entries(players).map(([userId]) => {
return <Participant id={userId} />;
})}
</div>
</div>
);
}
export class Agora {
createClient() {
this.rtcClient = AgoraRTC.createClient({ mode: 'rtc', codec: 'h264' });
}
hangEvents() {
this.rtcClient.on('stream-added', (evt) => {
const remoteStream = evt.stream;
const id = remoteStream.getId();
this.rtcClient.subscribe(remoteStream);
});
this.rtcClient.on('stream-subscribed', (evt) => {
const remoteStream = evt.stream;
const id = remoteStream.getId();
this.remoteStreams[id] = remoteStream;
remoteStream.play('remote_video_' + id);
});
}
join(channelId, uid = null, token = null) {
this.uid = uid;
this.channelId = channelId;
return new Promise((resolve) => {
this.rtcClient.join(token, channelId, uid, () => {
this.localStream = AgoraRTC.createStream({ streamID: this.uid });
this.localStream.init(() => {
this.localStream.play('local_stream');
this.rtcClient.publish(this.localStream);
resolve();
});
});
});
}
}
export default function Players() {
useEffect(() => {
setTimeout(async () => {
await agora.init();
await agora.hangEvents();
await agora.join('splendor_game', gameState.userId);
});
return () => {
agora.leave();
};
}, []);
const [players] = useFirebase('players', {});
return (
<div className={styles.participants}>
<div className={styles.videoGrid}>
<LocalVideo id='local'/>
{Object.entries(players).map(([userId]) => {
return <Participant id={userId} />;
})}
</div>
</div>
);
}
export class Agora {
createClient() {
this.rtcClient = AgoraRTC.createClient({ mode: 'rtc', codec: 'h264' });
}
hangEvents() {
this.rtcClient.on('stream-added', (evt) => {
const remoteStream = evt.stream;
const id = remoteStream.getId();
this.rtcClient.subscribe(remoteStream);
});
this.rtcClient.on('stream-subscribed', (evt) => {
const remoteStream = evt.stream;
const id = remoteStream.getId();
this.remoteStreams[id] = remoteStream;
remoteStream.play('remote_video_' + id);
});
}
join(channelId, uid = null, token = null) {
this.uid = uid;
this.channelId = channelId;
return new Promise((resolve) => {
this.rtcClient.join(token, channelId, uid, () => {
this.localStream = AgoraRTC.createStream({ streamID: this.uid });
this.localStream.init(() => {
this.localStream.play('local_stream');
this.rtcClient.publish(this.localStream);
resolve();
});
});
});
}
}
function LocalVideo() {
return (
<div className={styles.localStreamView}>
<div id='local_stream' className={styles.videoPlaceholder} />
</div>
);
}
class Agora {
...
toggleSelfAudio(audio) {
if (audio) {
this.localStream.unmuteAudio();
} else {
this.localStream.muteAudio();
}
}
...
}
function LocalVideo() {
const toggleAudio = useCallback((value) => {
agora.toggleSelfAudio(value);
}, []);
return (
<div className={styles.localStreamView}>
<div id='local_stream' className={styles.videoPlaceholder} />
<Flex center className={styles.toggleLocalAudio} onClick={toggleAudio}>
{agora.audio ? <Icon icon={faMicrophone} /> : <Icon icon={faMicrophoneSlash} />}
</Flex>
</div>
);
}
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
3
5
2
2
3
4
0
1
0
2
0
4
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';
import 'firebase/auth';
export default function Login() {
const uiConfig = {
signInFlow: 'popup',
signInOptions: [firebase.auth.EmailAuthProvider.PROVIDER_ID, firebase.auth.GoogleAuthProvider.PROVIDER_ID],
credentialHelper: 'none',
callbacks: {
signInSuccessWithAuthResult: (authResult) => {
signIn({ user: authResult.user });
return false;
},
},
};
return (
<Flex column center className={styles.main}>
<div>Greetings! Tell us who you are...</div>
<StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={firebase.auth()} />
</Flex>
);
}
export function signIn({ user }) {
gameState.setUserAuth(new User(user));
user.getIdTokenResult().then((idTokenResult) => {
gameState.setAdmin(!!idTokenResult?.claims?.admin);
gameState.initAuth();
});
}
3
5
2
2
3
4
0
1
0
2
0
4
export function changeValue(key, newValue, type = ChangeMode.SET) {
const ref = firebase.database().ref().child(key);
if(type === ChangeMode.UPDATE) {
return ref.update(newValue);
}
return ref.set(newValue);
}
export function onValue(key, callback) {
const unsubscribe = firebase.database()
.ref()
.child(key)
.on('value', (snapshot) => {
callback(snapshot?.val());
});
return unsubscribe;
}
export function changeValue(key, newValue, type = ChangeMode.SET) {
const ref = firebase.database().ref(state.gameId).child(key);
if(type === ChangeMode.UPDATE) {
return ref.update(newValue);
}
return ref.set(newValue);
}
export function onValue(key, callback) {
const unsubscribe = firebase.database()
.ref(state.gameId)
.child(key)
.on('value', (snapshot) => {
callback(snapshot?.val());
});
return unsubscribe;
}
3
5
2
2
3
4
0
1
0
2
0
4
Just do it.
You're awesome!
QUESTIONS?
@liadyosef