diff --git a/bincio/serve/routers/ideas.py b/bincio/serve/routers/ideas.py index d9e4d8c..e864e88 100644 --- a/bincio/serve/routers/ideas.py +++ b/bincio/serve/routers/ideas.py @@ -45,7 +45,7 @@ async def list_ideas( ideas.append(idea) def _sort_key(x: dict): s = x.get("status") or "open" - order = {"awaiting": 0, "open": 1, "done": 2} + order = {"awaiting": 0, "open": 1, "done": 2, "declined": 3} return (order.get(s, 1), -x["vote_count"], -x["created_at"]) ideas.sort(key=_sort_key) return JSONResponse({"ideas": ideas}) @@ -126,6 +126,28 @@ async def toggle_idea_status( return JSONResponse({"status": idea["status"]}) +@router.post("/api/ideas/{idea_id}/decline") +async def decline_idea( + idea_id: str, + bincio_session: Optional[str] = Cookie(default=None), +) -> JSONResponse: + user = deps._require_user(bincio_session) + if not user.is_admin: + raise HTTPException(403, "Forbidden") + dd = deps._get_data_dir() + path = _ideas_dir(dd) / f"{idea_id}.json" + if not path.exists(): + raise HTTPException(404, "Not found") + with open(path, "r+", encoding="utf-8") as f: + _fcntl.flock(f, _fcntl.LOCK_EX) + idea = json.load(f) + idea["status"] = "open" if idea.get("status") == "declined" else "declined" + f.seek(0) + f.truncate() + json.dump(idea, f, ensure_ascii=False, indent=2) + return JSONResponse({"status": idea["status"]}) + + @router.post("/api/ideas/{idea_id}/comment") async def set_idea_comment( idea_id: str, diff --git a/site/src/components/IdeasPage.svelte b/site/src/components/IdeasPage.svelte index 9711f1f..066118e 100644 --- a/site/src/components/IdeasPage.svelte +++ b/site/src/components/IdeasPage.svelte @@ -181,7 +181,23 @@ } function statusOrder(s: string | undefined) { - return s === 'awaiting' ? 0 : s === 'done' ? 2 : 1; + return s === 'awaiting' ? 0 : s === 'done' ? 2 : s === 'declined' ? 3 : 1; + } + + async function declineIdea(idea: Idea) { + const r = await fetch(`/api/ideas/${idea.id}/decline`, { + method: 'POST', + credentials: 'include', + }); + if (r.ok) { + const d = await r.json(); + idea.status = d.status; + ideas = [...ideas].sort((a, b) => + statusOrder(a.status) - statusOrder(b.status) || + b.vote_count - a.vote_count || + b.created_at - a.created_at + ); + } } async function toggleStatus(idea: Idea) { @@ -200,8 +216,9 @@ } } - $: awaitingIdeas = ideas.filter(i => i.status === 'awaiting'); - $: otherIdeas = ideas.filter(i => i.status !== 'awaiting'); + $: awaitingIdeas = ideas.filter(i => i.status === 'awaiting'); + $: otherIdeas = ideas.filter(i => i.status !== 'awaiting' && i.status !== 'declined'); + $: declinedIdeas = ideas.filter(i => i.status === 'declined'); async function deleteIdea(idea: Idea) { if (!confirm(`Delete "${idea.title}"?`)) return; @@ -301,11 +318,30 @@ {/each} {/if} + + {#if declinedIdeas.length > 0} +
+

+ ✗ Won't implement +

+ +
+ {/if} {/if} {#snippet ideaContent(idea: Idea)} - {@const done = idea.status === 'done'} + {@const done = idea.status === 'done'} {@const awaiting = idea.status === 'awaiting'} + {@const declined = idea.status === 'declined'}
+ {#if declined} + + {:else} + {@const nextLabel = done ? 'reopen' : awaiting ? 'done' : 'awaiting'} + {@const btnColor = done ? 'var(--text-5)' : awaiting ? 'var(--accent)' : '#f59e0b'} + {@const borderColor = done ? 'var(--border-sub)' : awaiting ? 'var(--accent)' : '#f59e0b'} + + + {/if} {/if} {#if isAdmin || idea.author === myHandle}