From fa489d4b729ffa834538011d2db6326ae72c8124 Mon Sep 17 00:00:00 2001
From: Coracle Bot <329+coracle-bot@noreply.coracle.social>
Date: Mon, 21 Sep 2026 20:25:01 +0000
Subject: [PATCH] Redesign the goal list and detail pages (#582)
---
e2e/USER_STORIES.md | 11 +-
e2e/specs/community.spec.ts | 44 ++--
src/app/components/CommentCompose.svelte | 2 +-
src/app/components/GoalActions.svelte | 3 +-
src/app/components/GoalCreate.svelte | 58 ++++-
src/app/components/GoalItem.svelte | 37 ++--
src/app/components/GoalMeter.svelte | 41 ++++
src/app/components/GoalStatus.svelte | 42 ++++
src/app/components/GoalSummary.svelte | 63 +++---
src/app/components/GoalSupporters.svelte | 111 ++++++++++
src/app/components/NoteContentGoal.svelte | 4 +
.../components/NoteContentMinimalGoal.svelte | 10 +-
src/app/components/ReactionSummary.svelte | 82 +++----
src/app/components/ThunkFailure.svelte | 7 +-
src/app/components/ThunkPending.svelte | 8 +-
src/app/components/ThunkStatus.svelte | 7 +-
.../components/ThunkStatusOrDeleted.svelte | 2 +-
src/app/goals.ts | 41 ++++
src/lib/components/ImagesInput.svelte | 62 +++---
src/lib/components/Spinner.svelte | 6 +
src/lib/components/badge.css | 6 +
src/lib/components/card.css | 3 +-
src/routes/spaces/[relay]/goals/+page.svelte | 203 +++++++++++++++---
.../spaces/[relay]/goals/[id]/+page.svelte | 156 +++++++-------
24 files changed, 768 insertions(+), 241 deletions(-)
create mode 100644 src/app/components/GoalMeter.svelte
create mode 100644 src/app/components/GoalStatus.svelte
create mode 100644 src/app/components/GoalSupporters.svelte
create mode 100644 src/app/goals.ts
diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md
index d46cc1a9..10737d01 100644
--- a/e2e/USER_STORIES.md
+++ b/e2e/USER_STORIES.md
@@ -812,11 +812,14 @@ members can see the momentum.
Acceptance:
- Creating a goal requires a title and details; the sats target defaults to 1000
- and can be set by field or slider.
+ and can be set by field or slider, and a deadline and cover image are optional.
+- The goals page is a board: space-wide totals, live/funded/ended filters, and a
+ sort by recent activity, age, progress or deadline.
- The goal's detail page shows the amount funded against its target on a
- progress bar.
-- A goal with contributions shows a contributor count and how long it has been
- running.
+ progress meter, with what is left to raise and how long it has left or has
+ been running.
+- A goal with contributions names its backers, ranked by what each gave, with
+ the comment that came with the zap.
### US-051 — Post, edit, and close out a classified listing
diff --git a/e2e/specs/community.spec.ts b/e2e/specs/community.spec.ts
index c3fff7b3..97f19c99 100644
--- a/e2e/specs/community.spec.ts
+++ b/e2e/specs/community.spec.ts
@@ -594,23 +594,25 @@ test("US-050 create a funding goal and track its progress", async ({seed, as}) =
await openCard(roof, "Repair the roof")
- await expect(page.getByRole("heading", {name: "Repair the roof"})).toBeVisible()
+ // The space bar names the goal alongside the hero, so both carry it as a heading.
+ await expect(page.getByRole("heading", {name: "Repair the roof"}).first()).toBeVisible()
- const goalCard = page.locator(".card.z-feature").filter({hasText: "funded of 6000 sats"})
+ // Amounts are localized, so the thousands separator is the environment's to decide.
+ const goalCard = page.locator(".card.z-feature").filter({hasText: /of 6,?000 sats/})
- // Exactly, since "funded of 6000 sats" carries "0 sats" inside it.
- await expect(goalCard.getByText("0 sats", {exact: true})).toBeVisible()
- await expect(goalCard.locator("progress")).toHaveJSProperty("value", 0)
- await expect(goalCard.locator("progress")).toHaveJSProperty("max", 6000)
+ await expect(goalCard.getByText("0%", {exact: true})).toBeVisible()
+ await expect(goalCard.getByText("sats to go", {exact: true}).locator("xpath=..")).toContainText(
+ /6,?000/,
+ )
await page.goto(`${spacePath(url)}/goals/${running.id}`)
- const paCard = page.locator(".card.z-feature").filter({hasText: "funded of 50000 sats"})
+ const paCard = page.locator(".card.z-feature").filter({hasText: /of 50,?000 sats/})
- await expect(paCard.getByText("contributors", {exact: true}).locator("xpath=..")).toContainText(
- "0",
+ await expect(paCard.getByText("backers", {exact: true}).locator("xpath=..")).toContainText("0")
+ await expect(paCard.getByText("days running", {exact: true}).locator("xpath=..")).toContainText(
+ "3",
)
- await expect(paCard.getByText("days old", {exact: true}).locator("xpath=..")).toContainText("3")
// Registered after the page was opened, so it answers ahead of the empty dufflepud `as()`
// installs, and before the navigation below, since a zapper is looked up once per page load.
@@ -625,16 +627,24 @@ test("US-050 create a funding goal and track its progress", async ({seed, as}) =
await page.goto(`${spacePath(url)}/goals/${soundproofing.id}`)
- const soundCard = page.locator(".card.z-feature").filter({hasText: "funded of 20000 sats"})
+ const soundCard = page.locator(".card.z-feature").filter({hasText: /of 20,?000 sats/})
- await expect(soundCard.getByText("1500 sats", {exact: true})).toBeVisible()
- await expect(soundCard.locator("progress")).toHaveJSProperty("value", 1500)
+ await expect(soundCard.getByText(/^1,?500$/)).toBeVisible()
+ await expect(soundCard.getByText("8%", {exact: true})).toBeVisible()
+ await expect(soundCard.getByText("backers", {exact: true}).locator("xpath=..")).toContainText("2")
await expect(
- soundCard.getByText("contributors", {exact: true}).locator("xpath=.."),
+ soundCard.getByText("days running", {exact: true}).locator("xpath=.."),
).toContainText("2")
- await expect(soundCard.getByText("days old", {exact: true}).locator("xpath=..")).toContainText(
- "2",
- )
+
+ // Each backer is named and ranked by what they gave, biggest first.
+ const supporters = page
+ .locator(".card")
+ .filter({has: page.getByRole("heading", {name: "Supporters"})})
+ .last()
+
+ await expect(supporters).toContainText("Alice Anderson")
+ await expect(supporters.getByText(/^1,?000$/)).toBeVisible()
+ await expect(supporters.getByText(/^500$/)).toBeVisible()
})
test("US-051 post, edit, and close out a classified listing", async ({seed, as}) => {
diff --git a/src/app/components/CommentCompose.svelte b/src/app/components/CommentCompose.svelte
index 808e3a71..63d14657 100644
--- a/src/app/components/CommentCompose.svelte
+++ b/src/app/components/CommentCompose.svelte
@@ -133,7 +133,7 @@