Move card action buttons to top-right corner and add Mark Played to alternates
Reposition approve/reject/played buttons as absolute top-right overlay on each card. Add Mark Played buttons to alternate match cards with a new play-alt API endpoint, and right-align all alternate action buttons. Made-with: Cursor
This commit is contained in:
@@ -338,6 +338,7 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-card:hover { border-color: #3a3a3a; }
|
.request-card:hover { border-color: #3a3a3a; }
|
||||||
@@ -422,7 +423,9 @@
|
|||||||
.card-actions {
|
.card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.5rem;
|
position: absolute;
|
||||||
|
top: 0.625rem;
|
||||||
|
right: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@@ -541,6 +544,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alt-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-title {
|
.alt-title {
|
||||||
|
|||||||
@@ -32,26 +32,35 @@ def _render_alternates(request_id: int, alternates_json: str, status: str) -> st
|
|||||||
|
|
||||||
esc = html.escape
|
esc = html.escape
|
||||||
show_approve = status == "pending"
|
show_approve = status == "pending"
|
||||||
|
show_play = status in ("pending", "approved")
|
||||||
items = []
|
items = []
|
||||||
for idx, alt in enumerate(alts):
|
for idx, alt in enumerate(alts):
|
||||||
artwork = esc(alt.get("artwork_url", "").replace("600x600", "100x100"))
|
artwork = esc(alt.get("artwork_url", "").replace("600x600", "100x100"))
|
||||||
title = esc(alt.get("title", ""))
|
title = esc(alt.get("title", ""))
|
||||||
artist = esc(alt.get("artist", ""))
|
artist = esc(alt.get("artist", ""))
|
||||||
url = esc(alt.get("apple_music_url", ""))
|
url = esc(alt.get("apple_music_url", ""))
|
||||||
approve_btn = ""
|
buttons = []
|
||||||
if show_approve:
|
if show_approve:
|
||||||
approve_btn = (
|
buttons.append(
|
||||||
f'<button hx-post="/api/requests/{request_id}/approve-alt/{idx}"'
|
f'<button hx-post="/api/requests/{request_id}/approve-alt/{idx}"'
|
||||||
f' hx-swap="outerHTML" hx-target="#request-{request_id}"'
|
f' hx-swap="outerHTML" hx-target="#request-{request_id}"'
|
||||||
f' class="btn btn-approve btn-sm admin-only"'
|
f' class="btn btn-approve btn-sm admin-only"'
|
||||||
f' onclick="event.stopPropagation();">Approve</button>'
|
f' onclick="event.stopPropagation();">Approve</button>'
|
||||||
)
|
)
|
||||||
|
if show_play:
|
||||||
|
buttons.append(
|
||||||
|
f'<button hx-post="/api/requests/{request_id}/play-alt/{idx}"'
|
||||||
|
f' hx-swap="outerHTML" hx-target="#request-{request_id}"'
|
||||||
|
f' class="btn btn-played btn-sm admin-only"'
|
||||||
|
f' onclick="event.stopPropagation();">Mark Played</button>'
|
||||||
|
)
|
||||||
|
actions_html = f'<div class="alt-actions admin-only">{"".join(buttons)}</div>' if buttons else ""
|
||||||
items.append(
|
items.append(
|
||||||
f'<div class="alt-card" onclick="event.stopPropagation(); window.open(\'{url}\',\'_blank\')">'
|
f'<div class="alt-card" onclick="event.stopPropagation(); window.open(\'{url}\',\'_blank\')">'
|
||||||
f'<img class="alt-art" src="{artwork}" alt="" loading="lazy" />'
|
f'<img class="alt-art" src="{artwork}" alt="" loading="lazy" />'
|
||||||
f'<div class="alt-info"><span class="alt-title">{title}</span>'
|
f'<div class="alt-info"><span class="alt-title">{title}</span>'
|
||||||
f'<span class="alt-artist">{artist}</span></div>'
|
f'<span class="alt-artist">{artist}</span></div>'
|
||||||
f'{approve_btn}</div>'
|
f'{actions_html}</div>'
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
'<details class="alternates" onclick="event.stopPropagation();">'
|
'<details class="alternates" onclick="event.stopPropagation();">'
|
||||||
@@ -104,8 +113,8 @@ def render_request_card(req: SongRequestModel) -> str:
|
|||||||
<div class="card-album">{esc(req.album)}</div>
|
<div class="card-album">{esc(req.album)}</div>
|
||||||
<div class="card-meta">Requested by {esc(req.requester_nick)} in {esc(req.channel)} at {created}</div>
|
<div class="card-meta">Requested by {esc(req.requester_nick)} in {esc(req.channel)} at {created}</div>
|
||||||
<span class="card-status-badge">{esc(req.status)}</span>
|
<span class="card-status-badge">{esc(req.status)}</span>
|
||||||
{actions}
|
|
||||||
</div>
|
</div>
|
||||||
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
{alternates_html}
|
{alternates_html}
|
||||||
</div>"""
|
</div>"""
|
||||||
@@ -181,6 +190,7 @@ class WebServer:
|
|||||||
app.router.add_get("/api/channels", self._handle_channels)
|
app.router.add_get("/api/channels", self._handle_channels)
|
||||||
app.router.add_get("/api/requests", self._handle_api_get)
|
app.router.add_get("/api/requests", self._handle_api_get)
|
||||||
app.router.add_post("/api/requests/{request_id}/approve-alt/{alt_idx}", self._handle_approve_alt)
|
app.router.add_post("/api/requests/{request_id}/approve-alt/{alt_idx}", self._handle_approve_alt)
|
||||||
|
app.router.add_post("/api/requests/{request_id}/play-alt/{alt_idx}", self._handle_play_alt)
|
||||||
app.router.add_post("/api/requests/{request_id}/{action}", self._handle_api_action)
|
app.router.add_post("/api/requests/{request_id}/{action}", self._handle_api_action)
|
||||||
app.router.add_get("/api/export/markdown", self._handle_export_markdown)
|
app.router.add_get("/api/export/markdown", self._handle_export_markdown)
|
||||||
app.router.add_get("/api/status", self._handle_get_status)
|
app.router.add_get("/api/status", self._handle_get_status)
|
||||||
@@ -446,6 +456,46 @@ class WebServer:
|
|||||||
|
|
||||||
return web.Response(text=card_html, content_type="text/html")
|
return web.Response(text=card_html, content_type="text/html")
|
||||||
|
|
||||||
|
async def _handle_play_alt(self, request: web.Request) -> web.Response:
|
||||||
|
denied = self._require_auth(request)
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
|
||||||
|
try:
|
||||||
|
request_id = int(request.match_info["request_id"])
|
||||||
|
alt_idx = int(request.match_info["alt_idx"])
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
return web.Response(text="Bad request", status=400)
|
||||||
|
|
||||||
|
req = self._store.get(request_id)
|
||||||
|
if not req:
|
||||||
|
return web.Response(text="Request not found", status=404)
|
||||||
|
|
||||||
|
try:
|
||||||
|
alts = json.loads(req.alternates_json) if req.alternates_json else []
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
alts = []
|
||||||
|
|
||||||
|
if alt_idx < 0 or alt_idx >= len(alts):
|
||||||
|
return web.Response(text="Invalid alternate index", status=400)
|
||||||
|
|
||||||
|
alt = alts[alt_idx]
|
||||||
|
self._store.swap_alternate(request_id, alt)
|
||||||
|
req = self._store.update_status(request_id, "played")
|
||||||
|
if not req:
|
||||||
|
return web.Response(text="Request not found", status=404)
|
||||||
|
|
||||||
|
card_html = render_request_card(req)
|
||||||
|
await self._broadcast("request-update", card_html)
|
||||||
|
|
||||||
|
if self._on_status_change:
|
||||||
|
try:
|
||||||
|
self._on_status_change(req)
|
||||||
|
except Exception:
|
||||||
|
log.exception("SongRequest: on_status_change callback failed")
|
||||||
|
|
||||||
|
return web.Response(text=card_html, content_type="text/html")
|
||||||
|
|
||||||
async def _handle_export_markdown(self, request: web.Request) -> web.Response:
|
async def _handle_export_markdown(self, request: web.Request) -> web.Response:
|
||||||
denied = self._require_auth(request)
|
denied = self._require_auth(request)
|
||||||
if denied:
|
if denied:
|
||||||
|
|||||||
Reference in New Issue
Block a user