node-mongo-demo
node.js and mongodb demo
git clone https://9o.is/git/node-mongo-demo.git
commit dfa4a2b8c2aeee1de31eed0581de866beda84d5d parent b47b9621c1b4439085b17a1958b6c060c01ee7df Author: Jul <jul@9o.is> Date: Mon, 27 Jan 2025 08:15:06 -0500 create a UI demo Diffstat:
| M | frontend/package-lock.json | | | 18 | ++++++++++-------- |
| M | frontend/package.json | | | 5 | ++--- |
| M | frontend/src/api/index.js | | | 15 | +++++++++++++-- |
| M | frontend/src/components/Home/Home.js | | | 4 | ++++ |
| A | frontend/src/components/Lucky7.js | | | 184 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
5 files changed, 213 insertions(+), 13 deletions(-)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json @@ -15,7 +15,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "axios": "^1.6.5", + "axios": "^1.7.0", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -5901,11 +5901,12 @@ } }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -8974,15 +8975,16 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, diff --git a/frontend/package.json b/frontend/package.json @@ -10,7 +10,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "axios": "^1.6.5", + "axios": "^1.7.0", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -46,4 +46,4 @@ "last 1 safari version" ] } -} -\ No newline at end of file +} diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js @@ -12,4 +12,16 @@ API.interceptors.request.use((req) => { export const login = (formData) => API.post("/api/user/login", formData); export const signUp = (formData) => API.post("/api/user/signup", formData); export const changePassword = (formData) => - API.post("/api/user/changePassword", formData); -\ No newline at end of file + API.post("/api/user/changePassword", formData); + + +export const lucky7Leaderboard = () => API.post("/api/lucky7/leaderboard") +export const lucky7Session = () => API.post("/api/lucky7/sessions") +export const lucky7Bet = lucky => API.post("/api/lucky7/bets", { lucky }); +export const lucky7BetEvents = id => API.get(`/api/lucky7/bets/${id}/events`, { + headers: { + 'Accept': 'text/event-stream', + }, + responseType: 'stream', + adapter: 'fetch', +}); diff --git a/frontend/src/components/Home/Home.js b/frontend/src/components/Home/Home.js @@ -1,6 +1,7 @@ import React from "react"; import { Container, Grow, Paper, Typography } from "@mui/material"; import { jwtDecode } from "jwt-decode"; +import Lucky7 from "../Lucky7.js"; const Home = () => { @@ -14,9 +15,12 @@ const Home = () => { <Container component="main" maxWidth="sm"> <Paper elevation={3}> {isSingedIn !== "null" && isSingedIn !== null ? ( + <> <Typography variant="h4" align="center" color="primary"> {`Welcome ${user.name}`} </Typography> + <Lucky7 user={user} /> + </> ) : ( <Typography variant="h4" align="center" color="primary"> Login to Play diff --git a/frontend/src/components/Lucky7.js b/frontend/src/components/Lucky7.js @@ -0,0 +1,184 @@ +import React from "react"; +import * as api from "../api"; + +const useSession = () => { + const [session, setSession] = React.useState(undefined); + + React.useEffect(() => { + const func = async () => { + const response = await api.lucky7Session(); + setSession(response.data) + }; + func(); + }, []); + + return { + session, + }; +}; + +const useBetEvents = (id, onEvent) => { + const [listening, setListening] = React.useState(false); + const [events, setEvents] = React.useState([]); + + React.useEffect(() => { + const connect = async () => { + const response = await api.lucky7BetEvents(id); + const stream = response.data; + + const reader = stream.pipeThrough(new TextDecoderStream()).getReader(); + setListening(true); + while (true) { + const { value, done } = await reader.read(); + if (done) { + setListening(false); + break; + } + const json = JSON.parse(value); + setEvents((events) => [...events, json]) + onEvent(json); + } + }; + if (onEvent && !listening) connect(); + }, [id, onEvent]); + + return { + listening, + events, + }; +}; + +const Lucky7Bet = props => { + const { + onGameEvent, + id, + lucky, + rollAt, + roll, + win, + } = props; + + const { listening, events } = useBetEvents(id, onGameEvent) + + return ( + <> + id: {id}<br/> + Lucky: {String(lucky)}<br/> + rollAt: {rollAt}<br/> + dice roll: {roll?.[0]} {roll?.[1]}<br/> + win: {String(win ?? "")}<br/> + {onGameEvent ? ( + <> + <h4>Events</h4> + Listening: {String(listening)}<br/> + <ol> + {events.map((e, i) => <li key={i}>{ JSON.stringify(e) }</li>)} + </ol> + </> + ) : null} + </> + ); +}; + +const getNextGameTime = (createdAt) => { + const startDateTime = new Date(createdAt); + const currentSeconds = Math.ceil(new Date() / 1000); + const startSeconds = Math.floor(startDateTime.getTime() / 1000); + const difference = currentSeconds - startSeconds; + + const nextChunk = Math.ceil(difference / 15) * 15; + return new Date((startSeconds + nextChunk) * 1000); +}; + +const millisRemaining = date => date - Date.now(); +const secondsRemaining = date => Math.floor((date - Date.now()) / 1000); + +const Timer = ({ createdAt }) => { + const reset = 15; + const [nextGameTime, setNextGameTime] = React.useState(getNextGameTime(createdAt)); + const [seconds, setSeconds] = React.useState(secondsRemaining(nextGameTime)); + + React.useEffect(() => { + let timeout; + + const resetTimer = () => { + const nextGameTime = getNextGameTime(createdAt); + setNextGameTime(nextGameTime); + setSeconds(secondsRemaining(nextGameTime)); + timeout = setTimeout(resetTimer, millisRemaining(nextGameTime)); + }; + + resetTimer(); + + const interval = setInterval(() => { + setSeconds((prevSeconds) => prevSeconds === 1 ? reset : prevSeconds - 1); + }, 1000); + + return () => { + clearTimeout(timeout); + clearInterval(interval); + }; + }, [createdAt, setSeconds, setNextGameTime]); + + return ( + <> + Timer: {seconds}s<br/> + </> + ); +}; + +const Lucky7 = user => { + const { session } = useSession(); + + const [status, setStatus] = React.useState(undefined); + const [nextGameBet, setNextGameBet] = React.useState(undefined); + const [lastGameBet, setLastGameBet] = React.useState(undefined); + + const handleBet = lucky => async () => { + try { + const response = await api.lucky7Bet(lucky); + setNextGameBet(response.data); + setStatus(undefined); + } catch (error) { + if (error.status === 400 && error.response.data.code === "TIMER") { + setStatus(error.response.data.message); + } else { + console.error(error) + } + } + }; + + const onGameEvent = bet => { + if (bet.state === "finished") { + setStatus(undefined); + setNextGameBet(undefined); + setLastGameBet(bet) + } + }; + + return ( + <> + {session ? <Timer createdAt={session.createdAt} /> : null} + <button onClick={handleBet(true)}>Lucky</button> + <button onClick={handleBet(false)}>Unlucky</button> + {status ? <p>{status}</p> : null} + {nextGameBet ? ( + <> + <h3>Next Game Bet</h3> + <Lucky7Bet onGameEvent={onGameEvent} {...nextGameBet} /> + </> + ) : null} + {lastGameBet ? ( + <> + <h3>Last Game Bet</h3> + <Lucky7Bet {...lastGameBet} /> + </> + ) : null} + <h3>Session</h3> + <p>{JSON.stringify(session)}</p> + </> + ); +}; + +export default Lucky7; +