2025-08-01

This commit is contained in:
2025-08-01 13:03:58 -05:00
parent f25a69cf78
commit 1a7a6a2d50
14 changed files with 512 additions and 114 deletions

View File

@@ -1,16 +1,39 @@
from django.contrib import admin from django.contrib import admin
from .models import League, Season, UserSeasonEntry, Movie, MovieMetric, Pick from .models import League, Season, UserSeasonEntry, Movie, MovieMetric, Pick
from draft.models import DraftSession
# Register your models here. # Register your models here.
admin.site.register(League) admin.site.register(League)
class PickInline(admin.TabularInline): # or TabularInline
extra = 0
model = Pick
can_delete = True
show_change_link = True
class UserSeasonEntryInline(admin.TabularInline):
extra = 0
model = UserSeasonEntry
class MovieMetricInline(admin.TabularInline):
extra = 0
model = MovieMetric
class DraftSessionInline(admin.TabularInline):
extra = 0
model = DraftSession
show_change_link = True
class SeasonAdmin(admin.ModelAdmin): class SeasonAdmin(admin.ModelAdmin):
inlines = [UserSeasonEntryInline, DraftSessionInline, PickInline]
readonly_fields = ('slug',) readonly_fields = ('slug',)
class MovieAdmin(admin.ModelAdmin):
inlines = [MovieMetricInline]
admin.site.register(Season, SeasonAdmin) admin.site.register(Season, SeasonAdmin)
admin.site.register(UserSeasonEntry) admin.site.register(UserSeasonEntry)
admin.site.register(Movie) admin.site.register(Movie, MovieAdmin)
admin.site.register(MovieMetric) admin.site.register(MovieMetric)
admin.site.register(Pick) admin.site.register(Pick)

View File

@@ -0,0 +1 @@
import pymojo as boxofficemojo

View File

@@ -148,4 +148,4 @@ CHANNEL_LAYERS = {
}, },
} }
HASHIDS_SALT = "your-very-secret-salt-string" HASHIDS_SALT = os.getenv("BOF_HASHIDS_SALT", "your-very-secret-salt-string")

View File

@@ -1,10 +1,18 @@
from django.contrib import admin from django.contrib import admin
from draft.models import DraftSession, DraftParticipant, DraftMoviePool, DraftPick, DraftSessionSettings from draft.models import DraftSession, DraftParticipant, DraftMoviePool, DraftPick, DraftSessionSettings
# Register your models here. class DraftSessionSettingsInline(admin.TabularInline): # or TabularInline
model = DraftSessionSettings
can_delete = False
show_change_link = True
class DraftParticipantInline(admin.TabularInline):
extra = 0
model = DraftParticipant
class DraftSessionAdmin(admin.ModelAdmin): class DraftSessionAdmin(admin.ModelAdmin):
... inlines = [DraftSessionSettingsInline, DraftParticipantInline]
readonly_fields = ('hashed_id',) readonly_fields = ('hashed_id',)
# Register your models here.
admin.site.register(DraftSession, DraftSessionAdmin) admin.site.register(DraftSession, DraftSessionAdmin)
admin.site.register(DraftSessionSettings) admin.site.register(DraftSessionSettings)
admin.site.register(DraftParticipant) admin.site.register(DraftParticipant)

95
draft/constants.py Normal file
View File

@@ -0,0 +1,95 @@
from enum import IntEnum
class DraftMessage:
# Server
INFORM_PHASE_CHANGE = "inform.phase.change"
CONFIRM_PHASE_ADVANCE = "confirm.phase.advance"
INFORM_STATUS = "inform.status"
# Client
REQUEST_PHASE_ADVANCE = "request.phase.advance"
REQUEST_INFORM_STATUS = "request.inform.status"
# Waiting Phase
## Server
INFORM_JOIN_USER = "inform.join.user"
REQUEST_JOIN_PARTICIPANT = "request.join.participant"
REQUEST_JOIN_ADMIN = "request.join.admin"
## Client
NOTIFY_JOIN_USER = "notify.join.user"
CONFIRM_JOIN_PARTICIPANT = "confirm.join.participant"
CONFIRM_JOIN_ADMIN = "confirm.join.admin"
# Determine Order
## Server
CONFIRM_DETERMINE_DRAFT_ORDER = "confirm.determine.draft_order"
## Client
REQUEST_DETERMINE_DRAFT_ORDER = "request.determine.draft_order"
class DraftPhase(IntEnum):
WAITING = 0
DETERMINE_ORDER = 10
NOMINATION = 20
BIDDING = 30
AWARD = 40
FINALIZE = 50
def __str__(self):
return self.name.lower()
class DraftGroupChannelNames:
def __init__(self, id):
self.prefix = f"draft.{id}"
@property
def session(self):
return f"{self.prefix}.session"
@property
def admin(self):
return f"{self.prefix}.admin"
@property
def participant(self):
return f"{self.prefix}.participant"
class DraftCacheKeys:
def __init__(self, id):
self.prefix = f"draft:{id}"
@property
def admins(self):
return f"{self.prefix}:admins"
@property
def participants(self):
return f"{self.prefix}:participants"
# @property
# def state(self):
# return f"{self.prefix}:state"
# @property
# def current_movie(self):
# return f"{self.prefix}:current_movie"
# @property
# def bids(self):
# return f"{self.prefix}:bids"
# @property
# def participants(self):
# return f"{self.prefix}:participants"
# @property
# def bid_timer_end(self):
# return f"{self.prefix}:bid_timer_end"
# def user_status(self, user_id):
# return f"{self.prefix}:user:{user_id}:status"
# def user_channel(self, user_id):
# return f"{self.prefix}:user:{user_id}:channel"

View File

@@ -4,24 +4,197 @@ from django.core.exceptions import PermissionDenied
from boxofficefantasy.models import League, Season from boxofficefantasy.models import League, Season
from boxofficefantasy.views import parse_season_slug from boxofficefantasy.views import parse_season_slug
from draft.models import DraftSession, DraftPick, DraftMoviePool, DraftParticipant from draft.models import DraftSession, DraftPick, DraftMoviePool, DraftParticipant
from django.core.cache import cache
from draft.constants import DraftMessage, DraftPhase, DraftGroupChannelNames
import random import random
class DraftConsumer(AsyncJsonWebsocketConsumer): class DraftConsumerBase(AsyncJsonWebsocketConsumer):
async def connect(self): async def connect(self):
draft_session_id_hashed = self.scope["url_route"]["kwargs"].get("draft_session_id_hashed") draft_session_id_hashed = self.scope["url_route"]["kwargs"].get(
"draft_session_id_hashed"
)
league_slug = self.scope["url_route"]["kwargs"].get("league_slug") league_slug = self.scope["url_route"]["kwargs"].get("league_slug")
season_slug = self.scope["url_route"]["kwargs"].get("season_slug") season_slug = self.scope["url_route"]["kwargs"].get("season_slug")
self.draft_session = await self.get_draft_session(draft_session_id_hashed=draft_session_id_hashed, league_slug=league_slug, season_slug=season_slug) self.draft_session = await self.get_draft_session(
draft_session_id_hashed=draft_session_id_hashed,
)
self.room_group_name = f"draft_{self.draft_session.season.league.slug}_{self.draft_session.season.slug}" self.draft_group_names = f"draft_admin_{self.draft_session.hashed_id}"
self.draft_participant_group_channels = DraftGroupChannelNames(draft_session_id_hashed)
# Auth check (optional)
self.user = self.scope["user"] self.user = self.scope["user"]
if not self.user.is_authenticated: if not self.user.is_authenticated:
await self.close() await self.close()
return return
else:
await self.accept()
async def receive_json(self, content):
event_type = content.get("type")
user = self.scope["user"]
async def user_joined(self, event):
await self.send_json(
{
"type": "user.joined",
"user": event["user"].username,
"user_type": event["user_type"],
"users": event["users"],
}
)
async def send_draft_summary(self):
state = cache.get(self.draft_status_cache_key, {})
await self.send_json(
{
"type": "draft_summary",
"phase": state.get("phase", "not started"),
"movie": state.get("movie"),
"current_bid": state.get("current_bid"),
"time_remaining": state.get("time_remaining"),
"you_are_next": state.get("you_are_next", False),
}
)
# === Broadcast handlers ===
async def draft_status(self, event):
await self.send_json(
{
"type": "draft.status",
"status": event["status"],
}
)
# === DB Access ===
@database_sync_to_async
def get_draft_session(
self, draft_session_id_hashed, league_slug, season_slug
) -> DraftSession:
draft_session_id = DraftSession.decode_id(draft_session_id_hashed)
if draft_session_id:
draft_session = DraftSession.objects.select_related(
"season", "season__league"
).get(pk=draft_session_id)
elif league_slug and season_slug:
label, year = parse_season_slug(season_slug)
season = Season.objects.filter(label=label, year=year).first()
draft_session = (
DraftSession.objects.select_related("season", "season__league")
.filter(season=season)
.first()
)
else:
raise Exception()
return draft_session
@database_sync_to_async
def get_draft_participants(self) -> list[DraftParticipant]:
# Replace this with real queryset to fetch users in draft
participants = DraftParticipant.objects.select_related("user").filter(
draft=self.draft_session
)
connected_ids = cache.get(self.draft_connected_participants_cache_key, set())
for p in participants:
p.is_connected = p in connected_ids
return list(participants.all())
class DraftAdminConsumer(DraftConsumerBase):
async def connect(self):
await super().connect()
if not self.user.is_staff:
await self.close()
return
await self.channel_layer.group_add(
self.draft_admin_group_name, self.channel_name
)
# await self.channel_layer.group_send(
# self.draft_participant_group_name,
# {"type": "user.joined", "user": self.user, "user_type": "admin"},
# )
await self.channel_layer.group_send(
self.draft_admin_group_name,
{"type": "user.joined", "user": self.user, "user_type": "admin"},
)
async def receive_json(self, content):
await super().receive_json(content)
event_type = content.get("type")
user = self.scope["user"]
if event_type == "start.draft":
await self.start_draft()
elif event_type == "user.joined":
pass
elif event_type == "nominate":
await self.nominate(content.get("movie"))
elif event_type == "bid":
await self.place_bid(content.get("amount"), self.scope["user"].username)
elif event_type == "message":
await self.channel_layer.group_send(
self.draft_participant_group_name,
{
"type": "chat.message",
"user": self.scope["user"].username,
"message": content.get("message"),
},
)
# === Draft logic (stubbed for now) ===
async def start_draft(self):
# Example: shuffle draft order
participants = await self.get_draft_participants()
draft_order = random.sample(participants, len(participants))
connected_participants = cache.get(
self.draft_connected_participants_cache_key, ()
)
initial_state = {
"phase": "nominating",
"current_nominee": None,
"current_bid": None,
"participants": [
{"user": p.user.username, "is_connected": p in connected_participants}
for p in await self.get_draft_participants()
],
"draft_order": [p.user.username for p in draft_order],
"current_turn_index": 0,
"picks": [],
}
cache.set(self.draft_status_cache_key, initial_state)
for group_name in [
self.draft_admin_group_name,
self.draft_participant_group_name,
]:
# await self.channel_layer.group_send(
# group_name,
# {
# "type": "draft.start"
# }
# )
await self.channel_layer.group_send(
group_name,
{
"type": "draft.status",
"status": cache.get(self.draft_status_cache_key),
},
)
class DraftParticipantConsumer(DraftConsumerBase):
async def connect(self):
await super().connect()
await self.channel_layer.group_add(
self.draft_participant_group_name, self.channel_name
)
try: try:
await self.add_draft_participant() await self.add_draft_participant()
@@ -29,42 +202,46 @@ class DraftConsumer(AsyncJsonWebsocketConsumer):
await self.close() await self.close()
return return
await self.channel_layer.group_add(self.room_group_name, self.channel_name) await self.send_json(
await self.accept()
await self.send_json({"type": "connection.accepted", "user": self.user.username, "budget": self.participant.budget})
# Notify others (optional)
await self.channel_layer.group_send(
self.room_group_name,
{ {
"type": "user.joined", "type": "connection.accepted",
"user": self.user.username, "user": self.user.username,
"budget": self.participant.budget "is_staff": self.user.is_staff,
} }
) )
await self.channel_layer.group_send(
self.draft_participant_group_name,
{
"type": "user.joined",
"user": self.user,
"user_type": "participant",
"participants": [],
},
)
await self.channel_layer.group_send(
self.draft_admin_group_name,
{"type": "user.joined", "user": self.user, "user_type": "participant"},
)
async def disconnect(self, close_code): async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.room_group_name, self.channel_name) await self.channel_layer.group_discard(
self.draft_participant_group_name, self.channel_name
)
async def receive_json(self, content): async def receive_json(self, content):
event_type = content.get("type") event_type = content.get("type")
user = self.scope["user"] user = self.scope["user"]
if event_type == "start_draft": if event_type == "user.joined":
if user.is_staff: pass
await self.start_draft()
else:
await self.send_json({
"type": "error",
"message": "insufficient privelleges"
})
elif event_type == "nominate": elif event_type == "nominate":
await self.nominate(content.get("movie")) await self.nominate(content.get("movie"))
elif event_type == "bid": elif event_type == "bid":
await self.place_bid(content.get("amount"), self.scope["user"].username) await self.place_bid(content.get("amount"), self.scope["user"].username)
elif event_type == "message": elif event_type == "message":
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.room_group_name, self.draft_participant_group_name,
{ {
"type": "chat.message", "type": "chat.message",
"user": self.scope["user"].username, "user": self.scope["user"].username,
@@ -74,65 +251,43 @@ class DraftConsumer(AsyncJsonWebsocketConsumer):
# === Broadcast handlers === # === Broadcast handlers ===
async def user_joined(self, event):
await self.send_json({
"type": "user.joined",
"user": event["user"]
})
async def chat_message(self, event): async def chat_message(self, event):
await self.send_json({ await self.send_json(
"type": "chat.message",
"user": event["user"],
"message": event["message"],
})
async def draft_update(self, event):
await self.send_json({
"type": "draft.update",
"state": event["state"],
})
# === Draft logic (stubbed for now) ===
async def start_draft(self):
# Example: shuffle draft order
players = await self.get_draft_participants()
draft_order = random.sample(players, len(players))
await self.channel_layer.group_send(
self.room_group_name,
{ {
"type": "draft.update", "type": "chat.message",
"state": { "user": event["user"],
"status": "started",
"order": [p.user.username for p in draft_order],
}
} }
) )
async def draft_update(self, event):
await self.send_json(
{
"type": "draft.update",
"state": event["state"],
}
)
# === Draft logic (stubbed for now) ===
async def nominate(self, movie_title): async def nominate(self, movie_title):
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.room_group_name, self.draft_participant_group_name,
{ {
"type": "draft.update", "type": "draft.update",
"state": { "state": {
"status": "nominating", "status": "nominating",
"movie": movie_title, "movie": movie_title,
} },
} },
) )
async def place_bid(self, amount, user): async def place_bid(self, amount, user):
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.room_group_name, self.draft_participant_group_name,
{ {
"type": "draft.update", "type": "draft.update",
"state": { "state": {"status": "bidding", "bid": {"amount": amount, "user": user}},
"status": "bidding", },
"bid": {"amount": amount, "user": user}
}
}
) )
# === Example DB Access === # === Example DB Access ===
@@ -142,26 +297,5 @@ class DraftConsumer(AsyncJsonWebsocketConsumer):
self.participant, _ = DraftParticipant.objects.get_or_create( self.participant, _ = DraftParticipant.objects.get_or_create(
user=self.user, user=self.user,
draft=self.draft_session, draft=self.draft_session,
defaults={ defaults={"budget": self.draft_session.settings.starting_budget},
"budget":self.draft_session.settings.starting_budget
}
) )
@database_sync_to_async
def get_draft_participants(self):
# Replace this with real queryset to fetch users in draft
return list(DraftParticipant.objects.select_related('user').filter(draft=self.draft_session).all())
@database_sync_to_async
def get_draft_session(self, draft_session_id_hashed, league_slug, season_slug):
draft_session_id = DraftSession.decode_id(draft_session_id_hashed)
if draft_session_id:
draft_session = DraftSession.objects.select_related('season', 'season__league').get(pk=draft_session_id)
elif league_slug and season_slug:
label, year = parse_season_slug(season_slug)
season = Season.objects.filter(label=label, year=year).first()
draft_session = DraftSession.objects.select_related('season', 'season__league').filter(season=season).first()
else:
raise Exception()
return draft_session

View File

@@ -1,7 +1,7 @@
from django.urls import path from django.urls import path
from . import consumers from draft.consumers import DraftParticipantConsumer, DraftAdminConsumer
websocket_urlpatterns = [ websocket_urlpatterns = [
path(r"ws/draft/session/<str:draft_session_id_hashed>/", consumers.DraftConsumer.as_asgi()), path(r"ws/draft/session/<str:draft_session_id_hashed>/participant", DraftParticipantConsumer.as_asgi()),
# path(r"ws/draft/<slug:league_slug>/<slug:season_slug>/", consumers.DraftConsumer.as_asgi()), path(r"ws/draft/session/<str:draft_session_id_hashed>/admin", DraftAdminConsumer.as_asgi()),
] ]

View File

@@ -1,8 +1,8 @@
<h1>Draft Room: {{ league.name }} {{ season.label }} {{ season.year }}</h1> <h1>Draft Room: {{ league.name }} {{ season.label }} {{ season.year }}</h1>
{%load static%} {% load static %}
<div id="draft-app" data-draft-id="{{draft_id_hashed}}" data-room-name="{{ room_name }}"></div> <div id="draft-app" data-draft-id="{{draft_id_hashed}}"></div>
{% if DEBUG %} {% if DEBUG %}
<script src="http://localhost:3000/dist/bundle.js"></script> <script src="http://localhost:3000/dist/bundle.js"></script>
{% else %} {% else %}
<script src="{% static 'bundle.js' %}"></script> <script src="{% static 'bundle.js' %}"></script>
{% endif %} {% endif %}

View File

@@ -0,0 +1,8 @@
<h1>Draft Room: {{ league.name }} {{ season.label }} {{ season.year }}</h1>
{% load static %}
<div id="draft-admin-app" data-draft-id="{{ draft_id_hashed }}"></div>
{% if DEBUG %}
<script src="http://localhost:3000/dist/bundle.js"></script>
{% else %}
<script src="{% static 'bundle.js' %}"></script>
{% endif %}

View File

@@ -6,5 +6,6 @@ app_name = "draft"
urlpatterns = [ urlpatterns = [
# path("", views.draft_room, name="room"), # path("", views.draft_room, name="room"),
path("session/<str:draft_session_id_hashed>/", views.draft_room, name="session"), path("session/<str:draft_session_id_hashed>/", views.draft_room, name="session"),
path("session/<str:draft_session_id_hashed>/<str:is_admin>", views.draft_room, name="admin_session"),
# path("<slug:league_slug>/<slug:season_slug>/", views.draft_room_list, name="room"), # path("<slug:league_slug>/<slug:season_slug>/", views.draft_room_list, name="room"),
] ]

View File

@@ -5,9 +5,8 @@ from boxofficefantasy.views import parse_season_slug
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from boxofficefantasy_project.utils import decode_id from boxofficefantasy_project.utils import decode_id
# @login_required(login_url='/login/') @login_required(login_url='/login/')
def draft_room(request, league_slug=None, season_slug=None, draft_session_id_hashed=None): def draft_room(request, league_slug=None, season_slug=None, draft_session_id_hashed=None, is_admin=""):
if draft_session_id_hashed: if draft_session_id_hashed:
draft_session_id = decode_id(draft_session_id_hashed) draft_session_id = decode_id(draft_session_id_hashed)
draft_session = get_object_or_404(DraftSession, id=draft_session_id) draft_session = get_object_or_404(DraftSession, id=draft_session_id)
@@ -20,9 +19,14 @@ def draft_room(request, league_slug=None, season_slug=None, draft_session_id_has
season = get_object_or_404(Season, league=league, label__iexact=label, year=year) season = get_object_or_404(Season, league=league, label__iexact=label, year=year)
draft_session = get_object_or_404(DraftSession, season=season) draft_session = get_object_or_404(DraftSession, season=season)
return render(request, "draft/room.dj.html", { context = {
"draft_id_hashed": draft_session.hashed_id, "draft_id_hashed": draft_session.hashed_id,
"league": league, "league": league,
"season": season, "season": season,
"room_name": f"{league.slug}-{season.slug}" }
})
if is_admin == "admin":
return render(request, "draft/room_admin.dj.html", context)
else:
return render(request, "draft/room.dj.html", context)

View File

@@ -1,9 +1,112 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from "react";
export const useWebSocketStatus = (wsUrl) => {
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socket = new WebSocket(wsUrl);
socket.onopen = () => setIsConnected(true);
socket.onclose = () => setIsConnected(false);
socket.onerror = () => setIsConnected(false);
return () => socket.close();
}, [wsUrl]);
return isConnected;
};
export const WebSocketStatus = ({ wsUrl }) => {
const isConnected = useWebSocketStatus(wsUrl);
return (
<div className="d-flex align-items-center gap-2">
<span
className="status-dot"
style={{
width: "10px",
height: "10px",
borderRadius: "50%",
backgroundColor: isConnected ? "green" : "red",
display: "inline-block",
}}
></span>
<span>{isConnected ? "Connected" : "Disconnected"}</span>
</div>
);
};
export const DraftAdmin = ({ draftSessionId }) => { export const DraftAdmin = ({ draftSessionId }) => {
const [latestMessage, setLatestMessage] = useState(null); const [latestMessage, setLatestMessage] = useState(null);
const [connectedParticipants, setConnectedParticipants] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const socketRef = useRef(null); const socketRef = useRef(null);
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/`; const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`;
useEffect(() => {
socketRef.current = new WebSocket(wsUrl);
socketRef.current.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(event)
setLatestMessage(data);
if (data.type == "user.joined") {
// setConnectedParticipants =
}
else if (data.type == "draft_summary"){
console.log(data)
}
};
socketRef.current.onclose = () => {
console.warn("WebSocket connection closed.");
};
return () => {
socketRef.current.close();
};
}, [wsUrl]);
const handleStartDraft = () => {
socketRef.current.send(JSON.stringify({ type: "start.draft" }));
}
const handleRequestDraftSummary = () => {
socketRef.current.send(JSON.stringify({ type: 'request_summary' }))
}
return (
<div className="container draft-panel">
<h3>Draft Admin Panel</h3>
<WebSocketStatus wsUrl={wsUrl} />
<label>Latest Message</label>
<input
type="text"
readOnly disabled
value={latestMessage ? JSON.stringify(latestMessage) : ""}
/>
<label>Connected Particpants</label>
<input
type="text"
readOnly disabled
value={connectedParticipants ? JSON.stringify(connectedParticipants) : ""}
/>
<button onClick={handleStartDraft} className="btn btn-primary mt-2">
Start Draft
</button>
<button onClick={handleRequestDraftSummary} className="btn btn-primary mt-2">
Request status
</button>
</div>
);
};
export const DraftParticipant = ({ draftSessionId }) => {
const [latestMessage, setLatestMessage] = useState(null);
const socketRef = useRef(null);
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/participant`;
useEffect(() => { useEffect(() => {
socketRef.current = new WebSocket(wsUrl); socketRef.current = new WebSocket(wsUrl);
@@ -11,6 +114,9 @@ export const DraftAdmin = ({ draftSessionId }) => {
socketRef.current.onmessage = (event) => { socketRef.current.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
setLatestMessage(data); setLatestMessage(data);
if (data.type == "draft_summary") {
console.log('draft_summary', data)
}
}; };
socketRef.current.onclose = () => { socketRef.current.onclose = () => {
@@ -27,18 +133,15 @@ export const DraftAdmin = ({ draftSessionId }) => {
} }
return ( return (
<div className="container mt-4"> <div className="container draft-panel">
<h1>Draft Admin Panel</h1> <h3 >Draft Participant Panel</h3>
<WebSocketStatus wsUrl={wsUrl} />
<label>Latest Message</label> <label>Latest Message</label>
<input <input
type="text" type="text"
className="form-control"
readOnly disabled readOnly disabled
value={latestMessage ? JSON.stringify(latestMessage) : ""} value={latestMessage ? JSON.stringify(latestMessage) : ""}
/> />
<button onClick={handleStartDraft} className="btn btn-primary">
Start Draft
</button>
</div> </div>
); );
}; };

View File

@@ -3,13 +3,20 @@ console.log("Webpack HMR loaded!");
import React from "react"; import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import {DraftAdmin} from './apps/draft/index.jsx' import {DraftAdmin, DraftParticipant} from './apps/draft/index.jsx'
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const draftApp = document.getElementById("draft-app"); const draftAdminApp = document.getElementById("draft-admin-app");
const draftApp = document.getElementById("draft-app")
if (draftApp) { if (draftApp) {
const root = createRoot(draftApp); const root = createRoot(draftApp);
const draftId = draftApp.dataset.draftId const draftId = draftApp.dataset.draftId
root.render(<DraftParticipant draftSessionId={draftId} />);
}
if (draftAdminApp) {
const root = createRoot(draftAdminApp);
const draftId = draftAdminApp.dataset.draftId
root.render(<DraftAdmin draftSessionId={draftId} />); root.render(<DraftAdmin draftSessionId={draftId} />);
} }
}); });

View File

@@ -15,4 +15,18 @@
font-family: "Graphique"; font-family: "Graphique";
font-size: x-large; font-size: x-large;
} }
}
.draft-panel {
@extend .mt-4 ;
@extend .border ;
@extend .rounded-2 ;
@extend .p-2;
@extend .pt-1;
label {
@extend .form-label;
}
input {
@extend .form-control;
}
} }