-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
1,298 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Fast Press Game Custom Plugin</title> | ||
<!-- Tailwind CSS --> | ||
<script src="https://cdn.tailwindcss.com"></script> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<!-- React and ReactDOM --> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script> | ||
<!-- Babel --> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script> | ||
<script | ||
type="text/babel" | ||
src="./js/hooks/useCustomObjectClient.js?ver0.0.1" | ||
></script> | ||
<script type="text/babel" src="./js/hooks/useGameLogic.js"></script> | ||
<script type="text/babel" src="./js/app.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
const { Fragment, useEffect, useState } = React; | ||
|
||
function InitView({ isInit, loadingNode, children }) { | ||
if (!isInit) return loadingNode; | ||
return <React.Fragment>{children}</React.Fragment>; | ||
} | ||
|
||
function StandbyView({ isStandby, children }) { | ||
if (!isStandby) return null; | ||
return <React.Fragment>{children}</React.Fragment>; | ||
} | ||
|
||
function PlayingView({ isPlaying, children }) { | ||
if (!isPlaying) return null; | ||
return <React.Fragment>{children}</React.Fragment>; | ||
} | ||
|
||
function ResultView({ hasResult, children }) { | ||
if (!hasResult) return null; | ||
return <React.Fragment>{children}</React.Fragment>; | ||
} | ||
|
||
function HostView({ isHost, children }) { | ||
if (!isHost) return null; | ||
return <React.Fragment>{children}</React.Fragment>; | ||
} | ||
|
||
function GuestView({ isHost, children }) { | ||
if (isHost) return null; | ||
return <React.Fragment>{children}</React.Fragment>; | ||
} | ||
|
||
function FastPressGameApp() { | ||
const { | ||
user, | ||
isGameMaster, | ||
isGameStarted, | ||
isStandby, | ||
fastest, | ||
winner, | ||
startGame, | ||
answer, | ||
reset, | ||
} = useGameLogic(); | ||
|
||
return ( | ||
<div className="container mx-auto text-center min-h-screen bg-white flex flex-col justify-center items-center gap-4 select-none"> | ||
<InitView | ||
isInit={user.id} | ||
loadingNode={<div className="text-4xl">Connect to object</div>} | ||
> | ||
<StandbyView isStandby={!isGameStarted && !isStandby && !winner}> | ||
<HostView isHost={isGameMaster}> | ||
<div className="flex flex-col gap-4"> | ||
<div className="text-xl">Fast Press Game</div> | ||
<button | ||
onClick={startGame} | ||
className="bg-blue-300 hover:bg-blue-400 active:bg-blue-500 p-6 rounded shadow text-4xl" | ||
> | ||
Start | ||
</button> | ||
</div> | ||
</HostView> | ||
<GuestView isHost={isGameMaster}> | ||
<div className="flex flex-col gap-4"> | ||
<div className="text-xl">Fast Press Game</div> | ||
<div className="text-4xl">Game Standby</div> | ||
</div> | ||
</GuestView> | ||
</StandbyView> | ||
<PlayingView isPlaying={isGameStarted && !isStandby && !winner}> | ||
<button | ||
onClick={answer} | ||
className="bg-red-500 hover:bg-red-600 active:bg-red-700 text-white p-6 rounded-full h-36 w-36 shadow text-4xl" | ||
> | ||
Press | ||
</button> | ||
<HostView isHost={isGameMaster}> | ||
<button onClick={reset} className="border-2 py-1 px-2 rounded"> | ||
Reset | ||
</button> | ||
</HostView> | ||
</PlayingView> | ||
<ResultView hasResult={winner}> | ||
<div className="flex flex-col gap-2 p-4"> | ||
<div className="text-2xl font-bold">Fastest</div> | ||
<div className="text-5xl font-bold whitespace-pre-wrap break-all select-text"> | ||
{winner && fastest.sort((a, b) => a.time - b.time)[0].name} | ||
</div> | ||
</div> | ||
<HostView isHost={isGameMaster}> | ||
<button onClick={reset} className="border-2 py-1 px-2 rounded"> | ||
Reset | ||
</button> | ||
</HostView> | ||
</ResultView> | ||
</InitView> | ||
</div> | ||
); | ||
} | ||
|
||
ReactDOM.render(<FastPressGameApp />, document.getElementById('root')); |
247 changes: 247 additions & 0 deletions
247
fast-press-game/public/js/hooks/useCustomObjectClient.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
const { useState, useRef, useCallback, useEffect } = React; | ||
|
||
class Participant { | ||
constructor(data = {}) { | ||
this._id = data.id; | ||
this.email = data.email; | ||
this.name = data.name; | ||
this.objectId = data.objectId; | ||
this.objectType = data.objectType; | ||
this.avatarUrl = data.avatarUrl; | ||
this._workspaceId = data.workspaceId; | ||
this.isHost = data.isHost; | ||
this.isSelf = data.isSelf; | ||
this.isVisitor = data.isVisitor; | ||
this.language = data.language; | ||
this.status = data.status; // joined, subscribed, etc. | ||
} | ||
get id() { | ||
return this._id && this._id.toString(); | ||
} | ||
get workspaceId() { | ||
return this._workspaceId && this._workspaceId.toString(); | ||
} | ||
} | ||
|
||
class Message { | ||
constructor(data = {}) { | ||
this.source = data.source; | ||
this.event = data.event; | ||
this.objectId = data.objectId; | ||
this.message = data.message; | ||
this._to = data.to; // Optional, only used for direct messages | ||
} | ||
get to() { | ||
return this._to && this._to.toString(); | ||
} | ||
} | ||
|
||
class MessageEvent { | ||
constructor(data = {}) { | ||
this.type = data.type; | ||
this.payload = data.payload; | ||
} | ||
} | ||
|
||
// Managing user information | ||
function useUsers() { | ||
const [user, setUser] = useState(new Participant()); | ||
const [users, setUsers] = useState([]); | ||
return { user, users, setUser, setUsers }; | ||
} | ||
// Event handling | ||
function useEventHandlers() { | ||
const eventFunction = useRef(() => {}); | ||
const userEventFunction = useRef(() => {}); | ||
const messageEventFunction = useRef(() => {}); | ||
const onEvent = useCallback(callback => { | ||
eventFunction.current = callback; | ||
}, []); | ||
const onUserEvent = useCallback(callback => { | ||
userEventFunction.current = callback; | ||
}, []); | ||
const onMessageEvent = useCallback(callback => { | ||
messageEventFunction.current = callback; | ||
}, []); | ||
return { | ||
onEvent, | ||
onUserEvent, | ||
onMessageEvent, | ||
eventFunction, | ||
userEventFunction, | ||
messageEventFunction, | ||
}; | ||
} | ||
// Sending messages | ||
function useMessageEmitter(user) { | ||
const postMessage = useCallback( | ||
message => { | ||
const messageInstance = new MessageEvent(message); | ||
window.parent.postMessage(messageInstance, '*'); | ||
}, | ||
[window] | ||
); | ||
const broadcast = useCallback( | ||
(event, message) => { | ||
postMessage({ | ||
type: 'ovice_broadcast_message', | ||
payload: { | ||
event: event, | ||
message: message, | ||
objectId: user.objectId && user.objectId.toString(), | ||
source: user.id && user.id, | ||
}, | ||
}); | ||
}, | ||
[user, postMessage] | ||
); | ||
const emitTo = useCallback( | ||
(userId, event, message) => { | ||
postMessage({ | ||
type: 'ovice_emit_message', | ||
payload: { | ||
event: event, | ||
message: message, | ||
objectId: user.objectId && user.objectId.toString(), | ||
source: user.id && user.id, | ||
to: userId && userId, | ||
}, | ||
}); | ||
}, | ||
[user, postMessage] | ||
); | ||
return { postMessage, broadcast, emitTo }; | ||
} | ||
// Calculating status | ||
function useStatus(user, users) { | ||
const isStaticObject = users.length | ||
? users.every(user => !user.isHost) | ||
: false; | ||
const isDynamicObject = users.length | ||
? users.some(user => user.isHost) | ||
: false; | ||
const isHost = user.isHost === true; | ||
const isMaster = isHost || (isStaticObject ? user.id === users[0].id : false); | ||
const isJoined = | ||
users.length && | ||
user.id && | ||
users.some(v => user.id === v.id && v.status === 'joined'); | ||
return { isStaticObject, isDynamicObject, isHost, isMaster, isJoined }; | ||
} | ||
// Initialization and updating the user list | ||
function useInitialization(postMessage) { | ||
const init = useCallback(() => { | ||
postMessage({ | ||
type: 'ovice_get_participants', | ||
}); | ||
}, [postMessage]); | ||
const updateUsers = useCallback(() => { | ||
postMessage({ | ||
type: 'ovice_get_participants', | ||
}); | ||
}, [postMessage]); | ||
return { init, updateUsers }; | ||
} | ||
function useCustomObjectClient() { | ||
const [lastMessage, setLastMessage] = useState(null); | ||
// Managing user information | ||
const { user, users, setUser, setUsers } = useUsers(); | ||
// Managing event handlers | ||
const { | ||
onEvent, | ||
onUserEvent, | ||
onMessageEvent, | ||
eventFunction, | ||
userEventFunction, | ||
messageEventFunction, | ||
} = useEventHandlers(); | ||
// Sending messages | ||
const { postMessage, broadcast, emitTo } = useMessageEmitter(user); | ||
// Calculating status | ||
const { isStaticObject, isDynamicObject, isHost, isMaster, isJoined } = | ||
useStatus(user, users); | ||
// Initialization and updating the user list | ||
const { init, updateUsers } = useInitialization(postMessage); | ||
// Message handler | ||
const handleMessage = useCallback( | ||
event => { | ||
const eventInstance = new MessageEvent(event.data); | ||
eventFunction.current && eventFunction.current(eventInstance); | ||
setLastMessage(event.data); | ||
switch (eventInstance.type) { | ||
case 'ovice_participants': | ||
const participants = eventInstance.payload.map( | ||
p => new Participant(p) | ||
); | ||
setUsers(participants); | ||
setUser( | ||
participants.find(participant => { | ||
return participant.isSelf; | ||
}) || new Participant() | ||
); | ||
break; | ||
case 'ovice_participant_subscribed': | ||
case 'ovice_participant_unsubscribed': | ||
case 'ovice_participant_joined': | ||
case 'ovice_participant_left': | ||
const participant = new Participant(eventInstance.payload); | ||
if (participant.isSelf) { | ||
setUser(participant); | ||
} | ||
userEventFunction.current && userEventFunction.current(participant); | ||
break; | ||
case 'ovice_confirmation': | ||
postMessage({ type: 'ovice_ready_confirmed' }); | ||
break; | ||
case 'ovice_message': | ||
const message = new Message(eventInstance.payload); | ||
messageEventFunction.current && messageEventFunction.current(message); | ||
break; | ||
default: | ||
break; | ||
} | ||
}, | ||
[setUsers, setUser, postMessage] | ||
); | ||
// Registering event listener and cleanup on initialization | ||
useEffect(() => { | ||
window.addEventListener('message', handleMessage); | ||
init(); | ||
return () => { | ||
window.removeEventListener('message', handleMessage); | ||
}; | ||
}, [handleMessage, init]); | ||
// Updating user information | ||
useEffect(() => { | ||
let timeoutId = null; | ||
if ( | ||
lastMessage && | ||
(lastMessage.type.startsWith('ovice_participant_') || | ||
lastMessage.type.startsWith('ovice_other_participant_')) | ||
) { | ||
timeoutId = setTimeout(updateUsers, 1000); | ||
} | ||
return () => { | ||
if (timeoutId) { | ||
clearTimeout(timeoutId); | ||
} | ||
}; | ||
}, [lastMessage, updateUsers]); | ||
// Returning the set of values provided by the custom hook | ||
return { | ||
user, | ||
users, | ||
isStaticObject, | ||
isDynamicObject, | ||
isHost, | ||
isMaster, | ||
isJoined, | ||
updateUsers, | ||
postMessage, | ||
broadcast, | ||
emitTo, | ||
onEvent, | ||
onUserEvent, | ||
onMessageEvent, | ||
}; | ||
} |
Oops, something went wrong.