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:
cottongin
2026-03-28 12:53:33 -04:00
parent 7ebd22568b
commit 3372ce77fa
2 changed files with 66 additions and 5 deletions

View File

@@ -338,6 +338,7 @@
border: 1px solid var(--border);
border-radius: var(--radius);
transition: all 0.2s ease;
position: relative;
}
.request-card:hover { border-color: #3a3a3a; }
@@ -422,7 +423,9 @@
.card-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
position: absolute;
top: 0.625rem;
right: 0.625rem;
}
.btn {
@@ -541,6 +544,14 @@
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.alt-actions {
display: flex;
gap: 0.375rem;
flex-shrink: 0;
margin-left: auto;
}
.alt-title {

View File

@@ -32,26 +32,35 @@ def _render_alternates(request_id: int, alternates_json: str, status: str) -> st
esc = html.escape
show_approve = status == "pending"
show_play = status in ("pending", "approved")
items = []
for idx, alt in enumerate(alts):
artwork = esc(alt.get("artwork_url", "").replace("600x600", "100x100"))
title = esc(alt.get("title", ""))
artist = esc(alt.get("artist", ""))
url = esc(alt.get("apple_music_url", ""))
approve_btn = ""
buttons = []
if show_approve:
approve_btn = (
buttons.append(
f'<button hx-post="/api/requests/{request_id}/approve-alt/{idx}"'
f' hx-swap="outerHTML" hx-target="#request-{request_id}"'
f' class="btn btn-approve btn-sm admin-only"'
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(
f'<div class="alt-card" onclick="event.stopPropagation(); window.open(\'{url}\',\'_blank\')">'
f'<img class="alt-art" src="{artwork}" alt="" loading="lazy" />'
f'<div class="alt-info"><span class="alt-title">{title}</span>'
f'<span class="alt-artist">{artist}</span></div>'
f'{approve_btn}</div>'
f'{actions_html}</div>'
)
return (
'<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-meta">Requested by {esc(req.requester_nick)} in {esc(req.channel)} at {created}</div>
<span class="card-status-badge">{esc(req.status)}</span>
{actions}
</div>
{actions}
</div>
{alternates_html}
</div>"""
@@ -181,6 +190,7 @@ class WebServer:
app.router.add_get("/api/channels", self._handle_channels)
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}/play-alt/{alt_idx}", self._handle_play_alt)
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/status", self._handle_get_status)
@@ -446,6 +456,46 @@ class WebServer:
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:
denied = self._require_auth(request)
if denied: