ideas: add inline edit for own ideas (author + admin)
This commit is contained in:
@@ -2828,6 +2828,34 @@ async def toggle_idea_status(
|
|||||||
return JSONResponse({"status": 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}")
|
@app.delete("/api/ideas/{idea_id}")
|
||||||
async def delete_idea(
|
async def delete_idea(
|
||||||
idea_id: str,
|
idea_id: str,
|
||||||
|
|||||||
@@ -24,6 +24,42 @@
|
|||||||
let submitting = false;
|
let submitting = false;
|
||||||
let formError = '';
|
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 {
|
function relativeTime(ts: number): string {
|
||||||
const diff = Math.floor(Date.now() / 1000) - ts;
|
const diff = Math.floor(Date.now() / 1000) - ts;
|
||||||
if (diff < 60) return 'just now';
|
if (diff < 60) return 'just now';
|
||||||
@@ -216,35 +252,68 @@
|
|||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-start justify-between gap-2">
|
{#if editingId === idea.id}
|
||||||
<p class="font-medium text-sm" style="color: var(--text-primary)">
|
<input
|
||||||
{#if done}<span class="mr-1.5 text-green-500">✓</span>{/if}{idea.title}
|
bind:value={editTitle}
|
||||||
</p>
|
class="w-full rounded-lg px-3 py-1.5 text-sm mb-2"
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
style="background: var(--bg-elevated); border: 1px solid var(--border-sub); color: var(--text-primary)"
|
||||||
{#if isAdmin}
|
/>
|
||||||
<button
|
<textarea
|
||||||
on:click={() => toggleStatus(idea)}
|
bind:value={editBody}
|
||||||
class="text-xs px-1.5 py-0.5 rounded transition-colors"
|
rows="2"
|
||||||
style="color: {done ? 'var(--text-5)' : 'var(--accent)'}; border: 1px solid {done ? 'var(--border-sub)' : 'var(--accent)'}"
|
class="w-full rounded-lg px-3 py-1.5 text-sm mb-2 resize-none"
|
||||||
title={done ? 'Mark as open' : 'Mark as done'}
|
style="background: var(--bg-elevated); border: 1px solid var(--border-sub); color: var(--text-primary)"
|
||||||
>{done ? 'reopen' : 'done'}</button>
|
></textarea>
|
||||||
{/if}
|
<div class="flex gap-2">
|
||||||
{#if isAdmin || idea.author === myHandle}
|
<button
|
||||||
<button
|
on:click={() => saveEdit(idea)}
|
||||||
on:click={() => deleteIdea(idea)}
|
disabled={editSaving || !editTitle.trim()}
|
||||||
class="text-xs px-1.5 py-0.5 rounded transition-colors hover:text-red-400"
|
class="px-3 py-1 rounded-lg text-xs font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||||
style="color: var(--text-5)"
|
style="background: var(--accent)"
|
||||||
title="Delete"
|
>{editSaving ? 'Saving…' : 'Save'}</button>
|
||||||
>×</button>
|
<button
|
||||||
{/if}
|
on:click={cancelEdit}
|
||||||
|
class="px-3 py-1 rounded-lg text-xs transition-colors"
|
||||||
|
style="color: var(--text-5)"
|
||||||
|
>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{#if idea.body}
|
<div class="flex items-start justify-between gap-2">
|
||||||
<p class="text-xs mt-1" style="color: var(--text-4)">{idea.body}</p>
|
<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}
|
{/if}
|
||||||
<p class="text-xs mt-1.5" style="color: var(--text-5)">
|
|
||||||
@{idea.author} · {relativeTime(idea.created_at)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user