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
|
|
@ -219,6 +219,10 @@ export interface ActivityPublic {
|
|||
viewer_hearted: boolean;
|
||||
/** True when the authenticated viewer has bookmarked this activity. */
|
||||
viewer_bookmarked: boolean;
|
||||
/** Total "done" marks (people who have actually completed the activity). */
|
||||
done_count: number;
|
||||
/** True when the authenticated viewer has marked this activity done. */
|
||||
viewer_done: boolean;
|
||||
/** Effective sort position for THIS viewer: a custom value if they've
|
||||
* dragged this row, otherwise -created_at so unsorted/new rows float
|
||||
* to the top. Used by the client to compute drop-target midpoints. */
|
||||
|
|
@ -244,6 +248,8 @@ export interface ActivitySemi {
|
|||
heart_count: number;
|
||||
viewer_hearted: boolean;
|
||||
viewer_bookmarked: boolean;
|
||||
done_count: number;
|
||||
viewer_done: boolean;
|
||||
/** Effective sort position for THIS viewer: a custom value if they've
|
||||
* dragged this row, otherwise -created_at so unsorted/new rows float
|
||||
* to the top. Used by the client to compute drop-target midpoints. */
|
||||
|
|
@ -264,6 +270,11 @@ export interface ActivityPrivate {
|
|||
heart_count: number;
|
||||
viewer_hearted: boolean;
|
||||
viewer_bookmarked: boolean;
|
||||
// "Done" DOES apply to private rows: the owner can use it as a personal
|
||||
// todo checkbox. done_count is therefore always 0 or 1 (just the owner)
|
||||
// for private rows. viewer_done reflects the owner's own state.
|
||||
done_count: number;
|
||||
viewer_done: boolean;
|
||||
/** Effective sort position for THIS viewer: a custom value if they've
|
||||
* dragged this row, otherwise -created_at so unsorted/new rows float
|
||||
* to the top. Used by the client to compute drop-target midpoints. */
|
||||
|
|
@ -294,6 +305,8 @@ export interface ActivityFriends {
|
|||
heart_count: number;
|
||||
viewer_hearted: boolean;
|
||||
viewer_bookmarked: boolean;
|
||||
done_count: number;
|
||||
viewer_done: boolean;
|
||||
/** Effective sort position for THIS viewer: a custom value if they've
|
||||
* dragged this row, otherwise -created_at so unsorted/new rows float
|
||||
* to the top. Used by the client to compute drop-target midpoints. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue