Add live bidding UI and backend support; integrate react-bootstrap
- Added 'react-bootstrap' to frontend dependencies for improved UI components.
- Updated bid placement mechanics: backend now stores bids as a list of {user, amount}; frontend displays live bid leaderboard, including highest bid.
- Implemented bid placement form and UI in participant draft screen.
- Used React-Bootstrap Collapse for nominee menu accordion behavior.
- Expanded DraftStateManager and websocket consumers to broadcast bid updates in the new format.
- Added missing 'bids' syncing to all relevant state handling code.
- Improved styling for bidding, panel headers, and pick lists in SCSS; leveraged Bootstrap variables/utilities more extensively.
- Other minor JS, Python, and style tweaks for better stability and robustness.
This commit is contained in:
@@ -26,7 +26,7 @@ SECRET_KEY = "django-insecure-_rrxhe5i6uqap!52u(1zi8x$820duvf5s_!9!bc4ghbyyktol0
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["localhost"]
|
||||
ALLOWED_HOSTS = ["localhost", "kif.local"]
|
||||
|
||||
# TMDB API KEY
|
||||
TMDB_API_KEY = os.environ.get("TMDB_API_KEY")
|
||||
|
||||
@@ -32,6 +32,7 @@ class DraftMessage(StrEnum):
|
||||
BID_START_INFORM = "bid.start.inform" # server -> client (movie, ends_at)
|
||||
BID_START_REQUEST = "bid.start.request" # server -> client (movie, ends_at)
|
||||
BID_PLACE_REQUEST = "bid.place.request" # client -> server (amount)
|
||||
BID_PLACE_CONFIRM = "bid.update.confirm" # server -> client (high bid)
|
||||
BID_UPDATE_INFORM = "bid.update.inform" # server -> client (high bid)
|
||||
BID_END_INFORM = "bid.end.inform" # server -> client (winner)
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||
return self.user.is_authenticated
|
||||
|
||||
async def receive_json(self, content):
|
||||
logger.info(f"receiving message {content}")
|
||||
event_type = content.get("type")
|
||||
if event_type == DraftMessage.STATUS_SYNC_REQUEST:
|
||||
await self.send_json(
|
||||
@@ -206,7 +207,7 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
{
|
||||
"type": "broadcast.session",
|
||||
"subtype": DraftMessage.BID_START_INFORM,
|
||||
"payload": self.get_draft_status(),
|
||||
"payload": {**self.draft_state},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -315,6 +316,18 @@ class DraftParticipantConsumer(DraftConsumerBase):
|
||||
},
|
||||
)
|
||||
|
||||
if event_type == DraftMessage.BID_PLACE_REQUEST:
|
||||
bid_amount = content.get('payload',{}).get('bid_amount')
|
||||
self.draft_state.place_bid(self.user, bid_amount)
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": "broadcast.session",
|
||||
"subtype": DraftMessage.BID_PLACE_CONFIRM,
|
||||
"payload": {**self.draft_state},
|
||||
},
|
||||
)
|
||||
|
||||
# === Broadcast handlers ===
|
||||
|
||||
async def broadcast_participant(self, event):
|
||||
|
||||
@@ -181,13 +181,15 @@ class DraftStateManager:
|
||||
self.cache.set(self.cache_keys.current_movie, movie_id)
|
||||
self.cache.delete(self.cache_keys.bids)
|
||||
|
||||
def place_bid(self, user_id: int, amount: int):
|
||||
def place_bid(self, user: User, amount: int|str):
|
||||
if isinstance(amount, str):
|
||||
amount = int(amount)
|
||||
bids = self.get_bids()
|
||||
bids[user_id] = amount
|
||||
bids.append({"user":user.username, "amount":amount})
|
||||
self.cache.set(self.cache_keys.bids, json.dumps(bids))
|
||||
|
||||
def get_bids(self) -> dict:
|
||||
return json.loads(self.cache.get(self.cache_keys.bids) or "{}")
|
||||
return json.loads(self.cache.get(self.cache_keys.bids) or "[]")
|
||||
|
||||
def current_movie(self) -> Movie | None:
|
||||
movie_id = self.cache.get(self.cache_keys.current_movie)
|
||||
@@ -216,7 +218,7 @@ class DraftStateManager:
|
||||
"draft_index": self.draft_index,
|
||||
"connected_participants": self.connected_participants,
|
||||
"current_movie": self.cache.get(self.cache_keys.current_movie),
|
||||
# "bids": self.get_bids(),
|
||||
"bids": self.get_bids(),
|
||||
"bidding_timer_end": self.get_timer_end(),
|
||||
"bidding_timer_start": self.get_timer_start(),
|
||||
"current_pick": picks[0] if picks else None,
|
||||
|
||||
276
frontend/package-lock.json
generated
276
frontend/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.7",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1709,6 +1710,15 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@@ -2221,12 +2231,89 @@
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/hooks": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
||||
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/ui": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz",
|
||||
"integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@react-aria/ssr": "^3.5.0",
|
||||
"@restart/hooks": "^0.5.0",
|
||||
"@types/warning": "^3.0.3",
|
||||
"dequal": "^2.0.3",
|
||||
"dom-helpers": "^5.2.0",
|
||||
"uncontrollable": "^8.0.4",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/ui/node_modules/@restart/hooks": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz",
|
||||
"integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/ui/node_modules/uncontrollable": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
|
||||
"integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -2375,6 +2462,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
@@ -2389,6 +2482,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||
@@ -2439,6 +2550,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
|
||||
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
@@ -3169,6 +3286,12 @@
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clone-deep": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
@@ -3372,6 +3495,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -3435,6 +3564,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -3480,6 +3618,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -4232,6 +4380,15 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||
@@ -4768,6 +4925,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -5084,6 +5250,30 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types-extra": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
|
||||
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-is": "^16.3.2",
|
||||
"warning": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -5172,6 +5362,37 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-bootstrap": {
|
||||
"version": "2.10.10",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz",
|
||||
"integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.7",
|
||||
"@restart/hooks": "^0.4.9",
|
||||
"@restart/ui": "^1.9.4",
|
||||
"@types/prop-types": "^15.7.12",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
"classnames": "^2.3.2",
|
||||
"dom-helpers": "^5.2.1",
|
||||
"invariant": "^2.2.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"prop-types-extra": "^1.1.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"uncontrollable": "^7.2.1",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.14.8",
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
@@ -5185,6 +5406,34 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
@@ -6123,7 +6372,6 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
@@ -6140,6 +6388,21 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/uncontrollable": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
|
||||
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@types/react": ">=16.9.11",
|
||||
"invariant": "^2.2.4",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
@@ -6269,6 +6532,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.7",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^18.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export const ParticipantList = ({ isAdmin, draftState, draftDetails, currentUser
|
||||
const { draft_order, draft_index, connected_participants } = draftState
|
||||
const { participants } = draftDetails
|
||||
|
||||
const ListTag = draft_order.length > 0 ? "ol" : "ul"
|
||||
const listItems = draft_order.length > 0 ? draft_order.map(d => participants.find(p => p.username == d)) : participants
|
||||
const ListTag = draft_order?.length > 0 ? "ol" : "ul"
|
||||
const listItems = draft_order?.length > 0 ? draft_order.map(d => participants.find(p => p.username == d)) : participants
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,9 @@ export const WebSocketStatus = ({ socket }) => {
|
||||
console.log('socket changed', socket)
|
||||
if (!socket) return;
|
||||
|
||||
// Set initial state according to readyState
|
||||
setIsConnected(socket.readyState === 1);
|
||||
|
||||
const handleOpen = () => { console.log('socket open'); setIsConnected(true) };
|
||||
const handleClose = () => { console.log('socket close'); setIsConnected(false) };
|
||||
const handleError = () => { console.log('socket error'); setIsConnected(false) };
|
||||
|
||||
@@ -49,7 +49,8 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||
bidding_timer_end,
|
||||
bidding_timer_start,
|
||||
current_pick,
|
||||
next_picks
|
||||
next_picks,
|
||||
bids
|
||||
} = payload;
|
||||
|
||||
if (type == DraftMessage.STATUS_SYNC_INFORM) {
|
||||
@@ -66,6 +67,7 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||
...(bidding_timer_end ? { bidding_timer_end: Number(bidding_timer_end) } : {}),
|
||||
...(current_pick ? { current_pick } : {}),
|
||||
...(next_picks ? { next_picks } : {}),
|
||||
...(bids ? {bids} : {})
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// DraftAdmin.jsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { useWebSocket } from "../common/WebSocketContext.jsx";
|
||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||
@@ -9,11 +9,13 @@ import { DraftMoviePool } from "../common/DraftMoviePool.jsx";
|
||||
import { ParticipantList } from "../common/ParticipantList.jsx";
|
||||
import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx"
|
||||
import { handleDraftStatusMessages } from '../common/utils.js'
|
||||
// import { Collapse } from 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||
import { Collapse, ListGroup } from "react-bootstrap";
|
||||
|
||||
const NominateMenu = ({ socket, draftState, draftDetails, currentUser }) => {
|
||||
|
||||
const NominateMenu = ({ socket, draftState, draftDetails, currentUser, }) => {
|
||||
if (!socket || isEmptyObject(draftDetails) || isEmptyObject(draftState)) return;
|
||||
const currentDrafter = draftState.draft_order[draftState.draft_index]
|
||||
if (currentUser != currentDrafter) return;
|
||||
const [open, setOpen] = useState(false);
|
||||
const { movies } = draftDetails
|
||||
|
||||
const requestNomination = (event) => {
|
||||
@@ -28,21 +30,35 @@ const NominateMenu = ({ socket, draftState, draftDetails, currentUser }) => {
|
||||
}))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmptyObject(draftState) || isEmptyObject(draftState.current_pick)) return;
|
||||
|
||||
if (currentUser == draftState.current_pick.participant) {
|
||||
setOpen(true)
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// collapse.toggle()
|
||||
}, [draftState])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label>Nominate</label>
|
||||
<div className="d-flex">
|
||||
<form onSubmit={requestNomination}>
|
||||
<select className="form-control" name="movie">
|
||||
{movies.map(m => (
|
||||
<option key={m.id} value={m.id}>{m.title}</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="btn btn-primary">Nominate</button>
|
||||
</form>
|
||||
<Collapse in={open} className="nominate-menu">
|
||||
<div> {/* Everything must be wrapped in one parent */}
|
||||
<label>Nominate</label>
|
||||
<div className="d-flex">
|
||||
<form onSubmit={requestNomination}>
|
||||
<select className="form-control" name="movie">
|
||||
{movies.map(m => (
|
||||
<option key={m.id} value={m.id}>{m.title}</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="btn btn-primary">Nominate</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
|
||||
export const DraftParticipant = ({ draftSessionId }) => {
|
||||
@@ -84,81 +100,137 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
function submitBidRequest(event) {
|
||||
event.preventDefault()
|
||||
const form = event.target
|
||||
const formData = new FormData(form)
|
||||
console.log('submitting bid...')
|
||||
socket.send(JSON.stringify({
|
||||
type: DraftMessage.BID_PLACE_REQUEST,
|
||||
payload: {
|
||||
bid_amount: formData.get('bidAmount'),
|
||||
user: currentUser
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<section class="panel draft-live">
|
||||
<header class="panel-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="panel-title">Draft Live</h2>
|
||||
<div class="d-flex gap-1">
|
||||
<div class="phase-indicator badge bg-primary">{DraftPhaseLabel[draftState.phase]}</div>
|
||||
<section className="panel draft-live">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">Draft Live</h2>
|
||||
<div className="d-flex gap-1">
|
||||
<div className="phase-indicator badge bg-primary">{DraftPhaseLabel[draftState.phase]}</div>
|
||||
<WebSocketStatus socket={socket} />
|
||||
</div>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<div class="draft-live-state-container">
|
||||
<div className="panel-body">
|
||||
<div className="draft-live-state-container">
|
||||
<DraftCountdownClock endTime={draftState.bidding_timer_end}></DraftCountdownClock>
|
||||
<div class="pick-description">
|
||||
{console.log("draft_state", draftState)}
|
||||
<div>Round {draftState.current_pick?.round}</div>
|
||||
<div>Pick {draftState.current_pick?.pick_in_round}</div>
|
||||
<div>{draftState.current_pick?.overall+1} Overall</div>
|
||||
<div className="pick-description">
|
||||
{console.log("draft_state", draftState)}
|
||||
<div>Round {draftState.current_pick?.round}</div>
|
||||
<div>Pick {draftState.current_pick?.pick_in_round}</div>
|
||||
<div>{draftState.current_pick?.overall + 1} Overall</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="current-movie card"></div>
|
||||
<div class="bid-controls btn-group"></div>
|
||||
<ParticipantList
|
||||
currentUser={draftState.current_pick?.participant}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
/>
|
||||
<div className="bid-status">
|
||||
<div className="d-flex">
|
||||
<div className="flex-grow-1 text-center">
|
||||
{draftState.bids?.length > 0 ? Math.max(draftState.bids?.map(i=>i.bid_amount)) : ""}
|
||||
</div>
|
||||
<div className="flex-grow-1 text-center">
|
||||
highest bid
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ol className="bid-list">
|
||||
{draftState.bids?.map((bid, idx) => (
|
||||
<li key={idx}>{bid.user}: {bid.amount}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bid-controls btn-group d-flex flex-column">
|
||||
<form id="bid" onSubmit={submitBidRequest}>
|
||||
<div className="d-flex">
|
||||
<div className="flex-grow-1 text-center">
|
||||
<input type="number" id="bidAmount" name="bidAmount"></input>
|
||||
</div>
|
||||
<div className="flex-grow-1 text-center">
|
||||
<button className="flex-grow-1">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<ul className="pick-list">
|
||||
<li>
|
||||
<div>Current Pick: {draftState.current_pick?.participant}</div>
|
||||
</li>
|
||||
<li
|
||||
>
|
||||
<div>Next Pick: {draftState.next_picks ? draftState.next_picks[0]?.participant : ""}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel draft-board">
|
||||
<header class="panel-header">
|
||||
<h2 class="panel-title">Draft Board</h2>
|
||||
<section className="panel draft-catalog">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">Draft Catalog</h2>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<div class="current-movie-detail card"></div>
|
||||
<div class="movie-filters"></div>
|
||||
<div className="panel-body">
|
||||
<div className="current-movie card">
|
||||
<span>Current Nomination: {movies.find(i => draftState.current_movie == i.id)?.title}</span>
|
||||
</div>
|
||||
<NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu>
|
||||
<div className="movie-filters"></div>
|
||||
|
||||
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="panel my-team">
|
||||
<header class="panel-header">
|
||||
<h2 class="panel-title">My Team</h2>
|
||||
<section className="panel my-team">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">My Team</h2>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<ul class="team-movie-list list-group">
|
||||
<li class="team-movie-item list-group-item"></li>
|
||||
<div className="panel-body">
|
||||
<ul className="team-movie-list list-group">
|
||||
<li className="team-movie-item list-group-item"></li>
|
||||
</ul>
|
||||
<div class="budget-status"></div>
|
||||
<div className="budget-status"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="panel teams">
|
||||
<header class="panel-header">
|
||||
<h2 class="panel-title">Teams</h2>
|
||||
<section className="panel teams">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">Teams</h2>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<ul class="team-list list-group">
|
||||
<li class="team-item list-group-item">
|
||||
<div class="team-name fw-bold"></div>
|
||||
<ul class="team-movie-list list-group list-group-flush">
|
||||
<li class="team-movie-item list-group-item"></li>
|
||||
<div className="panel-body">
|
||||
<ParticipantList
|
||||
currentUser={currentUser}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
/>
|
||||
<ul className="team-list list-group">
|
||||
<li className="team-item list-group-item">
|
||||
<div className="team-name fw-bold"></div>
|
||||
<ul className="team-movie-list list-group list-group-flush">
|
||||
<li className="team-movie-item list-group-item"></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
@use "../../node_modules/bootstrap/scss/bootstrap.scss";
|
||||
@use "./fonts/graphique.css";
|
||||
@import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Oswald:wght@200..700&display=swap");
|
||||
// Import only functions & variables
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
|
||||
.navbar {
|
||||
// background-color: #582f0e;
|
||||
@@ -128,19 +131,41 @@
|
||||
#draft-admin-root {
|
||||
@extend .flex-grow-1;
|
||||
.wrapper:first-child {
|
||||
@extend .p-2;
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* allow panels to wrap */
|
||||
gap: 1rem; /* space between panels */
|
||||
justify-content: center; /* center the panels horizontally */
|
||||
|
||||
.panel {
|
||||
@extend .border;
|
||||
@extend .shadow-sm;
|
||||
@extend .rounded-2;
|
||||
flex: 1 1 350px; /* grow/shrink, base width */
|
||||
max-width: 450px; /* never go beyond this */
|
||||
min-width: 300px; /* keeps them from getting too small */
|
||||
header.panel-header {
|
||||
@extend .p-1;
|
||||
@extend .text-uppercase;
|
||||
@extend .align-items-center;
|
||||
@extend .border-bottom;
|
||||
@extend .border-secondary;
|
||||
background-color: $blue-100;
|
||||
@extend .rounded-top-2;
|
||||
.panel-title {
|
||||
@extend .fw-bold;
|
||||
@extend .fs-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel.draft-live {
|
||||
header.panel-header {
|
||||
@extend .d-flex;
|
||||
@extend .justify-content-between;
|
||||
}
|
||||
.draft-live-state-container {
|
||||
@extend .d-flex;
|
||||
background-color: $green-100;
|
||||
.countdown-clock {
|
||||
@extend .fs-1;
|
||||
@extend .fw-bold;
|
||||
@@ -152,6 +177,26 @@
|
||||
@extend .col;
|
||||
}
|
||||
}
|
||||
div:has(.pick-list), div:has(.bid-list){
|
||||
ul {
|
||||
@extend .list-group;
|
||||
}
|
||||
li {
|
||||
@extend .list-group-item;
|
||||
}
|
||||
}
|
||||
.bid-status {
|
||||
min-height: 50px;
|
||||
}
|
||||
.bid-controls {
|
||||
button {
|
||||
@extend .btn;
|
||||
@extend .btn-primary;
|
||||
}
|
||||
input {
|
||||
@extend .form-control;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.7",
|
||||
"react-bootstrap": "^2.10.10"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user