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-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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user