ideas: add inline edit for own ideas (author + admin)

This commit is contained in:
Davide Scaini
2026-05-13 19:52:25 +02:00
parent aa1c0b38c0
commit b9a21e8bcc
2 changed files with 124 additions and 27 deletions
+28
View File
@@ -2828,6 +2828,34 @@ async def toggle_idea_status(
return JSONResponse({"status": idea["status"]})
@app.patch("/api/ideas/{idea_id}")
async def edit_idea(
idea_id: str,
data: IdeaBody,
bincio_session: Optional[str] = Cookie(default=None),
) -> JSONResponse:
user = _require_user(bincio_session)
dd = _get_data_dir()
path = _ideas_dir(dd) / f"{idea_id}.json"
if not path.exists():
raise HTTPException(404, "Not found")
title = data.title.strip()[:200]
body = data.body.strip()[:2000]
if not title:
raise HTTPException(400, "Title required")
with open(path, "r+", encoding="utf-8") as f:
_fcntl.flock(f, _fcntl.LOCK_EX)
idea = json.load(f)
if not user.is_admin and idea.get("author") != user.handle:
raise HTTPException(403, "Forbidden")
idea["title"] = title
idea["body"] = body
f.seek(0)
f.truncate()
json.dump(idea, f, ensure_ascii=False, indent=2)
return JSONResponse({"ok": True, "title": title, "body": body})
@app.delete("/api/ideas/{idea_id}")
async def delete_idea(
idea_id: str,
+96 -27
View File
@@ -24,6 +24,42 @@
let submitting = false;
let formError = '';
let editingId: string | null = null;
let editTitle = '';
let editBody = '';
let editSaving = false;
function startEdit(idea: Idea) {
editingId = idea.id;
editTitle = idea.title;
editBody = idea.body;
}
function cancelEdit() {
editingId = null;
}
async function saveEdit(idea: Idea) {
if (!editTitle.trim()) return;
editSaving = true;
try {
const r = await fetch(`/api/ideas/${idea.id}`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: editTitle.trim(), body: editBody.trim() }),
});
if (r.ok) {
idea.title = editTitle.trim();
idea.body = editBody.trim();
ideas = ideas;
editingId = null;
}
} finally {
editSaving = false;
}
}
function relativeTime(ts: number): string {
const diff = Math.floor(Date.now() / 1000) - ts;
if (diff < 60) return 'just now';
@@ -216,35 +252,68 @@
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2">
<p class="font-medium text-sm" style="color: var(--text-primary)">
{#if done}<span class="mr-1.5 text-green-500"></span>{/if}{idea.title}
</p>
<div class="flex items-center gap-1 shrink-0">
{#if isAdmin}
<button
on:click={() => toggleStatus(idea)}
class="text-xs px-1.5 py-0.5 rounded transition-colors"
style="color: {done ? 'var(--text-5)' : 'var(--accent)'}; border: 1px solid {done ? 'var(--border-sub)' : 'var(--accent)'}"
title={done ? 'Mark as open' : 'Mark as done'}
>{done ? 'reopen' : 'done'}</button>
{/if}
{#if isAdmin || idea.author === myHandle}
<button
on:click={() => deleteIdea(idea)}
class="text-xs px-1.5 py-0.5 rounded transition-colors hover:text-red-400"
style="color: var(--text-5)"
title="Delete"
>×</button>
{/if}
{#if editingId === idea.id}
<input
bind:value={editTitle}
class="w-full rounded-lg px-3 py-1.5 text-sm mb-2"
style="background: var(--bg-elevated); border: 1px solid var(--border-sub); color: var(--text-primary)"
/>
<textarea
bind:value={editBody}
rows="2"
class="w-full rounded-lg px-3 py-1.5 text-sm mb-2 resize-none"
style="background: var(--bg-elevated); border: 1px solid var(--border-sub); color: var(--text-primary)"
></textarea>
<div class="flex gap-2">
<button
on:click={() => saveEdit(idea)}
disabled={editSaving || !editTitle.trim()}
class="px-3 py-1 rounded-lg text-xs font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-50"
style="background: var(--accent)"
>{editSaving ? 'Saving…' : 'Save'}</button>
<button
on:click={cancelEdit}
class="px-3 py-1 rounded-lg text-xs transition-colors"
style="color: var(--text-5)"
>Cancel</button>
</div>
</div>
{#if idea.body}
<p class="text-xs mt-1" style="color: var(--text-4)">{idea.body}</p>
{:else}
<div class="flex items-start justify-between gap-2">
<p class="font-medium text-sm" style="color: var(--text-primary)">
{#if done}<span class="mr-1.5 text-green-500"></span>{/if}{idea.title}
</p>
<div class="flex items-center gap-1 shrink-0">
{#if isAdmin}
<button
on:click={() => toggleStatus(idea)}
class="text-xs px-1.5 py-0.5 rounded transition-colors"
style="color: {done ? 'var(--text-5)' : 'var(--accent)'}; border: 1px solid {done ? 'var(--border-sub)' : 'var(--accent)'}"
title={done ? 'Mark as open' : 'Mark as done'}
>{done ? 'reopen' : 'done'}</button>
{/if}
{#if isAdmin || idea.author === myHandle}
<button
on:click={() => startEdit(idea)}
class="text-xs px-1.5 py-0.5 rounded transition-colors hover:text-white"
style="color: var(--text-5)"
title="Edit"
></button>
<button
on:click={() => deleteIdea(idea)}
class="text-xs px-1.5 py-0.5 rounded transition-colors hover:text-red-400"
style="color: var(--text-5)"
title="Delete"
>×</button>
{/if}
</div>
</div>
{#if idea.body}
<p class="text-xs mt-1" style="color: var(--text-4)">{idea.body}</p>
{/if}
<p class="text-xs mt-1.5" style="color: var(--text-5)">
@{idea.author} · {relativeTime(idea.created_at)}
</p>
{/if}
<p class="text-xs mt-1.5" style="color: var(--text-5)">
@{idea.author} · {relativeTime(idea.created_at)}
</p>
</div>
</li>
{/each}