feat(activity): "Gjort" mark with statistics
Per-user "I've done this" toggle alongside hearts and bookmarks.
Hearts express approval; gjort expresses completion. Both contribute
to public statistics so readers can see what people LIKE versus what
people actually DO.
Backend:
- New activity_done table (composite PK on activity_id + user_id,
CASCADE on both refs, mirrors activity_hearts).
- POST/DELETE /api/activities/:id/done. Unlike heart/bookmark, "gjort"
works on every visibility the viewer can see — private (owner-only,
acts as a personal todo checkbox), friends-only (mutual-friend +
no-block check, mirrors GET /:id), public, semi. Non-viewers get
404 to avoid leaking existence.
- buildBulkLookups + serialize extended with done_count + viewer_done
so the list endpoint stays at constant queries per render.
- Public-list endpoint (server/users.ts) bulk-fetches done counts
alongside heart counts; viewer_done is always false (unauth view).
Types: Activity{Public,Semi,Private,Friends} all gain done_count +
viewer_done. Private's count is at most 1 (only the owner can write).
UI: new "✓ Gjort" / "☐ Gjort" button in the action row with the same
optimistic-toggle + localOverride pattern as hearts. Anonymous viewers
on public activities see a muted "✓ N" stat. Title hint clarifies
the intent: "Dette har jeg gjort" vs "Du har gjort dette."
Tests: 2 new in engagement.test.ts — toggle + idempotency on public,
owner-only access on private (non-owner gets 404).
This commit is contained in:
parent
38db772b4f
commit
bbb5ad2bdd
7 changed files with 263 additions and 17 deletions
|
|
@ -152,6 +152,33 @@
|
|||
}
|
||||
}
|
||||
|
||||
// --- "Gjort" (completion mark) ------------------------------------------
|
||||
// Works on EVERY visibility the viewer can see. For private rows it acts
|
||||
// as a personal checkbox; for public/semi/friends it contributes to the
|
||||
// visible done_count statistic.
|
||||
let doneBusy = $state(false);
|
||||
async function toggleDone() {
|
||||
if (!session.user || doneBusy) return;
|
||||
doneBusy = true;
|
||||
const wasDone = view.viewer_done;
|
||||
localOverride = {
|
||||
...view,
|
||||
viewer_done: !wasDone,
|
||||
done_count: view.done_count + (wasDone ? -1 : 1),
|
||||
};
|
||||
try {
|
||||
const updated = wasDone
|
||||
? await api.undoneActivity(view.id)
|
||||
: await api.doneActivity(view.id);
|
||||
localOverride = updated;
|
||||
onChanged?.(updated);
|
||||
} catch {
|
||||
localOverride = null;
|
||||
} finally {
|
||||
doneBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bookmarks -----------------------------------------------------------
|
||||
let bookmarkBusy = $state(false);
|
||||
async function toggleBookmark() {
|
||||
|
|
@ -327,6 +354,23 @@
|
|||
<span class="muted" aria-label="Antall hjerter">♡ {view.heart_count}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- "Gjort" works on every visibility — including the owner's private
|
||||
rows where it's a personal todo checkbox. -->
|
||||
{#if session.user}
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleDone}
|
||||
disabled={doneBusy}
|
||||
aria-pressed={view.viewer_done}
|
||||
aria-label={view.viewer_done ? 'Marker som ikke gjort' : 'Marker som gjort'}
|
||||
title={view.viewer_done ? 'Du har gjort dette' : 'Dette har jeg gjort'}
|
||||
class={view.viewer_done ? 'primary' : ''}
|
||||
>
|
||||
{view.viewer_done ? '✓ Gjort' : '☐ Gjort'} {view.done_count > 0 ? view.done_count : ''}
|
||||
</button>
|
||||
{:else if view.visibility !== 'private' && view.done_count > 0}
|
||||
<span class="muted" aria-label="Antall som har gjort dette">✓ {view.done_count}</span>
|
||||
{/if}
|
||||
{#if canEdit && onEdit}
|
||||
<button type="button" onclick={startEdit}>Rediger</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ export const api = {
|
|||
http<Activity>(`/activities/${encodeURIComponent(id)}/heart`, { method: 'POST' }),
|
||||
unheartActivity: (id: string) =>
|
||||
http<Activity>(`/activities/${encodeURIComponent(id)}/heart`, { method: 'DELETE' }),
|
||||
doneActivity: (id: string) =>
|
||||
http<Activity>(`/activities/${encodeURIComponent(id)}/done`, { method: 'POST' }),
|
||||
undoneActivity: (id: string) =>
|
||||
http<Activity>(`/activities/${encodeURIComponent(id)}/done`, { method: 'DELETE' }),
|
||||
bookmarkActivity: (id: string) =>
|
||||
http<Activity>(`/activities/${encodeURIComponent(id)}/bookmark`, { method: 'POST' }),
|
||||
unbookmarkActivity: (id: string) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue