From 8987baaedf7e60467e07b8e6010917a7b30958ee Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Sat, 22 Aug 2026 14:12:06 -0400 Subject: [PATCH 01/13] Added thoughts on "how do we convey what this is all about?" --- specs/product/how-to-convey-this.md | 244 ++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 specs/product/how-to-convey-this.md diff --git a/specs/product/how-to-convey-this.md b/specs/product/how-to-convey-this.md new file mode 100644 index 00000000..745b3fc5 --- /dev/null +++ b/specs/product/how-to-convey-this.md @@ -0,0 +1,244 @@ +# My initial prompt + +I forget, something about "how can we make it clear what this system is for?" + +# AI response + +The current landing reads like every other organizer tool. “Start a cause. Build a Movement. Change the world.” plus Start → Grow → Deliver could sit on NationBuilder, Change.org, Kickstarter, or a 501(c)(3) starter kit. None of that is what the vision docs actually argue. + +The real claim is late aggregation. Familiar systems compress people into a platform, party, petition, or charity first, then act. Commonality keeps each person on their own words and money until the last moment, then funds the overlap. That is why the project is named Commonality, and it is the thing the homepage currently hides. + +What people will map this onto (and how to break the mapping) + +┌─────────────────────────┬───────────────────────────┬─────────────────────────────────────────────────────────────────┐ +│ They think it is… │ What they expect │ What this actually is │ +├─────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────┤ +│ Change.org / petition │ Collect names, beg an │ You do not need the institution. You fund the work yourselves. │ +│ │ institution │ │ +├─────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────┤ +│ Kickstarter / GoFundMe │ One creator, one campaign │ A cause is many independent claims and many projects. │ +│ │ │ Delegation and at-cost reimbursement are how it scales past one │ +│ │ │ campaign. │ +├─────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────┤ +│ Charity / 501(c)(3) │ Give to an org, trust the │ No group treasury, no grant committee. Pledge a project; refund │ +│ │ annual report │ if the threshold fails; books are the chain. │ +├─────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────┤ +│ Political party / “join │ Swallow a bundle, wait │ Sign one plank. No membership. Value at every step, not after a │ +│ the movement” │ for election day │ win. │ +├─────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────┤ +│ Discord / meetup / │ Recruit into your group │ Organizers circulate a link. There is no directory and no │ +│ NationBuilder │ and keep them there │ membership boundary. Coalitions are discovered, not enrolled. │ +└─────────────────────────┴───────────────────────────┴─────────────────────────────────────────────────────────────────┘ + +If the first screen does not make that table felt, people will use the wrong product in their head and bounce when they hit “no browse, no ranking, sign this sentence.” + +What to put on the landing instead of a lifecycle slogan + +Lead with the mechanism, not the job-to-be-done of starting an org. + +Headline (recommended): +Don’t join a coalition. Discover one. + +Subhead: +Write claims people can sign one at a time, in their own words. The system finds who already agrees on the outcome — even if they would never share a party, a charity, or a Slack. Money moves only when enough people actually show up. + +That is the organic-coalitions thesis in one breath. It also respects founder-first: the customer is someone who would otherwise have to found an org, a PAC, or a Kickstarter and then spend years herding a bundle. + +CTA stays “Start a cause.” That part is fine. The copy around it should not promise “build a Movement.” + +Alternate headlines (if you want options) + +1. Say what you mean. Fund what you already agree on. + Closest to “no need to compromise.” +2. A cause without a party. Funding without a gatekeeper. + Contrast-first; good if the page has a comparison table below. +3. Every signature is useful. Nothing waits for election day. + Best if the visitor is a tired activist. Matches “easier than politics.” +4. State it. Sign it. Pay only if it happens. + Operational; names assurance contracts without jargon. + +I would not use 4 as the only line — it still sounds like Kickstarter — but it is a strong third beat. + +Replace Start → Grow → Deliver + +That sequence is the lifecycle of any org. It also implies “recruit a following, then ship,” which is the opposite of “late aggregation.” + +Better three beats (same UI component, different titles): + +1. State — Independent, signable claims. Not a manifesto people have to swallow. +2. Overlap — Implication and bridges connect people who used different words for the same outcome. Nobody has to join your team or adopt your framing. +3. Fund — Pledges that refund if the threshold is missed; later money can reimburse early scouts. Track records stay public. + +Shorter slogan form of that: State → Overlap → Fund. + +If you want it even less “product funnel”: Your words. Their signatures. Shared work. + +How to show the difference (not just slogan it) + +A slogan will not carry this. One screen of contrast will. + +1. A one-screen “not this / this” under the hero. Three rows is enough: party, charity, Kickstarter. One sentence each. Do not bury this in Docs. +2. Show a plank, not a brand. A cause as three independent sentences with separate signer counts beats “Start a movement.” The product already works this way; the landing talks as if a cause were a club. +3. Show two camps funding one project (the rural water example from the organic-coalitions doc). That picture is the whole thesis. No slogan beats it. +4. Name the absences as features: no directory, no membership, no pooled treasury, no permission. CauseStarter is a lens, not a marketplace. If you do not say that, “Start a cause” looks broken when they cannot browse causes. +5. Do not lead with crypto, AI, or “change the world.” Those are how it is possible. The reason to care is: you keep your wording, you do not wait for a win, and gatekeepers are optional. + +Pitch you could paste + +│ Most systems that want to “do good at scale” ask you to join something: a party, a charity, a campaign, a Discord. You compromise on the wording, pool the money, and wait for a committee or an election. +│ +│ CauseStarter is for people who will not swallow the bundle. You publish the claims you actually believe. Others sign only the sentences they mean. Overlap is discovered, not negotiated. Funding is per project, with refunds if the threshold is not met. +│ +│ You do not need a 501(c)(3), a grant officer, or a majority. You need sentences people will stand behind, and work those sentences can pay for. + +That is different from Kickstarter (ecosystem, not a campaign), different from Change.org (you fund, you do not petition), and different from a party (no membership, value before critical mass). + +If you want this on the actual homepage, the change is small: rewrite the hero + HowItWorksSteps, and add a three-row contrast. I would not ship “Build a Movement” again; it trains the wrong instinct. + + +# My response + +Late aggregation is part of it. + +Organic coalitions is neat but doesn't really feel like it should be the main headline. + +I feel like one of the big themes is less need for coordination. (I'm not suggesting we phrase it that way, I'm just trying to convey the thoughts in my head so that you'll understand what I'm getting at.) + +And another big one is designing the system so that the crowd can be tapped to fill various roles in a way that *doesn't* require them to do stuff they *don't* want to do. + +"I'd be happy to contribute $X/month (as long as enough others do too) to the cause, but..." + "...ugh, I don't have time to actually follow all the individual potential projects and make all those decisions." (Just pledge the $X/month and delegate the decisions to someone else.) + "...ugh, I don't want to just donate to the big charity org, I don't trust them not to waste the money." (Don't delegate to a big org, delegate to a friend you trust.) + +"I'd be happy to watch the ecosystem for worthwhile projects to fund, but..." + "...ugh, I don't even know what projects are out there." (Find or start a cause page with the statements you want to watch for; it should give you a nice populated list of projects, without you needing to have any special connections.) + "...ugh, I can't tell whether this project creator is a scammer or incompetent." (Be a delegate who only does retroactive funding.) + "...ugh, I can't put that much of my *own* money towards that." (Be an early funder who asks for reimbursement.) + +"I'd be happy to do this work, but..." + "...ugh, I can't afford to pay for it myself." (Get funded by others.) + "...ugh, I don't even know who to go to to get funding." (Just post the project and talk to a friend who's a bit better connected in the trust graph, get him to do an alignment attestation; you don't need to have a direct connection to a centralized gatekeeper.) + +"I'd be happy to sign a statement sorta like that, but..." + "...ugh, I don't love the way that one is phrased." (Write your own.) + "...ugh, if I write my own it'll have zero signers." (Write it in such a way that the implication attester can infer indirect support from people who've signed similar statements.) + "...ugh, the statement that others have signed doesn't actually imply the better statement I have in mind." (Write a bridge statement, submit it as a suggestion to one of the existing AI mediator services; if it agrees, it'll send out nudges to its subscribers.) + +"I'd be happy to follow a cause page sorta like that one, but..." + "...ugh, I don't love the exact combination of statements they chose." (Create your own cause.) + "...ugh, if I write my own it'll have zero signers or projects." (If you're willing to choose *some* of the same statements, your cause will show the signers and projects that those statements have. Even if you need to write your own, the implication system and bridging system may help.) + +The bottom line is something like: we could actually do a *much* better job of collectively making this cause happen. Project-doers will be able to get money for worthwhile projects; donors who care about the cause will be able to help financially without spending much time or too much risk of wasting their money; people who have time or expertise can contribute those without needing to contribute money; all of this information and money can be collected and disseminated without needing to get everyone to agree on every idea or agree on who the leaders should be. In general, we can cooperate, pool our money and our skills, in a way that has always felt like it *should* be possible ("there are *so* many of us, *why* can't we get anything done???") but was previously blocked due to inability to coordinate. This ecosystem can just *work*. Like... all of the above "I'd be happy to do X, but ugh, Y" statements are really just addressing various obstacles; the core vision is simple ("let's have cooperate on stuff they agree about") and a bunch of the features of this system are just about addressing all the reasons why that's not as easy as it feels like it should be. + +Does that make sense? How can we convey all that (in the landing page or docs or UI in general or whatever)? + + +# AI response + +Yes. That is the product. Organic coalitions and late aggregation are how the machine is allowed to be this uncoordinated. They should not be the first thing a stranger has to care about. + +The thing in your head is two layers: + +1. The simple job: people who already agree should be able to put money, work, and attention on the same outcomes. +2. The actual invention: split that job into roles people would already take, and remove the “ugh” that currently forces everyone to also do the parts they hate (committee, wording compromise, full-time project diligence, trusting a big org, knowing a grant officer, writing a statement nobody else will sign). + +That is not “less coordination” as a slogan. It is you only contribute the part you would contribute anyway. The coordination tax is what made “there are so many of us — why can’t we get anything done?” feel like a law of nature. This system is the claim that it was never a law; it was a pile of ughs. + +Headline + +Do not lead with mechanism. Lead with the stuck crowd. + +Recommended: +There are enough of us. We just couldn’t work together. + +Subhead: +Give money without becoming a grant officer. Spot projects without bankrolling them. Do the work without knowing a foundation. Sign what you actually mean. The rest is optional. + +That is the “I’d be happy to, but ugh” thesis without listing every ugh. + +Other headlines in the same family: + +• Do the part you’d do anyway. +• Cooperate without a committee. +• The cause doesn’t need a boss. It needs the jobs people will actually take. + +I would not use “coordination,” “aggregation,” or “organic coalitions” on the hero. Keep those for people who click “how.” + +CTA can stay Start a cause for founders, with a quieter second line: or pick one job below. CauseStarter is founder-first, but the story has to be about the whole crowd or founders will think they still have to recruit a complete org. + +Landing shape + +Three blocks. That is enough. + +1. Hero — the stuck-crowd line above. + +2. Jobs, not features. Four cards, each one ugh → one out. Not “delegation / LazyGiving / implication.” Those names are implementation. + +┌────────────────┬───────────────────────────────────────────┬──────────────────────────────────────────────────────────┐ +│ I’d be happy │ …but ugh │ So you │ +│ to… │ │ │ +├────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────────────────┤ +│ Put in $X/ │ I won’t read every project, and I don’t │ Pledge, and hand the choices to a person you actually │ +│ month │ trust the big org │ trust │ +├────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────────────────┤ +│ Hunt for good │ I don’t know what’s out there, and I can │ Follow statements you care about; fund proven work, or │ +│ work │ ’t tell who’s a scam │ fund early and get reimbursed │ +├────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────────────────┤ +│ Do the work │ I can’t self-fund, and I don’t know a │ Publish the project; a friend who is one hop better- │ +│ │ gatekeeper │ connected can vouch that it fits │ +├────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────────────────┤ +│ Stand behind │ I hate that wording, and my wording will │ Write yours; similar signatures still count, and a │ +│ an idea │ have zero signers │ bridge can invite the rest │ +└────────────────┴───────────────────────────────────────────┴──────────────────────────────────────────────────────────┘ + +Under each card, one sentence of “you never have to do the other jobs.” That is the design principle made visible. + +3. The punchline, once: +Nobody has to agree on a leader, a manifesto, or a treasury. Overlap is enough. Money and work meet there. + +Then Start a cause. Optional: “See a worked example” → Christianity / local food, used as a crowd of roles, not as a pretty brand. + +Skip Start → Grow → Deliver. The sequence is not a lifecycle. It is pick a job. + +Docs: one page that is this, verbatim + +The vision tree already has pieces (delegation, no-compromise, pitches, legacy frustrations) but they are argued as advantages of a system. Your note is argued as unblocking people. Those are different documents. + +Add something like docs/end-user/commonality/vision-and-strategy/the-jobs.md (name it in product language, not “roles in the protocol”) that is almost exactly your ugh list. That page is the canonical pitch. pitches.md can point at it. + +Structure: + +• One paragraph: the job is cooperate on agreement; everything else is removing ughs. +• Then your four sections (money, attention/judgment, work, wording/causes), each ugh as a heading. +• Close: this is why it can just work. No civilizational-competence encore on that page. + +Founder docs should say: you are not recruiting members into an org. You are publishing a place where those jobs can attach. That is the difference from “start a movement.” + +UI: every empty state is an ugh, not a feature name + +This is how the landing stays true after click. + +• Cause with no pledges: “People will put in $X/month if they don’t have to pick projects. Invite a delegate they already trust.” +• Project list empty: “Publish a project. You don’t need a grant officer — an alignment vouch from someone in the graph is enough to show up for people watching these statements.” +• Statement with few signers: “Write it so it can sit next to similar claims. Direct signatures are not the only support.” +• “Create your own cause”: “Reuse some of the same statements and you inherit their signers and projects. You are not starting from zero unless you insist on a totally new set.” +• Delegation CTA: not “Become a delegate.” “Only want to judge, not pay? Watch this list. Only fund what’s already done if you don’t want to gamble.” + +Tool cards (Tally, LazyGiving, boards) should answer which ugh they remove, not which domain they are. + +Do not add a cause directory to tell this story. The story is “follow statements / a link an organizer sent,” which is already the lens posture. + +What not to lead with + +• Organic coalitions: a bonus of not forcing a bundle. Put it under wording/bridges, or a “and sometimes people who thought they disagreed…” aside. +• Late aggregation: true, but it is the engineer’s name for “you don’t have to agree on a leader first.” +• Kickstarter/Change.org comparison table: useful later; the first contrast is org vs crowd-of-jobs, not platform vs platform. +• “Change the world / build a Movement”: that reintroduces the committee in the visitor’s head. + +One paragraph you could paste on the homepage + +│ People who care about the same things have always felt like they should be able to pool money and skill. They usually can’t, because every existing way of doing it demands extra jobs nobody wanted: pick every project, trust a big org, phrase the manifesto, elect a board, know a gatekeeper, wait until enough people join. +│ +│ Here you take the job you’d take anyway. Money can be lazy and cautious. Attention can be expert and cheap. Work can ask for funding without a foundation. Wording can be yours without isolating you. Agreement does the collecting. Nobody has to run the whole thing. + +If you want this in the product next, the smallest honest change is: rewrite the hero, replace How it works with the four jobs, and retarget the empty-state copy on cause / project / statement pages to the matching ugh. I would not expand the landing into the full ugh catalog; that catalog belongs on one docs page and in the UI at the moment the ugh appears. From 0aac8941b53907f684f83ed2586c755e2f052b18 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Sat, 22 Aug 2026 15:00:32 -0400 Subject: [PATCH 02/13] Pitch CauseStarter as jobs, not a movement lifecycle. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Start → Grow → Deliver with the “do the part you’d do anyway” catalog, ship in-app /docs from end-user markdown, and add empty-state tips plus a /welcome route. --- causestarter/README.md | 8 +- causestarter/index.html | 2 +- causestarter/package.json | 2 + causestarter/src/App.tsx | 32 ++- .../src/components/CauseBridgesSection.tsx | 4 +- causestarter/src/components/CrowdJobs.tsx | 46 ++++ .../src/components/HowItWorksSteps.tsx | 72 ----- causestarter/src/components/JobTip.tsx | 34 +++ causestarter/src/components/YourCauses.tsx | 4 +- .../src/components/YourNudgersAndNudges.tsx | 4 +- causestarter/src/components/YourProjects.tsx | 4 +- .../src/components/YourSignedStatements.tsx | 2 +- causestarter/src/lib/jobs.test.ts | 10 + causestarter/src/lib/jobs.ts | 52 ++++ causestarter/src/lib/tools.ts | 4 +- causestarter/src/pages/CauseDetailPage.tsx | 29 +- causestarter/src/pages/CauseFundingPage.tsx | 15 +- causestarter/src/pages/DocsPage.test.tsx | 41 +++ causestarter/src/pages/DocsPage.tsx | 252 ++++++++++++++++-- causestarter/src/pages/HomePage.test.tsx | 36 +++ causestarter/src/pages/HomePage.tsx | 104 +------- causestarter/src/pages/StatementPage.tsx | 29 +- causestarter/src/pages/WelcomePage.tsx | 106 ++++++++ causestarter/src/virtual-end-user-docs.d.ts | 4 + causestarter/src/wagmi.ts | 4 +- causestarter/vite.config.ts | 7 +- causestarter/vitest.config.ts | 3 +- docs/end-user/causestarter/index.md | 33 +++ docs/end-user/causestarter/start-a-cause.md | 33 +++ docs/end-user/causestarter/the-jobs.md | 61 +++++ docs/end-user/commonality/index.md | 2 + .../commonality/vision-and-strategy/README.md | 2 + .../vision-and-strategy/pitches.md | 2 + .../vision-and-strategy/the-jobs.md | 9 + docs/end-user/tldr-for-llms.md | 8 + package-lock.json | 2 + specs/user-docs.md | 1 + ui/endUserDocsPlugin.ts | 6 +- 38 files changed, 829 insertions(+), 240 deletions(-) create mode 100644 causestarter/src/components/CrowdJobs.tsx delete mode 100644 causestarter/src/components/HowItWorksSteps.tsx create mode 100644 causestarter/src/components/JobTip.tsx create mode 100644 causestarter/src/lib/jobs.test.ts create mode 100644 causestarter/src/lib/jobs.ts create mode 100644 causestarter/src/pages/DocsPage.test.tsx create mode 100644 causestarter/src/pages/HomePage.test.tsx create mode 100644 causestarter/src/pages/WelcomePage.tsx create mode 100644 causestarter/src/virtual-end-user-docs.d.ts create mode 100644 docs/end-user/causestarter/index.md create mode 100644 docs/end-user/causestarter/start-a-cause.md create mode 100644 docs/end-user/causestarter/the-jobs.md create mode 100644 docs/end-user/commonality/vision-and-strategy/the-jobs.md diff --git a/causestarter/README.md b/causestarter/README.md index 339a6d11..f90cfd59 100644 --- a/causestarter/README.md +++ b/causestarter/README.md @@ -207,6 +207,12 @@ See [`cause-assist/README.md`](../cause-assist/README.md). Bridge-cluster wordin ## Design notes +- **Landing pitch is jobs, not a movement lifecycle.** Hero and `/docs/the-jobs` + (`docs/end-user/causestarter/the-jobs.md`) are the “do the part you’d do anyway” + catalog. Do not restore Start → Grow → Deliver or “build a Movement.” +- **In-app docs** (`/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`, + and `commonality/` via `endUserDocsPlugin`. Keep markdown links relative so + they resolve in that viewer. - **CauseStarter is a lens, not a directory** ([ADR 0008](../specs/decisions/0008-operated-surfaces-are-lenses.md)). It authors no discovery: no search, browse, ranking, featuring, or leaderboards. A cause is reached at `/cause/:causeId` through a link its organizer circulates. @@ -290,7 +296,7 @@ Then **restart Grok** so MCP tools load. | `wallet-account-menu` | Hardhat account picker (localhost only) | | `wallet-hardhat-0` … `wallet-hardhat-9` | Pick Hardhat account | | `wallet-disconnect` | Disconnect | -| `home-start-cause` | Home CTA → create a draft and open the cause editor | +| `home-start-cause` | Home / `/welcome` CTA → create a draft and open the cause editor | | `nav-start` | Desktop/mobile nav “Start” → same (creates a new draft) | | `cause-detail-page` | Cause page root (where all editing happens; brand-new drafts show “Start a cause” coach copy here) | | `issue-guidance` | Static coach copy for what an issue is | diff --git a/causestarter/index.html b/causestarter/index.html index 06753020..cd1d754d 100644 --- a/causestarter/index.html +++ b/causestarter/index.html @@ -4,7 +4,7 @@ - + CauseStarter diff --git a/causestarter/package.json b/causestarter/package.json index 4955ee21..d4d476e0 100644 --- a/causestarter/package.json +++ b/causestarter/package.json @@ -25,7 +25,9 @@ "connectkit": "^1.9.1", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.18.2", + "rehype-sanitize": "^6.0.0", "viem": "2.54.3", "wagmi": "^3.6.21" }, diff --git a/causestarter/src/App.tsx b/causestarter/src/App.tsx index e73394cd..7b3caa2f 100644 --- a/causestarter/src/App.tsx +++ b/causestarter/src/App.tsx @@ -1,6 +1,9 @@ +import { lazy, Suspense } from 'react' import { BrowserRouter, HashRouter, Navigate, Route, Routes } from 'react-router-dom' +import { CircularProgress, Stack } from '@mui/material' import { CauseShell } from './shell/CauseShell' import { HomePage } from './pages/HomePage' +import { WelcomePage } from './pages/WelcomePage' import { StartCauseRedirect } from './pages/StartCauseRedirect' import { StartBridgeRedirect } from './pages/StartBridgeRedirect' import { BridgeClusterPage } from './pages/BridgeClusterPage' @@ -12,7 +15,6 @@ import { CauseBoardLeaderboardPage } from './pages/CauseBoardLeaderboardPage' import { StatementBoardLeaderboardPage } from './pages/StatementBoardLeaderboardPage' import { StatementBoardRedirect } from './pages/StatementBoardRedirect' import { StatementPage } from './pages/StatementPage' -import { DocsPage } from './pages/DocsPage' import { StatementsPage } from './pages/StatementsPage' import { SettingsPage } from './pages/SettingsPage' import { ProjectDetailPage, ProjectLeaderboardPage } from './pages/ProjectDetailPage' @@ -40,6 +42,16 @@ import { } from './pages/DelegationPages' import { NotFoundPage } from './pages/NotFoundPage' +const DocsPage = lazy(() => import('./pages/DocsPage').then((mod) => ({ default: mod.DocsPage }))) + +function DocsFallback() { + return ( + + + + ) +} + function isHashRouting(): boolean { return import.meta.env.MODE === 'ipfs' || import.meta.env.VITE_HASH_ROUTING === 'true' } @@ -52,6 +64,7 @@ export default function App() { } /> + } /> {/* No intermediate form — creates a draft and opens the editor. */} } /> } /> @@ -105,7 +118,22 @@ export default function App() { } /> } /> } /> - } /> + }> + + + )} + /> + }> + + + )} + /> } /> } /> } /> diff --git a/causestarter/src/components/CauseBridgesSection.tsx b/causestarter/src/components/CauseBridgesSection.tsx index e30a18ae..7cd761cd 100644 --- a/causestarter/src/components/CauseBridgesSection.tsx +++ b/causestarter/src/components/CauseBridgesSection.tsx @@ -168,7 +168,9 @@ export function CauseBridgesSection({ cause, variant = 'organizer' }: CauseBridg {rows.length === 0 && !(organizer && cause.mediator) && ( - No bridges yet. + No bridges yet. If you would sign something like their statement but theirs + does not imply yours, write a bridge. A suggester can nudge its subscribers + if it agrees. )} diff --git a/causestarter/src/components/CrowdJobs.tsx b/causestarter/src/components/CrowdJobs.tsx new file mode 100644 index 00000000..10739687 --- /dev/null +++ b/causestarter/src/components/CrowdJobs.tsx @@ -0,0 +1,46 @@ +import { Box, Button, Paper, Stack, Typography } from '@mui/material' +import { Link as RouterLink } from 'react-router-dom' +import { CROWD_JOBS, JOBS_DOC_PATH } from '../lib/jobs' + +export function CrowdJobs() { + return ( + + {CROWD_JOBS.map((job) => ( + + + {job.title} + + + {job.happyTo} + + + But: {job.ugh} + + + So: {job.soYou} + + + ))} + + + + + ) +} diff --git a/causestarter/src/components/HowItWorksSteps.tsx b/causestarter/src/components/HowItWorksSteps.tsx deleted file mode 100644 index 3b356540..00000000 --- a/causestarter/src/components/HowItWorksSteps.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Box, Stack, Typography } from '@mui/material' -import FlagIcon from '@mui/icons-material/Flag' -import GroupsIcon from '@mui/icons-material/Groups' -import RocketLaunchIcon from '@mui/icons-material/RocketLaunch' -import type { ReactNode } from 'react' - -const steps: Array<{ icon: ReactNode; title: string; body: string }> = [ - { - icon: , - title: 'Start', - body: 'Shape the cause into clear, independent statements people can sincerely support.', - }, - { - icon: , - title: 'Grow', - body: 'Bring in supporters, volunteers, and collaborators with clear asks.', - }, - { - icon: , - title: 'Deliver', - body: 'Fund projects, back creators, and keep score in public.', - }, -] - -export function HowItWorksSteps() { - return ( - - {steps.map((step, index) => ( - - - {index + 1} - - - - {step.icon} - - {step.title} - - - - {step.body} - - - - ))} - - ) -} diff --git a/causestarter/src/components/JobTip.tsx b/causestarter/src/components/JobTip.tsx new file mode 100644 index 00000000..3aebfa8e --- /dev/null +++ b/causestarter/src/components/JobTip.tsx @@ -0,0 +1,34 @@ +import { Alert, Link, Typography } from '@mui/material' +import { Link as RouterLink } from 'react-router-dom' +import { CROWD_JOBS, jobsDocHref, type CrowdJobId } from '../lib/jobs' + +export function JobTip({ + job, + title, + children, + testId, +}: { + job?: CrowdJobId + title?: string + children: string + testId?: string +}) { + const catalog = job ? CROWD_JOBS.find((entry) => entry.id === job) : undefined + const href = jobsDocHref(catalog) + + return ( + + {title && ( + + {title} + + )} + + {children}{' '} + + Do the part you’d do anyway + + + + ) +} diff --git a/causestarter/src/components/YourCauses.tsx b/causestarter/src/components/YourCauses.tsx index a1593a43..e5cf4cab 100644 --- a/causestarter/src/components/YourCauses.tsx +++ b/causestarter/src/components/YourCauses.tsx @@ -67,7 +67,9 @@ export function YourCauses({ {!loading && causes.length === 0 && ( - No causes yet on this device. Start one, or open a cause from its organizer’s link. + No causes on this device. Start one if you want a different combination of + statements — reuse overlapping ones so you are not starting from zero. Or open + a cause from its organizer’s link; there is no directory. )} diff --git a/causestarter/src/components/YourNudgersAndNudges.tsx b/causestarter/src/components/YourNudgersAndNudges.tsx index 72c6e0ca..e6c2b66b 100644 --- a/causestarter/src/components/YourNudgersAndNudges.tsx +++ b/causestarter/src/components/YourNudgersAndNudges.tsx @@ -101,8 +101,8 @@ export function YourNudgersAndNudges() { {trustedNudgers.length === 0 ? ( - No suggesters yet. Open a cause and opt in to a suggester to see - suggestions here. + No suggesters yet. Opt in on a cause if you would sign a better wording but + do not want to hunt for it — a mediator can nudge you when a bridge fits. ) : ( diff --git a/causestarter/src/components/YourProjects.tsx b/causestarter/src/components/YourProjects.tsx index dc06b803..244ee7d0 100644 --- a/causestarter/src/components/YourProjects.tsx +++ b/causestarter/src/components/YourProjects.tsx @@ -62,7 +62,9 @@ export function YourProjects() { {connected && !loading && projects.length === 0 && ( - No projects yet. Create one, contribute, or bookmark a project from its page. + No projects yet. If you would do the work but cannot self-fund, create one + and ask a better-connected friend for an alignment vouch — no grant officer. + Contribute or bookmark if your job is money or attention instead. )} diff --git a/causestarter/src/components/YourSignedStatements.tsx b/causestarter/src/components/YourSignedStatements.tsx index f68e779a..e446f1a5 100644 --- a/causestarter/src/components/YourSignedStatements.tsx +++ b/causestarter/src/components/YourSignedStatements.tsx @@ -17,7 +17,7 @@ export function YourSignedStatements() { Statements diff --git a/causestarter/src/lib/jobs.test.ts b/causestarter/src/lib/jobs.test.ts new file mode 100644 index 00000000..adb5d798 --- /dev/null +++ b/causestarter/src/lib/jobs.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest' +import { CROWD_JOBS, jobsDocHref } from './jobs' + +describe('jobs catalog', () => { + it('has four jobs with in-app doc anchors', () => { + expect(CROWD_JOBS.map((job) => job.id)).toEqual(['money', 'attention', 'work', 'wording']) + expect(jobsDocHref()).toBe('/docs/the-jobs') + expect(jobsDocHref(CROWD_JOBS[0])).toBe('/docs/the-jobs#money') + }) +}) diff --git a/causestarter/src/lib/jobs.ts b/causestarter/src/lib/jobs.ts new file mode 100644 index 00000000..4b5699cf --- /dev/null +++ b/causestarter/src/lib/jobs.ts @@ -0,0 +1,52 @@ +export type CrowdJobId = 'money' | 'attention' | 'work' | 'wording' + +export interface CrowdJob { + id: CrowdJobId + title: string + happyTo: string + ugh: string + soYou: string + docsHash: string +} + +/** The four jobs the landing and in-product tips keep repeating. */ +export const CROWD_JOBS: CrowdJob[] = [ + { + id: 'money', + title: 'Money', + happyTo: 'I’d put in $X/month if enough others do too.', + ugh: 'I will not read every project, and I do not trust a big org with a black box.', + soYou: 'Pledge with a refund if the threshold is missed. Hand the picking to a person you already trust.', + docsHash: 'money', + }, + { + id: 'attention', + title: 'Attention', + happyTo: 'I’d watch for work worth funding.', + ugh: 'I don’t know what’s out there, who’s a scam, or how to float early bets.', + soYou: 'Follow statements you mean. Fund proven work, or fund early and ask to be reimbursed at cost.', + docsHash: 'attention-and-judgment', + }, + { + id: 'work', + title: 'Work', + happyTo: 'I’d do this project.', + ugh: 'I can’t self-fund, and I don’t know a grant officer.', + soYou: 'Publish it. A friend one hop better-connected can vouch that it advances a statement people already watch.', + docsHash: 'work', + }, + { + id: 'wording', + title: 'Wording', + happyTo: 'I’d stand behind an idea like that.', + ugh: 'Not in those words — and my words will have zero signers.', + soYou: 'Write yours. Similar signatures can still count. A bridge can invite people whose statement does not yet imply yours.', + docsHash: 'wording', + }, +] + +export const JOBS_DOC_PATH = '/docs/the-jobs' + +export function jobsDocHref(job?: CrowdJob): string { + return job ? `${JOBS_DOC_PATH}#${job.docsHash}` : JOBS_DOC_PATH +} diff --git a/causestarter/src/lib/tools.ts b/causestarter/src/lib/tools.ts index cfd8ecde..e437121d 100644 --- a/causestarter/src/lib/tools.ts +++ b/causestarter/src/lib/tools.ts @@ -30,7 +30,7 @@ export const SUPPORTING_TOOLS: SupportingTool[] = [ id: 'delegation', name: 'Delegation', role: 'Trust others with funding judgment', - description: 'Let supporters who lack time follow a volunteer or collaborator they trust.', + description: 'Happy to put in money but not to pick projects? Hand the choices to a person you already trust.', domain: 'lazyGiving', path: '/delegation/notes', internalPath: '/delegation/notes', @@ -72,7 +72,7 @@ export const SUPPORTING_TOOLS: SupportingTool[] = [ id: 'commonality', name: 'Commonality', role: 'Thesis & movement layer', - description: 'Background on why public-goods funding can work without a central owner.', + description: 'The longer argument: cooperate on agreement without a committee, and why that can actually work.', domain: 'commonality', path: '/', levers: [], diff --git a/causestarter/src/pages/CauseDetailPage.tsx b/causestarter/src/pages/CauseDetailPage.tsx index b3752471..3f06ea2d 100644 --- a/causestarter/src/pages/CauseDetailPage.tsx +++ b/causestarter/src/pages/CauseDetailPage.tsx @@ -905,10 +905,10 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { )} {isFreshDraft ? ( - Tell CauseStarter what you want people to be able to support. It searches - published statements first and can propose new wording when none fit. - You decide what belongs in the cause. Nothing is published until you review - the exact statement text and CID in the page below and explicitly approve it. + A cause is a set of statements people can sign one at a time — not a club + they join. Reuse published statements when they already say what you mean; + you inherit their signers and projects. Write your own when they don’t. + Nothing is published until you review the exact text and CID below. ) : null} @@ -999,7 +999,9 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { {!live && ( - Write the statements this cause is made of. Publish each one when it is ready. + People sign these sentences, not your brand. If the wording you want would + have “zero signers,” write it so similar claims can still count — or start + from a published statement and keep your own extras beside it. )} @@ -1018,8 +1020,9 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { Describe your intent in the picker. It looks for reusable published statements - before offering new drafts. Reject or correct any suggestion that misses your - meaning; broad statements are fine when their proposition is clear. + before offering new drafts — that is how you avoid starting from zero signers. + Reject or correct any suggestion that misses your meaning; write one manually + if you need to. )} @@ -1138,8 +1141,10 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { {publishedCids.length === 0 ? ( Fundable Projects - - Publish a statement to see projects aligned with it. + + Publish a statement to see projects vouched as advancing it. You do not need a + grant officer: a friend who is one hop better-connected can vouch, and the + project shows up for everyone watching that statement. ) : ( @@ -1159,8 +1164,10 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { projectsHelp={ - Union of projects vouched for as advancing any published statement of this - cause. Alignment attaches to a statement, never to the cause as a whole. + Union of projects vouched as advancing any published statement here. + Alignment attaches to a statement, never to the cause as a whole. Watch + this list if your job is judgment, not a monthly check — including if you + only fund work that has already delivered. diff --git a/causestarter/src/pages/CauseFundingPage.tsx b/causestarter/src/pages/CauseFundingPage.tsx index 5ab062b9..e72f7ded 100644 --- a/causestarter/src/pages/CauseFundingPage.tsx +++ b/causestarter/src/pages/CauseFundingPage.tsx @@ -6,6 +6,7 @@ import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom' import { formatUnits } from 'viem' import { useAccount } from 'wagmi' import { ConnectWalletHint } from '../components/ConnectWalletHint' +import { JobTip } from '../components/JobTip' import { CauseConjunctionEarmark } from '../components/CauseConjunctionEarmark' import { useCauseMonthlyPledges } from '../hooks/useCauseMonthlyPledges' import { @@ -250,13 +251,13 @@ export function CauseFundingPage() { )} - - Create a one-time delegated fund or a monthly pledge earmarked for one immutable - statement — or for a combination you endorse as a package. The earmark is public, - auditable guidance — not a binding restriction on a delegate. If they direct the - money elsewhere, that will also be public. Choosing a delegate is public too. - The earmark does not follow later edits to this cause publication. - + + Happy to put in $X/month but not to pick every project? Pledge, and hand the + choices to a person you already trust — not a black-box charity. The earmark is + public guidance, not a lock; if they send the money elsewhere, that is public too. + Pledges that miss their threshold refund. The earmark stays on these statement + CIDs even if this cause page later changes. + {published.length === 0 ? ( diff --git a/causestarter/src/pages/DocsPage.test.tsx b/causestarter/src/pages/DocsPage.test.tsx new file mode 100644 index 00000000..7f924a2d --- /dev/null +++ b/causestarter/src/pages/DocsPage.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' +import { describe, expect, it, vi } from 'vitest' +import { DocsPage } from './DocsPage' + +vi.mock('../components/ToolCard', () => ({ + ToolCard: () => null, +})) + +function renderDocs(path: string) { + return render( + + + } /> + } /> + + , + ) +} + +describe('DocsPage', () => { + it('renders the CauseStarter home guide', () => { + renderDocs('/docs') + expect(screen.getByTestId('docs-page')).toBeInTheDocument() + expect(screen.getByText('CauseStarter')).toBeInTheDocument() + expect(screen.getByText(/Do the part you’d do anyway/i)).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'What is this?' })).toHaveAttribute('href', '/welcome') + }) + + it('renders the jobs catalog', () => { + renderDocs('/docs/the-jobs') + expect(screen.getByRole('heading', { name: /Do the part you’d do anyway/i })).toBeInTheDocument() + expect(screen.getByText('Money')).toBeInTheDocument() + expect(screen.getByText('Work')).toBeInTheDocument() + }) + + it('opens vision-and-strategy from the bundled commonality tree', () => { + renderDocs('/docs/vision-and-strategy') + expect(screen.getByRole('heading', { name: /Why Commonality/i })).toBeInTheDocument() + }) +}) diff --git a/causestarter/src/pages/DocsPage.tsx b/causestarter/src/pages/DocsPage.tsx index 859ceb08..c26e1e4f 100644 --- a/causestarter/src/pages/DocsPage.tsx +++ b/causestarter/src/pages/DocsPage.tsx @@ -1,33 +1,239 @@ -import { Box, Stack, Typography } from '@mui/material' +import { Link as RouterLink, useLocation } from 'react-router-dom' +import ReactMarkdown from 'react-markdown' +import rehypeSanitize from 'rehype-sanitize' +import type { Components } from 'react-markdown' +import { Box, Button, Divider, Paper, Stack, Typography } from '@mui/material' +import docModulesByRelativePath from 'virtual:end-user-docs' import { ToolCard } from '../components/ToolCard' import { SUPPORTING_TOOLS } from '../lib/tools' +import { getDomainUrl, type DomainId } from '../lib/domainUrls' -const sections = [ - { - key: 'reference' as const, - title: 'Example causes', - description: 'Worked examples of focused causes you can learn from.', - }, - { - key: 'thesis' as const, - title: 'Background', - description: 'Why this approach to public goods exists — optional reading.', - }, -] +const docModules: Record = docModulesByRelativePath + +const DOMAIN_FOLDERS: ReadonlySet = new Set([ + 'commonality', + 'lazyGiving', + 'alignment', + 'tally', + 'content-funding', + 'civility', + 'common-sense-majority', + 'conceptspace', + 'causestarter', +]) + +interface LoadedDoc { + content: string + pathForRelativeLinks: string +} + +function lookupMarkdown(internalPath: string): LoadedDoc | null { + const normalized = internalPath.replace(/\/$/, '') + const exact = `${normalized}.md` + if (docModules[exact]) return { content: docModules[exact], pathForRelativeLinks: normalized } + const readme = `${normalized}/README.md` + if (docModules[readme]) { + return { content: docModules[readme], pathForRelativeLinks: `${normalized}/README` } + } + const index = `${normalized}/index.md` + if (docModules[index]) { + return { content: docModules[index], pathForRelativeLinks: `${normalized}/index` } + } + return null +} + +function getDocContent(docPath: string): LoadedDoc | null { + const raw = docPath.replace(/^end-user\//, '').replace(/\/$/, '') || 'index' + if (raw === 'index' || raw === 'causestarter') { + return lookupMarkdown('causestarter') + } + const prefixed = [ + raw, + raw.startsWith('causestarter/') ? null : `causestarter/${raw}`, + raw.startsWith('commonality/') ? null : `commonality/${raw}`, + raw.startsWith('shared/') ? null : `shared/${raw}`, + ].filter((path): path is string => Boolean(path)) + for (const candidate of prefixed) { + const found = lookupMarkdown(candidate) + if (found) return found + } + return null +} + +function normalizeDocsRoute(href: string): string { + return href.replace(/\/README\.md$/, '').replace(/\.md$/, '').replace(/\/README$/, '') +} + +function publicDocsRoute(internalPath: string): string { + if (internalPath === 'causestarter' || internalPath === 'causestarter/index') return 'index' + if (internalPath.startsWith('causestarter/')) return internalPath.slice('causestarter/'.length) + if (internalPath === 'commonality') return 'vision-and-strategy' + if (internalPath.startsWith('commonality/')) return internalPath.slice('commonality/'.length) + if (internalPath.startsWith('shared/')) return internalPath.slice('shared/'.length) + return internalPath +} + +function docHomeDomain(internalPath: string): string | null { + const top = internalPath.split('/')[0] + if (top === 'shared' || top === 'causestarter' || top === 'commonality') return null + return DOMAIN_FOLDERS.has(top) ? top : null +} + +function buildDocHref(internalPath: string): string { + const home = docHomeDomain(internalPath) + const route = normalizeDocsRoute(`/docs/${publicDocsRoute(internalPath)}`) + if (home) { + return getDomainUrl(home as DomainId, route, route) + } + return route +} + +function headingText(children: unknown): string { + if (typeof children === 'string' || typeof children === 'number') return String(children) + if (Array.isArray(children)) return children.map(headingText).join('') + if (children && typeof children === 'object' && 'props' in children) { + return headingText((children as { props: { children?: unknown } }).props.children) + } + return '' +} + +function headingId(children: unknown): string { + return headingText(children) + .toLowerCase() + .replace(/['’]/g, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') +} + +function resolveHref(href: string, currentDocPath: string): string { + if (!href || href.startsWith('http') || href.startsWith('#')) return href + if (href.startsWith('/docs/end-user/')) { + return buildDocHref(href.replace('/docs/end-user/', '')) + } + if (href.startsWith('/docs/')) { + return normalizeDocsRoute(href) + } + if (href.startsWith('/')) return href + const currentDir = currentDocPath.includes('/') + ? currentDocPath.substring(0, currentDocPath.lastIndexOf('/')) + : '' + const combined = currentDir ? `${currentDir}/${href}` : href + const resolved: string[] = [] + for (const part of combined.split('/')) { + if (part === '..') resolved.pop() + else if (part !== '' && part !== '.') resolved.push(part) + } + return buildDocHref(resolved.join('/')) +} export function DocsPage() { - return ( - - - - Docs - - - Worked examples and background reading. Funding and growth tools live on each - cause page. - + const { pathname } = useLocation() + const docPath = pathname.replace(/^\/docs\/?/, '').replace(/\/$/, '') || 'index' + const loadedDoc = getDocContent(docPath) + const content = loadedDoc?.content ?? null + const pathForRelativeLinks = loadedDoc?.pathForRelativeLinks ?? docPath + + const components: Components = { + h1: ({ children }) => ( + + {children} + + ), + h2: ({ children }) => ( + + {children} + + ), + h3: ({ children }) => ( + + {children} + + ), + p: ({ children }) => {children}, + ul: ({ children }) => ( + + {children} + + ), + ol: ({ children }) => ( + + {children} + + ), + li: ({ children }) => ( + + {children} + + ), + a: ({ href, children }) => { + const resolved = href ? resolveHref(href, pathForRelativeLinks) : '#' + if (/^https?:\/\//.test(resolved)) { + return ( + + {children} + + ) + } + return {children} + }, + hr: () => , + blockquote: ({ children }) => ( + + {children} + ), + strong: ({ children }) => {children}, + em: ({ children }) => {children}, + } + if (!content) { + return ( + + + + We could not find that guide. + + + Try the CauseStarter docs home, or the jobs catalog. + + + + + ) + } + + const showExamples = docPath === 'index' || docPath === '' || docPath === 'causestarter' + + return ( + + + {content} + + {showExamples && } + + ) +} + +function DocsExampleTools() { + const sections = [ + { + key: 'reference' as const, + title: 'Example causes', + description: 'Worked examples of focused causes you can learn from.', + }, + { + key: 'thesis' as const, + title: 'On the other sites', + description: 'Optional reading and tools. They open in their own UIs.', + }, + ] + return ( + {sections.map((section) => { const tools = SUPPORTING_TOOLS.filter((tool) => tool.kind === section.key) if (tools.length === 0) return null diff --git a/causestarter/src/pages/HomePage.test.tsx b/causestarter/src/pages/HomePage.test.tsx new file mode 100644 index 00000000..718af6dd --- /dev/null +++ b/causestarter/src/pages/HomePage.test.tsx @@ -0,0 +1,36 @@ +import { render, screen } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { describe, expect, it, vi } from 'vitest' +import { HomePage } from './HomePage' + +vi.mock('../hooks/useUserCauses', () => ({ + useUserCauses: () => ({ causes: [], loading: false }), +})) + +vi.mock('../components/YourProjects', () => ({ + YourProjects: () => null, +})) + +vi.mock('../components/YourSignedStatements', () => ({ + YourSignedStatements: () => null, +})) + +vi.mock('../components/YourNudgersAndNudges', () => ({ + YourNudgersAndNudges: () => null, +})) + +describe('HomePage landing', () => { + it('leads with jobs, not Start → Grow → Deliver', () => { + render( + + + , + ) + expect(screen.getByTestId('home-landing')).toBeInTheDocument() + expect(screen.getByText(/there are enough of us/i)).toBeInTheDocument() + expect(screen.getByTestId('crowd-jobs')).toBeInTheDocument() + expect(screen.getByTestId('crowd-job-money')).toBeInTheDocument() + expect(screen.queryByText(/change the world/i)).toBeNull() + }) +}) + diff --git a/causestarter/src/pages/HomePage.tsx b/causestarter/src/pages/HomePage.tsx index c00f8ec7..45662f9d 100644 --- a/causestarter/src/pages/HomePage.tsx +++ b/causestarter/src/pages/HomePage.tsx @@ -1,108 +1,10 @@ -import { Box, Button, CircularProgress, Paper, Stack, Typography } from '@mui/material' -import { Link as RouterLink, useNavigate } from 'react-router-dom' -import { HowItWorksSteps } from '../components/HowItWorksSteps' +import { CircularProgress, Stack, Typography } from '@mui/material' import { YourCauses } from '../components/YourCauses' import { YourNudgersAndNudges } from '../components/YourNudgersAndNudges' import { YourProjects } from '../components/YourProjects' import { YourSignedStatements } from '../components/YourSignedStatements' import { useUserCauses } from '../hooks/useUserCauses' -import { createCausePath } from '../lib/causeStore' - -function LandingHome() { - const navigate = useNavigate() - - return ( - - - theme.palette.mode === 'light' - ? 'linear-gradient(160deg, rgba(15,118,110,0.10) 0%, rgba(255,252,247,0.95) 55%, #fff 100%)' - : 'linear-gradient(160deg, rgba(45,212,191,0.14) 0%, rgba(15,23,42,0.9) 60%, #0b1220 100%)', - }} - > - - CauseStarter - - - Start a cause. Build a Movement. Change the world. - - - Write clear statements people can support one at a time, then grow support with funding - and media tools when you need them. - - - - - {/* There is deliberately no "support a cause" counterpart: we list no - causes and rank none. You reach a cause through its organizer's own - link. Bridge writing lives on the cause-editing mediator section. */} - - - - - - How it works - - - - - - - Examples and background - - - Worked example causes and optional reading live under Docs. Funding and growth - tools stay on each cause page. - - - - - - - - - - - ) -} +import { WelcomePage } from './WelcomePage' export function HomePage() { const { causes, loading } = useUserCauses() @@ -135,5 +37,5 @@ export function HomePage() { ) } - return + return } diff --git a/causestarter/src/pages/StatementPage.tsx b/causestarter/src/pages/StatementPage.tsx index 817c4404..be6dd5f8 100644 --- a/causestarter/src/pages/StatementPage.tsx +++ b/causestarter/src/pages/StatementPage.tsx @@ -330,8 +330,10 @@ export function StatementPage() { projectsHelp={ - Projects vouched for as advancing this statement. Each is aligned with this - statement, not with a cause as a whole. + Projects vouched as advancing this statement — not a cause as a whole. + Do the work? Publish a project and get an alignment vouch; you do not need + a foundation intro. Only want to judge? Fund proven work, or fund early and + ask to be reimbursed at cost. @@ -345,13 +347,24 @@ export function StatementPage() { fullPageTo={`/statement/${statementCid}/board/leaderboard`} /> - + + Hate this statement’s company on someone else’s cause page? Start your own + cause and reuse this statement — you keep its signers and projects. Write a + different sentence if this wording is not what you mean; similar claims can + still count. + + + + + theme.palette.mode === 'light' + ? 'linear-gradient(160deg, rgba(15,118,110,0.10) 0%, rgba(255,252,247,0.95) 55%, #fff 100%)' + : 'linear-gradient(160deg, rgba(45,212,191,0.14) 0%, rgba(15,23,42,0.9) 60%, #0b1220 100%)', + }} + > + + CauseStarter + + + There are enough of us. We just couldn’t work together. + + + Give money without becoming a grant officer. Spot projects without bankrolling them. + Do the work without knowing a foundation. Sign what you actually mean. The rest is + optional. + + + + + + + + + + + Take the job you’d take anyway + + + Nobody has to agree on a leader, a manifesto, or a treasury. Overlap is enough. + + + + + + + Docs + + + How to start a cause, the full ugh-catalog, walkthroughs, and the longer argument. + There is no directory of other people’s causes — you get there by their link. + + + + + ) +} diff --git a/causestarter/src/virtual-end-user-docs.d.ts b/causestarter/src/virtual-end-user-docs.d.ts new file mode 100644 index 00000000..37975064 --- /dev/null +++ b/causestarter/src/virtual-end-user-docs.d.ts @@ -0,0 +1,4 @@ +declare module 'virtual:end-user-docs' { + const docs: Record + export default docs +} diff --git a/causestarter/src/wagmi.ts b/causestarter/src/wagmi.ts index 62ebb095..82ab8684 100644 --- a/causestarter/src/wagmi.ts +++ b/causestarter/src/wagmi.ts @@ -83,7 +83,7 @@ function buildWagmiConfig() { const defaultConnectors = getDefaultConnectors({ app: { name: 'CauseStarter', - description: 'Start a cause. Build a Movement. Change the world.', + description: 'Do the part you’d do anyway — cooperate on agreement without a committee.', url: typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8090', }, // Empty string → ConnectKit skips the WalletConnect connector (no broken project id). @@ -105,7 +105,7 @@ function buildWagmiConfig() { connectors: connectors as never, walletConnectProjectId: walletConnectProjectId || '', appName: 'CauseStarter', - appDescription: 'Start a cause. Build a Movement. Change the world.', + appDescription: 'Do the part you’d do anyway — cooperate on agreement without a committee.', appUrl: typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8090', enableAaveAccount: false, }), diff --git a/causestarter/vite.config.ts b/causestarter/vite.config.ts index aa526361..9c22508d 100644 --- a/causestarter/vite.config.ts +++ b/causestarter/vite.config.ts @@ -2,6 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import { defineConfig, loadEnv, type Plugin } from 'vite' import react from '@vitejs/plugin-react' +import { endUserDocsPlugin } from '../ui/endUserDocsPlugin.ts' const indexerUrl = process.env.INDEXER_URL ?? 'http://localhost:42069' @@ -13,7 +14,11 @@ export default defineConfig(({ mode }) => { build: { outDir: 'dist', }, - plugins: [react(), runtimeConfigPlugin(env)], + plugins: [ + react(), + runtimeConfigPlugin(env), + endUserDocsPlugin({ domain: 'causestarter' }), + ], resolve: { preserveSymlinks: true, // Single React/MUI/wagmi graph when bundling ui feature modules into CauseStarter. diff --git a/causestarter/vitest.config.ts b/causestarter/vitest.config.ts index 9666877e..d2250ea2 100644 --- a/causestarter/vitest.config.ts +++ b/causestarter/vitest.config.ts @@ -1,9 +1,10 @@ import path from 'node:path' import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' +import { endUserDocsPlugin } from '../ui/endUserDocsPlugin.ts' export default defineConfig({ - plugins: [react()], + plugins: [react(), endUserDocsPlugin({ domain: 'causestarter' })], resolve: { alias: { '@ui': path.resolve(__dirname, '../ui/src'), diff --git a/docs/end-user/causestarter/index.md b/docs/end-user/causestarter/index.md new file mode 100644 index 00000000..e01d522d --- /dev/null +++ b/docs/end-user/causestarter/index.md @@ -0,0 +1,33 @@ +# CauseStarter + +CauseStarter is a place to **publish a cause** — a set of independent, signable statements — and let people attach the jobs they would actually take: money, judgment, work, wording. + +It is not a directory of causes, not a party, and not a charity you join. You reach a cause through a link its organizer circulates. There is no ranking, featuring, or “popular causes” list. That absence is the product. + +## The idea in one breath + +People who already agree about something have always *felt* like they should be able to pool money and skill. They usually cannot, because every existing way of doing it demands extra jobs nobody wanted. + +Here you take the job you would take anyway. Agreement does the collecting. Nobody has to run the whole thing. + +The catalog of those jobs — and the “ugh” each one used to carry — is **[Do the part you’d do anyway](./the-jobs.md)**. Start there. + +The first-visit pitch (hero + the four jobs) is also the CauseStarter `/welcome` page. `/` only shows that pitch when this device has no causes yet. + +## What you can do here + +- **[Start a cause](./start-a-cause.md)** — retrieve or write statements, publish a roster, circulate the link. +- **Sign** a statement you mean, without swallowing a manifesto. +- **Pledge** money (refunded if the threshold is not met) and optionally **delegate** the project-picking. +- **Publish a project** aligned with a statement; a friend one hop better-connected can vouch that it fits. +- **Write a bridge** when someone else’s wording does not imply yours, and optionally send it to a suggester. + +## Background + +The longer case for this approach — why it is better than government and charity, how switching is easy, why it is hard to shut down — lives in the [vision and strategy](../commonality/vision-and-strategy/README.md) notes. You do not need those to use CauseStarter. + +Walkthroughs of the funding tools: + +- [A neighborhood throws a block party](../shared/use-case-walkthroughs/block-party.md) +- [Getting a research project funded](../shared/use-case-walkthroughs/research-funding.md) +- [A town transitions away from government funding](../shared/use-case-walkthroughs/defunding.md) diff --git a/docs/end-user/causestarter/start-a-cause.md b/docs/end-user/causestarter/start-a-cause.md new file mode 100644 index 00000000..c8cc58e8 --- /dev/null +++ b/docs/end-user/causestarter/start-a-cause.md @@ -0,0 +1,33 @@ +# Start a cause + +A cause in CauseStarter is **a published set of independent statements**, not a manifesto with members. People sign statements one at a time. Projects attach to statements, not to the cause as a blob. + +## Shape it + +1. **Start a cause** from Home or Causes. That opens an editor on this device; nothing is on-chain yet. +2. Describe the intent in the picker. It searches **published** statements first. Reject anything that is not what you mean. Write one manually if you need to. +3. Publish each statement when the exact text is right. You will see its CID before you confirm. +4. Publish a **roster** (title, optional description, slug) so the cause has a stable link: `/cause//`. Circulate that link. There is no directory. + +You can add or drop statements later. A new roster version does not rewrite old pledges; earmarks stay on the statement CIDs they were made against. + +## Invite jobs, not members + +You are not recruiting people into an organization. You are publishing a place where [jobs people will actually take](./the-jobs.md) can attach: + +- Signers who like *some* of the statements +- Donors who will pledge and maybe delegate +- Doers who publish projects and get an alignment vouch +- Mediators who write bridges when wording does not line up + +If someone hates your combination of statements, they should start their own cause and reuse the overlapping planks. That is success, not a fork to suppress. + +## Bridges + +If another camp’s published cause is close but not implying yours, create a bridge from the cause page. You publish the cluster under *your* key. CauseStarter does not message the other organizer; the citation is public on their page if they look. + +## What not to expect + +- No search or “popular causes.” +- No platform verdict that a cause is good — only that counts are recomputable. +- No requirement that supporters agree with every statement on the page. diff --git a/docs/end-user/causestarter/the-jobs.md b/docs/end-user/causestarter/the-jobs.md new file mode 100644 index 00000000..6441680a --- /dev/null +++ b/docs/end-user/causestarter/the-jobs.md @@ -0,0 +1,61 @@ +# Do the part you’d do anyway + +The core vision is simple: **people who agree should be able to cooperate on that agreement.** Money, work, and attention meet on the overlap. Nobody has to elect a board, swallow a manifesto, or wait until a movement is “big enough.” + +What used to block that was never a shortage of people who *cared*. It was a pile of extra jobs. “I’d be happy to do X, but ugh, Y.” CauseStarter and the rest of this substrate are mostly that ugh-removal kit. If you remember one thing, remember: **you only contribute the part you would contribute anyway.** + +## Money + +**I’d be happy to contribute $X/month (as long as enough others do too), but…** + +- **…I don’t have time to follow every project.** Pledge the $X/month and [delegate](../shared/key-ideas/delegation.md) the choices to someone you trust. Revoke anytime. +- **…I don’t want to donate to the big charity; I don’t trust them not to waste it.** Don’t. Delegate to a friend, not an institution. Your earmark is public guidance; if they send the money elsewhere, that is public too. +- **…I don’t want to be the sucker if nobody else shows up.** Use a pledge that [refunds if the goal isn’t met](../lazyGiving/assurance-contracts.md). You risk nothing. + +In CauseStarter this lives on a cause’s **Pledges** page and on **Delegation**. + +## Attention and judgment + +**I’d be happy to watch for worthwhile projects, but…** + +- **…I don’t even know what projects are out there.** Follow (or start) a cause whose statements you want to watch. The project list is the union of work vouched as advancing those statements. You do not need a special introduction. +- **…I can’t tell whether this creator is a scammer or incompetent.** Be a delegate who mostly [funds proven work](../lazyGiving/retroactive-funding.md). You reimburse early contributors at cost after results, instead of betting on a pitch. +- **…I can’t put that much of my *own* money on it.** Be an early funder who asks for reimbursement. If later donors close the loop, that giving budget can go to the next attempt. Your receipt is a track record, not a payout. + +The cause page’s **Fundable Projects** list is that watch surface. Trust settings (the gear) control whose vouches you see. + +## Work + +**I’d be happy to do this work, but…** + +- **…I can’t afford to pay for it myself.** Publish the project and let others pledge. Funds release if the threshold is met; otherwise everyone is refunded. +- **…I don’t even know who to go to for funding.** You do not need a grant officer. Post the project, talk to a friend who is a bit better-connected in the trust graph, and get an **alignment** vouch onto a statement people already watch. The project then shows up on those cause pages. + +Start from **Create Project** on the home page, or from a cause’s project list. + +## Wording + +**I’d be happy to sign a statement sorta like that, but…** + +- **…I don’t love the way that one is phrased.** Write your own. Signing is per statement, not per cause. +- **…if I write my own it’ll have zero signers.** Write it so an implication attester can connect it to similar claims. Indirect support can count; you are not starting from a blank petition. +- **…the statement others signed doesn’t actually imply the better one I have in mind.** Write a **bridge** and, if you want distribution, submit it as a suggestion to a mediator service. If it agrees, it can nudge its subscribers. + +Cause pages have a **Bridges** section. Suggesters you opt into appear on Home. + +## Your own cause page + +**I’d be happy to follow a cause page sorta like that one, but…** + +- **…I don’t love the exact combination of statements they chose.** Create your own cause. A cause is a set of statements, not a club you join. +- **…if I write my own it’ll have zero signers or projects.** Reuse *some* of the same statements and you inherit their signers and aligned projects. Even new wording can pick up overlap through implication and bridges. + +**Start a cause** is the organizer action. There is no browse list: circulate the link. + +## What this adds up to + +Project-doers can get money for worthwhile work. Donors can help financially without becoming grant officers or trusting a black-box charity. People with time or expertise can contribute those without writing a check. All of that information and money can move **without everyone agreeing on every idea or electing leaders.** + +That is why “there are *so* many of us — why can’t we get anything done?” was never a law of nature. It was coordination tax. This ecosystem is the claim that the tax was optional. + +For the longer argument (late aggregation, organic coalitions, why it is hard to stop), see [Why Commonality?](../commonality/vision-and-strategy/README.md). Role-by-role one-liners also live in [pitches](../commonality/vision-and-strategy/pitches.md). diff --git a/docs/end-user/commonality/index.md b/docs/end-user/commonality/index.md index 92490cd7..c2e12bb7 100644 --- a/docs/end-user/commonality/index.md +++ b/docs/end-user/commonality/index.md @@ -13,6 +13,8 @@ Commonality is one of several connected sites: This site is for the funding side: pledging, delegating, getting projects funded, and the broader case for why a new approach is needed. +**CauseStarter** is the organizer-facing front door: publish a cause (a set of signable statements) and let people attach only the jobs they would actually take. Start with [Do the part you’d do anyway](../causestarter/the-jobs.md). + ## See it in action diff --git a/docs/end-user/commonality/vision-and-strategy/README.md b/docs/end-user/commonality/vision-and-strategy/README.md index c9d026cb..414d2b05 100644 --- a/docs/end-user/commonality/vision-and-strategy/README.md +++ b/docs/end-user/commonality/vision-and-strategy/README.md @@ -54,4 +54,6 @@ It's not just a slightly better mousetrap. We are [remarkably bad at producing p - Something [much easier than politics](./so-what/easier-than-politics.md) for people who want to fund public goods but are tired of fighting over government. The response to "the government is hostile, how do we fund our priorities?" isn't "organize a massive political movement" — it's "just start using this." - And potentially a path toward [more-local government](./so-what/local-government.md): Commonality's mechanisms work structurally better at smaller scales, so voluntary public-goods funding has a natural gravity toward localism — which, over time, may shift power from higher-level government to communities, not through confrontation but by routing around it. +The everyday pitch is not this whole argument. It is: **[do the part you’d do anyway](./the-jobs.md)** — cooperate on agreement without the extra jobs that used to make that impossible. CauseStarter’s catalog of those jobs is [here](../../causestarter/the-jobs.md). + For a concrete walkthrough of how this plays out, see the [walkthrough](/docs/end-user/shared/use-case-walkthroughs/defunding.md). For tailored pitches to different types of users, see [pitches](./pitches.md). For the "won't this be used for evil?" question, see [ethics](./ethics.md). diff --git a/docs/end-user/commonality/vision-and-strategy/pitches.md b/docs/end-user/commonality/vision-and-strategy/pitches.md index 4df16231..b8aef5e7 100644 --- a/docs/end-user/commonality/vision-and-strategy/pitches.md +++ b/docs/end-user/commonality/vision-and-strategy/pitches.md @@ -33,3 +33,5 @@ Of course a cause still does need to actually gather users — get donors to ple ## The common thread Every pitch is about something individually useful. Nobody needs to be told "join our movement" or "here's our plan to persuade half the country." It's "here's this one small thing you can do, it's useful to you right now, and it doesn't depend on what anybody else does." + +The same idea, written as the “I’d be happy to X, but ugh Y” obstacles the substrate removes: **[Do the part you’d do anyway](../../causestarter/the-jobs.md)** ([strategy note](./the-jobs.md)). diff --git a/docs/end-user/commonality/vision-and-strategy/the-jobs.md b/docs/end-user/commonality/vision-and-strategy/the-jobs.md new file mode 100644 index 00000000..7ecf6d20 --- /dev/null +++ b/docs/end-user/commonality/vision-and-strategy/the-jobs.md @@ -0,0 +1,9 @@ +# Cooperate on agreement; skip the extra jobs + +The simple vision: people who already agree should be able to pool money and skill on that agreement. + +The invention is not a new kind of organization. It is **splitting the work into jobs people would already take**, and removing the extra job each role used to demand (pick every project, trust a big org, elect a board, know a grant officer, swallow someone else’s wording, wait for critical mass). + +Late aggregation is *how* that is allowed: you do not have to agree on a leader or a manifesto first. Organic coalitions are a *bonus* of not forcing a bundle. Neither is the headline. The headline is: **do the part you’d do anyway.** + +The user-facing catalog — money, attention, work, wording, “my own cause page” — lives in CauseStarter’s **[Do the part you’d do anyway](../../causestarter/the-jobs.md)**. Role-by-role one-liners: [pitches](./pitches.md). diff --git a/docs/end-user/tldr-for-llms.md b/docs/end-user/tldr-for-llms.md index a429f205..22e8d8db 100644 --- a/docs/end-user/tldr-for-llms.md +++ b/docs/end-user/tldr-for-llms.md @@ -7,6 +7,14 @@ Each entry follows the same shape: - **When a user encounters it** — UI surfaces or moments in the user's flow where this concept becomes load-bearing. - **What they might want help with** — typical assistant tasks around this concept. +## Do the part you’d do anyway (jobs, not an org) + +[Full page](/docs/end-user/causestarter/the-jobs.md) + +- **What it is:** The everyday pitch for CauseStarter / Commonality. Cooperate on agreement; split the work into jobs people would already take (money, attention, work, wording); remove the extra job each role used to demand. Not “join a movement.” +- **When a user encounters it:** CauseStarter landing, empty states on cause/project/statement pages, `/docs/the-jobs`. +- **What they might want help with:** Which job to take; why they don’t need a committee, grant officer, or matching manifesto; how delegation / refundable pledges / retroactive funding / bridges / reusing statements unblock the matching “ugh.” + ## Statements and the implication graph [Full page](/docs/end-user/tally/statements-and-implication-graph.md) diff --git a/package-lock.json b/package-lock.json index 957112aa..c3aa7096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -168,7 +168,9 @@ "connectkit": "^1.9.1", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", "react-router-dom": "^7.18.2", + "rehype-sanitize": "^6.0.0", "viem": "2.54.3", "wagmi": "^3.6.21" }, diff --git a/specs/user-docs.md b/specs/user-docs.md index cd6acf40..0ee4457c 100644 --- a/specs/user-docs.md +++ b/specs/user-docs.md @@ -29,5 +29,6 @@ Write the docs for humans (narrative, plain language). Then add a block to the d **User-facing docs live in:** - Role-based how-tos live on the site where the role is actually performed (e.g. `lazyGiving/get-your-project-funded.md`, `alignment/become-a-delegate.md`, `tally/express-what-you-care-about.md`). The cross-ecosystem index is in [docs/end-user/commonality/index.md](/docs/end-user/commonality/index.md) under "What can I do across the ecosystem?". Each role doc ends with an "On other sites" footer pointing at the cross-site connections. +- CauseStarter’s in-app docs (`causestarter/src` `/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`, and `commonality/`. The everyday pitch is [the-jobs.md](/docs/end-user/causestarter/the-jobs.md). - [docs/end-user/shared/use-case-walkthroughs/](/docs/end-user/shared/use-case-walkthroughs/README.md) — concrete scenarios - [docs/end-user/shared/key-ideas/](/docs/end-user/shared/key-ideas/README.md) — concept reference pages diff --git a/ui/endUserDocsPlugin.ts b/ui/endUserDocsPlugin.ts index ccf9180f..1f7e9231 100644 --- a/ui/endUserDocsPlugin.ts +++ b/ui/endUserDocsPlugin.ts @@ -22,8 +22,10 @@ interface EndUserDocsPluginOptions { function foldersForDomain(domain: string): string[] { // `shared/` renders in-context on every site; `commonality/` is the vision - // site and bundles vision + shared only. - if (domain === 'commonality') return ['shared', 'commonality'] + // site and bundles vision + shared only. CauseStarter is the founder lens: + // it also ships the vision tree so in-app /docs can open those pages. + if (domain === 'commonality') return ['shared', 'commonality', 'causestarter'] + if (domain === 'causestarter') return ['shared', 'causestarter', 'commonality'] return ['shared', domain] } From 28393d45acf02809d7ba1e440f4e756c7b9292d5 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Sun, 23 Aug 2026 20:16:11 -0400 Subject: [PATCH 03/13] Fiddling with the docs, though not great yet. --- docs/end-user/causestarter/index.md | 30 ++++++++++++----------------- docs/end-user/tldr-for-llms.md | 8 ++++++++ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/end-user/causestarter/index.md b/docs/end-user/causestarter/index.md index e01d522d..7246cf24 100644 --- a/docs/end-user/causestarter/index.md +++ b/docs/end-user/causestarter/index.md @@ -1,33 +1,27 @@ # CauseStarter -CauseStarter is a place to **publish a cause** — a set of independent, signable statements — and let people attach the jobs they would actually take: money, judgment, work, wording. +You followed a link to a **cause page**. That is how almost everyone gets here. There is no directory of causes to browse. -It is not a directory of causes, not a party, and not a charity you join. You reach a cause through a link its organizer circulates. There is no ranking, featuring, or “popular causes” list. That absence is the product. +This page is a place where **projects that advance these claims can get funded** — without a foundation, a board, or everyone agreeing on every sentence. -## The idea in one breath +Look at **Fundable Projects**. That list is the point. Alignment is to a statement, never to “the cause as a club,” so work can show up here if it advances *some* of these claims. You do not have to like the whole mix. -People who already agree about something have always *felt* like they should be able to pool money and skill. They usually cannot, because every existing way of doing it demands extra jobs nobody wanted. +**Pledge** if you would put money in anyway. The pledge [refunds if the goal isn’t met](../lazyGiving/assurance-contracts.md). You can [hand the picking](../shared/key-ideas/delegation.md) to a person you already trust instead of reading every project. -Here you take the job you would take anyway. Agreement does the collecting. Nobody has to run the whole thing. +**Publish work** if that’s your job. A friend one hop better-connected can vouch that it advances a statement people already watch. Then it can appear on every cause page that includes that statement — including this one. -The catalog of those jobs — and the “ugh” each one used to carry — is **[Do the part you’d do anyway](./the-jobs.md)**. Start there. +Signing a statement is free and optional. It marks which claims you personally mean, and the counts can show that more than one person cares. It does not enroll you. You are not a member by being here. -The first-visit pitch (hero + the four jobs) is also the CauseStarter `/welcome` page. `/` only shows that pitch when this device has no causes yet. +If this mix of claims is not the overlap you want to fund, you can reuse the ones you like on a page of your own. That is later, and it is success, not a split to police. -## What you can do here +The jobs — and the extra work each one used to demand — are in **[Do the part you’d do anyway](./the-jobs.md)**. Organizers who want to publish a new page: **[Start a cause](./start-a-cause.md)**. -- **[Start a cause](./start-a-cause.md)** — retrieve or write statements, publish a roster, circulate the link. -- **Sign** a statement you mean, without swallowing a manifesto. -- **Pledge** money (refunded if the threshold is not met) and optionally **delegate** the project-picking. -- **Publish a project** aligned with a statement; a friend one hop better-connected can vouch that it fits. -- **Write a bridge** when someone else’s wording does not imply yours, and optionally send it to a suggester. +## Where the money mechanics came from -## Background - -The longer case for this approach — why it is better than government and charity, how switching is easy, why it is hard to shut down — lives in the [vision and strategy](../commonality/vision-and-strategy/README.md) notes. You do not need those to use CauseStarter. - -Walkthroughs of the funding tools: +CauseStarter points those tools at a cause. Concrete stories: - [A neighborhood throws a block party](../shared/use-case-walkthroughs/block-party.md) - [Getting a research project funded](../shared/use-case-walkthroughs/research-funding.md) - [A town transitions away from government funding](../shared/use-case-walkthroughs/defunding.md) + +The longer case for this approach (why it is better than government and charity, why switching is easy, why it is hard to shut down) lives in the [vision and strategy](../commonality/vision-and-strategy/README.md) notes. You do not need those to use a cause page. diff --git a/docs/end-user/tldr-for-llms.md b/docs/end-user/tldr-for-llms.md index 22e8d8db..7195afaa 100644 --- a/docs/end-user/tldr-for-llms.md +++ b/docs/end-user/tldr-for-llms.md @@ -7,6 +7,14 @@ Each entry follows the same shape: - **When a user encounters it** — UI surfaces or moments in the user's flow where this concept becomes load-bearing. - **What they might want help with** — typical assistant tasks around this concept. +## CauseStarter (you landed on a cause page) + +[Full page](/docs/end-user/causestarter/index.md) + +- **What it is:** The newcomer briefing. Almost everyone arrives via a circulated cause URL, not a catalog. The page is a watch surface for **fundable projects** aligned with some of its statements — not a club, not a petition site. Signing is optional and free; pledging (refundable) and publishing work are the real jobs. +- **When a user encounters it:** In-app `/docs`, or when they ask what this cause page is. +- **What they might want help with:** What they can do without joining; how projects get on the list; pledging vs signing; when to start their own page vs using this one. + ## Do the part you’d do anyway (jobs, not an org) [Full page](/docs/end-user/causestarter/the-jobs.md) From 0739a3b298eaf7e4d7bcd27129fb12eb93a8443d Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 09:58:09 -0400 Subject: [PATCH 04/13] Planning a rename of "cause". --- causestarter/TODO.md | 1 + inbox.md | 2 + specs/glossary.md | 30 ++++- specs/product/README.md | 3 +- specs/product/cause-page-not-a-club.md | 145 ++++++++++++++++++++++++ specs/product/causes-as-publications.md | 7 ++ specs/product/founder-first.md | 3 + specs/product/how-to-convey-this.md | 16 +++ 8 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 specs/product/cause-page-not-a-club.md diff --git a/causestarter/TODO.md b/causestarter/TODO.md index 14102494..3b5aa68d 100644 --- a/causestarter/TODO.md +++ b/causestarter/TODO.md @@ -42,6 +42,7 @@ open **if they stay listed here**. - [ ] Statement `bookmarks` ref is still reserved infrastructure only — no CauseStarter (or main `ui`) surface for remembering a statement without signing it. - [ ] No Privy path / full parity with main `ui` wallet story yet. - [ ] Product: how CauseStarter ranks vs other domains in nav/marketing once it’s “the main thing.” +- [ ] **Copy (Adam):** two-step rename in [cause-page-not-a-club.md](/specs/product/cause-page-not-a-club.md) — fundable-projects board first, then organizer **cause board**. Do not sweep until he says so; new strings should follow that note. ## Architecture diff --git a/inbox.md b/inbox.md index f933758e..bbd14df8 100644 --- a/inbox.md +++ b/inbox.md @@ -31,6 +31,8 @@ Also, don't let any of the items get too long; usually there's a separate .md fi ### Docs / UI copy +- **(Ask)** Apply [cause-page-not-a-club.md](specs/product/cause-page-not-a-club.md) when you next pass over CauseStarter copy: two-step rename — today’s “cause board” (project list) → **fundable-projects board**, then organizer publication → **cause board** (don’t sweat leftover “cause page”). Sweep is yours; new copy should already follow that note. + - Decide whether to act on the fresh landing-copy positioning findings. The Civility grievance-first hero was reviewed and is fine; the verifier rubric was corrected so CSM’s recognition-register rule is not imposed on every vertical. Remaining findings are elsewhere: the umbrella Commonality landing still recruits generic end users despite the founder-first strategy, CSM front-loads the mediator toggle and uses “the other side’s bullshit,” Aligning repeats its main tradeoff several times, and Tally’s “Sign once, counted forever” headline presents a future goal as current capability. ### Features that I'm realizing would make a big difference diff --git a/specs/glossary.md b/specs/glossary.md index 0e567c31..21ffebe0 100644 --- a/specs/glossary.md +++ b/specs/glossary.md @@ -41,7 +41,7 @@ wrong (or this file is out of date and needs an ADR — see | **Reimbursement** | What a retroactive donation pays out to an early contributor — at cost, no upside | | **Note** | A `DelegatableNote`: a bucket of deposited funds whose spending authority can be delegated down a chain, revocably. The unit of delegated giving | | **Standing pledge** | A *recurring* funding commitment registered with `RecurringPledges`, executed periodically into a note | -| **Cause board** | The fundable-projects dashboard (heading **Fundable Projects**) inlined on a statement or cause page, where a donor sees aligned projects and decides where money goes | +| **Cause board** | *Today:* the fundable-projects dashboard (heading **Fundable Projects**) inlined on a statement or cause page. *Direction (not swept):* that list becomes the **fundable-projects board**; **cause board** moves to the organizer publication (mix of statements + that list + bridges etc.). See [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). | ### Judgments people and services publish @@ -62,7 +62,7 @@ wrong (or this file is out of date and needs an ADR — see | **Subsystem** | A capability: a contract family + SDK subsystem + UI feature module sharing one name | | **Site / UI domain** | A branded build that composes a subset of subsystems. There are eight | | **Bookmark** | A published cause or statement the user chose to keep, independently of signing. Cause bookmarks are cached locally and, with a connected wallet, stored in the `bookmarked-causes` mutable ref (public). Statement bookmarks use the separate `bookmarks` ref (statement CIDs). Unpublished cause drafts stay device-local. Never mix the two lists. User-facing verbs: bookmark / remove bookmark — never "save to device" or "delete cause" | -| **Cause page** | The versioned publication of a cause's title, summary, issue list, and mediator blurb. User-facing word for what code still calls a *roster* (`causestarter.roster`, `rosterCid`). Never say "roster" in UI copy. | +| **Cause page** | The versioned publication of a cause's title, summary, issue list, and mediator blurb. User-facing word for what code still calls a *roster* (`causestarter.roster`, `rosterCid`). Never say "roster" in UI copy. *Direction:* prefer **cause board** for this object; leftover “cause page” is fine. See [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). | | **Natural cause** | A cause playing the “this camp’s position” parent role in a [bridge cluster](./product/bridge-causes.md). Usually someone else’s publication; may be a **stand-in cause** the mediator wrote because that camp had no cause yet. | | **Stand-in cause** | A mediator-authored natural parent: a thin roster the mediator thinks the other camp believes, published under the mediator’s key and labeled as such. Not a modified cause (there is no prior parent to sliver). See [the-other-cause.md](/docs/founder/the-other-cause.md). | | **Modified cause** | A mediator-authored cause: wording the mediator thinks signers of a given natural cause might also accept, without feeling misrepresented. Usually a topical sliver, not a full rewrite of the parent. | @@ -145,13 +145,31 @@ retired as synonyms. `Contribution.participant`, `Refund.participant`, and `ContributorStats.participant` are now `.contributor`; the raw decoded-event field stays `participant` because that is the ABI arg name. -### 5. Portal → cause board (already ruled, half-finished) +### 5a. Bare “cause” names three things — *direction 2026-08-24, not swept* -Adam ruled 2026-06-12: **cause board** wins in user-facing copy; code identifiers, -routes, and directories keep `fundingportal*`. Still unfinished — `/portal/:statementCid` -routes, `ui/src/fundingportals/` copy, and ~22 files under `docs/end-user/` still say +- Ordinary English: the worldly goal. +- Glossary Part 1: a **statement** used as a funding anchor (a *role*). +- CauseStarter: the organizer **roster**. + +Direction: keep “cause” for the first two; the publication is a **cause board** +(fine leftover: **cause page**); do not call it a dashboard. A cause board may +cover multiple causes. Full note and **two-step rename** (fundable-projects +board first): [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). +Does not reverse ADR 0009. + +### 5. Portal → cause board (already ruled, half-finished; about to be superseded) + +Adam ruled 2026-06-12: **cause board** wins in user-facing copy for the +fundable-projects list; code identifiers, routes, and directories keep +`fundingportal*`. Still unfinished — `/portal/:statementCid` routes, +`ui/src/fundingportals/` copy, and ~22 files under `docs/end-user/` still say "funding portal". Tracked in [TODO.md](../TODO.md). +**Superseding direction (not swept):** that list becomes **fundable-projects +board**; **cause board** is reused for the organizer publication. Do the +fundable-projects rename *before* the publication rename. See +[cause-page-not-a-club.md](./product/cause-page-not-a-club.md). + ### 6. Smaller ones - **Campaign** — retired 2026-08-14. `campaignHeading`/`createCampaignLabel`/ diff --git a/specs/product/README.md b/specs/product/README.md index 47f34ad2..f0a41b63 100644 --- a/specs/product/README.md +++ b/specs/product/README.md @@ -3,7 +3,8 @@ Product-manager-level planning documents. These describe *what* to build and *why*, not *how*. - **[founder-first.md](founder-first.md)** — The vertical-founder posture: our customer is the founder standing up a vertical, not the end user of the generic sites. Carries the triage rule for all platform work, a map of which doc holds which piece, and the consolidated pivot backlog. Frozen rationale in [ADR 0005](/specs/decisions/0005-founder-first-verticals.md). -- **[causes-as-publications.md](causes-as-publications.md)** — Accepted target model for AI-assisted cause creation: causes are mutable, shareable publications over immutable statements; organizers and vertical operators are distinct roles; anchors remain optional semantic statements rather than page identities. Frozen rationale: [ADR 0009](../decisions/0009-causes-are-publications-over-statements.md). Implementation work: [plan](causes-as-publications-implementation-plan.md). +- **[causes-as-publications.md](causes-as-publications.md)** — Accepted target model for AI-assisted cause creation: causes are mutable, shareable publications over immutable statements; organizers and vertical operators are distinct roles; anchors remain optional semantic statements rather than page identities. Frozen rationale: [ADR 0009](../decisions/0009-causes-are-publications-over-statements.md). Implementation work: [plan](causes-as-publications-implementation-plan.md). Copy/weighting of that publication: **[cause-page-not-a-club.md](cause-page-not-a-club.md)** (direction; two-step rename; not a sweep yet). +- **[how-to-convey-this.md](how-to-convey-this.md)** — Landing/docs conversation log: what the system *is*, the jobs pitch, and later naming of the organizer page. - **[use-cases.md](use-cases.md)** — The canonical inventory of what people come here to *do*, organized by user goal rather than by subsystem. Each entry carries a status (Smooth / Rough / Missing / Compose / Blocked / Speculative) and its gap. Start here when prioritizing product work. - **[cause-taxonomy.md](cause-taxonomy.md)** — How a vertical founder populates an empty cause board: the gate (which legacy blocker is this cause hitting?) plus eight facets (subcause, scope, deliverable, posture, time shape, beneficiary, contestedness, publicness) that generate concrete examples. Worked example: the Christian board in [christian-pitch.md](/docs/founder/christian-pitch.md). - **[mvp.md](mvp.md)** — MVP scope: what's included in the first release, entry-point descriptions, what's deferred. diff --git a/specs/product/cause-page-not-a-club.md b/specs/product/cause-page-not-a-club.md new file mode 100644 index 00000000..1b811b02 --- /dev/null +++ b/specs/product/cause-page-not-a-club.md @@ -0,0 +1,145 @@ +# The organizer publication is a board, not a club + +**Status: direction, not swept.** Adam agreed with this framing on 2026-08-23 +and refined the nouns on 2026-08-24. He will apply copy/UI later (fresh LLM). +It does **not** reverse [ADR 0009](../decisions/0009-causes-are-publications-over-statements.md): +the object model (immutable statements; a mutable, shareable publication over a +roster of them; verticals as a separate operator role) stays. This note is about +**what we call that publication** and **how important it should feel**. + +Related: [causes-as-publications.md](./causes-as-publications.md), +[how-to-convey-this.md](./how-to-convey-this.md), +[glossary](../glossary.md), +[the jobs](/docs/end-user/causestarter/the-jobs.md), +[start a cause](/docs/end-user/causestarter/start-a-cause.md). + +## The problem + +CauseStarter names the organizer-owned roster a **cause**, on a site named +CauseStarter. That makes the roster feel like *the* thing you join or support. + +That is the wrong instinct. The protocol atoms are statements, implications, +alignment vouches, projects, and notes. People sign statements. Projects attach +to statements. Mediators operate at the statement (and plank-pair) level. The +roster is a **named view**: a title, a mix of independent claims, and the union +of work and wording that sits on those claims. Forking the mix is success, not +a schism. + +Calling that view “a cause” trains early aggregation: swallow the bundle, join +the movement. The rest of the system exists to avoid that. + +## Three things currently named “cause” + +| Sense | What it actually is | Keep calling it “cause”? | +|---|---|---| +| Ordinary English | The worldly thing you care about (clean water, the block party, not being defunded) | **Yes** — motivation, not an entity | +| Glossary Part 1 | A **statement** in its role as a funding anchor (`causeCid` is a statement CID) | **Yes** as a *role*, not a separate ID | +| CauseStarter roster | Versioned publication `(owner, slug)` → title, summary, ordered planks | **No** — this is the over-weighted one | + +The rename below is meant to **shift “cause” toward the first two senses**. A +cause board can be for **multiple** causes (several statements-as-anchors, or +several worldly aims on one page). It is not rigidly “one cause object.” + +## Two-step rename (do this in order) + +**Today (glossary, 2026-06-12):** **cause board** = the fundable-projects +dashboard (heading **Fundable Projects**), including leftover “funding portal” +copy. Code still says `fundingportal*`. + +**Step 1 — free the name.** Rename that surface to **fundable-projects +board** (hyphenate in running text the same way: fundable-projects board). +Identifiers, routes, and directories may lag (`fundingportal*` stays until a +later code pass). Heading can stay **Fundable Projects**. + +**Step 2 — reuse “cause board” for the organizer publication.** The thing at +`/cause/:owner/:slug` (today often “cause page” / “a cause”) becomes the +**cause board**: the shareable mix — title, planks, bridges, pledges, and a +**fundable-projects board** as the centerpiece (inlined summary and/or a +link to the full list). + +Do not go overboard replacing leftover **cause page**. That phrase is fine as +a synonym for the same URL. Prefer **cause board** in new copy. + +There isn’t a huge visual difference: the fundable-projects board is already +the centerpiece of the cause board, and the cause board already links to it +(and may summarize it). The cause board also includes other jobs — especially +**bridges** — so the two names are not interchangeable. + +Do **not** call either surface a **dashboard**. That word stays reserved for a +future personal view (“projects aligned with statements I signed”). + +## What to call the surfaces + +| Surface | Job | Noun (after the sweep) | +|---|---|---| +| Statement / plank | The thing you sign; projects attach here | **statement** / **claim** / **plank**. A statement *plays the cause role* when it is a funding anchor | +| Fundable-projects list | Aligned work you might fund | **fundable-projects board** (today still “cause board” in many files) | +| Organizer publication | Shareable mix + centerpiece list + bridges etc.; flyer URL | **cause board** (fine leftover: **cause page**) | +| Personal view (not built) | Projects aligned with *my* signed statements | **dashboard** / **my board** | + +User-facing verbs for the publication: *publish a board*, *start a board*, +*look at this board* — or keep *start a cause* in the English sense (“start +funding this worldly aim”) without implying membership. Never *join a cause*, +*members of this cause*, *support this cause* as if the mix were the funding +target. + +Do **not** rename the product off CauseStarter in the same pass. “Starter” can +mean “you start funding toward a cause (English)” without implying the roster +is the movement. + +## Thin ontologically, not thin for adoption + +The board is not protocol-fundamental. It **is** go-to-market-fundamental. + +Founder-first ([ADR 0005](../decisions/0005-founder-first-verticals.md)) makes +the organizer the customer. Almost everyone arrives via a circulated link; +there is no directory. The cause board is the **distribution handle**: title, +curated planks, mediator blurb, a stable URL. Organizers still do real +editorial work (retrieval, rejecting bad hits, publishing exact CIDs, +circulating, inviting bridges). Call it a board so it is a watch/fund +surface — not a club, and not a bookmark folder. + +Late aggregation still needs **attention** aggregation. It must not aggregate +**identity**. The board is a frame for attention. + +Analogy: Spotify playlists vs your library. Playlists are how music spreads; +you do not join a playlist. The personal dashboard is the returning-user loop; +organizer cause boards remain the acquisition surface. + +## Personalized dashboard + +A “projects on statements I signed” surface is the likely everyday home. It +does not replace organizer cause boards. You encounter a mix, sign the planks +you mean, then live on your own union. That personal surface can also span +**multiple causes** (several statements you signed). + +## What not to do + +- Do not rename the publication **dashboard**. That is a private updating + screen; it does not explain a shareable URL or organizer authorship; it + collides with the personal surface. +- Do not demote the board so hard that organizers think they are making a + bookmark folder, or that attention needs no frame. +- Do not reverse ADR 0009’s split (statement vs publication vs vertical). + Only stop using bare “cause” as the *name of the publication* in copy. +- Do not sweep **cause page** → **cause board** everywhere; prefer the new + term going forward. +- Do not skip step 1. If you call the publication a cause board while the + fundable-projects list is still called a cause board, the glossary is worse + than today. + +## Sweep when Adam is ready (fresh LLM) + +1. **Fundable-projects board:** UI copy, end-user docs, glossary Part 1 + “Cause board” row, glossary Part 2 §5 (portal → cause board is then + “portal → fundable-projects board”; code `fundingportal*` may still lag). +2. **Cause board** = organizer publication: new and high-traffic copy + (landing, CauseStarter in-app docs, empty states). Leave incidental + “cause page” alone unless it is confusing. +3. Later: identifiers (`/cause/:owner/:slug` can lag). Contract names stay. + Bridge-cluster terms (natural / modified / bridge cause) still mean + *publications* in the compound; separate pass. + +Until then, new copy should follow this note even if old copy has not been +swept: if you must name the fundable-projects list, say **fundable-projects +board**; if you must name the organizer URL, prefer **cause board**. diff --git a/specs/product/causes-as-publications.md b/specs/product/causes-as-publications.md index 8ae89eb7..6b85a2d9 100644 --- a/specs/product/causes-as-publications.md +++ b/specs/product/causes-as-publications.md @@ -26,6 +26,13 @@ to be turned into a new statement merely to acquire a name or URL. The cause ros the named, pointable object. Statements remain the semantic objects used for signing, support propagation, project alignment, cause boards, and funding intent. +User-facing copy should still not treat that roster as a club you join. Direction +(not swept): call it a **cause board** (fine leftover: **cause page**); first +rename today’s “cause board” (the project list) to **fundable-projects board**; +keep “cause” for ordinary English and for a statement’s funding-anchor role; a +cause board may cover multiple causes; do not call the publication a dashboard. +See [cause-page-not-a-club.md](./cause-page-not-a-club.md). + AI should do most of the unnatural translation work between ordinary human intentions and implication-friendly statements. Humans remain responsible for choosing what they mean, approving what is published or signed, supplying rhetoric and judgment, and diff --git a/specs/product/founder-first.md b/specs/product/founder-first.md index 6ae6d250..0ea2ac32 100644 --- a/specs/product/founder-first.md +++ b/specs/product/founder-first.md @@ -59,6 +59,9 @@ authoritative copy; this list is the map. - [ ] Resolve the naming question: new site, or rename Commonality to something like "CauseStarter"? The umbrella brand currently names the substrate, not the founder product. *(Adam — [inbox.md](/inbox.md))* +- [ ] Separate, copy-only: do not over-weight the organizer roster as “a cause” + you join; two-step noun change in [cause-page-not-a-club.md](./cause-page-not-a-club.md). + Does not settle the umbrella-vs-CauseStarter brand question. ### Make the founder's job easier (platform work) diff --git a/specs/product/how-to-convey-this.md b/specs/product/how-to-convey-this.md index 745b3fc5..90d7deee 100644 --- a/specs/product/how-to-convey-this.md +++ b/specs/product/how-to-convey-this.md @@ -242,3 +242,19 @@ One paragraph you could paste on the homepage │ Here you take the job you’d take anyway. Money can be lazy and cautious. Attention can be expert and cheap. Work can ask for funding without a foundation. Wording can be yours without isolating you. Agreement does the collecting. Nobody has to run the whole thing. If you want this in the product next, the smallest honest change is: rewrite the hero, replace How it works with the four jobs, and retarget the empty-state copy on cause / project / statement pages to the matching ugh. I would not expand the landing into the full ugh catalog; that catalog belongs on one docs page and in the UI at the moment the ugh appears. + +--- + +# 2026-08-23 — is a “cause” even the thing? + +Adam: the CauseStarter “cause” (organizer-owned mix of statements + project board) +is not protocol-fundamental; labelling it a cause on a site named CauseStarter +makes it feel like the thing you join. Maybe it is just a dashboard. Maybe the +primary loop is a personalized board of projects on statements *you* signed. + +Direction (agreed, not swept): keep “cause” as ordinary English and as a +statement’s funding-anchor role; call the publication a **cause board** (fine +leftover: **cause page**); first rename today’s “cause board” (the project list) +to **fundable-projects board**. A cause board can cover multiple causes. +Reserve **dashboard** for the personal surface. Full write-up: +[cause-page-not-a-club.md](./cause-page-not-a-club.md). From 96f30a8158c56105506f34bce7432a734f2c6a2b Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 13:26:57 -0400 Subject: [PATCH 05/13] Renaming to "fundable-projects board". --- CONTINUITY.md | 8 +++++ causestarter/TODO.md | 2 +- .../src/components/RosterPublishPanel.tsx | 10 +++--- causestarter/src/pages/CauseDetailPage.tsx | 2 +- causestarter/src/pages/StatementPage.tsx | 4 +-- .../end-user/alignment/help-connect-things.md | 2 +- docs/end-user/alignment/index.md | 2 +- docs/end-user/alignment/pledge-to-a-cause.md | 2 +- .../end-user/alignment/successful-projects.md | 2 +- docs/end-user/causestarter/index.md | 12 +++---- docs/end-user/causestarter/start-a-cause.md | 8 ++--- docs/end-user/causestarter/the-jobs.md | 12 +++---- docs/end-user/civility/index.md | 2 +- .../why-does-tally-help.md | 4 +-- .../end-user/common-sense-majority/why-now.md | 2 +- docs/end-user/commonality/index.md | 4 +-- .../credible-solution/discovery.md | 2 +- .../ease-of-adoption/donor-project-tension.md | 2 +- .../ease-of-adoption/for-established-orgs.md | 4 +-- .../ease-of-adoption/tip-jar-upgrade-path.md | 4 +-- .../why-its-better/openness.md | 2 +- .../why-its-better/organic-coalitions.md | 2 +- .../why-its-better/what-its-better-for.md | 2 +- .../lazyGiving/get-your-project-funded.md | 6 ++-- .../shared/key-ideas/trust-networks.md | 4 +-- .../common-sense-majority.md | 2 +- .../shared/use-case-walkthroughs/defunding.md | 4 +-- .../local-funding-shift.md | 2 +- .../noninflammatory-content.md | 6 ++-- docs/end-user/tally/index.md | 2 +- .../tally/statements-and-implication-graph.md | 4 +-- docs/end-user/tldr-for-llms.md | 12 +++---- inbox.md | 2 +- specs/glossary.md | 36 +++++++++---------- specs/product/cause-page-not-a-club.md | 25 ++++++------- specs/tech/subsystems/aligning/ui.md | 8 ++--- .../tech/subsystems/fundingportals/README.md | 11 +++--- .../pages/BrowseStatementsPage.tsx | 2 +- ui/src/conceptspace/pages/HomePage.tsx | 4 +-- ui/src/domains/alignment/LandingPage.tsx | 4 +-- ui/src/domains/alignment/manifest.tsx | 2 +- ui/src/domains/civility/ContentPages.tsx | 2 +- .../common-sense-majority/CsmPages.tsx | 2 +- .../common-sense-majority/LandingPage.tsx | 4 +-- .../commonality/ForOrganizationsPage.tsx | 4 +-- ui/src/domains/commonality/FounderPage.tsx | 2 +- .../domains/content-funding/ContentPages.tsx | 4 +-- .../fundingportals/components/CauseBoard.tsx | 8 ++--- .../components/CauseLeaderboard.tsx | 2 +- .../components/FundingPortalSummary.test.tsx | 12 +++---- .../components/FundingPortalSummary.tsx | 6 ++-- .../pages/ExplorerPage.test.tsx | 4 +-- ui/src/fundingportals/pages/ExplorerPage.tsx | 4 +-- .../pages/StatementFundingPortalPage.test.tsx | 4 +-- .../pages/StatementFundingPortalPage.tsx | 2 +- ui/test-plan.md | 4 +-- 56 files changed, 153 insertions(+), 143 deletions(-) diff --git a/CONTINUITY.md b/CONTINUITY.md index 950e42d1..748481c3 100644 --- a/CONTINUITY.md +++ b/CONTINUITY.md @@ -1779,6 +1779,14 @@ Temporary, reversible: `./scripts/services.sh --start` and `./scripts/deploy-cau `./scripts/data.sh --seed` now defaults to **tiny** (was small). `gen:small` and `gen:tiny` both pass `--skip-invariants`. Statement publish reuses one document store (or parallel PublishedData writes across Hardhat wallets, receipts awaited in a batch). Seed RPC clients poll every 50ms. See `fake-data-generation/generateStatements.ts`, `fake-data-generation/seedRpc.ts`, `scripts/data.sh`. +## 2026-08-24 — Cause board is the organizer publication, not the project list + +Copy sweep of [cause-page-not-a-club.md](specs/product/cause-page-not-a-club.md): +the Aligning/project list is now **fundable-projects board** in UI copy and +end-user docs; **cause board** is the CauseStarter organizer publication +(leftover “cause page” left in comments). Routes still `/portal/:cid` and +`/cause/:owner/:slug`; identifiers `fundingportal*` lag. + ## 2026-08-20 — Anvil `--state` restart dump Local `hardhat-node` was coming back empty after `stack.restart-consistency` because Docker SIGTERM did not make Anvil dump `/data/state.json` (then a fresh deploy + trust wiring filled ~41 blocks with no seed). Fix: `scripts/anvil-docker-entrypoint.sh` maps SIGTERM→SIGINT, compose `--state-interval 15` and 60s grace, restart check records block height and SIGINTs Anvil before stop. Recreate `hardhat-node` to mount the wrapper. diff --git a/causestarter/TODO.md b/causestarter/TODO.md index 3b5aa68d..143b9aa9 100644 --- a/causestarter/TODO.md +++ b/causestarter/TODO.md @@ -42,7 +42,7 @@ open **if they stay listed here**. - [ ] Statement `bookmarks` ref is still reserved infrastructure only — no CauseStarter (or main `ui`) surface for remembering a statement without signing it. - [ ] No Privy path / full parity with main `ui` wallet story yet. - [ ] Product: how CauseStarter ranks vs other domains in nav/marketing once it’s “the main thing.” -- [ ] **Copy (Adam):** two-step rename in [cause-page-not-a-club.md](/specs/product/cause-page-not-a-club.md) — fundable-projects board first, then organizer **cause board**. Do not sweep until he says so; new strings should follow that note. +- [x] **Copy:** two-step rename in [cause-page-not-a-club.md](/specs/product/cause-page-not-a-club.md) — fundable-projects board first, then organizer **cause board**. Identifiers and leftover “cause page” still lag. ## Architecture diff --git a/causestarter/src/components/RosterPublishPanel.tsx b/causestarter/src/components/RosterPublishPanel.tsx index b19fb10f..10effcc7 100644 --- a/causestarter/src/components/RosterPublishPanel.tsx +++ b/causestarter/src/components/RosterPublishPanel.tsx @@ -86,7 +86,7 @@ export function RosterPublishPanel({ Publish this cause Title, summary, statement list, and mediator blurb publish together as a versioned - cause page. The URL stays stable when you edit; each publish is a new version. + cause board. The URL stays stable when you edit; each publish is a new version. @@ -118,7 +118,7 @@ export function RosterPublishPanel({ fullWidth size="small" disabled={busy} - helperText="Shown at the top of the cause page and sealed into the published version." + helperText="Shown at the top of the cause board and sealed into the published version." slotProps={{ htmlInput: { 'data-testid': 'roster-title' } }} /> - Connect a wallet to publish the cause page on chain. + Connect a wallet to publish the cause board on chain. )} {!canPublish && ( - Publish at least one statement before publishing the cause page. + Publish at least one statement before publishing the cause board. )} diff --git a/causestarter/src/pages/CauseDetailPage.tsx b/causestarter/src/pages/CauseDetailPage.tsx index 3f06ea2d..bca2d76b 100644 --- a/causestarter/src/pages/CauseDetailPage.tsx +++ b/causestarter/src/pages/CauseDetailPage.tsx @@ -672,7 +672,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { return } if (!isConnected || !address || !writeClients) { - setError('Connect your wallet to publish the cause page.') + setError('Connect your wallet to publish the cause board.') return } setPublishingRoster(true) diff --git a/causestarter/src/pages/StatementPage.tsx b/causestarter/src/pages/StatementPage.tsx index be6dd5f8..0e519dd2 100644 --- a/causestarter/src/pages/StatementPage.tsx +++ b/causestarter/src/pages/StatementPage.tsx @@ -352,8 +352,8 @@ export function StatementPage() { sx={{ p: 2, borderRadius: 3, border: '1px solid', borderColor: 'divider' }} > - Hate this statement’s company on someone else’s cause page? Start your own - cause and reuse this statement — you keep its signers and projects. Write a + Hate this statement’s company on someone else’s cause board? Start your own + board and reuse this statement — you keep its signers and projects. Write a different sentence if this wording is not what you mean; similar claims can still count. diff --git a/docs/end-user/alignment/help-connect-things.md b/docs/end-user/alignment/help-connect-things.md index 0aeba15c..351477a4 100644 --- a/docs/end-user/alignment/help-connect-things.md +++ b/docs/end-user/alignment/help-connect-things.md @@ -16,7 +16,7 @@ Your influence grows with trust. As people follow you and see that your attestat You browse cause portals on Aligning, projects on LazyGiving, or content on Content Funding. When you encounter something you believe genuinely aligns with a cause, you submit an attestation: "This project / this piece of content is aligned with [cause]." -Anyone who has marked you as trusted in their settings sees your attestation. That makes the project visible in their cause board — where it wasn't before. +Anyone who has marked you as trusted in their settings sees your attestation. That makes the project visible in their fundable-projects board — where it wasn't before. You can also vouch for other attesters. If you trust someone's judgment in a particular domain, pointing to them amplifies their reach. The trust network is composable: the people you trust, and the people they trust, all propagate your attestations through the network. diff --git a/docs/end-user/alignment/index.md b/docs/end-user/alignment/index.md index 8141518d..9464d39f 100644 --- a/docs/end-user/alignment/index.md +++ b/docs/end-user/alignment/index.md @@ -31,5 +31,5 @@ There's no committee deciding what counts. The portal you see is a function of w - **[Statements and the implication graph](../tally/statements-and-implication-graph.md)** — Causes are statements. Statements that mean similar things get connected automatically, so portals span natural coalitions even when nobody coordinated wording. - **[Trust networks](../shared/key-ideas/trust-networks.md)** — How "people I trust, plus people they trust" gets computed into the filter that shapes your portal. -- **[Cause boards](../lazyGiving/assurance-contracts.md)** — The crowdfunding mechanism the projects themselves use. +- **[Assurance contracts](../lazyGiving/assurance-contracts.md)** — The crowdfunding mechanism the projects themselves use. - **[AI evaluators](ai-evaluators.md)** — Attesters can be AI services, not just people. This is how a cause pool funds a whole *kind* of content automatically. diff --git a/docs/end-user/alignment/pledge-to-a-cause.md b/docs/end-user/alignment/pledge-to-a-cause.md index a0ebc744..4cef7b1a 100644 --- a/docs/end-user/alignment/pledge-to-a-cause.md +++ b/docs/end-user/alignment/pledge-to-a-cause.md @@ -23,7 +23,7 @@ You can: ## Getting started -Find the cause or cause board you care about in Aligning, then create a delegated fund for that cause. Set an amount and choose a delegate. Delegate profiles and track-record views show their past decisions, what they've funded, and what others have said about them. +Find the cause or fundable-projects board you care about in Aligning, then create a delegated fund for that cause. Set an amount and choose a delegate. Delegate profiles and track-record views show their past decisions, what they've funded, and what others have said about them. If you don't know any delegates personally, look for delegates whose public track records align with your values. A few funded projects tells you more than any biography. diff --git a/docs/end-user/alignment/successful-projects.md b/docs/end-user/alignment/successful-projects.md index b9450973..3c4dacbc 100644 --- a/docs/end-user/alignment/successful-projects.md +++ b/docs/end-user/alignment/successful-projects.md @@ -9,7 +9,7 @@ A project can be aligned long before anyone is willing to say it succeeded. That ## The Successful projects page -Every cause board has a **Successful** view. It shows projects that +Every fundable-projects board has a **Successful** view. It shows projects that 1. people in your trust network have **vouched as successful** at this cause, and 2. still have **reimbursement outstanding** for early contributors. diff --git a/docs/end-user/causestarter/index.md b/docs/end-user/causestarter/index.md index 7246cf24..5d7a2647 100644 --- a/docs/end-user/causestarter/index.md +++ b/docs/end-user/causestarter/index.md @@ -1,20 +1,20 @@ # CauseStarter -You followed a link to a **cause page**. That is how almost everyone gets here. There is no directory of causes to browse. +You followed a link to a **cause board**. That is how almost everyone gets here. There is no directory of causes to browse. -This page is a place where **projects that advance these claims can get funded** — without a foundation, a board, or everyone agreeing on every sentence. +This page is a place where **projects that advance these claims can get funded** — without a foundation, a club, or everyone agreeing on every sentence. -Look at **Fundable Projects**. That list is the point. Alignment is to a statement, never to “the cause as a club,” so work can show up here if it advances *some* of these claims. You do not have to like the whole mix. +Look at **Fundable Projects**. That fundable-projects board is the centerpiece. Alignment is to a statement, never to “the cause as a club,” so work can show up here if it advances *some* of these claims. You do not have to like the whole mix. **Pledge** if you would put money in anyway. The pledge [refunds if the goal isn’t met](../lazyGiving/assurance-contracts.md). You can [hand the picking](../shared/key-ideas/delegation.md) to a person you already trust instead of reading every project. -**Publish work** if that’s your job. A friend one hop better-connected can vouch that it advances a statement people already watch. Then it can appear on every cause page that includes that statement — including this one. +**Publish work** if that’s your job. A friend one hop better-connected can vouch that it advances a statement people already watch. Then it can appear on every cause board that includes that statement — including this one. Signing a statement is free and optional. It marks which claims you personally mean, and the counts can show that more than one person cares. It does not enroll you. You are not a member by being here. If this mix of claims is not the overlap you want to fund, you can reuse the ones you like on a page of your own. That is later, and it is success, not a split to police. -The jobs — and the extra work each one used to demand — are in **[Do the part you’d do anyway](./the-jobs.md)**. Organizers who want to publish a new page: **[Start a cause](./start-a-cause.md)**. +The jobs — and the extra work each one used to demand — are in **[Do the part you’d do anyway](./the-jobs.md)**. Organizers who want to publish a new board: **[Start a cause](./start-a-cause.md)**. ## Where the money mechanics came from @@ -24,4 +24,4 @@ CauseStarter points those tools at a cause. Concrete stories: - [Getting a research project funded](../shared/use-case-walkthroughs/research-funding.md) - [A town transitions away from government funding](../shared/use-case-walkthroughs/defunding.md) -The longer case for this approach (why it is better than government and charity, why switching is easy, why it is hard to shut down) lives in the [vision and strategy](../commonality/vision-and-strategy/README.md) notes. You do not need those to use a cause page. +The longer case for this approach (why it is better than government and charity, why switching is easy, why it is hard to shut down) lives in the [vision and strategy](../commonality/vision-and-strategy/README.md) notes. You do not need those to use a cause board. diff --git a/docs/end-user/causestarter/start-a-cause.md b/docs/end-user/causestarter/start-a-cause.md index c8cc58e8..4cd25dd1 100644 --- a/docs/end-user/causestarter/start-a-cause.md +++ b/docs/end-user/causestarter/start-a-cause.md @@ -7,9 +7,9 @@ A cause in CauseStarter is **a published set of independent statements**, not a 1. **Start a cause** from Home or Causes. That opens an editor on this device; nothing is on-chain yet. 2. Describe the intent in the picker. It searches **published** statements first. Reject anything that is not what you mean. Write one manually if you need to. 3. Publish each statement when the exact text is right. You will see its CID before you confirm. -4. Publish a **roster** (title, optional description, slug) so the cause has a stable link: `/cause//`. Circulate that link. There is no directory. +4. Publish the **cause board** (title, optional description, slug) so it has a stable link: `/cause//`. Circulate that link. There is no directory. -You can add or drop statements later. A new roster version does not rewrite old pledges; earmarks stay on the statement CIDs they were made against. +You can add or drop statements later. A new board version does not rewrite old pledges; earmarks stay on the statement CIDs they were made against. ## Invite jobs, not members @@ -24,10 +24,10 @@ If someone hates your combination of statements, they should start their own cau ## Bridges -If another camp’s published cause is close but not implying yours, create a bridge from the cause page. You publish the cluster under *your* key. CauseStarter does not message the other organizer; the citation is public on their page if they look. +If another camp’s published cause is close but not implying yours, create a bridge from the cause board. You publish the cluster under *your* key. CauseStarter does not message the other organizer; the citation is public on their board if they look. ## What not to expect - No search or “popular causes.” - No platform verdict that a cause is good — only that counts are recomputable. -- No requirement that supporters agree with every statement on the page. +- No requirement that supporters agree with every statement on the board. diff --git a/docs/end-user/causestarter/the-jobs.md b/docs/end-user/causestarter/the-jobs.md index 6441680a..767b188d 100644 --- a/docs/end-user/causestarter/the-jobs.md +++ b/docs/end-user/causestarter/the-jobs.md @@ -22,14 +22,14 @@ In CauseStarter this lives on a cause’s **Pledges** page and on **Delegation** - **…I can’t tell whether this creator is a scammer or incompetent.** Be a delegate who mostly [funds proven work](../lazyGiving/retroactive-funding.md). You reimburse early contributors at cost after results, instead of betting on a pitch. - **…I can’t put that much of my *own* money on it.** Be an early funder who asks for reimbursement. If later donors close the loop, that giving budget can go to the next attempt. Your receipt is a track record, not a payout. -The cause page’s **Fundable Projects** list is that watch surface. Trust settings (the gear) control whose vouches you see. +The cause board’s **Fundable Projects** list (the fundable-projects board) is that watch surface. Trust settings (the gear) control whose vouches you see. ## Work **I’d be happy to do this work, but…** - **…I can’t afford to pay for it myself.** Publish the project and let others pledge. Funds release if the threshold is met; otherwise everyone is refunded. -- **…I don’t even know who to go to for funding.** You do not need a grant officer. Post the project, talk to a friend who is a bit better-connected in the trust graph, and get an **alignment** vouch onto a statement people already watch. The project then shows up on those cause pages. +- **…I don’t even know who to go to for funding.** You do not need a grant officer. Post the project, talk to a friend who is a bit better-connected in the trust graph, and get an **alignment** vouch onto a statement people already watch. The project then shows up on those cause boards. Start from **Create Project** on the home page, or from a cause’s project list. @@ -41,13 +41,13 @@ Start from **Create Project** on the home page, or from a cause’s project list - **…if I write my own it’ll have zero signers.** Write it so an implication attester can connect it to similar claims. Indirect support can count; you are not starting from a blank petition. - **…the statement others signed doesn’t actually imply the better one I have in mind.** Write a **bridge** and, if you want distribution, submit it as a suggestion to a mediator service. If it agrees, it can nudge its subscribers. -Cause pages have a **Bridges** section. Suggesters you opt into appear on Home. +Cause boards have a **Bridges** section. Suggesters you opt into appear on Home. -## Your own cause page +## Your own cause board -**I’d be happy to follow a cause page sorta like that one, but…** +**I’d be happy to follow a cause board sorta like that one, but…** -- **…I don’t love the exact combination of statements they chose.** Create your own cause. A cause is a set of statements, not a club you join. +- **…I don’t love the exact combination of statements they chose.** Publish your own board. A cause board is a mix of statements, not a club you join. - **…if I write my own it’ll have zero signers or projects.** Reuse *some* of the same statements and you inherit their signers and aligned projects. Even new wording can pick up overlap through implication and bridges. **Start a cause** is the organizer action. There is no browse list: circulate the link. diff --git a/docs/end-user/civility/index.md b/docs/end-user/civility/index.md index 5a25e0d9..dd92060c 100644 --- a/docs/end-user/civility/index.md +++ b/docs/end-user/civility/index.md @@ -51,7 +51,7 @@ Every funding decision ultimately belongs to a human donor. But two things keep > "Sure — I'll put $10 a month toward making more noninflammatory content exist. I'll let my friend Andrew, who follows this stuff more closely than I do, make the actual picks." *…and then never think about it again.* -That's the experience Civility is built around. And from the other side, a creator looks at the cause board and sees real money — pledged and waiting — specifically earmarked for noninflammatory content, and thinks, *"Huh. I could write some of that."* Visible demand pulls supply into existence. +That's the experience Civility is built around. And from the other side, a creator looks at the fundable-projects board and sees real money — pledged and waiting — specifically earmarked for noninflammatory content, and thinks, *"Huh. I could write some of that."* Visible demand pulls supply into existence. ## Part of a bigger bridge-building effort diff --git a/docs/end-user/common-sense-majority/why-does-tally-help.md b/docs/end-user/common-sense-majority/why-does-tally-help.md index ae405b9d..bc93bea7 100644 --- a/docs/end-user/common-sense-majority/why-does-tally-help.md +++ b/docs/end-user/common-sense-majority/why-does-tally-help.md @@ -2,7 +2,7 @@ Suppose the system reports that on topic T, 1.8 million people support a moderate-left statement, 2.1 million support a moderate-right one, and both imply the same common-ground statement. So what? Why does counting heads change anything? -The short answer is that the tally is doing several jobs at once — and each of them depends on a piece of infrastructure that didn't exist before: an AI mediator that watches the numbers, [bridging patterns](./hidden-majority-patterns.md) that make the common ground legible, and cause boards that turn counts into money. +The short answer is that the tally is doing several jobs at once — and each of them depends on a piece of infrastructure that didn't exist before: an AI mediator that watches the numbers, [bridging patterns](./hidden-majority-patterns.md) that make the common ground legible, and fundable-projects boards that turn counts into money. ## The tally and the mediator feed each other @@ -24,7 +24,7 @@ Once that pattern is visible, moderates on each side have a concrete way to see ## The numbers connect to funding -The [Aligning cause boards](/docs/end-user/alignment/index.md) turn visible support into actual money flowing toward content and projects that serve the moderate majority. The head-count doesn't directly cause any money to flow, but a big verified supporter count is a signal to potential project-creators and donors: "there's a lot of people over there, so if you're inclined to bring your energy or money there, it's likely to be fruitful." Tally numbers — especially verified ones — are exactly the kind of demand signal that should (and will, once we finish implementing that feature in the Aligning system's Fundable Project Explorer service) steer where funding goes. +The [Aligning fundable-projects boards](/docs/end-user/alignment/index.md) turn visible support into actual money flowing toward content and projects that serve the moderate majority. The head-count doesn't directly cause any money to flow, but a big verified supporter count is a signal to potential project-creators and donors: "there's a lot of people over there, so if you're inclined to bring your energy or money there, it's likely to be fruitful." Tally numbers — especially verified ones — are exactly the kind of demand signal that should (and will, once we finish implementing that feature in the Aligning system's Fundable Project Explorer service) steer where funding goes. ## It shifts the broader discourse diff --git a/docs/end-user/common-sense-majority/why-now.md b/docs/end-user/common-sense-majority/why-now.md index e0aefd3a..0d385a0a 100644 --- a/docs/end-user/common-sense-majority/why-now.md +++ b/docs/end-user/common-sense-majority/why-now.md @@ -46,4 +46,4 @@ The combination is what matters. Trustless infrastructure alone doesn't solve th - Subjective judgments are made by inspectable AI, with full configurability for those who want to verify. - The whole thing produces value at every step — no threshold to cross before it starts working, no single point of failure to attack. -The political tools to reveal the hidden majority — head counts, cause boards, bridge statements — existed conceptually for a long time. The infrastructure to run them without a trusted intermediary didn't. Now it does. +The political tools to reveal the hidden majority — head counts, fundable-projects boards, bridge statements — existed conceptually for a long time. The infrastructure to run them without a trusted intermediary didn't. Now it does. diff --git a/docs/end-user/commonality/index.md b/docs/end-user/commonality/index.md index c2e12bb7..39328143 100644 --- a/docs/end-user/commonality/index.md +++ b/docs/end-user/commonality/index.md @@ -2,7 +2,7 @@ Commonality is a movement — and a set of tools — for **internet-age coordination on public goods**. We're remarkably bad at producing the things we collectively need: shared infrastructure, independent journalism, scientific research, neighborhood projects, political organizing. New tech (assurance contracts, delegation, blockchains, AI) makes a much better approach viable. Commonality is what that better approach looks like. -The funding infrastructure here is the concrete instrument of the movement: assurance contracts that let you pledge without risk, delegation that lets you contribute without having to pick projects yourself, retroactive funding that reimburses early contributors at cost, and cause boards that route money to whatever serves a cause you care about. +The funding infrastructure here is the concrete instrument of the movement: assurance contracts that let you pledge without risk, delegation that lets you contribute without having to pick projects yourself, retroactive funding that reimburses early contributors at cost, and fundable-projects boards that route money to whatever serves a cause you care about. Commonality is one of several connected sites: @@ -72,7 +72,7 @@ The ideas the funding tools rest on: Ideas that live mainly on neighboring sites but are load-bearing here too: -- **[Statements and the implication graph](../tally/statements-and-implication-graph.md)** — what cause boards point at; the consumer surface is [Tally](../tally/index.md). +- **[Statements and the implication graph](../tally/statements-and-implication-graph.md)** — what fundable-projects boards point at; the consumer surface is [Tally](../tally/index.md). - **[Content funding](../content-funding/content-funding.md)** — the specialized contracts the Content Funding site is built on. - **[Trust networks](../shared/key-ideas/trust-networks.md)** — the attester / nudger graph; the underlying infrastructure is Conceptspace. diff --git a/docs/end-user/commonality/vision-and-strategy/credible-solution/discovery.md b/docs/end-user/commonality/vision-and-strategy/credible-solution/discovery.md index c9f92697..431d797c 100644 --- a/docs/end-user/commonality/vision-and-strategy/credible-solution/discovery.md +++ b/docs/end-user/commonality/vision-and-strategy/credible-solution/discovery.md @@ -4,6 +4,6 @@ Problem: With far more projects than anyone can evaluate, how do you find the go Government's solution: grant review panels and planning committees. A small number of people evaluate a large number of proposals. Bottlenecked, slow, subject to political influence. -Commonality's solution: cause boards with a trust network feeding delegates a stream of aligned projects. Discovery is distributed across many individuals (alignment attesters, delegates, the community), each contributing their own knowledge. The system aggregates their judgments per-user rather than requiring everyone to agree on one committee's picks. +Commonality's solution: fundable-projects boards with a trust network feeding delegates a stream of aligned projects. Discovery is distributed across many individuals (alignment attesters, delegates, the community), each contributing their own knowledge. The system aggregates their judgments per-user rather than requiring everyone to agree on one committee's picks. This is an instance of the broader [openness](../why-its-better/openness.md) principle: anyone can publish, filtering is crowdsourced through trust networks rather than centralized gatekeepers. diff --git a/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/donor-project-tension.md b/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/donor-project-tension.md index 0a86c06b..ddaead10 100644 --- a/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/donor-project-tension.md +++ b/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/donor-project-tension.md @@ -24,7 +24,7 @@ Even at Level 1, the sole donor gets real benefits from using Commonality rather - **Standardized infrastructure.** No need to build custom grant-management systems, reporting frameworks, or accountability mechanisms. The blockchain handles all of that. - **Verifiable track record.** The funder builds a public, auditable history of what they've funded and how those projects turned out. This is valuable for the funder's own reputation — especially for foundations that want to demonstrate impact. - - **Project ecosystem visibility.** The project shows up in cause boards, can receive alignment attestations, and is legible to the broader Commonality ecosystem. Even if the funder is currently the only one paying, the project is *discoverable* by others in a way that a private grant isn't. + - **Project ecosystem visibility.** The project shows up in fundable-projects boards, can receive alignment attestations, and is legible to the broader Commonality ecosystem. Even if the funder is currently the only one paying, the project is *discoverable* by others in a way that a private grant isn't. - **Upgrade path preserved.** If the funder later decides they're fine with co-funders, or the project creator wants to diversify their funding base, the infrastructure is already there. No migration needed. And from the project creator's side, using Commonality even with a sole donor means: diff --git a/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/for-established-orgs.md b/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/for-established-orgs.md index 1694e422..af8bef17 100644 --- a/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/for-established-orgs.md +++ b/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/for-established-orgs.md @@ -20,7 +20,7 @@ A new charity faces a brutal chicken-and-egg: you need donors to trust you, but The alignment-attestation system — where attesters vouch that a project aligns with a cause — serves as a straightforward drop-in for how orgs already evaluate proposals. -**Start closed:** Hardcode your org as the single trusted attester for your cause board. Nothing changes about your decision-making process. You're just recording "this project fits our mission" decisions onchain instead of in an internal database. Minimal effort, immediate benefits. +**Start closed:** Hardcode your org as the single trusted attester for your fundable-projects board. Nothing changes about your decision-making process. You're just recording "this project fits our mission" decisions onchain instead of in an internal database. Minimal effort, immediate benefits. **Open gradually:** Once you're using the system with yourself as sole attester, it's easy to start accepting others: - Accept attestations from specific trusted partners. @@ -29,7 +29,7 @@ The alignment-attestation system — where attesters vouch that a project aligns This is a [dial, not a switch](./dial-not-switch.md). The org controls the pace. -**The common-ground angle:** Here's where it gets interesting. A progressive environmental org and a conservative rural-community org might never collaborate directly. But they might both care about "clean drinking water for rural communities." If they each write alignment statements about their priorities, the implication-attestation system connects them automatically. A water filtration project shows up in *both* cause boards — funded from both sides, without either side needing to acknowledge the other or compromise on broader ideology. +**The common-ground angle:** Here's where it gets interesting. A progressive environmental org and a conservative rural-community org might never collaborate directly. But they might both care about "clean drinking water for rural communities." If they each write alignment statements about their priorities, the implication-attestation system connects them automatically. A water filtration project shows up in *both* fundable-projects boards — funded from both sides, without either side needing to acknowledge the other or compromise on broader ideology. Nobody has to agree on *why* clean water matters. The system just surfaces that they agree on *what* should be done. diff --git a/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/tip-jar-upgrade-path.md b/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/tip-jar-upgrade-path.md index 66ad8046..bba1cba4 100644 --- a/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/tip-jar-upgrade-path.md +++ b/docs/end-user/commonality/vision-and-strategy/ease-of-adoption/tip-jar-upgrade-path.md @@ -10,7 +10,7 @@ An unconditional donation can use Commonality without becoming a financial produ - a durable record of who contributed, how much, and when; - meaningful leaderboard recognition for early support; -- a portable contribution history that can participate in Commonality's cause boards and trust system. +- a portable contribution history that can participate in Commonality's fundable-projects boards and trust system. A donor who wants no possible reimbursement can choose **Donate normally**. They keep the recognition receipt but permanently forgo the reimbursement claim. @@ -34,7 +34,7 @@ Compared with a plain tip jar, receipt-backed donations provide: - **No financial design required.** Start with ordinary donations and recognition receipts. - **A gradual project path.** Add assurance thresholds only when a defined piece of future work needs enough support to proceed. - **Access to skilled scouts.** Delegates and early contributors can identify promising work, build public track records, and recycle reimbursed giving budgets into new projects. -- **Connection to Commonality.** Work can appear on cause boards, receive delegated funding, and benefit from alignment and success attestations. +- **Connection to Commonality.** Work can appear on fundable-projects boards, receive delegated funding, and benefit from alignment and success attestations. ## Subscriptions diff --git a/docs/end-user/commonality/vision-and-strategy/why-its-better/openness.md b/docs/end-user/commonality/vision-and-strategy/why-its-better/openness.md index eec25b60..af0efa73 100644 --- a/docs/end-user/commonality/vision-and-strategy/why-its-better/openness.md +++ b/docs/end-user/commonality/vision-and-strategy/why-its-better/openness.md @@ -11,7 +11,7 @@ In Commonality, this applies throughout: Anyone can publish whatever they want, which will inevitably produce a huge mass of mostly-garbage. The filtering happens afterward, through the same mechanisms that work on the rest of the internet: - **Social signals.** Sorting by trending (velocity of new signatures), number of supporters, or amount of funding pledged naturally surfaces the things that real people care about. - - **Delegation and trust networks.** You choose which attesters to trust. If you trust Alice and Alice says a project is aligned, it shows up in your cause board. If you don't trust Bob, his attestations are invisible to you. The filtering is per-user, not imposed by a central authority. + - **Delegation and trust networks.** You choose which attesters to trust. If you trust Alice and Alice says a project is aligned, it shows up in your fundable-projects board. If you don't trust Bob, his attestations are invisible to you. The filtering is per-user, not imposed by a central authority. - **Implication attestations.** You don't need to find the "right" statement — say what you want, and the system connects you with similar statements that have more traction. The popular ones float up without anyone deciding they should. - **Skin in the game.** Assurance contracts mean money is at stake. A project with $50K in pledges from 200 people has been filtered by something much more meaningful than upvotes — people put their actual money behind it (conditionally, but still). diff --git a/docs/end-user/commonality/vision-and-strategy/why-its-better/organic-coalitions.md b/docs/end-user/commonality/vision-and-strategy/why-its-better/organic-coalitions.md index 7a7f54bc..7101d7e8 100644 --- a/docs/end-user/commonality/vision-and-strategy/why-its-better/organic-coalitions.md +++ b/docs/end-user/commonality/vision-and-strategy/why-its-better/organic-coalitions.md @@ -17,7 +17,7 @@ What this looks like in practice: - A progressive environmental org publishes: "Rural communities deserve access to clean drinking water regardless of economic status." - A conservative community group publishes: "Local communities should control their own water infrastructure without federal interference." - An implication attester notices that both statements support a concrete project: "Build a water filtration system for Millbrook County." - - The project shows up in *both* groups' cause boards. It gets funded from both sides. Neither group had to acknowledge the other, compromise on language, or join a coalition. + - The project shows up in *both* groups' fundable-projects boards. It gets funded from both sides. Neither group had to acknowledge the other, compromise on language, or join a coalition. Nobody had to agree on *why* clean water matters. The system just surfaced that they agree on *what* should be done. diff --git a/docs/end-user/commonality/vision-and-strategy/why-its-better/what-its-better-for.md b/docs/end-user/commonality/vision-and-strategy/why-its-better/what-its-better-for.md index 028e2713..c4274808 100644 --- a/docs/end-user/commonality/vision-and-strategy/why-its-better/what-its-better-for.md +++ b/docs/end-user/commonality/vision-and-strategy/why-its-better/what-its-better-for.md @@ -4,7 +4,7 @@ We already have governments and charities funding public goods. The world is not Building a vertical and need concrete examples rather than categories? These seven are the *gate*; `specs/product/cause-taxonomy.md` is the -generator that turns them into a populated cause board. +generator that turns them into a populated fundable-projects board. ## The kinds it's better for diff --git a/docs/end-user/lazyGiving/get-your-project-funded.md b/docs/end-user/lazyGiving/get-your-project-funded.md index d038b674..1e9d4629 100644 --- a/docs/end-user/lazyGiving/get-your-project-funded.md +++ b/docs/end-user/lazyGiving/get-your-project-funded.md @@ -22,13 +22,13 @@ You don't have to convince every individual donor. You have to convince the scou You write up your project: what it is, what it will accomplish, how much you need. You set a funding goal and optionally a deadline. -Your project is published on-chain. Donors find it by browsing, through cause boards connected to relevant causes, or because a delegate or attester they trust has pointed them toward it. +Your project is published on-chain. Donors find it by browsing, through fundable-projects boards connected to relevant causes, or because a delegate or attester they trust has pointed them toward it. When pledges reach your goal, funds are released. If the deadline passes without reaching the goal, all pledges are automatically refunded. After you've delivered, your project remains open for **retroactive funding**. Donors who wanted to see results first can donate to its reimbursement waterfall. That money becomes available to early contributors pro-rata, capped at exactly what each person put in; it does not pay the project a second time and never includes interest or a premium. Reimbursement lets scouts use the same giving budget on another early project, while their public record helps them attract delegated funds. -**Alignment attestations** help a lot. If someone trusted by a community attests that your project aligns with a cause they care about, you become visible in that community's cause board — and visible to delegates who fund on that community's behalf. Reach out to people you know and ask them to vouch for your project's alignment. No money involved; just a statement of connection. +**Alignment attestations** help a lot. If someone trusted by a community attests that your project aligns with a cause they care about, you become visible in that community's fundable-projects board — and visible to delegates who fund on that community's behalf. Reach out to people you know and ask them to vouch for your project's alignment. No money involved; just a statement of connection. ## Getting started @@ -42,6 +42,6 @@ If you know anyone in the relevant community, ask them to attest that your proje A LazyGiving campaign is a one-off ask. Most projects benefit from a presence on neighboring sites too: -- **[Set up a cause board](../alignment/index.md)** on Aligning — if your project has ongoing operating costs, a portal lets delegates route monthly cause pledges to you. LazyGiving handles the launch ask; Aligning handles the sustaining drip. +- **[Set up a fundable-projects board](../alignment/index.md)** on Aligning — if your project has ongoing operating costs, a portal lets delegates route monthly cause pledges to you. LazyGiving handles the launch ask; Aligning handles the sustaining drip. - **[Get content pieces funded](../content-funding/get-your-content-funded.md)** on Content Funding — if your project produces individual articles, videos, or posts, each one can be funded separately. - **[Ask for attestations](../alignment/help-connect-things.md)** — find people in the community whose attestations carry weight, and ask them to vouch that your project aligns with relevant causes. diff --git a/docs/end-user/shared/key-ideas/trust-networks.md b/docs/end-user/shared/key-ideas/trust-networks.md index 6992761b..ad1a3ca7 100644 --- a/docs/end-user/shared/key-ideas/trust-networks.md +++ b/docs/end-user/shared/key-ideas/trust-networks.md @@ -8,7 +8,7 @@ You might trust a few friends, a few public figures in fields you care about, an The system has a lot of information flowing through it — project endorsements ("project P serves cause C"), statement connections ("statement S1 implies statement S2"), content quality evaluations. Without filtering, it would be noise. Trust networks are the filter. -You only see endorsements from people in your trust network. That means the projects in your cause board, the statement connections in your implication graph, and the content evaluations you see are all filtered through the judgment of people you (directly or transitively) trust. +You only see endorsements from people in your trust network. That means the projects in your fundable-projects board, the statement connections in your implication graph, and the content evaluations you see are all filtered through the judgment of people you (directly or transitively) trust. ## No central gatekeepers @@ -17,5 +17,5 @@ This is the alternative to having a platform decide what's legitimate. Different ## How this shows up in practice - You set up your trust network by marking a few people or organizations as trusted. The system computes the transitive closure automatically. -- Everything you see — cause boards, statement pages, project recommendations — is filtered through your trust network. +- Everything you see — fundable-projects boards, statement pages, project recommendations — is filtered through your trust network. - You can adjust your trust network at any time. Add or remove someone, and your view of the system updates accordingly. diff --git a/docs/end-user/shared/use-case-walkthroughs/common-sense-majority.md b/docs/end-user/shared/use-case-walkthroughs/common-sense-majority.md index 0ab96503..e1cbf943 100644 --- a/docs/end-user/shared/use-case-walkthroughs/common-sense-majority.md +++ b/docs/end-user/shared/use-case-walkthroughs/common-sense-majority.md @@ -41,7 +41,7 @@ The common-sense majority has always been there. It just couldn't see itself. Now that the demand is visible, things start to happen: -- **A cause board** for this cause now has real demand signal. There's money behind it — pledges from people who said they'd support this kind of thing. +- **A fundable-projects board** for this cause now has real demand signal. There's money behind it — pledges from people who said they'd support this kind of thing. - **Delegates** start directing funds toward specific projects and content that serve this cause. - **Content creators** see the portal and start targeting it — writing the kind of thoughtful, noninflammatory content that people said they wanted. - **Projects emerge** — maybe a media outlet that commits to the standard, maybe a set of AI evaluation tools, maybe a directory of writers who consistently produce good work. diff --git a/docs/end-user/shared/use-case-walkthroughs/defunding.md b/docs/end-user/shared/use-case-walkthroughs/defunding.md index a4ca14f4..81a7535c 100644 --- a/docs/end-user/shared/use-case-walkthroughs/defunding.md +++ b/docs/end-user/shared/use-case-walkthroughs/defunding.md @@ -34,7 +34,7 @@ The province announces the funding cut. Now things get serious. - Maria sets up a **standby assurance contract**: "If provincial funding is cut, fund the mentorship program at $120K/year." Monthly pledges, $10K/month threshold. - 180 families pledge an average of $60/month. That's $10,800/month in publicly verifiable, locked pledges — past the threshold, but the community keeps going to build a buffer. - A local business owner pledges $2K/month through a bridge operator (tax-deductible via the program's existing charity). - - The wider network kicks in: three families from neighboring towns who signed the broader "rural communities" statement see Millbrook's project in their cause board (via the implication attestation) and pledge $50/month each. + - The wider network kicks in: three families from neighboring towns who signed the broader "rural communities" statement see Millbrook's project in their fundable-projects board (via the implication attestation) and pledge $50/month each. The pledges are onchain. The province can see exactly how much is committed and by whom. This isn't a petition with signatures — it's $13K/month in conditionally locked money. The community is demonstrating that the program survives without provincial funding. This is the [credible threat](../../commonality/vision-and-strategy/hard-to-stop/credible-threat.md). @@ -50,7 +50,7 @@ Alternatively: the province follows through on the cut. The assurance contract a - The $10K/month threshold is met (it grew to $11.5K during the standoff as more people pledged). Monthly funding flows to the program. - Maria, as delegate, directs the supplementary delegated funds ($2K, now grown to $5K as the crisis attracted more delegators) toward specific needs — new mentoring materials, a part-time coordinator. - - James attests alignment for a neighboring town's similar program, which now shows up in Millbrook's cause board. A donor who pledged for Millbrook sees the neighboring program and pledges there too. + - James attests alignment for a neighboring town's similar program, which now shows up in Millbrook's fundable-projects board. A donor who pledged for Millbrook sees the neighboring program and pledges there too. - The program's track record — months of successful delivery, transparent spending, growing community support — is all onchain. When the program applies for a private foundation grant six months later, the application includes verifiable proof of community backing. The foundation, impressed by the evidence, provides matching funds. ## What this illustrates diff --git a/docs/end-user/shared/use-case-walkthroughs/local-funding-shift.md b/docs/end-user/shared/use-case-walkthroughs/local-funding-shift.md index b9349dd9..724d8e82 100644 --- a/docs/end-user/shared/use-case-walkthroughs/local-funding-shift.md +++ b/docs/end-user/shared/use-case-walkthroughs/local-funding-shift.md @@ -24,7 +24,7 @@ None of this is about water infrastructure yet. Each town is just building the m - A resident of Millbrook creates a statement: "Our region's water infrastructure needs upgrading and we shouldn't have to wait for the province." - An implication attester (AI service) notices that people in Cedarville and Harton have signed similar statements — "Cedarville needs water treatment upgrades," "rural communities deserve reliable water infrastructure." It connects these through the implication graph. - - Now supporters in all three towns can see each other's statements and pledges in their cause boards. Nobody organized this. The system discovered the shared interest. + - Now supporters in all three towns can see each other's statements and pledges in their fundable-projects boards. Nobody organized this. The system discovered the shared interest. Ravi in Cedarville sees Millbrook's water-related pledges. Diane in Harton sees both. They start talking — not because an institution connected them, but because the implication graph made the overlap visible. diff --git a/docs/end-user/shared/use-case-walkthroughs/noninflammatory-content.md b/docs/end-user/shared/use-case-walkthroughs/noninflammatory-content.md index a2e2cfab..1df0572e 100644 --- a/docs/end-user/shared/use-case-walkthroughs/noninflammatory-content.md +++ b/docs/end-user/shared/use-case-walkthroughs/noninflammatory-content.md @@ -54,9 +54,9 @@ You find a delegate you trust — someone who's good at curating thoughtful poli ### The content creator -You're a writer. You look at the cause board for this cause and see real money — pledged and available — from people who want exactly the kind of writing you want to do. You write a piece about immigration that makes the conservative case without painting progressives as naive or unpatriotic. You submit it to an AI evaluation service that assesses whether it meets the standard: does it steelman the opposing view? Does it avoid contempt? Could a reasonable person who disagrees engage with it without feeling attacked? +You're a writer. You look at the fundable-projects board for this cause and see real money — pledged and available — from people who want exactly the kind of writing you want to do. You write a piece about immigration that makes the conservative case without painting progressives as naive or unpatriotic. You submit it to an AI evaluation service that assesses whether it meets the standard: does it steelman the opposing view? Does it avoid contempt? Could a reasonable person who disagrees engage with it without feeling attacked? -It passes. The AI issues an attestation: this piece meets the standard. Donors whose cause boards are configured to trust this evaluator now see your piece. Some fund it directly; others have delegated their decisions to taste-makers who fund it on their behalf. Either way, money flows to you for doing exactly what you wanted to do. +It passes. The AI issues an attestation: this piece meets the standard. Donors whose fundable-projects boards are configured to trust this evaluator now see your piece. Some fund it directly; others have delegated their decisions to taste-makers who fund it on their behalf. Either way, money flows to you for doing exactly what you wanted to do. ### The active reader / taste-maker @@ -71,7 +71,7 @@ The writer submits it to an AI evaluator that's designed to assess content from Now here's what happens: -- Left-leaning donors who pledged to "constructive discourse" see it in their cause board — because the implication graph connected their statement to this content's cause. +- Left-leaning donors who pledged to "constructive discourse" see it in their fundable-projects board — because the implication graph connected their statement to this content's cause. - Right-leaning donors who pledged to "content that doesn't paint us as villains" also see it — through a completely different path in the implication graph. - Both groups end up funding the same piece, without coordinating or even knowing about each other. diff --git a/docs/end-user/tally/index.md b/docs/end-user/tally/index.md index 06e5afe7..59b41b67 100644 --- a/docs/end-user/tally/index.md +++ b/docs/end-user/tally/index.md @@ -35,7 +35,7 @@ Petitions usually feel like shouting into a void. You sign one, it goes nowhere, Tally exists to fix two specific problems with that: - **Fragmented support.** A thousand people who all believe roughly the same thing end up scattered across a hundred different petitions, and nobody — including them — can see that they're a coalition. The implication graph aggregates that scattered support into something visible. -- **No follow-through.** Signing a petition is the end of the interaction. Tally is designed so a signature can become a signal that other tools — cause boards, organizing sites, content-funding contracts — can act on. Your signature on a statement about clean water can route real money toward projects that serve it. +- **No follow-through.** Signing a petition is the end of the interaction. Tally is designed so a signature can become a signal that other tools — fundable-projects boards, organizing sites, content-funding contracts — can act on. Your signature on a statement about clean water can route real money toward projects that serve it. Tally is the consumer-facing front door. The signing primitives, trust network, and implication graph it sits on top of are shared infrastructure used by sibling sites in the [Commonality](/docs) ecosystem. diff --git a/docs/end-user/tally/statements-and-implication-graph.md b/docs/end-user/tally/statements-and-implication-graph.md index 6def63e6..30095c1c 100644 --- a/docs/end-user/tally/statements-and-implication-graph.md +++ b/docs/end-user/tally/statements-and-implication-graph.md @@ -16,7 +16,7 @@ This is how the system builds coalitions without anyone having to build them. Yo ## How this shows up in practice -- **Cause boards** show you projects aligned with statements you've signed — including projects aligned with statements *implied by* your statements. You don't have to go looking; the system surfaces what's relevant. +- **Fundable-projects boards** show you projects aligned with statements you've signed — including projects aligned with statements *implied by* your statements. You don't have to go looking; the system surfaces what's relevant. - **Supporter counts** include both direct signers and indirect supporters (people who signed statements that imply yours). That number reflects the real, latent demand for a cause — often much larger than anyone expected. - **Projects** connect to causes through endorsements: someone (or some AI) vouches that a project serves a particular cause. If that cause is connected to your statements through the implication graph, the project appears in your portal. @@ -36,6 +36,6 @@ For a detailed look at the different shapes these patterns take, see the [common ## Cross-partisan discovery -The system surfaces common ground even between groups that would never coordinate directly. A progressive environmental organization and a conservative community group might each sign statements in their own language — "rural communities deserve access to clean drinking water regardless of economic status" vs. "local communities should control their own water infrastructure without federal interference." Different words, different politics. But an implication attester can notice that both statements support the same concrete project: a water filtration system for a specific county. The project appears in both groups' cause boards. Neither group needs to acknowledge the other, agree on framing, or join a coalition. They just both end up funding the same thing. +The system surfaces common ground even between groups that would never coordinate directly. A progressive environmental organization and a conservative community group might each sign statements in their own language — "rural communities deserve access to clean drinking water regardless of economic status" vs. "local communities should control their own water infrastructure without federal interference." Different words, different politics. But an implication attester can notice that both statements support the same concrete project: a water filtration system for a specific county. The project appears in both groups' fundable-projects boards. Neither group needs to acknowledge the other, agree on framing, or join a coalition. They just both end up funding the same thing. Nobody had to agree on *why* clean water matters. The system found that they agreed on *what should happen*. diff --git a/docs/end-user/tldr-for-llms.md b/docs/end-user/tldr-for-llms.md index 7195afaa..a9baeebc 100644 --- a/docs/end-user/tldr-for-llms.md +++ b/docs/end-user/tldr-for-llms.md @@ -28,8 +28,8 @@ Each entry follows the same shape: [Full page](/docs/end-user/tally/statements-and-implication-graph.md) - **What it is:** Users express what they care about by signing statements (free-text). The implication graph is a system of "S1 implies S2" relationships (generated by AI services) that connects related statements automatically. Together, these form the backbone of how supply (projects) meets demand (donors) without centralized coordination. -- **When a user encounters it:** When signing a statement, when viewing a cause board, when seeing supporter counts, when understanding why a particular project appeared in their feed. -- **What they might want help with:** Writing or finding a statement that expresses what they care about; understanding why certain projects show up in their cause board; understanding the supporter count on a statement page. +- **When a user encounters it:** When signing a statement, when viewing a fundable-projects board, when seeing supporter counts, when understanding why a particular project appeared in their feed. +- **What they might want help with:** Writing or finding a statement that expresses what they care about; understanding why certain projects show up in their fundable-projects board; understanding the supporter count on a statement page. ## Assurance contracts @@ -59,8 +59,8 @@ Each entry follows the same shape: [Full page](/docs/end-user/alignment/successful-projects.md) -- **What it is:** A second attestation type on Aligning, parallel to alignment vouches. An *alignment* vouch says a project is *trying* to serve a cause; a *success* vouch says it *delivered*. Both anchor to the same cause statement and ride the same trust graph + implication propagation. The **Successful projects** view on a cause board shows projects that (a) the viewer's trust network has vouched as successful and (b) still have donation receipts outstanding — a call-to-action queue for retroactive funders, not an official "this succeeded" pronouncement. It is the primary surface for donors who want to support proven work without predicting winners or detecting scams in advance. -- **When a user encounters it:** When viewing a cause board's Successful tab; when deciding which proven projects' early contributors to reimburse; when vouching that a project delivered (the mirror of vouching for alignment); when reading a "vouched successful" section on a LazyGiving project page. +- **What it is:** A second attestation type on Aligning, parallel to alignment vouches. An *alignment* vouch says a project is *trying* to serve a cause; a *success* vouch says it *delivered*. Both anchor to the same cause statement and ride the same trust graph + implication propagation. The **Successful projects** view on a fundable-projects board shows projects that (a) the viewer's trust network has vouched as successful and (b) still have donation receipts outstanding — a call-to-action queue for retroactive funders, not an official "this succeeded" pronouncement. It is the primary surface for donors who want to support proven work without predicting winners or detecting scams in advance. +- **When a user encounters it:** When viewing a fundable-projects board's Successful tab; when deciding which proven projects' early contributors to reimburse; when vouching that a project delivered (the mirror of vouching for alignment); when reading a "vouched successful" section on a LazyGiving project page. - **What they might want help with:** Understanding the difference between aligned and successful; understanding that the page reflects others' vouches (trust-filtered), not a platform verdict; finding proven projects with outstanding reimbursements to fund; donating to close a project's reimbursement loop; posting a success vouch for a project they believe delivered; registering disagreement with a vouch ("I don't agree this delivered") by downgrading trust in whoever vouched — the consequence of a bad success vouch is reputational, not financial. ## Credible threats @@ -76,7 +76,7 @@ Each entry follows the same shape: [Full page](/docs/end-user/content-funding/content-funding.md) - **What it is:** LazyGiving (assurance contracts) pointed at social-media content. A creator groups pieces of content (tweets, YouTube videos, Substack posts) into a contract with a funding goal; contributors contribute toward the items they value; funds release only if the goal is met, else everyone is refunded. A content contract *is* a LazyGiving project. Creators claim their channel by posting a verification code from the account; third parties can start a contract for an unclaimed channel, but funds wait for the creator. Via Aligning, cause pools (optionally using AI evaluators) can fund whole *kinds* of content — the engine behind funding noninflammatory/insightful/etc. content. -- **When a user encounters it:** When pledging toward a specific piece of content; when grouping content into a contract and setting a goal; when claiming a channel as a creator; when browsing a content-focused cause board. +- **When a user encounters it:** When pledging toward a specific piece of content; when grouping content into a contract and setting a goal; when claiming a channel as a creator; when browsing a content-focused fundable-projects board. - **What they might want help with:** Claiming their channel; setting up a contract and funding goal; understanding the pledge-and-refund mechanism; finding content to fund; understanding how cause pools fund kinds of content. ## Trust networks @@ -84,7 +84,7 @@ Each entry follows the same shape: [Full page](shared/key-ideas/trust-networks.md) - **What it is:** Each user chooses who they trust. The system follows trust transitively (if you trust A and A trusts B, you see B's endorsements). This is how the system filters noise and surfaces relevant information without central gatekeepers. -- **When a user encounters it:** When choosing who to trust in settings; when seeing project endorsements in their cause board; when understanding why certain projects or content appear (or don't appear) in their feed. +- **When a user encounters it:** When choosing who to trust in settings; when seeing project endorsements in their fundable-projects board; when understanding why certain projects or content appear (or don't appear) in their feed. - **What they might want help with:** Setting up their trust network; understanding why they're seeing (or not seeing) certain endorsements; understanding how trust chains work; using **Downtrust** to register disagreement with a vouch (alignment *or* success) by decreasing/zeroing trust in whoever made it — a first-class action aimed at the voucher, the symmetric counterpart to vouching. ## How actions compound diff --git a/inbox.md b/inbox.md index bbd14df8..f9faac14 100644 --- a/inbox.md +++ b/inbox.md @@ -31,7 +31,7 @@ Also, don't let any of the items get too long; usually there's a separate .md fi ### Docs / UI copy -- **(Ask)** Apply [cause-page-not-a-club.md](specs/product/cause-page-not-a-club.md) when you next pass over CauseStarter copy: two-step rename — today’s “cause board” (project list) → **fundable-projects board**, then organizer publication → **cause board** (don’t sweat leftover “cause page”). Sweep is yours; new copy should already follow that note. +- **(Tell)** Applied [cause-page-not-a-club.md](specs/product/cause-page-not-a-club.md) copy sweep: glossary two-step rename, end-user docs, Aligning/fundable-projects UI strings, CauseStarter high-traffic docs + organizer publish copy. Leftover “cause page” in comments, `/cause/:owner/:slug` and `fundingportal*` identifiers, and incidental “funding portal” docs still lag. - Decide whether to act on the fresh landing-copy positioning findings. The Civility grievance-first hero was reviewed and is fine; the verifier rubric was corrected so CSM’s recognition-register rule is not imposed on every vertical. Remaining findings are elsewhere: the umbrella Commonality landing still recruits generic end users despite the founder-first strategy, CSM front-loads the mediator toggle and uses “the other side’s bullshit,” Aligning repeats its main tradeoff several times, and Tally’s “Sign once, counted forever” headline presents a future goal as current capability. diff --git a/specs/glossary.md b/specs/glossary.md index 21ffebe0..4c5e8448 100644 --- a/specs/glossary.md +++ b/specs/glossary.md @@ -41,7 +41,7 @@ wrong (or this file is out of date and needs an ADR — see | **Reimbursement** | What a retroactive donation pays out to an early contributor — at cost, no upside | | **Note** | A `DelegatableNote`: a bucket of deposited funds whose spending authority can be delegated down a chain, revocably. The unit of delegated giving | | **Standing pledge** | A *recurring* funding commitment registered with `RecurringPledges`, executed periodically into a note | -| **Cause board** | *Today:* the fundable-projects dashboard (heading **Fundable Projects**) inlined on a statement or cause page. *Direction (not swept):* that list becomes the **fundable-projects board**; **cause board** moves to the organizer publication (mix of statements + that list + bridges etc.). See [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). | +| **Fundable-projects board** | The list of aligned work you might fund (heading **Fundable Projects**), inlined on a statement or cause board and also a full page. Code still says `fundingportal*` / `/portal/:statementCid`. Formerly called **portal** and then **cause board**. | ### Judgments people and services publish @@ -62,7 +62,8 @@ wrong (or this file is out of date and needs an ADR — see | **Subsystem** | A capability: a contract family + SDK subsystem + UI feature module sharing one name | | **Site / UI domain** | A branded build that composes a subset of subsystems. There are eight | | **Bookmark** | A published cause or statement the user chose to keep, independently of signing. Cause bookmarks are cached locally and, with a connected wallet, stored in the `bookmarked-causes` mutable ref (public). Statement bookmarks use the separate `bookmarks` ref (statement CIDs). Unpublished cause drafts stay device-local. Never mix the two lists. User-facing verbs: bookmark / remove bookmark — never "save to device" or "delete cause" | -| **Cause page** | The versioned publication of a cause's title, summary, issue list, and mediator blurb. User-facing word for what code still calls a *roster* (`causestarter.roster`, `rosterCid`). Never say "roster" in UI copy. *Direction:* prefer **cause board** for this object; leftover “cause page” is fine. See [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). | +| **Cause board** | The organizer publication at `/cause/:owner/:slug`: title, summary, ordered planks, bridges, pledges, and a **fundable-projects board** as the centerpiece. Code still says *roster* (`causestarter.roster`, `rosterCid`). Never say "roster" in UI copy. Not a dashboard (that word is reserved for a future personal view). Leftover **cause page** is fine. See [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). | +| **Cause page** | Leftover synonym for **cause board** (the organizer publication). Prefer **cause board** in new copy. | | **Natural cause** | A cause playing the “this camp’s position” parent role in a [bridge cluster](./product/bridge-causes.md). Usually someone else’s publication; may be a **stand-in cause** the mediator wrote because that camp had no cause yet. | | **Stand-in cause** | A mediator-authored natural parent: a thin roster the mediator thinks the other camp believes, published under the mediator’s key and labeled as such. Not a modified cause (there is no prior parent to sliver). See [the-other-cause.md](/docs/founder/the-other-cause.md). | | **Modified cause** | A mediator-authored cause: wording the mediator thinks signers of a given natural cause might also accept, without feeling misrepresented. Usually a topical sliver, not a full rewrite of the parent. | @@ -145,29 +146,26 @@ retired as synonyms. `Contribution.participant`, `Refund.participant`, and `ContributorStats.participant` are now `.contributor`; the raw decoded-event field stays `participant` because that is the ABI arg name. -### 5a. Bare “cause” names three things — *direction 2026-08-24, not swept* +### 5a. Bare “cause” names three things — *swept in copy 2026-08-24* - Ordinary English: the worldly goal. - Glossary Part 1: a **statement** used as a funding anchor (a *role*). -- CauseStarter: the organizer **roster**. +- CauseStarter: the organizer **roster** — now called **cause board** in + user-facing copy (leftover **cause page** is fine). Identifiers and + `/cause/:owner/:slug` may lag. -Direction: keep “cause” for the first two; the publication is a **cause board** -(fine leftover: **cause page**); do not call it a dashboard. A cause board may -cover multiple causes. Full note and **two-step rename** (fundable-projects -board first): [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). -Does not reverse ADR 0009. - -### 5. Portal → cause board (already ruled, half-finished; about to be superseded) +Keep “cause” for the first two. Do not call the publication a dashboard. +A cause board may cover multiple causes. Does not reverse ADR 0009. See +[cause-page-not-a-club.md](./product/cause-page-not-a-club.md). -Adam ruled 2026-06-12: **cause board** wins in user-facing copy for the -fundable-projects list; code identifiers, routes, and directories keep -`fundingportal*`. Still unfinished — `/portal/:statementCid` routes, -`ui/src/fundingportals/` copy, and ~22 files under `docs/end-user/` still say -"funding portal". Tracked in [TODO.md](../TODO.md). +### 5. Portal → cause board → fundable-projects board (*copy swept 2026-08-24*) -**Superseding direction (not swept):** that list becomes **fundable-projects -board**; **cause board** is reused for the organizer publication. Do the -fundable-projects rename *before* the publication rename. See +Adam ruled 2026-06-12: **cause board** for the fundable-projects list in +user-facing copy; code kept `fundingportal*`. **Superseded 2026-08-24:** +that list is the **fundable-projects board**; **cause board** is the +organizer publication. Code identifiers, routes, and directories still +lag (`fundingportal*`, `/portal/:statementCid`). Leftover “funding portal” +in older docs is unfinished, not a third noun. See [cause-page-not-a-club.md](./product/cause-page-not-a-club.md). ### 6. Smaller ones diff --git a/specs/product/cause-page-not-a-club.md b/specs/product/cause-page-not-a-club.md index 1b811b02..547cadf1 100644 --- a/specs/product/cause-page-not-a-club.md +++ b/specs/product/cause-page-not-a-club.md @@ -1,7 +1,9 @@ # The organizer publication is a board, not a club -**Status: direction, not swept.** Adam agreed with this framing on 2026-08-23 -and refined the nouns on 2026-08-24. He will apply copy/UI later (fresh LLM). +**Status: copy sweep started 2026-08-24.** Adam agreed with this framing on 2026-08-23 +and refined the nouns on 2026-08-24. Glossary, high-traffic CauseStarter copy, +Aligning/fundable-projects UI strings, and end-user docs were updated in this +pass. Identifiers, routes, and leftover “cause page” / “funding portal” still lag. It does **not** reverse [ADR 0009](../decisions/0009-causes-are-publications-over-statements.md): the object model (immutable statements; a mutable, shareable publication over a roster of them; verticals as a separate operator role) stays. This note is about @@ -128,18 +130,17 @@ you mean, then live on your own union. That personal surface can also span fundable-projects list is still called a cause board, the glossary is worse than today. -## Sweep when Adam is ready (fresh LLM) +## Sweep status (copy 2026-08-24; identifiers later) -1. **Fundable-projects board:** UI copy, end-user docs, glossary Part 1 - “Cause board” row, glossary Part 2 §5 (portal → cause board is then - “portal → fundable-projects board”; code `fundingportal*` may still lag). -2. **Cause board** = organizer publication: new and high-traffic copy - (landing, CauseStarter in-app docs, empty states). Leave incidental - “cause page” alone unless it is confusing. +1. **Fundable-projects board:** done in UI copy, end-user docs, glossary Part 1 + and Part 2 §5. Code `fundingportal*` / `/portal/:statementCid` still lag. +2. **Cause board** = organizer publication: done in glossary and high-traffic + CauseStarter docs / publish empty states. Leftover “cause page” in comments + and incidental copy left on purpose. 3. Later: identifiers (`/cause/:owner/:slug` can lag). Contract names stay. Bridge-cluster terms (natural / modified / bridge cause) still mean *publications* in the compound; separate pass. -Until then, new copy should follow this note even if old copy has not been -swept: if you must name the fundable-projects list, say **fundable-projects -board**; if you must name the organizer URL, prefer **cause board**. +New copy should keep following this note: if you must name the fundable-projects +list, say **fundable-projects board**; if you must name the organizer URL, +prefer **cause board**. diff --git a/specs/tech/subsystems/aligning/ui.md b/specs/tech/subsystems/aligning/ui.md index 9a3146f9..c5fe06f1 100644 --- a/specs/tech/subsystems/aligning/ui.md +++ b/specs/tech/subsystems/aligning/ui.md @@ -88,16 +88,16 @@ If a wallet is connected, highlight the current user's row and show a summary ca ## Integration with Concept Space (Statement Page) -Not a cause board page itself, but the cause board adds a section to the concept space's statement page (`/statement/:statementCid`). +Not a fundable-projects board page itself, but the fundable-projects board adds a section to the concept space's statement page (`/statement/:statementCid`). -### "Cause Board" Section +### "Fundable Projects" Section - **Total Funding Raised** - Count of aligned projects (direct + indirect) -- A "View Cause Board" link/button going to `/portal/:statementCid` +- A "View fundable-projects board" link/button going to `/portal/:statementCid` - Top 3 projects by funding progress (as a preview) -This is the primary entry point from the concept space into the cause board. +This is the primary entry point from the concept space into the fundable-projects board. ## Integration with LazyGiving (Project Detail Page) diff --git a/specs/tech/subsystems/fundingportals/README.md b/specs/tech/subsystems/fundingportals/README.md index d6ca6621..49cffbca 100644 --- a/specs/tech/subsystems/fundingportals/README.md +++ b/specs/tech/subsystems/fundingportals/README.md @@ -1,10 +1,13 @@ -# Cause boards / funding portals +# Fundable-projects boards / funding portals -A cause board (historically called a funding portal in code and older docs) is a statement-anchored view that helps donors fund projects aligned with a cause. +A **fundable-projects board** (historically a funding portal in code, then +briefly called a cause board) is a statement-anchored list of aligned work +donors might fund. **Cause board** now means the organizer publication; see +[cause-page-not-a-club.md](../../../product/cause-page-not-a-club.md). ## Current status -The implementation still uses the `fundingportal` package/path name in several places, but user-facing copy should prefer **cause board**. Treat `fundingportal` as a technical/internal name until the code paths are renamed. +The implementation still uses the `fundingportal` package/path name in several places. Treat `fundingportal` as a technical/internal name until the code paths are renamed. ## Product role @@ -17,7 +20,7 @@ The implementation still uses the `fundingportal` package/path name in several p - UI components: `ui/src/fundingportals/` - Product boundary: `specs/product/ui-domains.md` under **Aligning — cause-based funding** -- Related tests: `ui/test-plan.md` under **Cause board** +- Related tests: `ui/test-plan.md` under **Fundable-projects board** ## Naming note diff --git a/ui/src/conceptspace/pages/BrowseStatementsPage.tsx b/ui/src/conceptspace/pages/BrowseStatementsPage.tsx index 4cbc7825..2a4eacc0 100644 --- a/ui/src/conceptspace/pages/BrowseStatementsPage.tsx +++ b/ui/src/conceptspace/pages/BrowseStatementsPage.tsx @@ -81,7 +81,7 @@ export function BrowseStatementsPage() { - A statement is a plain-English belief or value that anyone can sign. The system automatically connects related statements — even when worded differently — so people who care about the same things find each other without having to coordinate. Statements are also the entry point to cause boards for aligned projects and content. + A statement is a plain-English belief or value that anyone can sign. The system automatically connects related statements — even when worded differently — so people who care about the same things find each other without having to coordinate. Statements are also the entry point to fundable-projects boards for aligned projects and content. The supporter chips below count direct signatures on each statement. Open a statement to see indirect support inferred through trusted implication sources. diff --git a/ui/src/conceptspace/pages/HomePage.tsx b/ui/src/conceptspace/pages/HomePage.tsx index a23ff288..965222b6 100644 --- a/ui/src/conceptspace/pages/HomePage.tsx +++ b/ui/src/conceptspace/pages/HomePage.tsx @@ -25,8 +25,8 @@ const gettingStartedSteps = [ { title: 'Learn about cause funding', description: - 'Cause exploration lives on Alignment, where statements connect to cause boards and aligned projects.', - cta: 'Learn about cause boards', + 'Cause exploration lives on Alignment, where statements connect to fundable-projects boards and aligned projects.', + cta: 'Learn about fundable-projects boards', to: '/docs/key-ideas/funding-portals', }, ] diff --git a/ui/src/domains/alignment/LandingPage.tsx b/ui/src/domains/alignment/LandingPage.tsx index 2000791a..7cae8618 100644 --- a/ui/src/domains/alignment/LandingPage.tsx +++ b/ui/src/domains/alignment/LandingPage.tsx @@ -19,14 +19,14 @@ const sections = [ { title: 'Or pick projects yourself', description: - "Prefer the hands-on path? Start with Explore Causes, open a cause statement, then use its cause board to see the projects aligned with it — curated by your trust network, not a gatekeeper. Fund the ones you like directly on their LazyGiving project pages.", + "Prefer the hands-on path? Start with Explore Causes, open a cause statement, then use its fundable-projects board to see the projects aligned with it — curated by your trust network, not a gatekeeper. Fund the ones you like directly on their LazyGiving project pages.", path: '/explore', cta: 'Explore causes', }, { title: "Causes don't need exact wording", description: - "A cause is just a Conceptspace statement. The implication graph connects statements that mean similar things — so a cause board pulls in projects vouched against any cause that implies yours, even when phrased differently. Organic coalitions, no coordination required.", + "A cause is just a Conceptspace statement. The implication graph connects statements that mean similar things — so a fundable-projects board pulls in projects vouched against any cause that implies yours, even when phrased differently. Organic coalitions, no coordination required.", domain: 'tally', path: '/docs/tally/statements-and-implication-graph', cta: 'More on implication', diff --git a/ui/src/domains/alignment/manifest.tsx b/ui/src/domains/alignment/manifest.tsx index 3d920d56..4d7ac64e 100644 --- a/ui/src/domains/alignment/manifest.tsx +++ b/ui/src/domains/alignment/manifest.tsx @@ -32,7 +32,7 @@ export const alignmentManifest: DomainManifest = { { label: 'Set up delegation', domain: 'lazyGiving', path: '/delegation/notes/new' }, { label: 'Open LazyGiving', domain: 'lazyGiving', path: '/' }, ], - footerText: 'Aligning helps donors fund causes through cause boards and transparent alignment attestations; delegation is managed from LazyGiving and Content Funding.', + footerText: 'Aligning helps donors fund causes through fundable-projects boards and transparent alignment attestations; delegation is managed from LazyGiving and Content Funding.', }, features: { conceptspace: false, diff --git a/ui/src/domains/civility/ContentPages.tsx b/ui/src/domains/civility/ContentPages.tsx index a827b176..d99c3e8e 100644 --- a/ui/src/domains/civility/ContentPages.tsx +++ b/ui/src/domains/civility/ContentPages.tsx @@ -275,7 +275,7 @@ export function NoninflammatoryAboutPage() { The whole thing, in one breath - "Sure — I'll put $10 a month toward making more noninflammatory content exist. I'll let my friend Andrew, who follows this stuff more closely than I do, make the actual picks." …and then never think about it again. From the other side, a creator looks at the cause board, sees a real pool of money earmarked for noninflammatory content, and thinks, "I could write some of that." Visible demand pulls supply into existence. + "Sure — I'll put $10 a month toward making more noninflammatory content exist. I'll let my friend Andrew, who follows this stuff more closely than I do, make the actual picks." …and then never think about it again. From the other side, a creator looks at the fundable-projects board, sees a real pool of money earmarked for noninflammatory content, and thinks, "I could write some of that." Visible demand pulls supply into existence. diff --git a/ui/src/domains/common-sense-majority/CsmPages.tsx b/ui/src/domains/common-sense-majority/CsmPages.tsx index 18368add..22fddb49 100644 --- a/ui/src/domains/common-sense-majority/CsmPages.tsx +++ b/ui/src/domains/common-sense-majority/CsmPages.tsx @@ -18,7 +18,7 @@ const csmProductSignposts = [ }, { title: 'Fund ongoing causes on Aligning', - description: 'Aligning hosts cause boards for causes and cause-aligned projects; CSM uses it rather than embedding its board routes here.', + description: 'Aligning hosts fundable-projects boards for causes and cause-aligned projects; CSM uses it rather than embedding its board routes here.', href: getDomainUrl('alignment', '/', { fallbackHref: '#' }), cta: 'Go to Aligning', }, diff --git a/ui/src/domains/common-sense-majority/LandingPage.tsx b/ui/src/domains/common-sense-majority/LandingPage.tsx index b11a23af..d8683916 100644 --- a/ui/src/domains/common-sense-majority/LandingPage.tsx +++ b/ui/src/domains/common-sense-majority/LandingPage.tsx @@ -60,10 +60,10 @@ export function CsmLandingPage() { eyebrow: 'Funding surface', title: 'Browse CSM-aligned causes and content', description: - 'Aligning uses the mission statement as the cause root for CSM-aligned projects, content, and organizing work. Follow the cause board to see what trusted attesters say is aligned with it.', + 'Aligning uses the mission statement as the cause root for CSM-aligned projects, content, and organizing work. Follow the fundable-projects board to see what trusted attesters say is aligned with it.', domain: 'alignment' as const, path: missionStatementAlignmentPath, - cta: 'Open the CSM cause board', + cta: 'Open the CSM fundable-projects board', }, ] diff --git a/ui/src/domains/commonality/ForOrganizationsPage.tsx b/ui/src/domains/commonality/ForOrganizationsPage.tsx index a594d0e8..892c0bc0 100644 --- a/ui/src/domains/commonality/ForOrganizationsPage.tsx +++ b/ui/src/domains/commonality/ForOrganizationsPage.tsx @@ -58,12 +58,12 @@ export function CommonalityForOrganizationsPage() { spotlights={[ { label: 'You do not have to switch anything', - text: 'The first useful step is to hardcode your org as the only trusted attester for your own cause board. Your process is unchanged — you are simply recording "this project fits our mission" in public. Everything past that point is a dial you control, not a switch someone else flips.', + text: 'The first useful step is to hardcode your org as the only trusted attester for your own fundable-projects board. Your process is unchanged — you are simply recording "this project fits our mission" in public. Everything past that point is a dial you control, not a switch someone else flips.', }, ]} heroActions={[ { label: 'Read the case for established orgs', path: '/docs/vision-and-strategy/ease-of-adoption/for-established-orgs' }, - { label: 'Browse cause boards', href: getDomainUrl('alignment', '/', { fallbackHref: '#' }), variant: 'outlined' }, + { label: 'Browse fundable-projects boards', href: getDomainUrl('alignment', '/', { fallbackHref: '#' }), variant: 'outlined' }, ]} sections={sections} > diff --git a/ui/src/domains/commonality/FounderPage.tsx b/ui/src/domains/commonality/FounderPage.tsx index 91c7f6f3..54213285 100644 --- a/ui/src/domains/commonality/FounderPage.tsx +++ b/ui/src/domains/commonality/FounderPage.tsx @@ -28,7 +28,7 @@ const sections = [ const verticals = [ ['LazyGiving', 'Individual assurance contracts for public-goods projects.', 'lazyGiving'], - ['Aligning', 'Ongoing cause funding through cause boards and alignment attestations.', 'alignment'], + ['Aligning', 'Ongoing cause funding through fundable-projects boards and alignment attestations.', 'alignment'], ['Delegation', 'Donor-delegate relationships and transparent delegate track records.', 'lazyGiving'], ['Tally', 'Statement signing and indirect support counts.', 'tally'], ['Content Funding', 'Funding contracts for content and creators.', 'content-funding'], diff --git a/ui/src/domains/content-funding/ContentPages.tsx b/ui/src/domains/content-funding/ContentPages.tsx index 1e549ed4..8b068f8b 100644 --- a/ui/src/domains/content-funding/ContentPages.tsx +++ b/ui/src/domains/content-funding/ContentPages.tsx @@ -180,7 +180,7 @@ export function ContentFundingExploreKindsPage() { {kind} - Fund creator/channel contracts for this kind of work here, or use Aligning when you want a cause board organized around a statement. + Fund creator/channel contracts for this kind of work here, or use Aligning when you want a fundable-projects board organized around a statement. ))} @@ -279,7 +279,7 @@ export function ContentFundingAboutPage() { How this site relates to the other sites - Content Funding uses LazyGiving-style escrow and payout contracts specialized for creators and content items, with channel verification handled by the platform API. It is organized around channels and contracts; for statement- or cause-centric cause boards, use Aligning. Tally is where people inspect or sign statements. + Content Funding uses LazyGiving-style escrow and payout contracts specialized for creators and content items, with channel verification handled by the platform API. It is organized around channels and contracts; for statement- or cause-centric fundable-projects boards, use Aligning. Tally is where people inspect or sign statements. diff --git a/ui/src/fundingportals/components/CauseBoard.tsx b/ui/src/fundingportals/components/CauseBoard.tsx index fd47f7a4..8c4f4d9d 100644 --- a/ui/src/fundingportals/components/CauseBoard.tsx +++ b/ui/src/fundingportals/components/CauseBoard.tsx @@ -301,7 +301,7 @@ export function CauseBoard({ } catch (err) { if (!cancelled) { console.error('Error loading cause board:', err) - setError(err instanceof Error ? err.message : 'Failed to load cause board') + setError(err instanceof Error ? err.message : 'Failed to load fundable-projects board') } } finally { if (!cancelled) { @@ -498,8 +498,8 @@ export function CauseBoard({ )} @@ -509,7 +509,7 @@ export function CauseBoard({ onChange={(_, value: 'aligned' | 'successful' | 'reimbursed' | 'failed') => setProjectTab(value) } - aria-label="Cause board project views" + aria-label="Fundable-projects board views" variant="scrollable" allowScrollButtonsMobile sx={{ diff --git a/ui/src/fundingportals/components/CauseLeaderboard.tsx b/ui/src/fundingportals/components/CauseLeaderboard.tsx index 523cb40d..c0dcf76d 100644 --- a/ui/src/fundingportals/components/CauseLeaderboard.tsx +++ b/ui/src/fundingportals/components/CauseLeaderboard.tsx @@ -216,7 +216,7 @@ export function CauseLeaderboard({ } const resolvedBack: CauseBoardNavLink = backLink ?? { - label: '← Back to Cause Board', + label: '← Back to fundable-projects board', to: `/portal/${loadCids[0] ?? ''}`, } diff --git a/ui/src/fundingportals/components/FundingPortalSummary.test.tsx b/ui/src/fundingportals/components/FundingPortalSummary.test.tsx index 6bb02cd5..377aad99 100644 --- a/ui/src/fundingportals/components/FundingPortalSummary.test.tsx +++ b/ui/src/fundingportals/components/FundingPortalSummary.test.tsx @@ -190,7 +190,7 @@ describe('FundingPortalSummary', () => { render() await waitFor(() => { - expect(screen.getByText('Failed to load cause board summary')).toBeInTheDocument() + expect(screen.getByText('Failed to load fundable-projects board summary')).toBeInTheDocument() }) }) }) @@ -201,19 +201,19 @@ describe('FundingPortalSummary', () => { vi.mocked(getAllAlignedProjectsForCause).mockResolvedValue([]) }) - it('shows "Cause Board" heading', async () => { + it('shows "Fundable Projects" heading', async () => { render() await waitFor(() => { - expect(screen.getByText('Cause Board')).toBeInTheDocument() + expect(screen.getByText('Fundable Projects')).toBeInTheDocument() }) }) - it('shows "View Cause Board" button linking to /portal/:statementCid', async () => { + it('shows "View fundable-projects board" button linking to /portal/:statementCid', async () => { render() await waitFor(() => { - const link = screen.getByRole('link', { name: 'View Cause Board' }) + const link = screen.getByRole('link', { name: 'View fundable-projects board' }) expect(link).toHaveAttribute('href', '/portal/QmMyCid') }) }) @@ -316,7 +316,7 @@ describe('FundingPortalSummary', () => { render() await waitFor(() => { - expect(screen.getByText('Cause Board')).toBeInTheDocument() + expect(screen.getByText('Fundable Projects')).toBeInTheDocument() }) expect(screen.queryByText('Top Projects by Funding Progress')).not.toBeInTheDocument() }) diff --git a/ui/src/fundingportals/components/FundingPortalSummary.tsx b/ui/src/fundingportals/components/FundingPortalSummary.tsx index c9cece1c..9aafd2f2 100644 --- a/ui/src/fundingportals/components/FundingPortalSummary.tsx +++ b/ui/src/fundingportals/components/FundingPortalSummary.tsx @@ -105,7 +105,7 @@ export function FundingPortalSummary({ } catch (err) { if (!cancelled) { console.error('Error loading cause board summary:', err) - setError(err instanceof Error ? err.message : 'Failed to load cause board summary') + setError(err instanceof Error ? err.message : 'Failed to load fundable-projects board summary') } } finally { if (!cancelled) setLoading(false) @@ -134,14 +134,14 @@ export function FundingPortalSummary({ - Cause Board + Fundable Projects diff --git a/ui/src/fundingportals/pages/ExplorerPage.test.tsx b/ui/src/fundingportals/pages/ExplorerPage.test.tsx index 63aaa16c..76dbce90 100644 --- a/ui/src/fundingportals/pages/ExplorerPage.test.tsx +++ b/ui/src/fundingportals/pages/ExplorerPage.test.tsx @@ -318,13 +318,13 @@ describe('ExplorerPage', () => { }) }) - it('shows Open Cause Board links', async () => { + it('shows Open fundable-projects board links', async () => { mockExplorerData() renderWithRouter() await waitFor(() => { - const portalLinks = screen.getAllByRole('link', { name: 'Open Cause Board' }) + const portalLinks = screen.getAllByRole('link', { name: 'Open fundable-projects board' }) expect(portalLinks.length).toBeGreaterThan(0) expect(portalLinks[0]).toHaveAttribute('href', '/portal/bafyEntry1') }) diff --git a/ui/src/fundingportals/pages/ExplorerPage.tsx b/ui/src/fundingportals/pages/ExplorerPage.tsx index 607d3d61..491252b1 100644 --- a/ui/src/fundingportals/pages/ExplorerPage.tsx +++ b/ui/src/fundingportals/pages/ExplorerPage.tsx @@ -255,7 +255,7 @@ export function ExplorerPage() { - No curated cause collection is available yet, but you can still discover causes without configuring anything: browse public cause statements on Tally, open a statement you care about, then use its cause board link to continue. + No curated cause collection is available yet, but you can still discover causes without configuring anything: browse public cause statements on Tally, open a statement you care about, then use its fundable-projects board link to continue. If you want a personalized explorer later, add a trusted explorer nudger in Tally's trust settings. @@ -371,7 +371,7 @@ export function ExplorerPage() { component={RouterLink} to={`/portal/${entry.cid}`} > - Open Cause Board + Open fundable-projects board diff --git a/ui/src/fundingportals/pages/StatementFundingPortalPage.test.tsx b/ui/src/fundingportals/pages/StatementFundingPortalPage.test.tsx index d0559358..13ba9ddb 100644 --- a/ui/src/fundingportals/pages/StatementFundingPortalPage.test.tsx +++ b/ui/src/fundingportals/pages/StatementFundingPortalPage.test.tsx @@ -203,7 +203,7 @@ describe('StatementFundingPortalPage', () => { await waitFor(() => { expect( screen.getByLabelText( - 'Refreshing your trust network. This portal is currently filtered using 2 accounts in your network. Results may still change as more are discovered.' + 'Refreshing your trust network. This fundable-projects board is currently filtered using 2 accounts in your network. Results may still change as more are discovered.' ) ).toBeInTheDocument() }) @@ -220,7 +220,7 @@ describe('StatementFundingPortalPage', () => { await waitFor(() => { expect( screen.getByLabelText( - 'Refreshing your trust network. Until any trusted accounts are found, this cause board still shows all project vouches.' + 'Refreshing your trust network. Until any trusted accounts are found, this fundable-projects board still shows all project vouches.' ) ).toBeInTheDocument() }) diff --git a/ui/src/fundingportals/pages/StatementFundingPortalPage.tsx b/ui/src/fundingportals/pages/StatementFundingPortalPage.tsx index e3d590ce..fb7ced01 100644 --- a/ui/src/fundingportals/pages/StatementFundingPortalPage.tsx +++ b/ui/src/fundingportals/pages/StatementFundingPortalPage.tsx @@ -10,7 +10,7 @@ export function StatementFundingPortalPage() { const { statementCid } = useParams<{ statementCid: string }>() if (!statementCid) { - return Missing statement for this cause board. + return Missing statement for this fundable-projects board. } return diff --git a/ui/test-plan.md b/ui/test-plan.md index 1d2e1cae..c62604d0 100644 --- a/ui/test-plan.md +++ b/ui/test-plan.md @@ -67,7 +67,7 @@ - `ConnectWalletPrompt` (4 tests — wallet prompt message, Paper wrapper, typography styling, padding/margin styles) - `utils.ts` (41 tests — getProjectStatus: succeeded/refunding/active states, bigint inputs, deadline edge cases; STATUS_COLORS/LABELS mappings; formatRelativeDeadline: ended/minutes/hours/days formatting; computeUserTokenBalance: contributions/refunds/burns aggregation, address filtering, zero/negative balance filtering, address normalization; computeContributorStats: aggregation, filtering, sorting, currency defaults) -### Cause board +### Fundable-projects board - `AttestAlignmentForm` (18 tests) - `AlignedProjectCard` (19 tests) - `computeAvailableDelegatableFunding` utility (7 tests — empty attestations, inactive notes, fetch failures, single-currency sum, multi-currency grouping, null filtering, mixed active/inactive) @@ -123,7 +123,7 @@ Maps each route surface to its Vitest and/or Playwright coverage. | `/projects` | `lazy-giving/pages/BrowseProjectsPage.test.tsx` | `lazyGiving-flow.spec.ts` | | `/projects/new` | `lazy-giving/pages/CreateProjectPage.test.tsx` | `lazyGiving-flow.spec.ts` | | `/projects/:address` | `lazy-giving/pages/ProjectDetailPage.test.tsx` | `lazyGiving-flow.spec.ts` | -| `/portal/:cid` (cause board) | `fundingportals/pages/StatementFundingPortalPage.test.tsx` | — | +| `/portal/:cid` (fundable-projects board) | `fundingportals/pages/StatementFundingPortalPage.test.tsx` | — | | `/portal/:cid/leaderboard` | `fundingportals/pages/CauseLeaderboardPage.test.tsx` | — | ### Content Funding domain routes (wrapped) From ad1976f384707cce075816cf0b3d21c6bac6dee9 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 15:58:16 -0400 Subject: [PATCH 06/13] Ship occupied-home personal dashboard and cause-board copy. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Occupied home now shows a derived fundable-projects union over signed statements, with organizer cause boards as a secondary list. UI/docs call organizer publications “cause boards,” and local start/wipe notes the bootstrap TrustSets so seed is not blocked by an empty indexer. --- causestarter/Dockerfile | 2 + causestarter/README.md | 6 +- causestarter/TODO.md | 1 + causestarter/e2e/bookmarks.spec.ts | 2 +- causestarter/e2e/connect-and-start.spec.ts | 12 +- .../src/components/RosterPublishPanel.tsx | 4 +- causestarter/src/components/YourCauses.tsx | 22 ++-- .../src/components/YourDashboard.test.tsx | 105 ++++++++++++++++++ causestarter/src/components/YourDashboard.tsx | 103 +++++++++++++++++ causestarter/src/pages/BridgeClusterPage.tsx | 2 +- .../src/pages/CauseBoardLeaderboardPage.tsx | 8 +- .../src/pages/CauseContentBoardPage.tsx | 4 +- causestarter/src/pages/CauseDetailPage.tsx | 24 ++-- causestarter/src/pages/CauseFundingPage.tsx | 4 +- causestarter/src/pages/CauseMediatorPage.tsx | 6 +- causestarter/src/pages/DocsPage.tsx | 4 +- causestarter/src/pages/HomePage.test.tsx | 39 ++++++- causestarter/src/pages/HomePage.tsx | 35 +++--- causestarter/src/pages/ProjectDetailPage.tsx | 2 +- .../pages/StatementBoardLeaderboardPage.tsx | 2 +- causestarter/src/pages/WelcomePage.tsx | 6 +- causestarter/src/shell/CauseShell.tsx | 7 +- docs/end-user/causestarter/index.md | 4 +- docs/end-user/causestarter/start-a-cause.md | 4 +- docs/end-user/causestarter/the-jobs.md | 6 +- docs/end-user/tldr-for-llms.md | 6 +- inbox.md | 2 + scripts/data.sh | 29 +++-- scripts/stop-wipe-restart.sh | 13 +-- specs/glossary.md | 3 +- specs/product/README.md | 2 +- specs/product/cause-page-not-a-club.md | 13 ++- specs/product/personal-dashboard.md | 77 +++++++++++++ workflow/local-development.md | 2 +- 34 files changed, 453 insertions(+), 108 deletions(-) create mode 100644 causestarter/src/components/YourDashboard.test.tsx create mode 100644 causestarter/src/components/YourDashboard.tsx create mode 100644 specs/product/personal-dashboard.md diff --git a/causestarter/Dockerfile b/causestarter/Dockerfile index 6669256f..4f295fda 100644 --- a/causestarter/Dockerfile +++ b/causestarter/Dockerfile @@ -43,6 +43,8 @@ COPY turbo.json ./turbo.json RUN --mount=type=cache,target=/root/.npm HUSKY=0 npm ci --legacy-peer-deps COPY sdk ./sdk +# In-app /docs bundles docs/end-user (endUserDocsPlugin). +COPY docs ./docs # Cause board reuses ui/fundingportals (+ shared, lazy-giving, content-funding, delegation). COPY ui ./ui COPY causestarter ./causestarter diff --git a/causestarter/README.md b/causestarter/README.md index f90cfd59..33e87ee4 100644 --- a/causestarter/README.md +++ b/causestarter/README.md @@ -297,8 +297,10 @@ Then **restart Grok** so MCP tools load. | `wallet-hardhat-0` … `wallet-hardhat-9` | Pick Hardhat account | | `wallet-disconnect` | Disconnect | | `home-start-cause` | Home / `/welcome` CTA → create a draft and open the cause editor | +| `home-dashboard` | Occupied home (connected wallet and/or cause boards on this device) | +| `home-dashboard-board` | Personal fundable-projects board (union over signed statements) | | `nav-start` | Desktop/mobile nav “Start” → same (creates a new draft) | -| `cause-detail-page` | Cause page root (where all editing happens; brand-new drafts show “Start a cause” coach copy here) | +| `cause-detail-page` | Cause page root (where all editing happens; brand-new drafts show “Start a cause board” coach copy here) | | `issue-guidance` | Static coach copy for what an issue is | | `cause-add-plank` | Add an issue | | `plank-text-N` | Nth issue's editable text (drafts only) | @@ -335,7 +337,7 @@ CAUSESTARTER_BASE_URL=http://localhost:5174 CAUSESTARTER_HASH_ROUTING=0 \ ### Example agent prompt > Use the browser. Open http://localhost:8090/, click Connect, choose Hardhat #0, -> click “Start a cause”, describe the cause, click Continue, then add and publish +> click “Start a cause board”, describe the cause, click Continue, then add and publish > issues on the cause page. This package stays thinner than `ui/` (no multi-domain matrix, no Privy). Product posture: diff --git a/causestarter/TODO.md b/causestarter/TODO.md index 143b9aa9..defa99ea 100644 --- a/causestarter/TODO.md +++ b/causestarter/TODO.md @@ -43,6 +43,7 @@ open **if they stay listed here**. - [ ] No Privy path / full parity with main `ui` wallet story yet. - [ ] Product: how CauseStarter ranks vs other domains in nav/marketing once it’s “the main thing.” - [x] **Copy:** two-step rename in [cause-page-not-a-club.md](/specs/product/cause-page-not-a-club.md) — fundable-projects board first, then organizer **cause board**. Identifiers and leftover “cause page” still lag. +- [ ] **Personal dashboard** ([personal-dashboard.md](/specs/product/personal-dashboard.md)): home hero = fundable-projects union over signed statements (first slice shipped). Do **not** reuse unpublished cause-board drafts. Starring / named subsets / MutableRef filters stay deferred. ## Architecture diff --git a/causestarter/e2e/bookmarks.spec.ts b/causestarter/e2e/bookmarks.spec.ts index b73f7b52..6e7214d5 100644 --- a/causestarter/e2e/bookmarks.spec.ts +++ b/causestarter/e2e/bookmarks.spec.ts @@ -130,7 +130,7 @@ test.describe('Cause bookmarks', () => { await clearBrowserStorage(page) await page.goto(appPath('/causes')) await connectHardhat(page, 1) - await expect(page.getByRole('heading', { name: 'Causes', exact: true })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Cause boards', exact: true })).toBeVisible() await expect(page.getByRole('heading', { name: title, exact: true })).toHaveCount(0, { timeout: 45_000, }) diff --git a/causestarter/e2e/connect-and-start.spec.ts b/causestarter/e2e/connect-and-start.spec.ts index bdc998ce..b8667232 100644 --- a/causestarter/e2e/connect-and-start.spec.ts +++ b/causestarter/e2e/connect-and-start.spec.ts @@ -55,6 +55,12 @@ test.describe('CauseStarter agent smoke', () => { await page.goto(appPath('/')) }) + test('occupied home shows the personal fundable-projects board after connect', async ({ page }) => { + await connectHardhat0(page) + await expect(page.getByTestId('home-dashboard-board')).toBeVisible({ timeout: 15_000 }) + await expect(page.getByRole('heading', { name: /your fundable projects/i })).toBeVisible() + }) + test('starts a cause and lands on its editable page', async ({ page }) => { await expect(page.getByTestId('wallet-connect-button')).toBeVisible() await expect(page.getByTestId('home-start-cause')).toBeVisible() @@ -67,7 +73,7 @@ test.describe('CauseStarter agent smoke', () => { // A brand-new cause has no planks, so no counts and nothing to select. await expect(page.getByTestId('cause-view-strip')).toBeHidden() await expect(page.getByTestId('cause-add-plank')).toBeVisible() - await expect(page.getByRole('heading', { name: /start a cause/i })).toBeVisible() + await expect(page.getByRole('heading', { name: /start a cause board/i })).toBeVisible() }) test('edits issues in place on the cause page', async ({ page }) => { @@ -105,11 +111,11 @@ test.describe('CauseStarter agent smoke', () => { await expect(page.getByTestId('view-count-all')).toBeVisible() }) - test('Causes page starts a cause and opens the editor while connected', async ({ page }) => { + test('Cause boards page starts a cause board and opens the editor while connected', async ({ page }) => { await connectHardhat0(page) await page.getByTestId('nav-causes').click() - await expect(page.getByRole('heading', { name: 'Causes' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Cause boards' })).toBeVisible() await page.getByTestId('causes-start-cause').click() await expect(page.getByTestId('cause-detail-page')).toBeVisible({ timeout: 10_000 }) diff --git a/causestarter/src/components/RosterPublishPanel.tsx b/causestarter/src/components/RosterPublishPanel.tsx index 10effcc7..31018f35 100644 --- a/causestarter/src/components/RosterPublishPanel.tsx +++ b/causestarter/src/components/RosterPublishPanel.tsx @@ -68,7 +68,7 @@ export function RosterPublishPanel({ onPublish, onPublishAnyway, }: RosterPublishPanelProps) { - const slugError = slug ? validateSlug(normalizeSlug(slug)) : 'Choose a URL slug for this cause.' + const slugError = slug ? validateSlug(normalizeSlug(slug)) : 'Choose a URL slug for this cause board.' const busy = checking || publishing || disabled const badgeMatches = Boolean( coherence @@ -83,7 +83,7 @@ export function RosterPublishPanel({ return ( - Publish this cause + Publish this cause board Title, summary, statement list, and mediator blurb publish together as a versioned cause board. The URL stays stable when you edit; each publish is a new version. diff --git a/causestarter/src/components/YourCauses.tsx b/causestarter/src/components/YourCauses.tsx index e5cf4cab..a5f72e76 100644 --- a/causestarter/src/components/YourCauses.tsx +++ b/causestarter/src/components/YourCauses.tsx @@ -10,11 +10,13 @@ export function YourCauses({ loading, footer, testId, + headingComponent = 'h1', }: { causes: CauseDraft[] loading: boolean footer?: ReactNode testId?: string + headingComponent?: 'h1' | 'h2' }) { const navigate = useNavigate() // "Live" is derived, not a status flag: a cause is live once any of its @@ -33,8 +35,8 @@ export function YourCauses({ }} > - - Causes + + Cause boards @@ -51,7 +53,7 @@ export function YourCauses({ }} onClick={() => navigate(createCausePath())} > - Start a cause + Start a cause board @@ -60,16 +62,16 @@ export function YourCauses({ - Loading causes… + Loading cause boards… )} {!loading && causes.length === 0 && ( - No causes on this device. Start one if you want a different combination of + No cause boards on this device. Start one if you want a different combination of statements — reuse overlapping ones so you are not starting from zero. Or open - a cause from its organizer’s link; there is no directory. + a cause board from its organizer’s link; there is no directory. )} @@ -77,11 +79,11 @@ export function YourCauses({ - Bookmarked causes + Bookmarked cause boards @@ -96,11 +98,11 @@ export function YourCauses({ - Cause drafts + Cause board drafts diff --git a/causestarter/src/components/YourDashboard.test.tsx b/causestarter/src/components/YourDashboard.test.tsx new file mode 100644 index 00000000..7b483571 --- /dev/null +++ b/causestarter/src/components/YourDashboard.test.tsx @@ -0,0 +1,105 @@ +import { cleanup, render, screen } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { YourDashboard } from './YourDashboard' + +const { useUserStatements, useAlignmentTrust } = vi.hoisted(() => ({ + useUserStatements: vi.fn(), + useAlignmentTrust: vi.fn(), +})) + +vi.mock('../hooks/useUserStatements', () => ({ + useUserStatements, +})) + +vi.mock('../hooks/useAlignmentTrust', () => ({ + useAlignmentTrust, +})) + +vi.mock('@ui/fundingportals', () => ({ + CauseBoard: ({ statementCids }: { statementCids: string[] }) => ( +
{statementCids.join(',')}
+ ), +})) + +vi.mock('@ui/shared', () => ({ + TrustNetworkRefreshIndicator: () => null, +})) + +vi.mock('./AlignmentTrustGate', () => ({ + AlignmentTrustGate: () =>
, +})) + +vi.mock('./ConnectWalletHint', () => ({ + ConnectWalletHint: ({ children }: { children: string }) =>
{children}
, +})) + +vi.mock('./StarterNetworkFilterNotice', () => ({ + StarterNetworkFilterCopy: () => null, +})) + +describe('YourDashboard', () => { + afterEach(cleanup) + + beforeEach(() => { + useAlignmentTrust.mockReturnValue({ + trustedAlignmentAttesters: new Set(), + alignmentTrustUnavailable: false, + showInitialTrustLoad: false, + trustError: null, + }) + }) + + it('asks to connect when there is no wallet', () => { + useUserStatements.mockReturnValue({ + statements: [], + loading: false, + connected: false, + error: null, + refresh: vi.fn(), + }) + render( + + + , + ) + expect(screen.getByText(/connect a wallet/i)).toBeInTheDocument() + expect(screen.queryByTestId('fundable-projects')).toBeNull() + }) + + it('shows an empty state when the wallet has not signed anything', () => { + useUserStatements.mockReturnValue({ + statements: [], + loading: false, + connected: true, + error: null, + refresh: vi.fn(), + }) + render( + + + , + ) + expect(screen.getByTestId('home-dashboard-empty')).toBeInTheDocument() + expect(screen.queryByTestId('fundable-projects')).toBeNull() + }) + + it('unions signed statement CIDs into the fundable-projects board', () => { + useUserStatements.mockReturnValue({ + statements: [ + { cid: 'bafy1' }, + { cid: 'bafy2' }, + ], + loading: false, + connected: true, + error: null, + refresh: vi.fn(), + }) + render( + + + , + ) + expect(screen.getByTestId('fundable-projects')).toHaveTextContent('bafy1,bafy2') + }) +}) diff --git a/causestarter/src/components/YourDashboard.tsx b/causestarter/src/components/YourDashboard.tsx new file mode 100644 index 00000000..bde6e2da --- /dev/null +++ b/causestarter/src/components/YourDashboard.tsx @@ -0,0 +1,103 @@ +import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material' +import { CauseBoard } from '@ui/fundingportals' +import { TrustNetworkRefreshIndicator } from '@ui/shared' +import { AlignmentTrustGate } from './AlignmentTrustGate' +import { ConnectWalletHint } from './ConnectWalletHint' +import { HeaderInfoTip } from './HeaderInfoTip' +import { StarterNetworkFilterCopy } from './StarterNetworkFilterNotice' +import { useAlignmentTrust } from '../hooks/useAlignmentTrust' +import { useUserStatements } from '../hooks/useUserStatements' + +const sectionHeadingSx = { fontWeight: 800, fontSize: { xs: '1.6rem', sm: '2rem' } } + +/** + * Personal fundable-projects board: union of work on statements this wallet signed. + * Not a cause board. Spec: specs/product/personal-dashboard.md + */ +export function YourDashboard() { + const { statements, loading, connected, error, refresh } = useUserStatements() + const { + trustedAlignmentAttesters, + alignmentTrustUnavailable, + showInitialTrustLoad, + trustError, + } = useAlignmentTrust() + const statementCids = statements.map((row) => row.cid).filter(Boolean) + + return ( + + + + Your fundable projects + + + + + Projects on statements you’ve signed. + + + {!connected && ( + + Connect a wallet to see projects on statements you have signed. + + )} + + {connected && loading && ( + + + + Loading signed statements… + + + )} + + {connected && !loading && error && ( + + {error} + + + )} + + {connected && !loading && !error && statementCids.length === 0 && ( + + Sign a statement from a cause board or a statement page. This list is the union of + work vouched as advancing those claims — it is not a private cause. + + )} + + {connected && !loading && !error && statementCids.length > 0 && ( + <> + {showInitialTrustLoad && ( + + + + )} + {(trustError || alignmentTrustUnavailable) && ( + + )} + + + Union of projects vouched as advancing any statement you signed. Alignment + attaches to a statement, never to a cause board as a club. + + + + } + /> + + )} + + ) +} diff --git a/causestarter/src/pages/BridgeClusterPage.tsx b/causestarter/src/pages/BridgeClusterPage.tsx index 90c343b2..3732ea00 100644 --- a/causestarter/src/pages/BridgeClusterPage.tsx +++ b/causestarter/src/pages/BridgeClusterPage.tsx @@ -911,7 +911,7 @@ export function BridgeClusterPage() { { const [owner, slug] = event.target.value.split('|') diff --git a/causestarter/src/pages/CauseBoardLeaderboardPage.tsx b/causestarter/src/pages/CauseBoardLeaderboardPage.tsx index c66d6edd..d7250d11 100644 --- a/causestarter/src/pages/CauseBoardLeaderboardPage.tsx +++ b/causestarter/src/pages/CauseBoardLeaderboardPage.tsx @@ -125,7 +125,7 @@ export function CauseBoardLeaderboardPage() { {loadError || 'Cause not found on this device.'} ) @@ -137,10 +137,10 @@ export function CauseBoardLeaderboardPage() { return ( - Publish a statement to see contributors across this cause. + Publish a statement to see contributors across this cause board. ) @@ -149,7 +149,7 @@ export function CauseBoardLeaderboardPage() { return ( ) } diff --git a/causestarter/src/pages/CauseContentBoardPage.tsx b/causestarter/src/pages/CauseContentBoardPage.tsx index a9ac2162..846d630c 100644 --- a/causestarter/src/pages/CauseContentBoardPage.tsx +++ b/causestarter/src/pages/CauseContentBoardPage.tsx @@ -135,7 +135,7 @@ export function CauseContentBoardPage() { {loadError || 'Cause not found on this device.'} ) @@ -153,7 +153,7 @@ export function CauseContentBoardPage() { return ( diff --git a/causestarter/src/pages/CauseDetailPage.tsx b/causestarter/src/pages/CauseDetailPage.tsx index bca2d76b..337ab7f1 100644 --- a/causestarter/src/pages/CauseDetailPage.tsx +++ b/causestarter/src/pages/CauseDetailPage.tsx @@ -499,7 +499,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { {loadError || 'Cause not found on this device.'} ) @@ -773,7 +773,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { label="Unpublished" sx={{ mb: 0.75 }} data-testid="cause-unpublished" - title="This cause exists only on this device so far. Others cannot open it until you publish." + title="This cause board exists only on this device so far. Others cannot open it until you publish." /> )} {routeRef?.versionCid && ( @@ -782,7 +782,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { color="info" label="Pinned version" sx={{ mb: 0.75 }} - title="This link is pinned to one published version. Later edits to the live cause will not change what you see here." + title="This link is pinned to one published version. Later edits to the live cause board will not change what you see here." /> )} {!isFreshDraft && ( @@ -790,7 +790,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { variant="overline" sx={{ letterSpacing: '0.14em', fontWeight: 700, color: 'primary.main', display: 'block' }} > - Cause + Cause board )} {cause.bridgeCluster && ( @@ -828,7 +828,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { component="h1" sx={{ fontWeight: 800, fontSize: { xs: '1.55rem', sm: '1.9rem' }, flex: 1, minWidth: 0 }} > - {isFreshDraft ? 'Start a cause' : displayTitle} + {isFreshDraft ? 'Start a cause board' : displayTitle} {stable && ( @@ -846,7 +846,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { } void navigator.clipboard.writeText(url).then(() => setShareCopiedOpen(true)) }} - aria-label="Share cause" + aria-label="Share cause board" sx={{ mt: 0.25, color: 'text.secondary' }} > @@ -854,7 +854,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { )} {canKeepOnDevice && ( - + { @@ -905,9 +905,9 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { )} {isFreshDraft ? ( - A cause is a set of statements people can sign one at a time — not a club - they join. Reuse published statements when they already say what you mean; - you inherit their signers and projects. Write your own when they don’t. + A cause board is a mix of statements people can sign one at a time — not a + club they join. Reuse published statements when they already say what you + mean; you inherit their signers and projects. Write your own when they don’t. Nothing is published until you review the exact text and CID below. ) : null} @@ -1031,7 +1031,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { {isEditing ? 'No statements selected yet. Start with the picker; you can reject every suggestion and write one manually.' - : 'This cause has no published statements yet.'} + : 'This cause board has no published statements yet.'} )} @@ -1242,7 +1242,7 @@ export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { if (reason === 'clickaway') return setBookmarkUndoOpen(false) }} - message="Removed from your causes" + message="Removed from your cause boards" action={( ) @@ -208,7 +208,7 @@ export function CauseFundingPage() { return ( diff --git a/causestarter/src/pages/CauseMediatorPage.tsx b/causestarter/src/pages/CauseMediatorPage.tsx index 9345e8fc..f7f48050 100644 --- a/causestarter/src/pages/CauseMediatorPage.tsx +++ b/causestarter/src/pages/CauseMediatorPage.tsx @@ -82,7 +82,7 @@ export function CauseMediatorPage() { this link. ) @@ -110,7 +110,7 @@ export function CauseMediatorPage() { For {causeTitle(cause)}. {mediator ? ' A service its organizer runs, under their own key and strategy prompt. Its suggestions reach you only if you opt in, and signing stays your choice.' - : ' This cause has no mediator service attached.'} + : ' This cause board has no mediator service attached.'} @@ -137,7 +137,7 @@ export function CauseMediatorPage() { Advanced. If you just want to write one bridge yourself, use{' '} Create a cluster - {' '}(parents are causes) or{' '} + {' '}(parents are cause boards) or{' '} write a statement-level triple {' '}— no service required. diff --git a/causestarter/src/pages/DocsPage.tsx b/causestarter/src/pages/DocsPage.tsx index c26e1e4f..9b1984e1 100644 --- a/causestarter/src/pages/DocsPage.tsx +++ b/causestarter/src/pages/DocsPage.tsx @@ -223,8 +223,8 @@ function DocsExampleTools() { const sections = [ { key: 'reference' as const, - title: 'Example causes', - description: 'Worked examples of focused causes you can learn from.', + title: 'Example cause boards', + description: 'Worked examples of focused cause boards you can learn from.', }, { key: 'thesis' as const, diff --git a/causestarter/src/pages/HomePage.test.tsx b/causestarter/src/pages/HomePage.test.tsx index 718af6dd..a680a4f6 100644 --- a/causestarter/src/pages/HomePage.test.tsx +++ b/causestarter/src/pages/HomePage.test.tsx @@ -1,10 +1,23 @@ -import { render, screen } from '@testing-library/react' +import { cleanup, render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { HomePage } from './HomePage' +const { useUserCauses, useAccount } = vi.hoisted(() => ({ + useUserCauses: vi.fn(), + useAccount: vi.fn(), +})) + vi.mock('../hooks/useUserCauses', () => ({ - useUserCauses: () => ({ causes: [], loading: false }), + useUserCauses, +})) + +vi.mock('wagmi', () => ({ + useAccount, +})) + +vi.mock('../components/YourDashboard', () => ({ + YourDashboard: () =>
, })) vi.mock('../components/YourProjects', () => ({ @@ -20,7 +33,11 @@ vi.mock('../components/YourNudgersAndNudges', () => ({ })) describe('HomePage landing', () => { + afterEach(cleanup) + it('leads with jobs, not Start → Grow → Deliver', () => { + useUserCauses.mockReturnValue({ causes: [], loading: false }) + useAccount.mockReturnValue({ isConnected: false }) render( @@ -29,8 +46,20 @@ describe('HomePage landing', () => { expect(screen.getByTestId('home-landing')).toBeInTheDocument() expect(screen.getByText(/there are enough of us/i)).toBeInTheDocument() expect(screen.getByTestId('crowd-jobs')).toBeInTheDocument() - expect(screen.getByTestId('crowd-job-money')).toBeInTheDocument() expect(screen.queryByText(/change the world/i)).toBeNull() + expect(screen.queryByTestId('home-dashboard-board')).toBeNull() }) -}) + it('puts the personal fundable-projects board first when the wallet is connected', () => { + useUserCauses.mockReturnValue({ causes: [], loading: false }) + useAccount.mockReturnValue({ isConnected: true, address: '0xabc' }) + render( + + + , + ) + expect(screen.getByTestId('home-dashboard')).toBeInTheDocument() + expect(screen.getByTestId('home-dashboard-board')).toBeInTheDocument() + expect(screen.queryByTestId('home-landing')).toBeNull() + }) +}) diff --git a/causestarter/src/pages/HomePage.tsx b/causestarter/src/pages/HomePage.tsx index 45662f9d..7d6f330f 100644 --- a/causestarter/src/pages/HomePage.tsx +++ b/causestarter/src/pages/HomePage.tsx @@ -1,5 +1,7 @@ import { CircularProgress, Stack, Typography } from '@mui/material' +import { useAccount } from 'wagmi' import { YourCauses } from '../components/YourCauses' +import { YourDashboard } from '../components/YourDashboard' import { YourNudgersAndNudges } from '../components/YourNudgersAndNudges' import { YourProjects } from '../components/YourProjects' import { YourSignedStatements } from '../components/YourSignedStatements' @@ -8,21 +10,26 @@ import { WelcomePage } from './WelcomePage' export function HomePage() { const { causes, loading } = useUserCauses() + const { isConnected } = useAccount() + const occupied = causes.length > 0 || isConnected - if (causes.length > 0) { + if (occupied) { return ( - - - - - - )} - /> + + + + + + + + )} + /> + ) } @@ -31,7 +38,7 @@ export function HomePage() { - Loading causes… + Loading cause boards… ) diff --git a/causestarter/src/pages/ProjectDetailPage.tsx b/causestarter/src/pages/ProjectDetailPage.tsx index 86f18f0c..ac199937 100644 --- a/causestarter/src/pages/ProjectDetailPage.tsx +++ b/causestarter/src/pages/ProjectDetailPage.tsx @@ -26,7 +26,7 @@ export function ProjectLeaderboardPage() { return ( ) diff --git a/causestarter/src/pages/StatementBoardLeaderboardPage.tsx b/causestarter/src/pages/StatementBoardLeaderboardPage.tsx index c5c58e50..2e3466f0 100644 --- a/causestarter/src/pages/StatementBoardLeaderboardPage.tsx +++ b/causestarter/src/pages/StatementBoardLeaderboardPage.tsx @@ -11,7 +11,7 @@ export function StatementBoardLeaderboardPage() { No statement specified. ) diff --git a/causestarter/src/pages/WelcomePage.tsx b/causestarter/src/pages/WelcomePage.tsx index b2dc3fb8..56894230 100644 --- a/causestarter/src/pages/WelcomePage.tsx +++ b/causestarter/src/pages/WelcomePage.tsx @@ -53,7 +53,7 @@ export function WelcomePage() { onClick={() => navigate(createCausePath())} sx={{ minHeight: 48, borderRadius: 999, fontWeight: 700, textTransform: 'none', px: 3 }} > - Start a cause + Start a cause board + )} Projects on statements you’ve signed. @@ -86,7 +109,13 @@ export function YourDashboard() { embedded surfaceTitle="Fundable Projects" projectLinks="local" + preview={ + preview + ? { limit: HOME_PREVIEW_LIMIT, fullPageTo: PERSONAL_DASHBOARD_PATH } + : undefined + } projectsHelp={ + preview ? undefined : ( Union of projects vouched as advancing any statement you signed. Alignment @@ -94,6 +123,7 @@ export function YourDashboard() { + ) } /> diff --git a/causestarter/src/pages/PersonalDashboardPage.tsx b/causestarter/src/pages/PersonalDashboardPage.tsx new file mode 100644 index 00000000..7edfbc80 --- /dev/null +++ b/causestarter/src/pages/PersonalDashboardPage.tsx @@ -0,0 +1,6 @@ +import { YourDashboard } from '../components/YourDashboard' + +/** Full personal fundable-projects board (not a cause board). */ +export function PersonalDashboardPage() { + return +} diff --git a/specs/product/cause-page-not-a-club.md b/specs/product/cause-page-not-a-club.md index e0f06734..ace34f6d 100644 --- a/specs/product/cause-page-not-a-club.md +++ b/specs/product/cause-page-not-a-club.md @@ -114,8 +114,9 @@ A “projects on statements I signed” surface is the likely everyday home. It does not replace organizer cause boards. You encounter a mix, sign the planks you mean, then live on your own union. That personal surface can also span **multiple causes** (several statements you signed). Spec and first slice: -[personal-dashboard.md](./personal-dashboard.md). Do not implement it as an -unpublished cause board. +[personal-dashboard.md](./personal-dashboard.md). Home shows a compact teaser; +the full list is `/dashboard`. Do not implement it as an unpublished cause +board. ## What not to do diff --git a/specs/product/personal-dashboard.md b/specs/product/personal-dashboard.md index 50808adb..f38def3e 100644 --- a/specs/product/personal-dashboard.md +++ b/specs/product/personal-dashboard.md @@ -1,7 +1,7 @@ # Personal dashboard (projects on statements you signed) -**Status: first slice specified and implemented 2026-08-24** (CauseStarter home -hero). Starring / named subsets remain out of scope. +**Status: first slice specified and implemented 2026-08-24** (home teaser + +`/dashboard` full list). Starring / named subsets remain out of scope. This is the surface reserved by [cause-page-not-a-club.md](./cause-page-not-a-club.md) under the names **dashboard** / **my board**. It does **not** reverse [ADR 0005](../decisions/0005-founder-first-verticals.md) or @@ -42,18 +42,21 @@ cause. Not a publication. ## First slice (build this) 1. CauseStarter **home**, when the wallet is connected **or** this device - already has cause boards: hero is the personal fundable-projects board. - Organizer drafts and bookmarks stay below (existing **Cause boards** - section). First-visit **Welcome** remains when disconnected and there are - no local/bookmarked boards. + already has cause boards: hero is a **teaser** of the personal + fundable-projects board (a few compact rows). Organizer drafts and + bookmarks stay below (existing **Cause boards** section). First-visit + **Welcome** remains when disconnected and there are no local/bookmarked + boards. 2. Reuse `CauseBoard` with `statementCids` = this wallet’s direct beliefs. Same starter-network / personal trust filter as other CauseStarter lists. + Home passes `preview` (compact cards, cap, no metrics/tabs). Content + contracts drop the channel-details block on the teaser. 3. Empty: not connected → connect hint. Connected, no signatures → short empty copy (sign from a cause board or statement). Do not mount the list on an empty CID set. -4. No new publication, no starring, no named subsets, no dedicated - `/dashboard` URL unless home embedding becomes too long (then a same-query - full page is fine; still not a roster). +4. Full list lives at **`/dashboard`** (same query; still not a roster). + Home “See all” is the way in. No new publication, no starring, no named + subsets. ## Later (do not build yet) diff --git a/ui/src/fundingportals/components/AlignedProjectCard.test.tsx b/ui/src/fundingportals/components/AlignedProjectCard.test.tsx index 8bbcbf58..a6b88dab 100644 --- a/ui/src/fundingportals/components/AlignedProjectCard.test.tsx +++ b/ui/src/fundingportals/components/AlignedProjectCard.test.tsx @@ -388,6 +388,32 @@ describe('AlignedProjectCard', () => { expect(screen.getByText('Fan-created')).toBeInTheDocument() }) + it('keeps the content-funding badge in compact mode but drops the channel details block', () => { + vi.mocked(useContentFundingState).mockReturnValue({ + state: {} as any, + channels: [makeChannelEntry({ canonicalChannelId: 'twitter:user:alice' })], + loading: false, + error: null, + projects: [], + contentAttestations: new Map(), + channelDisplayMetadata: new Map(), + vetoedEvents: [], + machinery: {} as any, + }) + + render( + , + ) + + expect(screen.getByText('Content Funding')).toBeInTheDocument() + expect(screen.queryByText('Channel Status')).not.toBeInTheDocument() + expect(screen.queryByText('Content Items')).not.toBeInTheDocument() + }) + it('shows channel display name for Twitter channels', () => { vi.mocked(useContentFundingState).mockReturnValue({ state: {} as any, diff --git a/ui/src/fundingportals/components/AlignedProjectCard.tsx b/ui/src/fundingportals/components/AlignedProjectCard.tsx index 0d6b0b5e..35647af0 100644 --- a/ui/src/fundingportals/components/AlignedProjectCard.tsx +++ b/ui/src/fundingportals/components/AlignedProjectCard.tsx @@ -203,11 +203,14 @@ export function AlignedProjectCard({ metadata, causeCid, projectLinks = 'lazyGiving', + compact = false, }: { project: AlignedProject metadata: ProjectMetadata | undefined causeCid?: string projectLinks?: ProjectLinkMode + /** Home teaser: smaller title, no content-contract channel block. */ + compact?: boolean }) { const status = getProjectStatus(project) const hasMinimum = BigInt(project.threshold) > 0n @@ -252,7 +255,7 @@ export function AlignedProjectCard({ {projectNav.kind === 'route' ? ( ) : ( @@ -372,12 +376,14 @@ function AlignedProjectCardDetails({ fundingProgress, progressPercent, contentFundingInfo, + compact, }: { project: AlignedProject hasMinimum: boolean fundingProgress: number progressPercent: number contentFundingInfo: ContentFundingInfo | null + compact: boolean }) { return ( @@ -393,10 +399,10 @@ function AlignedProjectCardDetails({ )} - {contentFundingInfo && } + {contentFundingInfo && !compact && } ) } diff --git a/ui/src/fundingportals/components/AlignedProjectsList.test.tsx b/ui/src/fundingportals/components/AlignedProjectsList.test.tsx index e3dc3952..ddd2a267 100644 --- a/ui/src/fundingportals/components/AlignedProjectsList.test.tsx +++ b/ui/src/fundingportals/components/AlignedProjectsList.test.tsx @@ -10,11 +10,12 @@ vi.mock('./projectMetadata', () => ({ })) -vi.mock('react-router-dom', () => ({ - Link: vi.fn(({ to, children, ...props }: any) => ( - {children} - )), -})) +vi.mock('react-router-dom', () => { + function Link({ to, children, ...props }: any) { + return {children} + } + return { Link, RouterLink: Link } +}) vi.mock('wagmi', () => ({ useAccount: vi.fn(), @@ -685,4 +686,32 @@ describe('AlignedProjectsList', () => { }) }) }) + + describe('Compact preview', () => { + it('hides sort/status chrome, caps the list, and links to the full page', async () => { + vi.mocked(getAllAlignedProjectsForCause).mockResolvedValue([ + makeProject({ projectAddress: ADDR_A, deadline: FAR_FUTURE }), + makeProject({ projectAddress: ADDR_B, deadline: String(Number(FAR_FUTURE) - 10) }), + makeProject({ projectAddress: ADDR_C, deadline: String(Number(FAR_FUTURE) - 20) }), + ]) + + render( + , + ) + + await waitFor(() => { + expect(screen.getAllByRole('link', { name: /Open project/i }).length).toBe(2) + }) + expect(screen.queryByRole('button', { name: 'Latest' })).toBeNull() + expect(screen.queryByRole('button', { name: 'Funding' })).toBeNull() + const seeAll = screen.getByTestId('aligned-projects-see-all') + expect(seeAll).toHaveAttribute('href', '/dashboard') + expect(seeAll).toHaveTextContent(/see all 3 projects/i) + }) + }) }) diff --git a/ui/src/fundingportals/components/AlignedProjectsList.tsx b/ui/src/fundingportals/components/AlignedProjectsList.tsx index 4cb2103e..d3fd123c 100644 --- a/ui/src/fundingportals/components/AlignedProjectsList.tsx +++ b/ui/src/fundingportals/components/AlignedProjectsList.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo } from 'react' +import { Link as RouterLink } from 'react-router-dom' import { useAccount } from 'wagmi' import { Box, @@ -62,6 +63,9 @@ export function AlignedProjectsList({ projectLinks = 'lazyGiving', statusFilterLock, embedded = false, + compact = false, + limit, + fullPageTo, }: { statementCid: string statementCids?: string[] @@ -72,6 +76,12 @@ export function AlignedProjectsList({ statusFilterLock?: Exclude /** Flatten heading/paper chrome when nested in the cause-board card. */ embedded?: boolean + /** Teaser density: hide sort/status chrome and shrink cards. */ + compact?: boolean + /** Cap how many cards to show (home preview). */ + limit?: number + /** In-app path for “See all” when {@link compact} or {@link limit} is set. */ + fullPageTo?: string }) { const cids = resolveStatementCids(statementCid, statementCids) const cidsKey = cids.join('\0') @@ -226,6 +236,8 @@ export function AlignedProjectsList({ return Number(b.deadline) - Number(a.deadline) } }) + const visible = limit != null ? sorted.slice(0, limit) : sorted + const hiddenCount = limit != null ? Math.max(0, sorted.length - limit) : 0 if (loading) { return ( @@ -257,6 +269,7 @@ export function AlignedProjectsList({ /> )} + {!compact && ( + )} {sorted.length === 0 ? ( @@ -332,16 +346,30 @@ export function AlignedProjectsList({ )} ) : ( - - {sorted.map((project) => ( + + {visible.map((project) => ( ))} + {fullPageTo && (compact || hiddenCount > 0) && ( + + )} )} diff --git a/ui/src/fundingportals/components/CauseBoard.tsx b/ui/src/fundingportals/components/CauseBoard.tsx index 8c4f4d9d..5e9c5a83 100644 --- a/ui/src/fundingportals/components/CauseBoard.tsx +++ b/ui/src/fundingportals/components/CauseBoard.tsx @@ -89,6 +89,11 @@ export interface CauseBoardProps { * matches plank counts and the starter-network notice. */ trustedAlignmentAttesters?: Iterable + /** + * Home teaser: skip metrics/tabs, cap the aligned list, compact cards. + * Full board lives at {@link preview.fullPageTo}. + */ + preview?: { limit: number; fullPageTo: string } } function defaultNavLinks(statementCid: string | undefined): CauseBoardNavLink[] { @@ -141,6 +146,7 @@ export function CauseBoard({ projectsHelp, projectLinks = 'lazyGiving', trustedAlignmentAttesters, + preview, }: CauseBoardProps) { const cids = useMemo( () => resolveStatementCids(statementCid, statementCids), @@ -225,6 +231,11 @@ export function CauseBoard({ : undefined async function load() { + if (preview) { + setLoading(false) + setError(null) + return + } if (loadCids.length === 0) { setLoading(false) setError('No statement specified.') @@ -319,6 +330,7 @@ export function CauseBoard({ machinery, cidsKey, embedded, + preview, trustedAttestersKey, trustedSetKey, alignmentOverrideKey, @@ -327,6 +339,24 @@ export function CauseBoard({ contentTrustKey, ]) + if (preview) { + return ( + + + + ) + } + if (loading) { return ( From 06042716a9fb495764a1f40f6b0fe9b7eb9383d7 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 16:51:07 -0400 Subject: [PATCH 08/13] Fold CauseStarter into ui and actually resume project folds. CauseStarter is now VITE_DOMAIN=causestarter in the ui package (CauseShell kept). getProjectFold returns the accumulator; loadProjectWithCache passes it and blockNumber_gte instead of replaying every event on reload. --- README.md | 2 +- causestarter/Dockerfile | 4 +- causestarter/README.md | 12 +- causestarter/TODO.md | 2 +- causestarter/package.json | 8 +- causestarter/src/App.tsx | 146 ----------- causestarter/src/main.tsx | 230 ------------------ docker-compose.yml | 8 +- package.json | 8 +- scripts/publish-ui-to-ipfs.mjs | 35 +-- sdk/src/subsystems/lazy-giving/queries.ts | 55 +++-- specs/product/ui-domains.md | 2 +- specs/tech/ui-domains.md | 13 +- ui/package.json | 1 + ui/scripts/build-domains.mjs | 2 +- ui/src/App.tsx | 25 +- ui/src/causestarter/App.tsx | 118 +++++++++ .../components/AlignmentTrustGate.test.tsx | 0 .../components/AlignmentTrustGate.tsx | 0 .../components/BridgeClusterAssist.test.tsx | 0 .../components/BridgeClusterAssist.tsx | 0 .../components/CauseBridgesSection.test.tsx | 0 .../components/CauseBridgesSection.tsx | 0 .../causestarter}/components/CauseCard.tsx | 0 .../CauseConjunctionEarmark.test.tsx | 0 .../components/CauseConjunctionEarmark.tsx | 0 .../components/CauseFundingSummary.test.tsx | 0 .../components/CauseFundingSummary.tsx | 0 .../components/CauseMediatorCard.test.tsx | 0 .../components/CauseMediatorCard.tsx | 0 .../components/CauseViewStrip.test.tsx | 0 .../components/CauseViewStrip.tsx | 0 .../components/ClusterMediatorOptIn.test.tsx | 0 .../components/ClusterMediatorOptIn.tsx | 0 .../components/ConnectWalletHint.test.tsx | 0 .../components/ConnectWalletHint.tsx | 0 .../causestarter}/components/CrowdJobs.tsx | 0 .../components/HeaderInfoTip.tsx | 0 .../src/causestarter}/components/JobTip.tsx | 0 .../components/MediatorEditor.tsx | 0 .../components/MonthlyPledgeSignal.test.tsx | 0 .../components/MonthlyPledgeSignal.tsx | 0 .../components/OrganizerIdentity.test.tsx | 0 .../components/OrganizerIdentity.tsx | 0 .../components/PlankRow.test.tsx | 0 .../src/causestarter}/components/PlankRow.tsx | 0 .../components/ProjectBookmarkButton.tsx | 0 .../causestarter}/components/ProjectCard.tsx | 0 .../components/RosterHistory.tsx | 0 .../components/RosterPublishPanel.test.tsx | 0 .../components/RosterPublishPanel.tsx | 0 .../components/SafetyRejectionDialog.tsx | 0 .../components/SelectedPlankSupport.test.tsx | 5 +- .../components/SelectedPlankSupport.tsx | 0 .../StarterNetworkFilterNotice.test.tsx | 0 .../components/StarterNetworkFilterNotice.tsx | 0 .../components/StatementPicker.test.tsx | 0 .../components/StatementPicker.tsx | 0 .../components/StatementSupportStats.tsx | 0 .../components/SupportButton.test.tsx | 0 .../components/SupportButton.tsx | 0 .../src/causestarter}/components/ToolCard.tsx | 0 .../causestarter}/components/WalletButton.tsx | 0 .../causestarter}/components/YourCauses.tsx | 0 .../components/YourDashboard.test.tsx | 0 .../components/YourDashboard.tsx | 0 .../components/YourNudgersAndNudges.test.tsx | 0 .../components/YourNudgersAndNudges.tsx | 0 .../components/YourProjects.test.tsx | 0 .../causestarter}/components/YourProjects.tsx | 0 .../components/YourSignedStatements.test.tsx | 0 .../components/YourSignedStatements.tsx | 0 .../hooks/useAlignmentTrust.test.ts | 0 .../causestarter}/hooks/useAlignmentTrust.ts | 0 .../hooks/useCauseMonthlyPledges.test.tsx | 0 .../hooks/useCauseMonthlyPledges.ts | 0 .../hooks/useCauseProjects.test.tsx | 0 .../causestarter}/hooks/useCauseProjects.ts | 0 .../causestarter}/hooks/useToolExamples.ts | 0 .../src/causestarter}/hooks/useUserCauses.ts | 0 .../causestarter}/hooks/useUserProjects.ts | 0 .../hooks/useUserStatements.test.ts | 0 .../causestarter}/hooks/useUserStatements.ts | 0 .../hooks/useViewCounts.test.tsx | 0 .../src/causestarter}/hooks/useViewCounts.ts | 0 .../src => ui/src/causestarter}/index.css | 0 .../causestarter}/lib/alignedContent.test.ts | 0 .../src/causestarter}/lib/alignedContent.ts | 0 .../lib/believerSetsCache.test.ts | 0 .../causestarter}/lib/believerSetsCache.ts | 0 .../lib/bridgeAssistBrief.test.ts | 0 .../causestarter}/lib/bridgeAssistBrief.ts | 0 .../causestarter}/lib/bridgeCluster.test.ts | 0 .../src/causestarter}/lib/bridgeCluster.ts | 0 .../causestarter}/lib/bridgeNudges.test.ts | 0 .../src/causestarter}/lib/bridgeNudges.ts | 0 .../src/causestarter}/lib/bridgeStore.test.ts | 0 .../src/causestarter}/lib/bridgeStore.ts | 0 .../causestarter}/lib/bridgeTriple.test.ts | 0 .../src/causestarter}/lib/bridgeTriple.ts | 0 .../causestarter}/lib/causeAssistClient.ts | 0 .../causestarter}/lib/causeBookmarks.test.ts | 0 .../src/causestarter}/lib/causeBookmarks.ts | 0 .../lib/causeCompatibility.test.ts | 0 .../src/causestarter}/lib/causeRoster.test.ts | 0 .../src/causestarter}/lib/causeRoster.ts | 0 .../src/causestarter}/lib/causeStore.test.ts | 0 .../src/causestarter}/lib/causeStore.ts | 0 .../src/causestarter}/lib/concurrency.test.ts | 0 .../src/causestarter}/lib/concurrency.ts | 0 .../src/causestarter}/lib/domainUrls.ts | 0 .../src/causestarter}/lib/exampleBank.test.ts | 0 .../src/causestarter}/lib/exampleBank.ts | 0 .../lib/fixtures/causeCompatibility.ts | 0 .../causestarter}/lib/hardhatAccounts.test.ts | 0 .../src/causestarter}/lib/hardhatAccounts.ts | 0 .../lib/hardhatLocalConnector.ts | 0 .../lib/implicationAttesterClient.test.ts | 0 .../lib/implicationAttesterClient.ts | 0 .../src/causestarter}/lib/jobs.test.ts | 0 .../src => ui/src/causestarter}/lib/jobs.ts | 0 .../lib/nearDuplicatePlanks.test.ts | 0 .../causestarter}/lib/nearDuplicatePlanks.ts | 0 .../lib/projectBookmarks.test.ts | 0 .../src/causestarter}/lib/projectBookmarks.ts | 0 .../causestarter}/lib/publishCombinator.ts | 0 .../src/causestarter}/lib/publishPlank.ts | 0 .../src/causestarter}/lib/runtimeConfig.ts | 0 .../causestarter}/lib/statementPicker.test.ts | 0 .../src/causestarter}/lib/statementPicker.ts | 0 .../causestarter}/lib/toolExamples.test.ts | 0 .../src/causestarter}/lib/toolExamples.ts | 0 .../src/causestarter}/lib/tools.test.ts | 0 .../src => ui/src/causestarter}/lib/tools.ts | 0 .../src/causestarter}/lib/useMachinery.ts | 0 .../src/causestarter}/lib/useWriteClients.ts | 0 .../causestarter}/lib/userProjects.test.ts | 0 .../src/causestarter}/lib/userProjects.ts | 8 +- .../causestarter}/pages/BridgeClusterPage.tsx | 0 .../pages/BridgeTriplePage.test.tsx | 0 .../causestarter}/pages/BridgeTriplePage.tsx | 0 .../pages/CauseBoardLeaderboardPage.tsx | 0 .../pages/CauseContentBoardPage.tsx | 0 .../causestarter}/pages/CauseDetailPage.tsx | 4 + .../causestarter}/pages/CauseFundingPage.tsx | 0 .../causestarter}/pages/CauseMediatorPage.tsx | 0 .../src/causestarter}/pages/CausesPage.tsx | 0 .../pages/ContentFundingPages.tsx | 0 .../causestarter}/pages/CreateProjectPage.tsx | 0 .../causestarter}/pages/DelegationPages.tsx | 0 .../src/causestarter}/pages/DocsPage.test.tsx | 5 +- .../src/causestarter}/pages/DocsPage.tsx | 0 .../src/causestarter}/pages/HomePage.test.tsx | 0 .../src/causestarter}/pages/HomePage.tsx | 0 .../src/causestarter}/pages/NotFoundPage.tsx | 0 .../pages/PersonalDashboardPage.tsx | 0 .../causestarter}/pages/ProjectDetailPage.tsx | 0 .../src/causestarter}/pages/SettingsPage.tsx | 0 .../pages/StartBridgeRedirect.test.ts | 0 .../pages/StartBridgeRedirect.tsx | 0 .../pages/StartCauseRedirect.tsx | 0 .../pages/StatementBoardLeaderboardPage.tsx | 0 .../pages/StatementBoardRedirect.tsx | 0 .../src/causestarter}/pages/StatementPage.tsx | 0 .../pages/StatementsPage.test.tsx | 0 .../causestarter}/pages/StatementsPage.tsx | 0 .../src/causestarter}/pages/WelcomePage.tsx | 0 .../src/causestarter}/shell/CauseShell.tsx | 0 .../src => ui/src/causestarter}/test/setup.ts | 0 .../causestarter}/virtual-end-user-docs.d.ts | 0 .../src => ui/src/causestarter}/wagmi.ts | 0 ui/src/domains/CrossDomainSmoke.test.tsx | 6 +- ui/src/domains/CrossLinkCrawler.test.tsx | 13 +- ui/src/domains/DomainDeepLinksSmoke.test.tsx | 5 + .../domains/ExternalLinksAllowlist.test.tsx | 1 + ui/src/domains/causestarter/manifest.tsx | 92 +++++++ ui/src/domains/index.ts | 6 +- ui/src/domains/lazyRoute.tsx | 4 +- ui/src/domains/types.ts | 2 + ui/src/shared/config/runtimeConfig.ts | 2 + ui/src/shared/hooks/useCachedProject.test.ts | 56 +++-- ui/src/shared/hooks/useCachedProject.ts | 62 ++--- ui/src/shared/index.ts | 2 +- ui/src/shared/routing/domainUrls.ts | 4 + ui/src/test/setup.ts | 1 + ui/tsconfig.app.json | 5 +- ui/vite.config.ts | 13 + ui/vitest.config.ts | 5 + 188 files changed, 454 insertions(+), 518 deletions(-) delete mode 100644 causestarter/src/App.tsx delete mode 100644 causestarter/src/main.tsx create mode 100644 ui/src/causestarter/App.tsx rename {causestarter/src => ui/src/causestarter}/components/AlignmentTrustGate.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/AlignmentTrustGate.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/BridgeClusterAssist.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/BridgeClusterAssist.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseBridgesSection.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseBridgesSection.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseCard.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseConjunctionEarmark.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseConjunctionEarmark.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseFundingSummary.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseFundingSummary.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseMediatorCard.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseMediatorCard.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseViewStrip.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CauseViewStrip.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/ClusterMediatorOptIn.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/ClusterMediatorOptIn.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/ConnectWalletHint.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/ConnectWalletHint.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/CrowdJobs.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/HeaderInfoTip.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/JobTip.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/MediatorEditor.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/MonthlyPledgeSignal.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/MonthlyPledgeSignal.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/OrganizerIdentity.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/OrganizerIdentity.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/PlankRow.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/PlankRow.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/ProjectBookmarkButton.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/ProjectCard.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/RosterHistory.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/RosterPublishPanel.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/RosterPublishPanel.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/SafetyRejectionDialog.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/SelectedPlankSupport.test.tsx (95%) rename {causestarter/src => ui/src/causestarter}/components/SelectedPlankSupport.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/StarterNetworkFilterNotice.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/StarterNetworkFilterNotice.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/StatementPicker.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/StatementPicker.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/StatementSupportStats.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/SupportButton.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/SupportButton.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/ToolCard.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/WalletButton.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourCauses.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourDashboard.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourDashboard.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourNudgersAndNudges.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourNudgersAndNudges.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourProjects.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourProjects.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourSignedStatements.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/components/YourSignedStatements.tsx (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useAlignmentTrust.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useAlignmentTrust.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useCauseMonthlyPledges.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useCauseMonthlyPledges.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useCauseProjects.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useCauseProjects.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useToolExamples.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useUserCauses.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useUserProjects.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useUserStatements.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useUserStatements.ts (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useViewCounts.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/hooks/useViewCounts.ts (100%) rename {causestarter/src => ui/src/causestarter}/index.css (100%) rename {causestarter/src => ui/src/causestarter}/lib/alignedContent.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/alignedContent.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/believerSetsCache.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/believerSetsCache.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeAssistBrief.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeAssistBrief.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeCluster.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeCluster.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeNudges.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeNudges.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeStore.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeStore.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeTriple.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/bridgeTriple.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeAssistClient.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeBookmarks.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeBookmarks.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeCompatibility.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeRoster.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeRoster.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeStore.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/causeStore.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/concurrency.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/concurrency.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/domainUrls.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/exampleBank.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/exampleBank.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/fixtures/causeCompatibility.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/hardhatAccounts.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/hardhatAccounts.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/hardhatLocalConnector.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/implicationAttesterClient.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/implicationAttesterClient.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/jobs.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/jobs.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/nearDuplicatePlanks.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/nearDuplicatePlanks.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/projectBookmarks.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/projectBookmarks.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/publishCombinator.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/publishPlank.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/runtimeConfig.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/statementPicker.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/statementPicker.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/toolExamples.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/toolExamples.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/tools.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/tools.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/useMachinery.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/useWriteClients.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/userProjects.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/lib/userProjects.ts (89%) rename {causestarter/src => ui/src/causestarter}/pages/BridgeClusterPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/BridgeTriplePage.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/BridgeTriplePage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/CauseBoardLeaderboardPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/CauseContentBoardPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/CauseDetailPage.tsx (99%) rename {causestarter/src => ui/src/causestarter}/pages/CauseFundingPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/CauseMediatorPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/CausesPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/ContentFundingPages.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/CreateProjectPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/DelegationPages.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/DocsPage.test.tsx (90%) rename {causestarter/src => ui/src/causestarter}/pages/DocsPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/HomePage.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/HomePage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/NotFoundPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/PersonalDashboardPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/ProjectDetailPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/SettingsPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/StartBridgeRedirect.test.ts (100%) rename {causestarter/src => ui/src/causestarter}/pages/StartBridgeRedirect.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/StartCauseRedirect.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/StatementBoardLeaderboardPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/StatementBoardRedirect.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/StatementPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/StatementsPage.test.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/StatementsPage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/pages/WelcomePage.tsx (100%) rename {causestarter/src => ui/src/causestarter}/shell/CauseShell.tsx (100%) rename {causestarter/src => ui/src/causestarter}/test/setup.ts (100%) rename {causestarter/src => ui/src/causestarter}/virtual-end-user-docs.d.ts (100%) rename {causestarter/src => ui/src/causestarter}/wagmi.ts (100%) create mode 100644 ui/src/domains/causestarter/manifest.tsx diff --git a/README.md b/README.md index 9442cb77..6345d9f2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Commonality is a system for decentralized crowdfunding of public goods: people c - [High-level project status](./workflow/project-status.md) AKA what milestone are we currently heading for: never deployed to mainnet yet, just did first testnet deployment, see also [MVP](./specs/product/mvp.md) - Product boundaries between the eight sites: [product UI domains](./specs/product/ui-domains.md). - Technical domain composition and live route ownership: [technical UI domains](./specs/tech/ui-domains.md) and the actual domain manifests under [`ui/src/domains/`](./ui/src/domains/). - - **CauseStarter** ([`causestarter/`](./causestarter/), backlog [`causestarter/TODO.md`](./causestarter/TODO.md)): founder-first core domain (eventual primary entry; Tally / LazyGiving / etc. de-emphasized as tools). Included in `./scripts/services.sh --start` and `./scripts/deploy-causestarter.sh`. Local start currently publishes **only** the CauseStarter IPFS bundle (`LOCAL_UI_DOMAINS`; restore all UIs with `LOCAL_UI_DOMAINS=all` — see [local development](./workflow/local-development.md)). LLM helpers: [`cause-assist/`](./cause-assist/). + - **CauseStarter** (SPA in [`ui/src/causestarter/`](./ui/src/causestarter/), `VITE_DOMAIN=causestarter`; glue/backlog [`causestarter/`](./causestarter/), [`causestarter/TODO.md`](./causestarter/TODO.md)): founder-first core domain (eventual primary entry; Tally / LazyGiving / etc. de-emphasized as tools). Included in `./scripts/services.sh --start` and `./scripts/deploy-causestarter.sh`. Local start currently publishes **only** the CauseStarter IPFS bundle (`LOCAL_UI_DOMAINS`; restore all UIs with `LOCAL_UI_DOMAINS=all` — see [local development](./workflow/local-development.md)). LLM helpers: [`cause-assist/`](./cause-assist/). - [Glossary](./specs/glossary.md) — the project's ubiquitous language. Read before naming anything; it also lists the known terminology drift. - [Architecture Decision Records](./specs/decisions/README.md) — immutable log of *why* consequential decisions were made (and what was rejected). Grep before reversing something that looks wrong. - [Role-based guidance](./workflow/roles/README.md) on what docs to read: diff --git a/causestarter/Dockerfile b/causestarter/Dockerfile index 4f295fda..318b1c62 100644 --- a/causestarter/Dockerfile +++ b/causestarter/Dockerfile @@ -58,13 +58,13 @@ ENV VITE_WALLETCONNECT_PROJECT_ID=${VITE_WALLETCONNECT_PROJECT_ID} ENV HUSKY=0 RUN npm run build --workspace=@commonality/sdk \ - && npm run build --workspace=causestarter + && VITE_DOMAIN=causestarter npm run build --workspace=ui # --------------------------------------------------------------------------- FROM nginx:1.27-alpine AS runtime COPY causestarter/nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=builder /workspace/causestarter/dist /usr/share/nginx/html +COPY --from=builder /workspace/ui/dist/causestarter /usr/share/nginx/html COPY causestarter/docker-entrypoint.d/30-indexer-upstream.sh /docker-entrypoint.d/30-indexer-upstream.sh COPY causestarter/docker-entrypoint.d/40-causestarter-config.sh /docker-entrypoint.d/40-causestarter-config.sh RUN chmod +x /docker-entrypoint.d/30-indexer-upstream.sh \ diff --git a/causestarter/README.md b/causestarter/README.md index 2eebe236..ce828414 100644 --- a/causestarter/README.md +++ b/causestarter/README.md @@ -1,9 +1,12 @@ # CauseStarter -Organizer-first reference lens for the Commonality substrate. Where the main -[`ui/`](../ui/) package is organized as multi-domain product sites (Commonality, -Civility, CSM, LazyGiving, …), **CauseStarter** is a single app organized around -the cause-starter job: +Organizer-first reference lens for the Commonality substrate. The SPA source +lives in [`ui/src/causestarter/`](../ui/src/causestarter/) and is built as +`VITE_DOMAIN=causestarter` (`npm run causestarter:dev` → Vite on **:5174**). +This directory keeps Docker/nginx, Playwright, and the product backlog. + +Where the other `ui/` domains are focused tool sites (Commonality, Civility, +CSM, LazyGiving, …), **CauseStarter** is organized around the cause-starter job: 1. **Organize a cause** — retrieve, review, and select the independent, signable statements it is made of 2. **Enroll people** — supporters (signers), volunteers, and collaborators @@ -54,6 +57,7 @@ as main `CreateStatementForm`), not browser → Kubo API upload. ```bash npm run causestarter:dev + # VITE_DOMAIN=causestarter in the ui package, port 5174 ``` Or from this package: `npm run dev`. diff --git a/causestarter/TODO.md b/causestarter/TODO.md index defa99ea..c33f8593 100644 --- a/causestarter/TODO.md +++ b/causestarter/TODO.md @@ -47,7 +47,7 @@ open **if they stay listed here**. ## Architecture -- [ ] Long-term: fold CauseStarter into the real domain/shell model (shared machinery, runtime config, domain manifests) as we make it the primary surface; avoid unbounded parallel app growth under a second package forever. +- [x] Fold CauseStarter into the real domain/shell model (`VITE_DOMAIN=causestarter` in `ui/`). Docker/e2e glue still lives in this directory. - [ ] Optional: extract Hardhat local connectors for reuse by main `ui` local DX. ## Local stack notes diff --git a/causestarter/package.json b/causestarter/package.json index d4d476e0..819d458b 100644 --- a/causestarter/package.json +++ b/causestarter/package.json @@ -5,11 +5,11 @@ "type": "module", "scripts": { "predev": "npm run build --workspace=@commonality/sdk", - "dev": "../node_modules/.bin/vite", - "typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit", - "build": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit && ../node_modules/.bin/vite build", + "dev": "npm run dev:causestarter --workspace=ui", + "typecheck": "npm run typecheck --workspace=ui", + "build": "VITE_DOMAIN=causestarter npm run build --workspace=ui", "lint": "eslint .", - "test": "vitest run", + "test": "npm run test:vitest --workspace=ui -- src/causestarter", "test:watch": "vitest", "test:e2e": "playwright test --config=playwright.config.ts", "test:e2e:headed": "playwright test --config=playwright.config.ts --headed", diff --git a/causestarter/src/App.tsx b/causestarter/src/App.tsx deleted file mode 100644 index a89b8bbc..00000000 --- a/causestarter/src/App.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { lazy, Suspense } from 'react' -import { BrowserRouter, HashRouter, Navigate, Route, Routes } from 'react-router-dom' -import { CircularProgress, Stack } from '@mui/material' -import { CauseShell } from './shell/CauseShell' -import { HomePage } from './pages/HomePage' -import { PersonalDashboardPage } from './pages/PersonalDashboardPage' -import { WelcomePage } from './pages/WelcomePage' -import { StartCauseRedirect } from './pages/StartCauseRedirect' -import { StartBridgeRedirect } from './pages/StartBridgeRedirect' -import { BridgeClusterPage } from './pages/BridgeClusterPage' -import { BridgeTriplePage } from './pages/BridgeTriplePage' -import { CausesPage } from './pages/CausesPage' -import { CauseDetailPage } from './pages/CauseDetailPage' -import { CauseMediatorPage } from './pages/CauseMediatorPage' -import { CauseBoardLeaderboardPage } from './pages/CauseBoardLeaderboardPage' -import { StatementBoardLeaderboardPage } from './pages/StatementBoardLeaderboardPage' -import { StatementBoardRedirect } from './pages/StatementBoardRedirect' -import { StatementPage } from './pages/StatementPage' -import { StatementsPage } from './pages/StatementsPage' -import { SettingsPage } from './pages/SettingsPage' -import { ProjectDetailPage, ProjectLeaderboardPage } from './pages/ProjectDetailPage' -import { CreateProjectPage } from './pages/CreateProjectPage' -import { CauseContentBoardPage } from './pages/CauseContentBoardPage' -import { CauseFundingPage } from './pages/CauseFundingPage' -import { - ContentFundingAboutPage, - ContentFundingBrowsePage, - ContentFundingChannelPage, - ContentFundingContractPage, - ContentFundingCreateContractPage, - ContentFundingCreatorDashboardPage, - ContentFundingCreatorsPage, - ContentFundingExploreKindsPage, - ContentFundingLandingPage, - ContentFundingMaterializeFutureContentPage, - ContentFundingStartContractPage, -} from './pages/ContentFundingPages' -import { - DelegateProfilePage, - DepositPage, - MyNotesPage, - NoteDetailPage, -} from './pages/DelegationPages' -import { NotFoundPage } from './pages/NotFoundPage' - -const DocsPage = lazy(() => import('./pages/DocsPage').then((mod) => ({ default: mod.DocsPage }))) - -function DocsFallback() { - return ( - - - - ) -} - -function isHashRouting(): boolean { - return import.meta.env.MODE === 'ipfs' || import.meta.env.VITE_HASH_ROUTING === 'true' -} - -export default function App() { - const Router = isHashRouting() ? HashRouter : BrowserRouter - - return ( - - - - } /> - } /> - } /> - {/* No intermediate form — creates a draft and opens the editor. */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* Local drafts use a UUID. Published causes use - /cause/:owner/:slug[@versionCid] — stable id + optional pin. - See docs/founder/shaping-your-cause-statements.md § roster. */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* No browse or search route by design: a cause is reached by its own - link, never by a directory we rank. See ADR 0005 and - specs/product/ui-operator-posture.md. */} - } /> - {/* Old standalone board URL: the dashboard now lives on the statement. */} - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - }> - - - )} - /> - }> - - - )} - /> - } /> - } /> - } /> - - - - ) -} diff --git a/causestarter/src/main.tsx b/causestarter/src/main.tsx deleted file mode 100644 index 5e7b53c2..00000000 --- a/causestarter/src/main.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { StrictMode, useCallback, useEffect, useMemo, useState } from 'react' -import { createRoot } from 'react-dom/client' -import { Box, CssBaseline, ThemeProvider, createTheme } from '@mui/material' -import type { PaletteMode, Theme } from '@mui/material' -import { WagmiProvider } from 'wagmi' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { ConnectKitProvider } from 'connectkit' -import { config, createMockConfig } from './wagmi' -import { - getRuntimeConfigValue as getUiSharedRuntimeConfigValue, - loadRuntimeConfig as loadUiSharedRuntimeConfig, -} from '@ui/shared' -import { getRuntimeConfig, getRuntimeConfigValue, loadRuntimeConfig } from './lib/runtimeConfig' -import App from './App' -import './index.css' - -const queryClient = new QueryClient() -const TOUCH_TARGET_MIN = 44 - -function getSystemColorMode(): PaletteMode { - if (typeof window === 'undefined') return 'light' - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' -} - -function createAppTheme(mode: PaletteMode): Theme { - return createTheme({ - palette: { - mode, - primary: { - main: mode === 'light' ? '#0f766e' : '#2dd4bf', - light: mode === 'light' ? '#14b8a6' : '#5eead4', - dark: mode === 'light' ? '#115e59' : '#0f766e', - contrastText: mode === 'light' ? '#ffffff' : '#042f2e', - }, - secondary: { - main: mode === 'light' ? '#c2410c' : '#fb923c', - }, - background: { - default: mode === 'light' ? '#fffaf4' : '#0a1018', - paper: mode === 'light' ? '#ffffff' : '#121a24', - }, - }, - typography: { - fontFamily: "'Avenir Next', 'Segoe UI', system-ui, sans-serif", - }, - shape: { - borderRadius: 12, - }, - components: { - MuiButton: { - styleOverrides: { - sizeSmall: { - '@media (pointer: coarse)': { - minHeight: TOUCH_TARGET_MIN, - }, - }, - }, - }, - MuiIconButton: { - styleOverrides: { - sizeSmall: { - '@media (pointer: coarse)': { - minWidth: TOUCH_TARGET_MIN, - minHeight: TOUCH_TARGET_MIN, - }, - }, - }, - }, - MuiCssBaseline: { - styleOverrides: (themeParam) => ({ - body: { - color: themeParam.palette.text.primary, - background: themeParam.palette.mode === 'light' - ? 'radial-gradient(circle at top, rgba(20,184,166,0.14), transparent 40%), linear-gradient(180deg, #fff7ed 0%, #fffaf4 45%, #f0fdfa 100%)' - : 'radial-gradient(circle at top, rgba(45,212,191,0.12), transparent 40%), linear-gradient(180deg, #071018 0%, #0a1018 50%, #111827 100%)', - }, - }), - }, - }, - }) -} - -declare global { - interface Window { - _setupTestWallet: typeof createMockConfig - } -} - -export function Root() { - const [mode, setMode] = useState(getSystemColorMode) - const [wagmiConfig, setWagmiConfig] = useState(config) - const theme = useMemo(() => createAppTheme(mode), [mode]) - - useEffect(() => { - document.documentElement.dataset.colorMode = mode - }, [mode]) - - useEffect(() => { - const media = window.matchMedia('(prefers-color-scheme: dark)') - const applySystemMode = () => setMode(media.matches ? 'dark' : 'light') - applySystemMode() - media.addEventListener('change', applySystemMode) - return () => media.removeEventListener('change', applySystemMode) - }, []) - - const setupTestWallet = useCallback( - (...args: Parameters) => { - const next = createMockConfig(...args) - setWagmiConfig(next) - return next - }, - [], - ) - - if (typeof window !== 'undefined') { - window._setupTestWallet = setupTestWallet - } - - return ( - - - - - - - - - - - - - ) -} - -// Cause board reuses ui/fundingportals, which reads contracts/URLs via -// ui/shared runtime config (separate module store from CauseStarter's own -// lib/runtimeConfig). Both must load the same config.json and stay aligned on -// shared keys until those stores are unified. Failure of either load fails boot. -// Keys cover useMachinery contract/RPC/IPFS surface, payment currency, and -// domain URLs used by embedded @ui/* board/project pages. -const SHARED_RUNTIME_KEYS = [ - 'VITE_EVENT_CACHE_URL', - 'VITE_IPFS_GATEWAY', - 'VITE_PLATFORM_API_URL', - 'VITE_MAINNET_RPC_URL', - 'VITE_ETH_RPC_URL', - 'VITE_BELIEFS_CONTRACT_ADDRESS', - 'VITE_IMPLICATIONS_CONTRACT_ADDRESS', - 'VITE_ASSURANCE_CONTRACT_FACTORY_ADDRESS', - 'VITE_ERC1155_FACTORY_ADDRESS', - 'VITE_DELEGATABLE_NOTES_CONTRACT_ADDRESS', - 'VITE_RECURRING_PLEDGES_CONTRACT_ADDRESS', - 'VITE_NOTE_INTENT_CONTRACT_ADDRESS', - 'VITE_ALIGNMENT_ATTESTATIONS_CONTRACT_ADDRESS', - 'VITE_MUTABLE_REF_UPDATER_CONTRACT_ADDRESS', - 'VITE_TRUST_REGISTRY_CONTRACT_ADDRESS', - 'VITE_DEFAULT_ALIGNMENT_TRUST_ROOT', - 'VITE_NUDGE_PUBLICATIONS_CONTRACT_ADDRESS', - 'VITE_PUBLISHED_DATA_CONTRACT_ADDRESS', - 'VITE_CONTENT_REGISTRY_ADDRESS', - 'VITE_CHANNEL_REGISTRY_ADDRESS', - 'VITE_CHANNEL_ESCROW_ADDRESS', - 'VITE_CREATOR_CONTRACT_FACTORY_ADDRESS', - 'VITE_PROJECT_FACTORY_CONTRACT_ADDRESS', - 'VITE_PAYMENT_TOKEN_ADDRESS', - 'VITE_PAYMENT_TOKEN_SYMBOL', - 'VITE_PAYMENT_TOKEN_DECIMALS', - 'VITE_CHAIN_ID', - 'VITE_COMMONALITY_URL', - 'VITE_LAZYGIVING_URL', - 'VITE_ALIGNMENT_URL', - 'VITE_TALLY_URL', - 'VITE_CONTENT_FUNDING_URL', - 'VITE_CIVILITY_URL', - 'VITE_COMMON_SENSE_MAJORITY_URL', - 'VITE_CONCEPTSPACE_URL', -] as const - -/** Fail boot on dual-store drift in local/dev; warn-only elsewhere. */ -function isStrictRuntimeConfigEnv(): boolean { - if (import.meta.env.DEV) return true - const environment = - getRuntimeConfig().COMMONALITY_ENVIRONMENT - ?? (import.meta.env.COMMONALITY_ENVIRONMENT as string | undefined) - return environment === 'local' -} - -function assertRuntimeConfigStoresAligned(): void { - const mismatches: string[] = [] - for (const key of SHARED_RUNTIME_KEYS) { - const host = getRuntimeConfigValue(key) - const shared = getUiSharedRuntimeConfigValue(key) - if ((host ?? '') !== (shared ?? '')) { - mismatches.push(`${key}: host=${host ?? '(unset)'} ui/shared=${shared ?? '(unset)'}`) - } - } - if (mismatches.length > 0) { - const detail = - '[CauseStarter] dual runtime-config stores disagree after loadRuntimeConfig; ' - + 'board/project pages may use different addresses than native pages:\n' - + mismatches.join('\n') - // Local/dev: fail boot so silent wrong contracts cannot ship a broken board. - // Production dual-store drift still warns until stores are unified. - if (isStrictRuntimeConfigEnv()) { - throw new Error(detail) - } - console.warn(detail) - } else if (import.meta.env.DEV) { - console.info( - '[CauseStarter] dual runtime-config stores aligned on shared keys', - Object.fromEntries( - SHARED_RUNTIME_KEYS.map((key) => [key, getRuntimeConfigValue(key) ?? '(unset)']), - ), - ) - } -} - -Promise.all([loadRuntimeConfig(), loadUiSharedRuntimeConfig()]) - .then(() => { - assertRuntimeConfigStoresAligned() - createRoot(document.getElementById('root')!).render( - - - , - ) - }) - .catch((error) => { - const message = error instanceof Error ? error.message : String(error) - document.getElementById('root')!.textContent = message - }) diff --git a/docker-compose.yml b/docker-compose.yml index 07326aa0..0983d76c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -393,10 +393,10 @@ services: # CauseStarter SPA hosting on local IPFS (core domain). ui-ipfs-publisher-causestarter: - image: commonality-causestarter-ipfs-publisher:dev + image: commonality-ui-ipfs-publisher:dev build: context: . - dockerfile: causestarter/Dockerfile.ipfs + dockerfile: ui/Dockerfile container_name: commonality-ui-ipfs-publisher-causestarter user: "${UID:-1000}:${GID:-1000}" command: ["node", "scripts/publish-ui-to-ipfs.mjs"] @@ -404,7 +404,6 @@ services: volumes: - ./.env:/workspace/.env:ro - ./ui/.env:/workspace/ui/.env:ro - - ./causestarter/.env:/workspace/causestarter/.env:ro - ./data/ui-ipfs/causestarter:/artifacts depends_on: ipfs: @@ -412,9 +411,10 @@ services: hardhat-deploy: condition: service_completed_successfully environment: - - UI_PACKAGE=causestarter + - VITE_DOMAIN=causestarter - VITE_DEFAULT_ALIGNMENT_TRUST_ROOT=${VITE_DEFAULT_ALIGNMENT_TRUST_ROOT:-0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f} - VITE_HASH_ROUTING=true + - VITE_ROUTER_MODE=hash - UI_IPFS_API_URL=http://ipfs:5001 - UI_IPFS_GATEWAY_URL=http://localhost:8080/ipfs - VITE_IPFS_GATEWAY=http://localhost:8080/ipfs diff --git a/package.json b/package.json index 4d888d49..24ce0966 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "build:docs": "turbo run docs --filter=@commonality/sdk --filter=@commonality/hardhat", "attester:build": "turbo run build --filter=@commonality/implication-attester", "ui:dev": "npm run dev --workspace=ui", - "causestarter:dev": "npm run dev --workspace=causestarter", - "causestarter:build": "npm run build --workspace=causestarter", - "causestarter:test": "npm run test --workspace=causestarter", + "causestarter:dev": "npm run dev:causestarter --workspace=ui", + "causestarter:build": "VITE_DOMAIN=causestarter npm run build --workspace=ui", + "causestarter:test": "npm run test:vitest --workspace=ui -- src/causestarter", "causestarter:deploy": "./scripts/deploy-causestarter.sh", "causestarter:deploy:stop": "./scripts/deploy-causestarter.sh --stop", "local:check": "./scripts/check-local-config-sync.sh", @@ -47,7 +47,7 @@ "cause-assist:typecheck": "npm run typecheck --workspace=@commonality/cause-assist", "coherence-badge-worker:test": "npm run test --workspace=@commonality/coherence-badge-worker", "alignment-trust-bootstrap:test": "npm run test --workspace=@commonality/alignment-trust-bootstrap", - "causestarter:typecheck": "npm run typecheck --workspace=causestarter", + "causestarter:typecheck": "npm run typecheck --workspace=ui", "ui:dev:commonality": "npm run dev:commonality --workspace=ui", "ui:dev:lazyGiving": "npm run dev:lazyGiving --workspace=ui", "ui:dev:alignment": "npm run dev:alignment --workspace=ui", diff --git a/scripts/publish-ui-to-ipfs.mjs b/scripts/publish-ui-to-ipfs.mjs index c64001c9..ae5273c8 100644 --- a/scripts/publish-ui-to-ipfs.mjs +++ b/scripts/publish-ui-to-ipfs.mjs @@ -8,14 +8,12 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const rootDir = path.resolve(__dirname, '..') -// UI_PACKAGE selects the app package: "ui" (multi-domain) or "causestarter". -const uiPackage = resolveUiPackage(process.env.UI_PACKAGE) -const buildDomain = uiPackage === 'causestarter' +// CauseStarter is a VITE_DOMAIN of the ui package. UI_PACKAGE=causestarter is +// accepted as an alias for VITE_DOMAIN=causestarter. +const buildDomain = process.env.UI_PACKAGE === 'causestarter' ? 'causestarter' : resolveDomain(process.env.VITE_DOMAIN) -const distDir = uiPackage === 'causestarter' - ? path.join(rootDir, 'causestarter', 'dist') - : path.join(rootDir, 'ui', 'dist', buildDomain) +const distDir = path.join(rootDir, 'ui', 'dist', buildDomain) const artifactDir = process.env.UI_IPFS_ARTIFACT_DIR || path.join(rootDir, 'data', 'ui-ipfs', buildDomain) const ipfsApiBaseUrl = (process.env.UI_IPFS_API_URL || 'http://ipfs:5001').replace(/\/$/, '') @@ -34,6 +32,7 @@ const LOCAL_STABLE_DOMAIN_URLS = { VITE_NONINFLAMMATORY_URL: getLocalStableUrl('civility', localStableGatewayPort), VITE_CSM_URL: getLocalStableUrl('common-sense-majority', localStableGatewayPort), VITE_CONCEPTSPACE_URL: getLocalStableUrl('conceptspace', localStableGatewayPort), + VITE_CAUSESTARTER_URL: getLocalStableUrl('causestarter', localStableGatewayPort), } const UI_ENV_ADDRESS_MAPPINGS = { @@ -58,11 +57,6 @@ const UI_ENV_ADDRESS_MAPPINGS = { PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS: 'VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS', } -function resolveUiPackage(value) { - if (value === 'causestarter') return 'causestarter' - return 'ui' -} - function parseEnvFile(content) { const entries = {} @@ -98,10 +92,7 @@ async function loadEnvFile(filePath) { async function loadUiBuildEnvFromFiles() { const rootEnv = await loadEnvFile(path.join(rootDir, '.env')) const uiEnv = await loadEnvFile(path.join(rootDir, 'ui', '.env')) - const causestarterEnv = uiPackage === 'causestarter' - ? await loadEnvFile(path.join(rootDir, 'causestarter', '.env')) - : {} - const env = { ...uiEnv, ...causestarterEnv } + const env = { ...uiEnv } for (const [sourceKey, viteKey] of Object.entries(UI_ENV_ADDRESS_MAPPINGS)) { if (rootEnv[sourceKey]) { @@ -250,18 +241,6 @@ async function writeArtifacts(result) { } function buildUiPackage(buildEnv) { - if (uiPackage === 'causestarter') { - console.log('Building CauseStarter for IPFS...') - runOrThrow('npm', ['run', 'build', '--workspace=@commonality/sdk'], { env: buildEnv }) - runOrThrow('npm', ['run', 'build', '--workspace=causestarter'], { - env: { - ...buildEnv, - VITE_HASH_ROUTING: 'true', - }, - }) - return - } - console.log(`Building ${buildDomain} UI in IPFS mode...`) runOrThrow('npm', ['run', 'ui:build:ipfs'], { env: buildEnv }) } @@ -276,7 +255,7 @@ async function main() { await writeArtifacts(result) console.log('') - console.log(uiPackage === 'causestarter' ? 'CauseStarter published to local IPFS.' : 'UI published to local IPFS.') + console.log(`${buildDomain} published to local IPFS.`) console.log(` CID: ${result.cid}`) console.log(` IPFS root: ${result.ipfsRootUrl}`) console.log(` SPA URL: ${result.spaUrl}`) diff --git a/sdk/src/subsystems/lazy-giving/queries.ts b/sdk/src/subsystems/lazy-giving/queries.ts index 7c12b00d..6764282d 100644 --- a/sdk/src/subsystems/lazy-giving/queries.ts +++ b/sdk/src/subsystems/lazy-giving/queries.ts @@ -128,32 +128,34 @@ async function readSettlementCurrency( // LazyGiving Queries // ============================================================================ +export interface ProjectFoldResult { + project: Project; + accumulator: ProjectAccumulator; +} + /** - * Get a crowdfunding project by its assurance contract address. - * - * Fetches and folds all project events, then reads threshold/deadline - * from the on-chain condition contract if a publicClient is available. + * Fold a project and return the resumable accumulator alongside the view model. * - * @param machinery - SDK machinery with event cache configuration - * @param assuranceContractAddress - Address of the project's assurance contract - * @param options - Optional configuration for resumable folding - * @param options.initialAccumulator - Previously saved accumulator to resume from (enables incremental fetching) - * @param options.blockNumber_gte - Only fetch events at or after this block number (used with initialAccumulator) - * @returns The project, or null if no creation event exists + * Pass `initialAccumulator` / `blockNumber_gte` to continue from a saved cursor + * instead of replaying every event. */ -export async function getProject( +export async function getProjectFold( machinery: SDKMachinery, assuranceContractAddress: string, options?: { initialAccumulator?: ProjectAccumulator; blockNumber_gte?: string; } -): Promise { +): Promise { const projectEvents = await fetchAndDecodeProjectEvents(machinery, assuranceContractAddress, { blockNumber_gte: options?.blockNumber_gte, }); const fundingCurrency = await readSettlementCurrency(machinery, assuranceContractAddress); - const { project: partial } = foldProject(projectEvents, options?.initialAccumulator, fundingCurrency); + const { project: partial, accumulator } = foldProject( + projectEvents, + options?.initialAccumulator, + fundingCurrency, + ); if (!partial) return null; let threshold = '0'; @@ -168,7 +170,32 @@ export async function getProject( } } - return { ...partial, threshold, deadline }; + return { project: { ...partial, threshold, deadline }, accumulator }; +} + +/** + * Get a crowdfunding project by its assurance contract address. + * + * Fetches and folds all project events, then reads threshold/deadline + * from the on-chain condition contract if a publicClient is available. + * + * @param machinery - SDK machinery with event cache configuration + * @param assuranceContractAddress - Address of the project's assurance contract + * @param options - Optional configuration for resumable folding + * @param options.initialAccumulator - Previously saved accumulator to resume from (enables incremental fetching) + * @param options.blockNumber_gte - Only fetch events at or after this block number (used with initialAccumulator) + * @returns The project, or null if no creation event exists + */ +export async function getProject( + machinery: SDKMachinery, + assuranceContractAddress: string, + options?: { + initialAccumulator?: ProjectAccumulator; + blockNumber_gte?: string; + } +): Promise { + const folded = await getProjectFold(machinery, assuranceContractAddress, options); + return folded?.project ?? null; } /** diff --git a/specs/product/ui-domains.md b/specs/product/ui-domains.md index 7a783587..de64be28 100644 --- a/specs/product/ui-domains.md +++ b/specs/product/ui-domains.md @@ -14,7 +14,7 @@ It is **not** the source of truth for landing-page copy, CTA wording, spotlight Four product sites for funding (LazyGiving, Aligning, Content Funding, Civility), one product site for signing (Tally), two movement sites (Commonality, CSM), and one mostly developer-facing infrastructure site (Conceptspace). -**CauseStarter** ([`causestarter/`](../../causestarter/)) is the primary cause-first reference lens on the same substrate (separate package from the eight multi-domain `ui/` builds for now). Organizers retrieve, review, publish, and circulate versioned cause rosters there; signing, aligned-project, and funding capabilities appear in cause context. The eight independent sites remain available as focused tools and verticals, consistent with ADR 0005; CauseStarter has no cause directory, ranking, or promotion. Known gaps: [`causestarter/TODO.md`](../../causestarter/TODO.md). Local stack: included in `./scripts/services.sh --start` (gateway + dedicated SPA) and `./scripts/deploy-causestarter.sh`. +**CauseStarter** is the primary cause-first reference lens on the same substrate, built as a ninth `VITE_DOMAIN` in [`ui/`](../../ui/) (`ui/src/causestarter/`, `ui/src/domains/causestarter/`). The package directory [`causestarter/`](../../causestarter/) still holds Docker/nginx/e2e glue and the product backlog. Organizers retrieve, review, publish, and circulate versioned cause rosters there; signing, aligned-project, and funding capabilities appear in cause context. The eight independent sites remain available as focused tools and verticals, consistent with ADR 0005; CauseStarter has no cause directory, ranking, or promotion. Known gaps: [`causestarter/TODO.md`](../../causestarter/TODO.md). Local stack: included in `./scripts/services.sh --start` (gateway + dedicated SPA) and `./scripts/deploy-causestarter.sh`. This four-bucket grouping — **funding / signing / movement / infrastructure** — is the canonical taxonomy of the eight sites. Other docs may cut the same eight sites along a different axis (subsystems vs. branded builds in [specs/README.md](../README.md), purpose-neutral vs. cause vertical in [marketing.md](./marketing.md)); those are orthogonal cuts for their own purposes and do not replace this one. Two placements are easy to get wrong: diff --git a/specs/tech/ui-domains.md b/specs/tech/ui-domains.md index 23665133..ea993540 100644 --- a/specs/tech/ui-domains.md +++ b/specs/tech/ui-domains.md @@ -1,6 +1,6 @@ # Multi-Domain UI Architecture -The eight UI domains (Commonality, LazyGiving, Aligning, Tally, Content Funding, Civility, Common Sense Majority, Conceptspace) are built from a single codebase but deployed as separate artifacts. For the product-level description of what each site is and why they exist, see [specs/product/ui-domains.md](../product/ui-domains.md). +The UI domains (the eight focused sites plus CauseStarter) are built from a single codebase but deployed as separate artifacts. For the product-level description of what each site is and why they exist, see [specs/product/ui-domains.md](../product/ui-domains.md). ## Shared codebase, separate builds @@ -11,7 +11,7 @@ All eight sites share: - Authentication and wallet infrastructure - Attestation display components -Each site is a separate build artifact that includes only the routes and features relevant to it. A `VITE_DOMAIN` environment variable selects which domain is built; it defaults to `commonality`. +Each site is a separate build artifact that includes only the routes and features relevant to it. A `VITE_DOMAIN` environment variable selects which domain is built; it defaults to `commonality`. CauseStarter is `VITE_DOMAIN=causestarter` (feature module `ui/src/causestarter/`). ## Directory shape @@ -21,6 +21,7 @@ This tree shows the important architectural folders, not every helper directory ``` ui/src/ ├── shared/ # Shared SDK, components, hooks, routing, branding helpers +├── causestarter/ # CauseStarter feature module (ninth domain) ├── conceptspace/ # Statement-signing feature module (used by Tally) ├── lazy-giving/ # Project/funding feature module (used by LazyGiving and funding verticals) ├── delegation/ # Delegation feature module (used by LazyGiving and funding verticals) @@ -36,6 +37,7 @@ ui/src/ │ ├── civility/ │ ├── common-sense-majority/ │ ├── conceptspace/ +│ ├── causestarter/ │ ├── components/ # Shared per-domain landing/shell components │ └── delegation/ # Legacy compatibility folder; Delegation is not a standalone build └── main.tsx # Selects the active domain build via VITE_DOMAIN @@ -57,16 +59,17 @@ dist/ ├── content-funding/ ├── civility/ ├── common-sense-majority/ -└── conceptspace/ +├── conceptspace/ +└── causestarter/ ``` Useful build commands (from the `ui/` directory): ``` npm run build # builds the active domain (VITE_DOMAIN, defaults to commonality) -npm run build:domains # builds all eight domains in one pass +npm run build:domains # builds all domains in one pass npm run build:ipfs # builds active domain in hash-routing mode for IPFS deployment -npm run build:ipfs:domains # builds all eight domains in IPFS mode +npm run build:ipfs:domains # builds all domains in IPFS mode ``` diff --git a/ui/package.json b/ui/package.json index 0648373b..08671297 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,6 +14,7 @@ "dev:civility": "VITE_DOMAIN=civility ../node_modules/.bin/vite", "dev:common-sense-majority": "VITE_DOMAIN=common-sense-majority ../node_modules/.bin/vite", "dev:conceptspace": "VITE_DOMAIN=conceptspace ../node_modules/.bin/vite", + "dev:causestarter": "VITE_DOMAIN=causestarter ../node_modules/.bin/vite --port 5174", "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit", "build": "tsc -b && tsc -p tsconfig.test.json --noEmit && ../node_modules/.bin/vite build", "build:domains": "tsc -b && tsc -p tsconfig.test.json --noEmit && node ./scripts/build-domains.mjs", diff --git a/ui/scripts/build-domains.mjs b/ui/scripts/build-domains.mjs index 28eeb967..b44efbdc 100644 --- a/ui/scripts/build-domains.mjs +++ b/ui/scripts/build-domains.mjs @@ -8,7 +8,7 @@ const __dirname = path.dirname(__filename) const uiDir = path.resolve(__dirname, '..') const distDir = path.join(uiDir, 'dist') const viteBin = path.join(uiDir, '..', 'node_modules', '.bin', 'vite') -const domains = ['commonality', 'lazyGiving', 'alignment', 'tally', 'content-funding', 'civility', 'common-sense-majority', 'conceptspace'] +const domains = ['commonality', 'lazyGiving', 'alignment', 'tally', 'content-funding', 'civility', 'common-sense-majority', 'conceptspace', 'causestarter'] const args = process.argv.slice(2) const modeIndex = args.indexOf('--mode') const mode = modeIndex >= 0 ? args[modeIndex + 1] : undefined diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 1ead8ea6..ea261909 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,10 +1,26 @@ -import { useEffect } from 'react' +import { useEffect, type ReactNode } from 'react' import { BrowserRouter, HashRouter, Route, Routes } from 'react-router-dom' import { AppShell } from './shared/components/AppShell' import { CrossDomainUnavailablePage } from './shared' import { NotFoundPage } from './shared' import { getActiveDomain } from './domains' import { isHashRouting, loadDisplayDenylist } from './shared' +import { CauseShell } from './causestarter/shell/CauseShell' + +function DomainChrome({ children }: { children: ReactNode }) { + const domain = getActiveDomain() + if (domain.useCauseShell) { + return {children} + } + return ( + + {children} + + ) +} function App() { const Router = isHashRouting() ? HashRouter : BrowserRouter @@ -16,16 +32,13 @@ function App() { return ( - + {domain.routes} } /> } /> - + ) } diff --git a/ui/src/causestarter/App.tsx b/ui/src/causestarter/App.tsx new file mode 100644 index 00000000..ce56abb0 --- /dev/null +++ b/ui/src/causestarter/App.tsx @@ -0,0 +1,118 @@ +import { lazy, Suspense, type ReactNode } from 'react' +import { Navigate, Route } from 'react-router-dom' +import { CircularProgress, Stack } from '@mui/material' +import { HomePage } from './pages/HomePage' +import { PersonalDashboardPage } from './pages/PersonalDashboardPage' +import { WelcomePage } from './pages/WelcomePage' +import { StartCauseRedirect } from './pages/StartCauseRedirect' +import { StartBridgeRedirect } from './pages/StartBridgeRedirect' +import { BridgeClusterPage } from './pages/BridgeClusterPage' +import { BridgeTriplePage } from './pages/BridgeTriplePage' +import { CausesPage } from './pages/CausesPage' +import { CauseDetailPage } from './pages/CauseDetailPage' +import { CauseMediatorPage } from './pages/CauseMediatorPage' +import { CauseBoardLeaderboardPage } from './pages/CauseBoardLeaderboardPage' +import { StatementBoardLeaderboardPage } from './pages/StatementBoardLeaderboardPage' +import { StatementBoardRedirect } from './pages/StatementBoardRedirect' +import { StatementPage } from './pages/StatementPage' +import { StatementsPage } from './pages/StatementsPage' +import { SettingsPage } from './pages/SettingsPage' +import { ProjectDetailPage, ProjectLeaderboardPage } from './pages/ProjectDetailPage' +import { CreateProjectPage } from './pages/CreateProjectPage' +import { CauseContentBoardPage } from './pages/CauseContentBoardPage' +import { CauseFundingPage } from './pages/CauseFundingPage' +import { + ContentFundingAboutPage, + ContentFundingBrowsePage, + ContentFundingChannelPage, + ContentFundingContractPage, + ContentFundingCreateContractPage, + ContentFundingCreatorDashboardPage, + ContentFundingCreatorsPage, + ContentFundingExploreKindsPage, + ContentFundingLandingPage, + ContentFundingMaterializeFutureContentPage, + ContentFundingStartContractPage, +} from './pages/ContentFundingPages' +import { + DelegateProfilePage, + DepositPage, + MyNotesPage, + NoteDetailPage, +} from './pages/DelegationPages' + +const DocsPage = lazy(() => import('./pages/DocsPage').then((mod) => ({ default: mod.DocsPage }))) + +function DocsFallback() { + return ( + + + + ) +} + +function docsRoute(): ReactNode { + return ( + }> + + + ) +} + +/** CauseStarter route table composed into `VITE_DOMAIN=causestarter`. */ +export const causeStarterRoutes: ReactNode = ( + <> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + } /> + +) diff --git a/causestarter/src/components/AlignmentTrustGate.test.tsx b/ui/src/causestarter/components/AlignmentTrustGate.test.tsx similarity index 100% rename from causestarter/src/components/AlignmentTrustGate.test.tsx rename to ui/src/causestarter/components/AlignmentTrustGate.test.tsx diff --git a/causestarter/src/components/AlignmentTrustGate.tsx b/ui/src/causestarter/components/AlignmentTrustGate.tsx similarity index 100% rename from causestarter/src/components/AlignmentTrustGate.tsx rename to ui/src/causestarter/components/AlignmentTrustGate.tsx diff --git a/causestarter/src/components/BridgeClusterAssist.test.tsx b/ui/src/causestarter/components/BridgeClusterAssist.test.tsx similarity index 100% rename from causestarter/src/components/BridgeClusterAssist.test.tsx rename to ui/src/causestarter/components/BridgeClusterAssist.test.tsx diff --git a/causestarter/src/components/BridgeClusterAssist.tsx b/ui/src/causestarter/components/BridgeClusterAssist.tsx similarity index 100% rename from causestarter/src/components/BridgeClusterAssist.tsx rename to ui/src/causestarter/components/BridgeClusterAssist.tsx diff --git a/causestarter/src/components/CauseBridgesSection.test.tsx b/ui/src/causestarter/components/CauseBridgesSection.test.tsx similarity index 100% rename from causestarter/src/components/CauseBridgesSection.test.tsx rename to ui/src/causestarter/components/CauseBridgesSection.test.tsx diff --git a/causestarter/src/components/CauseBridgesSection.tsx b/ui/src/causestarter/components/CauseBridgesSection.tsx similarity index 100% rename from causestarter/src/components/CauseBridgesSection.tsx rename to ui/src/causestarter/components/CauseBridgesSection.tsx diff --git a/causestarter/src/components/CauseCard.tsx b/ui/src/causestarter/components/CauseCard.tsx similarity index 100% rename from causestarter/src/components/CauseCard.tsx rename to ui/src/causestarter/components/CauseCard.tsx diff --git a/causestarter/src/components/CauseConjunctionEarmark.test.tsx b/ui/src/causestarter/components/CauseConjunctionEarmark.test.tsx similarity index 100% rename from causestarter/src/components/CauseConjunctionEarmark.test.tsx rename to ui/src/causestarter/components/CauseConjunctionEarmark.test.tsx diff --git a/causestarter/src/components/CauseConjunctionEarmark.tsx b/ui/src/causestarter/components/CauseConjunctionEarmark.tsx similarity index 100% rename from causestarter/src/components/CauseConjunctionEarmark.tsx rename to ui/src/causestarter/components/CauseConjunctionEarmark.tsx diff --git a/causestarter/src/components/CauseFundingSummary.test.tsx b/ui/src/causestarter/components/CauseFundingSummary.test.tsx similarity index 100% rename from causestarter/src/components/CauseFundingSummary.test.tsx rename to ui/src/causestarter/components/CauseFundingSummary.test.tsx diff --git a/causestarter/src/components/CauseFundingSummary.tsx b/ui/src/causestarter/components/CauseFundingSummary.tsx similarity index 100% rename from causestarter/src/components/CauseFundingSummary.tsx rename to ui/src/causestarter/components/CauseFundingSummary.tsx diff --git a/causestarter/src/components/CauseMediatorCard.test.tsx b/ui/src/causestarter/components/CauseMediatorCard.test.tsx similarity index 100% rename from causestarter/src/components/CauseMediatorCard.test.tsx rename to ui/src/causestarter/components/CauseMediatorCard.test.tsx diff --git a/causestarter/src/components/CauseMediatorCard.tsx b/ui/src/causestarter/components/CauseMediatorCard.tsx similarity index 100% rename from causestarter/src/components/CauseMediatorCard.tsx rename to ui/src/causestarter/components/CauseMediatorCard.tsx diff --git a/causestarter/src/components/CauseViewStrip.test.tsx b/ui/src/causestarter/components/CauseViewStrip.test.tsx similarity index 100% rename from causestarter/src/components/CauseViewStrip.test.tsx rename to ui/src/causestarter/components/CauseViewStrip.test.tsx diff --git a/causestarter/src/components/CauseViewStrip.tsx b/ui/src/causestarter/components/CauseViewStrip.tsx similarity index 100% rename from causestarter/src/components/CauseViewStrip.tsx rename to ui/src/causestarter/components/CauseViewStrip.tsx diff --git a/causestarter/src/components/ClusterMediatorOptIn.test.tsx b/ui/src/causestarter/components/ClusterMediatorOptIn.test.tsx similarity index 100% rename from causestarter/src/components/ClusterMediatorOptIn.test.tsx rename to ui/src/causestarter/components/ClusterMediatorOptIn.test.tsx diff --git a/causestarter/src/components/ClusterMediatorOptIn.tsx b/ui/src/causestarter/components/ClusterMediatorOptIn.tsx similarity index 100% rename from causestarter/src/components/ClusterMediatorOptIn.tsx rename to ui/src/causestarter/components/ClusterMediatorOptIn.tsx diff --git a/causestarter/src/components/ConnectWalletHint.test.tsx b/ui/src/causestarter/components/ConnectWalletHint.test.tsx similarity index 100% rename from causestarter/src/components/ConnectWalletHint.test.tsx rename to ui/src/causestarter/components/ConnectWalletHint.test.tsx diff --git a/causestarter/src/components/ConnectWalletHint.tsx b/ui/src/causestarter/components/ConnectWalletHint.tsx similarity index 100% rename from causestarter/src/components/ConnectWalletHint.tsx rename to ui/src/causestarter/components/ConnectWalletHint.tsx diff --git a/causestarter/src/components/CrowdJobs.tsx b/ui/src/causestarter/components/CrowdJobs.tsx similarity index 100% rename from causestarter/src/components/CrowdJobs.tsx rename to ui/src/causestarter/components/CrowdJobs.tsx diff --git a/causestarter/src/components/HeaderInfoTip.tsx b/ui/src/causestarter/components/HeaderInfoTip.tsx similarity index 100% rename from causestarter/src/components/HeaderInfoTip.tsx rename to ui/src/causestarter/components/HeaderInfoTip.tsx diff --git a/causestarter/src/components/JobTip.tsx b/ui/src/causestarter/components/JobTip.tsx similarity index 100% rename from causestarter/src/components/JobTip.tsx rename to ui/src/causestarter/components/JobTip.tsx diff --git a/causestarter/src/components/MediatorEditor.tsx b/ui/src/causestarter/components/MediatorEditor.tsx similarity index 100% rename from causestarter/src/components/MediatorEditor.tsx rename to ui/src/causestarter/components/MediatorEditor.tsx diff --git a/causestarter/src/components/MonthlyPledgeSignal.test.tsx b/ui/src/causestarter/components/MonthlyPledgeSignal.test.tsx similarity index 100% rename from causestarter/src/components/MonthlyPledgeSignal.test.tsx rename to ui/src/causestarter/components/MonthlyPledgeSignal.test.tsx diff --git a/causestarter/src/components/MonthlyPledgeSignal.tsx b/ui/src/causestarter/components/MonthlyPledgeSignal.tsx similarity index 100% rename from causestarter/src/components/MonthlyPledgeSignal.tsx rename to ui/src/causestarter/components/MonthlyPledgeSignal.tsx diff --git a/causestarter/src/components/OrganizerIdentity.test.tsx b/ui/src/causestarter/components/OrganizerIdentity.test.tsx similarity index 100% rename from causestarter/src/components/OrganizerIdentity.test.tsx rename to ui/src/causestarter/components/OrganizerIdentity.test.tsx diff --git a/causestarter/src/components/OrganizerIdentity.tsx b/ui/src/causestarter/components/OrganizerIdentity.tsx similarity index 100% rename from causestarter/src/components/OrganizerIdentity.tsx rename to ui/src/causestarter/components/OrganizerIdentity.tsx diff --git a/causestarter/src/components/PlankRow.test.tsx b/ui/src/causestarter/components/PlankRow.test.tsx similarity index 100% rename from causestarter/src/components/PlankRow.test.tsx rename to ui/src/causestarter/components/PlankRow.test.tsx diff --git a/causestarter/src/components/PlankRow.tsx b/ui/src/causestarter/components/PlankRow.tsx similarity index 100% rename from causestarter/src/components/PlankRow.tsx rename to ui/src/causestarter/components/PlankRow.tsx diff --git a/causestarter/src/components/ProjectBookmarkButton.tsx b/ui/src/causestarter/components/ProjectBookmarkButton.tsx similarity index 100% rename from causestarter/src/components/ProjectBookmarkButton.tsx rename to ui/src/causestarter/components/ProjectBookmarkButton.tsx diff --git a/causestarter/src/components/ProjectCard.tsx b/ui/src/causestarter/components/ProjectCard.tsx similarity index 100% rename from causestarter/src/components/ProjectCard.tsx rename to ui/src/causestarter/components/ProjectCard.tsx diff --git a/causestarter/src/components/RosterHistory.tsx b/ui/src/causestarter/components/RosterHistory.tsx similarity index 100% rename from causestarter/src/components/RosterHistory.tsx rename to ui/src/causestarter/components/RosterHistory.tsx diff --git a/causestarter/src/components/RosterPublishPanel.test.tsx b/ui/src/causestarter/components/RosterPublishPanel.test.tsx similarity index 100% rename from causestarter/src/components/RosterPublishPanel.test.tsx rename to ui/src/causestarter/components/RosterPublishPanel.test.tsx diff --git a/causestarter/src/components/RosterPublishPanel.tsx b/ui/src/causestarter/components/RosterPublishPanel.tsx similarity index 100% rename from causestarter/src/components/RosterPublishPanel.tsx rename to ui/src/causestarter/components/RosterPublishPanel.tsx diff --git a/causestarter/src/components/SafetyRejectionDialog.tsx b/ui/src/causestarter/components/SafetyRejectionDialog.tsx similarity index 100% rename from causestarter/src/components/SafetyRejectionDialog.tsx rename to ui/src/causestarter/components/SafetyRejectionDialog.tsx diff --git a/causestarter/src/components/SelectedPlankSupport.test.tsx b/ui/src/causestarter/components/SelectedPlankSupport.test.tsx similarity index 95% rename from causestarter/src/components/SelectedPlankSupport.test.tsx rename to ui/src/causestarter/components/SelectedPlankSupport.test.tsx index 94c7d594..31d21f09 100644 --- a/causestarter/src/components/SelectedPlankSupport.test.tsx +++ b/ui/src/causestarter/components/SelectedPlankSupport.test.tsx @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SelectedPlankSupport } from './SelectedPlankSupport' import { sendCallsPreferAtomic } from '../lib/causeRoster' import { getUserBelief } from '@commonality/sdk/conceptspace' +import type { IpfsCidV1 } from '@commonality/sdk/utils' vi.mock('wagmi', () => ({ useAccount: vi.fn(() => ({ address: '0x1111111111111111111111111111111111111111', isConnected: true })), @@ -21,7 +22,7 @@ vi.mock('./WalletButton', () => ({ WalletButton: () => const BELIEVES = 1 const NO_OPINION = 0 -const planks = [ +const planks: { cid: IpfsCidV1; text: string }[] = [ { cid: 'bafybeidagx4zc6phhtjng6f3sjzlicqm2ssq4eb6wskinjtuvkt275fmpy', text: 'School crossings should be safer.' }, { cid: 'bafybeifjzv3oc6zqklqvfmv2j5xgqqjped3zrm4y2a3s4u5v6w7x2y3z4a', text: 'Public parks should remain open.' }, ] @@ -59,7 +60,7 @@ describe('SelectedPlankSupport', () => { it('hides when every selected statement is already signed', async () => { vi.mocked(getUserBelief).mockResolvedValue({ - statementCid: planks[0].cid, + statementCid: planks[0].cid as IpfsCidV1, beliefState: BELIEVES, }) const { container } = render() diff --git a/causestarter/src/components/SelectedPlankSupport.tsx b/ui/src/causestarter/components/SelectedPlankSupport.tsx similarity index 100% rename from causestarter/src/components/SelectedPlankSupport.tsx rename to ui/src/causestarter/components/SelectedPlankSupport.tsx diff --git a/causestarter/src/components/StarterNetworkFilterNotice.test.tsx b/ui/src/causestarter/components/StarterNetworkFilterNotice.test.tsx similarity index 100% rename from causestarter/src/components/StarterNetworkFilterNotice.test.tsx rename to ui/src/causestarter/components/StarterNetworkFilterNotice.test.tsx diff --git a/causestarter/src/components/StarterNetworkFilterNotice.tsx b/ui/src/causestarter/components/StarterNetworkFilterNotice.tsx similarity index 100% rename from causestarter/src/components/StarterNetworkFilterNotice.tsx rename to ui/src/causestarter/components/StarterNetworkFilterNotice.tsx diff --git a/causestarter/src/components/StatementPicker.test.tsx b/ui/src/causestarter/components/StatementPicker.test.tsx similarity index 100% rename from causestarter/src/components/StatementPicker.test.tsx rename to ui/src/causestarter/components/StatementPicker.test.tsx diff --git a/causestarter/src/components/StatementPicker.tsx b/ui/src/causestarter/components/StatementPicker.tsx similarity index 100% rename from causestarter/src/components/StatementPicker.tsx rename to ui/src/causestarter/components/StatementPicker.tsx diff --git a/causestarter/src/components/StatementSupportStats.tsx b/ui/src/causestarter/components/StatementSupportStats.tsx similarity index 100% rename from causestarter/src/components/StatementSupportStats.tsx rename to ui/src/causestarter/components/StatementSupportStats.tsx diff --git a/causestarter/src/components/SupportButton.test.tsx b/ui/src/causestarter/components/SupportButton.test.tsx similarity index 100% rename from causestarter/src/components/SupportButton.test.tsx rename to ui/src/causestarter/components/SupportButton.test.tsx diff --git a/causestarter/src/components/SupportButton.tsx b/ui/src/causestarter/components/SupportButton.tsx similarity index 100% rename from causestarter/src/components/SupportButton.tsx rename to ui/src/causestarter/components/SupportButton.tsx diff --git a/causestarter/src/components/ToolCard.tsx b/ui/src/causestarter/components/ToolCard.tsx similarity index 100% rename from causestarter/src/components/ToolCard.tsx rename to ui/src/causestarter/components/ToolCard.tsx diff --git a/causestarter/src/components/WalletButton.tsx b/ui/src/causestarter/components/WalletButton.tsx similarity index 100% rename from causestarter/src/components/WalletButton.tsx rename to ui/src/causestarter/components/WalletButton.tsx diff --git a/causestarter/src/components/YourCauses.tsx b/ui/src/causestarter/components/YourCauses.tsx similarity index 100% rename from causestarter/src/components/YourCauses.tsx rename to ui/src/causestarter/components/YourCauses.tsx diff --git a/causestarter/src/components/YourDashboard.test.tsx b/ui/src/causestarter/components/YourDashboard.test.tsx similarity index 100% rename from causestarter/src/components/YourDashboard.test.tsx rename to ui/src/causestarter/components/YourDashboard.test.tsx diff --git a/causestarter/src/components/YourDashboard.tsx b/ui/src/causestarter/components/YourDashboard.tsx similarity index 100% rename from causestarter/src/components/YourDashboard.tsx rename to ui/src/causestarter/components/YourDashboard.tsx diff --git a/causestarter/src/components/YourNudgersAndNudges.test.tsx b/ui/src/causestarter/components/YourNudgersAndNudges.test.tsx similarity index 100% rename from causestarter/src/components/YourNudgersAndNudges.test.tsx rename to ui/src/causestarter/components/YourNudgersAndNudges.test.tsx diff --git a/causestarter/src/components/YourNudgersAndNudges.tsx b/ui/src/causestarter/components/YourNudgersAndNudges.tsx similarity index 100% rename from causestarter/src/components/YourNudgersAndNudges.tsx rename to ui/src/causestarter/components/YourNudgersAndNudges.tsx diff --git a/causestarter/src/components/YourProjects.test.tsx b/ui/src/causestarter/components/YourProjects.test.tsx similarity index 100% rename from causestarter/src/components/YourProjects.test.tsx rename to ui/src/causestarter/components/YourProjects.test.tsx diff --git a/causestarter/src/components/YourProjects.tsx b/ui/src/causestarter/components/YourProjects.tsx similarity index 100% rename from causestarter/src/components/YourProjects.tsx rename to ui/src/causestarter/components/YourProjects.tsx diff --git a/causestarter/src/components/YourSignedStatements.test.tsx b/ui/src/causestarter/components/YourSignedStatements.test.tsx similarity index 100% rename from causestarter/src/components/YourSignedStatements.test.tsx rename to ui/src/causestarter/components/YourSignedStatements.test.tsx diff --git a/causestarter/src/components/YourSignedStatements.tsx b/ui/src/causestarter/components/YourSignedStatements.tsx similarity index 100% rename from causestarter/src/components/YourSignedStatements.tsx rename to ui/src/causestarter/components/YourSignedStatements.tsx diff --git a/causestarter/src/hooks/useAlignmentTrust.test.ts b/ui/src/causestarter/hooks/useAlignmentTrust.test.ts similarity index 100% rename from causestarter/src/hooks/useAlignmentTrust.test.ts rename to ui/src/causestarter/hooks/useAlignmentTrust.test.ts diff --git a/causestarter/src/hooks/useAlignmentTrust.ts b/ui/src/causestarter/hooks/useAlignmentTrust.ts similarity index 100% rename from causestarter/src/hooks/useAlignmentTrust.ts rename to ui/src/causestarter/hooks/useAlignmentTrust.ts diff --git a/causestarter/src/hooks/useCauseMonthlyPledges.test.tsx b/ui/src/causestarter/hooks/useCauseMonthlyPledges.test.tsx similarity index 100% rename from causestarter/src/hooks/useCauseMonthlyPledges.test.tsx rename to ui/src/causestarter/hooks/useCauseMonthlyPledges.test.tsx diff --git a/causestarter/src/hooks/useCauseMonthlyPledges.ts b/ui/src/causestarter/hooks/useCauseMonthlyPledges.ts similarity index 100% rename from causestarter/src/hooks/useCauseMonthlyPledges.ts rename to ui/src/causestarter/hooks/useCauseMonthlyPledges.ts diff --git a/causestarter/src/hooks/useCauseProjects.test.tsx b/ui/src/causestarter/hooks/useCauseProjects.test.tsx similarity index 100% rename from causestarter/src/hooks/useCauseProjects.test.tsx rename to ui/src/causestarter/hooks/useCauseProjects.test.tsx diff --git a/causestarter/src/hooks/useCauseProjects.ts b/ui/src/causestarter/hooks/useCauseProjects.ts similarity index 100% rename from causestarter/src/hooks/useCauseProjects.ts rename to ui/src/causestarter/hooks/useCauseProjects.ts diff --git a/causestarter/src/hooks/useToolExamples.ts b/ui/src/causestarter/hooks/useToolExamples.ts similarity index 100% rename from causestarter/src/hooks/useToolExamples.ts rename to ui/src/causestarter/hooks/useToolExamples.ts diff --git a/causestarter/src/hooks/useUserCauses.ts b/ui/src/causestarter/hooks/useUserCauses.ts similarity index 100% rename from causestarter/src/hooks/useUserCauses.ts rename to ui/src/causestarter/hooks/useUserCauses.ts diff --git a/causestarter/src/hooks/useUserProjects.ts b/ui/src/causestarter/hooks/useUserProjects.ts similarity index 100% rename from causestarter/src/hooks/useUserProjects.ts rename to ui/src/causestarter/hooks/useUserProjects.ts diff --git a/causestarter/src/hooks/useUserStatements.test.ts b/ui/src/causestarter/hooks/useUserStatements.test.ts similarity index 100% rename from causestarter/src/hooks/useUserStatements.test.ts rename to ui/src/causestarter/hooks/useUserStatements.test.ts diff --git a/causestarter/src/hooks/useUserStatements.ts b/ui/src/causestarter/hooks/useUserStatements.ts similarity index 100% rename from causestarter/src/hooks/useUserStatements.ts rename to ui/src/causestarter/hooks/useUserStatements.ts diff --git a/causestarter/src/hooks/useViewCounts.test.tsx b/ui/src/causestarter/hooks/useViewCounts.test.tsx similarity index 100% rename from causestarter/src/hooks/useViewCounts.test.tsx rename to ui/src/causestarter/hooks/useViewCounts.test.tsx diff --git a/causestarter/src/hooks/useViewCounts.ts b/ui/src/causestarter/hooks/useViewCounts.ts similarity index 100% rename from causestarter/src/hooks/useViewCounts.ts rename to ui/src/causestarter/hooks/useViewCounts.ts diff --git a/causestarter/src/index.css b/ui/src/causestarter/index.css similarity index 100% rename from causestarter/src/index.css rename to ui/src/causestarter/index.css diff --git a/causestarter/src/lib/alignedContent.test.ts b/ui/src/causestarter/lib/alignedContent.test.ts similarity index 100% rename from causestarter/src/lib/alignedContent.test.ts rename to ui/src/causestarter/lib/alignedContent.test.ts diff --git a/causestarter/src/lib/alignedContent.ts b/ui/src/causestarter/lib/alignedContent.ts similarity index 100% rename from causestarter/src/lib/alignedContent.ts rename to ui/src/causestarter/lib/alignedContent.ts diff --git a/causestarter/src/lib/believerSetsCache.test.ts b/ui/src/causestarter/lib/believerSetsCache.test.ts similarity index 100% rename from causestarter/src/lib/believerSetsCache.test.ts rename to ui/src/causestarter/lib/believerSetsCache.test.ts diff --git a/causestarter/src/lib/believerSetsCache.ts b/ui/src/causestarter/lib/believerSetsCache.ts similarity index 100% rename from causestarter/src/lib/believerSetsCache.ts rename to ui/src/causestarter/lib/believerSetsCache.ts diff --git a/causestarter/src/lib/bridgeAssistBrief.test.ts b/ui/src/causestarter/lib/bridgeAssistBrief.test.ts similarity index 100% rename from causestarter/src/lib/bridgeAssistBrief.test.ts rename to ui/src/causestarter/lib/bridgeAssistBrief.test.ts diff --git a/causestarter/src/lib/bridgeAssistBrief.ts b/ui/src/causestarter/lib/bridgeAssistBrief.ts similarity index 100% rename from causestarter/src/lib/bridgeAssistBrief.ts rename to ui/src/causestarter/lib/bridgeAssistBrief.ts diff --git a/causestarter/src/lib/bridgeCluster.test.ts b/ui/src/causestarter/lib/bridgeCluster.test.ts similarity index 100% rename from causestarter/src/lib/bridgeCluster.test.ts rename to ui/src/causestarter/lib/bridgeCluster.test.ts diff --git a/causestarter/src/lib/bridgeCluster.ts b/ui/src/causestarter/lib/bridgeCluster.ts similarity index 100% rename from causestarter/src/lib/bridgeCluster.ts rename to ui/src/causestarter/lib/bridgeCluster.ts diff --git a/causestarter/src/lib/bridgeNudges.test.ts b/ui/src/causestarter/lib/bridgeNudges.test.ts similarity index 100% rename from causestarter/src/lib/bridgeNudges.test.ts rename to ui/src/causestarter/lib/bridgeNudges.test.ts diff --git a/causestarter/src/lib/bridgeNudges.ts b/ui/src/causestarter/lib/bridgeNudges.ts similarity index 100% rename from causestarter/src/lib/bridgeNudges.ts rename to ui/src/causestarter/lib/bridgeNudges.ts diff --git a/causestarter/src/lib/bridgeStore.test.ts b/ui/src/causestarter/lib/bridgeStore.test.ts similarity index 100% rename from causestarter/src/lib/bridgeStore.test.ts rename to ui/src/causestarter/lib/bridgeStore.test.ts diff --git a/causestarter/src/lib/bridgeStore.ts b/ui/src/causestarter/lib/bridgeStore.ts similarity index 100% rename from causestarter/src/lib/bridgeStore.ts rename to ui/src/causestarter/lib/bridgeStore.ts diff --git a/causestarter/src/lib/bridgeTriple.test.ts b/ui/src/causestarter/lib/bridgeTriple.test.ts similarity index 100% rename from causestarter/src/lib/bridgeTriple.test.ts rename to ui/src/causestarter/lib/bridgeTriple.test.ts diff --git a/causestarter/src/lib/bridgeTriple.ts b/ui/src/causestarter/lib/bridgeTriple.ts similarity index 100% rename from causestarter/src/lib/bridgeTriple.ts rename to ui/src/causestarter/lib/bridgeTriple.ts diff --git a/causestarter/src/lib/causeAssistClient.ts b/ui/src/causestarter/lib/causeAssistClient.ts similarity index 100% rename from causestarter/src/lib/causeAssistClient.ts rename to ui/src/causestarter/lib/causeAssistClient.ts diff --git a/causestarter/src/lib/causeBookmarks.test.ts b/ui/src/causestarter/lib/causeBookmarks.test.ts similarity index 100% rename from causestarter/src/lib/causeBookmarks.test.ts rename to ui/src/causestarter/lib/causeBookmarks.test.ts diff --git a/causestarter/src/lib/causeBookmarks.ts b/ui/src/causestarter/lib/causeBookmarks.ts similarity index 100% rename from causestarter/src/lib/causeBookmarks.ts rename to ui/src/causestarter/lib/causeBookmarks.ts diff --git a/causestarter/src/lib/causeCompatibility.test.ts b/ui/src/causestarter/lib/causeCompatibility.test.ts similarity index 100% rename from causestarter/src/lib/causeCompatibility.test.ts rename to ui/src/causestarter/lib/causeCompatibility.test.ts diff --git a/causestarter/src/lib/causeRoster.test.ts b/ui/src/causestarter/lib/causeRoster.test.ts similarity index 100% rename from causestarter/src/lib/causeRoster.test.ts rename to ui/src/causestarter/lib/causeRoster.test.ts diff --git a/causestarter/src/lib/causeRoster.ts b/ui/src/causestarter/lib/causeRoster.ts similarity index 100% rename from causestarter/src/lib/causeRoster.ts rename to ui/src/causestarter/lib/causeRoster.ts diff --git a/causestarter/src/lib/causeStore.test.ts b/ui/src/causestarter/lib/causeStore.test.ts similarity index 100% rename from causestarter/src/lib/causeStore.test.ts rename to ui/src/causestarter/lib/causeStore.test.ts diff --git a/causestarter/src/lib/causeStore.ts b/ui/src/causestarter/lib/causeStore.ts similarity index 100% rename from causestarter/src/lib/causeStore.ts rename to ui/src/causestarter/lib/causeStore.ts diff --git a/causestarter/src/lib/concurrency.test.ts b/ui/src/causestarter/lib/concurrency.test.ts similarity index 100% rename from causestarter/src/lib/concurrency.test.ts rename to ui/src/causestarter/lib/concurrency.test.ts diff --git a/causestarter/src/lib/concurrency.ts b/ui/src/causestarter/lib/concurrency.ts similarity index 100% rename from causestarter/src/lib/concurrency.ts rename to ui/src/causestarter/lib/concurrency.ts diff --git a/causestarter/src/lib/domainUrls.ts b/ui/src/causestarter/lib/domainUrls.ts similarity index 100% rename from causestarter/src/lib/domainUrls.ts rename to ui/src/causestarter/lib/domainUrls.ts diff --git a/causestarter/src/lib/exampleBank.test.ts b/ui/src/causestarter/lib/exampleBank.test.ts similarity index 100% rename from causestarter/src/lib/exampleBank.test.ts rename to ui/src/causestarter/lib/exampleBank.test.ts diff --git a/causestarter/src/lib/exampleBank.ts b/ui/src/causestarter/lib/exampleBank.ts similarity index 100% rename from causestarter/src/lib/exampleBank.ts rename to ui/src/causestarter/lib/exampleBank.ts diff --git a/causestarter/src/lib/fixtures/causeCompatibility.ts b/ui/src/causestarter/lib/fixtures/causeCompatibility.ts similarity index 100% rename from causestarter/src/lib/fixtures/causeCompatibility.ts rename to ui/src/causestarter/lib/fixtures/causeCompatibility.ts diff --git a/causestarter/src/lib/hardhatAccounts.test.ts b/ui/src/causestarter/lib/hardhatAccounts.test.ts similarity index 100% rename from causestarter/src/lib/hardhatAccounts.test.ts rename to ui/src/causestarter/lib/hardhatAccounts.test.ts diff --git a/causestarter/src/lib/hardhatAccounts.ts b/ui/src/causestarter/lib/hardhatAccounts.ts similarity index 100% rename from causestarter/src/lib/hardhatAccounts.ts rename to ui/src/causestarter/lib/hardhatAccounts.ts diff --git a/causestarter/src/lib/hardhatLocalConnector.ts b/ui/src/causestarter/lib/hardhatLocalConnector.ts similarity index 100% rename from causestarter/src/lib/hardhatLocalConnector.ts rename to ui/src/causestarter/lib/hardhatLocalConnector.ts diff --git a/causestarter/src/lib/implicationAttesterClient.test.ts b/ui/src/causestarter/lib/implicationAttesterClient.test.ts similarity index 100% rename from causestarter/src/lib/implicationAttesterClient.test.ts rename to ui/src/causestarter/lib/implicationAttesterClient.test.ts diff --git a/causestarter/src/lib/implicationAttesterClient.ts b/ui/src/causestarter/lib/implicationAttesterClient.ts similarity index 100% rename from causestarter/src/lib/implicationAttesterClient.ts rename to ui/src/causestarter/lib/implicationAttesterClient.ts diff --git a/causestarter/src/lib/jobs.test.ts b/ui/src/causestarter/lib/jobs.test.ts similarity index 100% rename from causestarter/src/lib/jobs.test.ts rename to ui/src/causestarter/lib/jobs.test.ts diff --git a/causestarter/src/lib/jobs.ts b/ui/src/causestarter/lib/jobs.ts similarity index 100% rename from causestarter/src/lib/jobs.ts rename to ui/src/causestarter/lib/jobs.ts diff --git a/causestarter/src/lib/nearDuplicatePlanks.test.ts b/ui/src/causestarter/lib/nearDuplicatePlanks.test.ts similarity index 100% rename from causestarter/src/lib/nearDuplicatePlanks.test.ts rename to ui/src/causestarter/lib/nearDuplicatePlanks.test.ts diff --git a/causestarter/src/lib/nearDuplicatePlanks.ts b/ui/src/causestarter/lib/nearDuplicatePlanks.ts similarity index 100% rename from causestarter/src/lib/nearDuplicatePlanks.ts rename to ui/src/causestarter/lib/nearDuplicatePlanks.ts diff --git a/causestarter/src/lib/projectBookmarks.test.ts b/ui/src/causestarter/lib/projectBookmarks.test.ts similarity index 100% rename from causestarter/src/lib/projectBookmarks.test.ts rename to ui/src/causestarter/lib/projectBookmarks.test.ts diff --git a/causestarter/src/lib/projectBookmarks.ts b/ui/src/causestarter/lib/projectBookmarks.ts similarity index 100% rename from causestarter/src/lib/projectBookmarks.ts rename to ui/src/causestarter/lib/projectBookmarks.ts diff --git a/causestarter/src/lib/publishCombinator.ts b/ui/src/causestarter/lib/publishCombinator.ts similarity index 100% rename from causestarter/src/lib/publishCombinator.ts rename to ui/src/causestarter/lib/publishCombinator.ts diff --git a/causestarter/src/lib/publishPlank.ts b/ui/src/causestarter/lib/publishPlank.ts similarity index 100% rename from causestarter/src/lib/publishPlank.ts rename to ui/src/causestarter/lib/publishPlank.ts diff --git a/causestarter/src/lib/runtimeConfig.ts b/ui/src/causestarter/lib/runtimeConfig.ts similarity index 100% rename from causestarter/src/lib/runtimeConfig.ts rename to ui/src/causestarter/lib/runtimeConfig.ts diff --git a/causestarter/src/lib/statementPicker.test.ts b/ui/src/causestarter/lib/statementPicker.test.ts similarity index 100% rename from causestarter/src/lib/statementPicker.test.ts rename to ui/src/causestarter/lib/statementPicker.test.ts diff --git a/causestarter/src/lib/statementPicker.ts b/ui/src/causestarter/lib/statementPicker.ts similarity index 100% rename from causestarter/src/lib/statementPicker.ts rename to ui/src/causestarter/lib/statementPicker.ts diff --git a/causestarter/src/lib/toolExamples.test.ts b/ui/src/causestarter/lib/toolExamples.test.ts similarity index 100% rename from causestarter/src/lib/toolExamples.test.ts rename to ui/src/causestarter/lib/toolExamples.test.ts diff --git a/causestarter/src/lib/toolExamples.ts b/ui/src/causestarter/lib/toolExamples.ts similarity index 100% rename from causestarter/src/lib/toolExamples.ts rename to ui/src/causestarter/lib/toolExamples.ts diff --git a/causestarter/src/lib/tools.test.ts b/ui/src/causestarter/lib/tools.test.ts similarity index 100% rename from causestarter/src/lib/tools.test.ts rename to ui/src/causestarter/lib/tools.test.ts diff --git a/causestarter/src/lib/tools.ts b/ui/src/causestarter/lib/tools.ts similarity index 100% rename from causestarter/src/lib/tools.ts rename to ui/src/causestarter/lib/tools.ts diff --git a/causestarter/src/lib/useMachinery.ts b/ui/src/causestarter/lib/useMachinery.ts similarity index 100% rename from causestarter/src/lib/useMachinery.ts rename to ui/src/causestarter/lib/useMachinery.ts diff --git a/causestarter/src/lib/useWriteClients.ts b/ui/src/causestarter/lib/useWriteClients.ts similarity index 100% rename from causestarter/src/lib/useWriteClients.ts rename to ui/src/causestarter/lib/useWriteClients.ts diff --git a/causestarter/src/lib/userProjects.test.ts b/ui/src/causestarter/lib/userProjects.test.ts similarity index 100% rename from causestarter/src/lib/userProjects.test.ts rename to ui/src/causestarter/lib/userProjects.test.ts diff --git a/causestarter/src/lib/userProjects.ts b/ui/src/causestarter/lib/userProjects.ts similarity index 89% rename from causestarter/src/lib/userProjects.ts rename to ui/src/causestarter/lib/userProjects.ts index a4c767b6..fdfa28b7 100644 --- a/causestarter/src/lib/userProjects.ts +++ b/ui/src/causestarter/lib/userProjects.ts @@ -1,5 +1,6 @@ import { getProject, getUserContributions, getUserCreatedProjects, type Project } from '@commonality/sdk/lazy-giving' import type { SDKMachinery } from '@commonality/sdk/machinery' +import { loadProjectWithCache, projectFoldCacheOptions } from '@ui/shared' import { readLazyGivingProjectMetadata } from '@ui/lazy-giving/metadata' import type { IpfsCidV1 } from '@commonality/sdk/utils' import { mapWithConcurrency, PLANK_QUERY_CONCURRENCY } from './concurrency' @@ -59,8 +60,13 @@ export async function loadUserProjects( } const ids = [...new Set([...created, ...contributed, ...bookmarked])] + const cacheOptions = projectFoldCacheOptions(machinery) const loaded = await mapWithConcurrency(ids, PLANK_QUERY_CONCURRENCY, async (id) => { - const project = await getProject(machinery, id).catch(() => null) + const project = ( + cacheOptions + ? await loadProjectWithCache(machinery, id, cacheOptions).catch(() => null) + : await getProject(machinery, id).catch(() => null) + ) if (!project) return null let name: string | undefined if (project.metadataCid) { diff --git a/causestarter/src/pages/BridgeClusterPage.tsx b/ui/src/causestarter/pages/BridgeClusterPage.tsx similarity index 100% rename from causestarter/src/pages/BridgeClusterPage.tsx rename to ui/src/causestarter/pages/BridgeClusterPage.tsx diff --git a/causestarter/src/pages/BridgeTriplePage.test.tsx b/ui/src/causestarter/pages/BridgeTriplePage.test.tsx similarity index 100% rename from causestarter/src/pages/BridgeTriplePage.test.tsx rename to ui/src/causestarter/pages/BridgeTriplePage.test.tsx diff --git a/causestarter/src/pages/BridgeTriplePage.tsx b/ui/src/causestarter/pages/BridgeTriplePage.tsx similarity index 100% rename from causestarter/src/pages/BridgeTriplePage.tsx rename to ui/src/causestarter/pages/BridgeTriplePage.tsx diff --git a/causestarter/src/pages/CauseBoardLeaderboardPage.tsx b/ui/src/causestarter/pages/CauseBoardLeaderboardPage.tsx similarity index 100% rename from causestarter/src/pages/CauseBoardLeaderboardPage.tsx rename to ui/src/causestarter/pages/CauseBoardLeaderboardPage.tsx diff --git a/causestarter/src/pages/CauseContentBoardPage.tsx b/ui/src/causestarter/pages/CauseContentBoardPage.tsx similarity index 100% rename from causestarter/src/pages/CauseContentBoardPage.tsx rename to ui/src/causestarter/pages/CauseContentBoardPage.tsx diff --git a/causestarter/src/pages/CauseDetailPage.tsx b/ui/src/causestarter/pages/CauseDetailPage.tsx similarity index 99% rename from causestarter/src/pages/CauseDetailPage.tsx rename to ui/src/causestarter/pages/CauseDetailPage.tsx index 337ab7f1..cc8df436 100644 --- a/causestarter/src/pages/CauseDetailPage.tsx +++ b/ui/src/causestarter/pages/CauseDetailPage.tsx @@ -77,6 +77,10 @@ function safetyState(verdict: { * Editing published rosters requires the organizer's connected wallet. * Unpublished local drafts can still be shaped on this device before publish. */ +export function CauseDetailEditPage() { + return +} + export function CauseDetailPage({ editMode = false }: { editMode?: boolean }) { const params = useParams<{ causeId?: string; owner?: string; slugPart?: string }>() const navigate = useNavigate() diff --git a/causestarter/src/pages/CauseFundingPage.tsx b/ui/src/causestarter/pages/CauseFundingPage.tsx similarity index 100% rename from causestarter/src/pages/CauseFundingPage.tsx rename to ui/src/causestarter/pages/CauseFundingPage.tsx diff --git a/causestarter/src/pages/CauseMediatorPage.tsx b/ui/src/causestarter/pages/CauseMediatorPage.tsx similarity index 100% rename from causestarter/src/pages/CauseMediatorPage.tsx rename to ui/src/causestarter/pages/CauseMediatorPage.tsx diff --git a/causestarter/src/pages/CausesPage.tsx b/ui/src/causestarter/pages/CausesPage.tsx similarity index 100% rename from causestarter/src/pages/CausesPage.tsx rename to ui/src/causestarter/pages/CausesPage.tsx diff --git a/causestarter/src/pages/ContentFundingPages.tsx b/ui/src/causestarter/pages/ContentFundingPages.tsx similarity index 100% rename from causestarter/src/pages/ContentFundingPages.tsx rename to ui/src/causestarter/pages/ContentFundingPages.tsx diff --git a/causestarter/src/pages/CreateProjectPage.tsx b/ui/src/causestarter/pages/CreateProjectPage.tsx similarity index 100% rename from causestarter/src/pages/CreateProjectPage.tsx rename to ui/src/causestarter/pages/CreateProjectPage.tsx diff --git a/causestarter/src/pages/DelegationPages.tsx b/ui/src/causestarter/pages/DelegationPages.tsx similarity index 100% rename from causestarter/src/pages/DelegationPages.tsx rename to ui/src/causestarter/pages/DelegationPages.tsx diff --git a/causestarter/src/pages/DocsPage.test.tsx b/ui/src/causestarter/pages/DocsPage.test.tsx similarity index 90% rename from causestarter/src/pages/DocsPage.test.tsx rename to ui/src/causestarter/pages/DocsPage.test.tsx index 7f924a2d..13203b1a 100644 --- a/causestarter/src/pages/DocsPage.test.tsx +++ b/ui/src/causestarter/pages/DocsPage.test.tsx @@ -24,7 +24,10 @@ describe('DocsPage', () => { expect(screen.getByTestId('docs-page')).toBeInTheDocument() expect(screen.getByText('CauseStarter')).toBeInTheDocument() expect(screen.getByText(/Do the part you’d do anyway/i)).toBeInTheDocument() - expect(screen.getByRole('link', { name: 'What is this?' })).toHaveAttribute('href', '/welcome') + expect(screen.getByRole('link', { name: /Start a cause board/i })).toHaveAttribute( + 'href', + '/docs/start-a-cause', + ) }) it('renders the jobs catalog', () => { diff --git a/causestarter/src/pages/DocsPage.tsx b/ui/src/causestarter/pages/DocsPage.tsx similarity index 100% rename from causestarter/src/pages/DocsPage.tsx rename to ui/src/causestarter/pages/DocsPage.tsx diff --git a/causestarter/src/pages/HomePage.test.tsx b/ui/src/causestarter/pages/HomePage.test.tsx similarity index 100% rename from causestarter/src/pages/HomePage.test.tsx rename to ui/src/causestarter/pages/HomePage.test.tsx diff --git a/causestarter/src/pages/HomePage.tsx b/ui/src/causestarter/pages/HomePage.tsx similarity index 100% rename from causestarter/src/pages/HomePage.tsx rename to ui/src/causestarter/pages/HomePage.tsx diff --git a/causestarter/src/pages/NotFoundPage.tsx b/ui/src/causestarter/pages/NotFoundPage.tsx similarity index 100% rename from causestarter/src/pages/NotFoundPage.tsx rename to ui/src/causestarter/pages/NotFoundPage.tsx diff --git a/causestarter/src/pages/PersonalDashboardPage.tsx b/ui/src/causestarter/pages/PersonalDashboardPage.tsx similarity index 100% rename from causestarter/src/pages/PersonalDashboardPage.tsx rename to ui/src/causestarter/pages/PersonalDashboardPage.tsx diff --git a/causestarter/src/pages/ProjectDetailPage.tsx b/ui/src/causestarter/pages/ProjectDetailPage.tsx similarity index 100% rename from causestarter/src/pages/ProjectDetailPage.tsx rename to ui/src/causestarter/pages/ProjectDetailPage.tsx diff --git a/causestarter/src/pages/SettingsPage.tsx b/ui/src/causestarter/pages/SettingsPage.tsx similarity index 100% rename from causestarter/src/pages/SettingsPage.tsx rename to ui/src/causestarter/pages/SettingsPage.tsx diff --git a/causestarter/src/pages/StartBridgeRedirect.test.ts b/ui/src/causestarter/pages/StartBridgeRedirect.test.ts similarity index 100% rename from causestarter/src/pages/StartBridgeRedirect.test.ts rename to ui/src/causestarter/pages/StartBridgeRedirect.test.ts diff --git a/causestarter/src/pages/StartBridgeRedirect.tsx b/ui/src/causestarter/pages/StartBridgeRedirect.tsx similarity index 100% rename from causestarter/src/pages/StartBridgeRedirect.tsx rename to ui/src/causestarter/pages/StartBridgeRedirect.tsx diff --git a/causestarter/src/pages/StartCauseRedirect.tsx b/ui/src/causestarter/pages/StartCauseRedirect.tsx similarity index 100% rename from causestarter/src/pages/StartCauseRedirect.tsx rename to ui/src/causestarter/pages/StartCauseRedirect.tsx diff --git a/causestarter/src/pages/StatementBoardLeaderboardPage.tsx b/ui/src/causestarter/pages/StatementBoardLeaderboardPage.tsx similarity index 100% rename from causestarter/src/pages/StatementBoardLeaderboardPage.tsx rename to ui/src/causestarter/pages/StatementBoardLeaderboardPage.tsx diff --git a/causestarter/src/pages/StatementBoardRedirect.tsx b/ui/src/causestarter/pages/StatementBoardRedirect.tsx similarity index 100% rename from causestarter/src/pages/StatementBoardRedirect.tsx rename to ui/src/causestarter/pages/StatementBoardRedirect.tsx diff --git a/causestarter/src/pages/StatementPage.tsx b/ui/src/causestarter/pages/StatementPage.tsx similarity index 100% rename from causestarter/src/pages/StatementPage.tsx rename to ui/src/causestarter/pages/StatementPage.tsx diff --git a/causestarter/src/pages/StatementsPage.test.tsx b/ui/src/causestarter/pages/StatementsPage.test.tsx similarity index 100% rename from causestarter/src/pages/StatementsPage.test.tsx rename to ui/src/causestarter/pages/StatementsPage.test.tsx diff --git a/causestarter/src/pages/StatementsPage.tsx b/ui/src/causestarter/pages/StatementsPage.tsx similarity index 100% rename from causestarter/src/pages/StatementsPage.tsx rename to ui/src/causestarter/pages/StatementsPage.tsx diff --git a/causestarter/src/pages/WelcomePage.tsx b/ui/src/causestarter/pages/WelcomePage.tsx similarity index 100% rename from causestarter/src/pages/WelcomePage.tsx rename to ui/src/causestarter/pages/WelcomePage.tsx diff --git a/causestarter/src/shell/CauseShell.tsx b/ui/src/causestarter/shell/CauseShell.tsx similarity index 100% rename from causestarter/src/shell/CauseShell.tsx rename to ui/src/causestarter/shell/CauseShell.tsx diff --git a/causestarter/src/test/setup.ts b/ui/src/causestarter/test/setup.ts similarity index 100% rename from causestarter/src/test/setup.ts rename to ui/src/causestarter/test/setup.ts diff --git a/causestarter/src/virtual-end-user-docs.d.ts b/ui/src/causestarter/virtual-end-user-docs.d.ts similarity index 100% rename from causestarter/src/virtual-end-user-docs.d.ts rename to ui/src/causestarter/virtual-end-user-docs.d.ts diff --git a/causestarter/src/wagmi.ts b/ui/src/causestarter/wagmi.ts similarity index 100% rename from causestarter/src/wagmi.ts rename to ui/src/causestarter/wagmi.ts diff --git a/ui/src/domains/CrossDomainSmoke.test.tsx b/ui/src/domains/CrossDomainSmoke.test.tsx index 37e261a4..2284aad5 100644 --- a/ui/src/domains/CrossDomainSmoke.test.tsx +++ b/ui/src/domains/CrossDomainSmoke.test.tsx @@ -6,7 +6,7 @@ import { domainManifests } from './index' import { isRouteResolvableDocLink } from './publicDocLinks' import type { DomainId } from './types' -const domainIds: DomainId[] = ['commonality', 'lazyGiving', 'alignment', 'tally', 'content-funding', 'civility', 'common-sense-majority', 'conceptspace'] +const domainIds: DomainId[] = ['commonality', 'lazyGiving', 'alignment', 'tally', 'content-funding', 'civility', 'common-sense-majority', 'conceptspace', 'causestarter'] const publicDocModules = import.meta.glob('../../../docs/end-user/**/*.md', { query: '?raw', import: 'default', eager: true }) as Record function renderDomainRoute(domainId: DomainId, path = '/') { @@ -90,6 +90,7 @@ describe.each(domainIds)('cross-domain smoke: %s', (domainId) => { civility: 'Civility', 'common-sense-majority': 'Common Sense Majority', conceptspace: 'Conceptspace', + causestarter: 'CauseStarter', } it('has branding copy for the domain', () => { @@ -122,7 +123,8 @@ describe.each(domainIds)('cross-domain smoke: %s', (domainId) => { }) }) - describe('landing page', () => { + // CauseStarter `/` is a wallet-backed dashboard, not a static pitch page. + describe.skipIf(domainId === 'causestarter')('landing page', () => { it('renders a hero title', () => { renderDomainRoute(domainId) const heading = screen.getByRole('heading', { level: 1 }) diff --git a/ui/src/domains/CrossLinkCrawler.test.tsx b/ui/src/domains/CrossLinkCrawler.test.tsx index e18488f6..be202c82 100644 --- a/ui/src/domains/CrossLinkCrawler.test.tsx +++ b/ui/src/domains/CrossLinkCrawler.test.tsx @@ -5,7 +5,10 @@ import { domainManifests } from './index' import { isRouteResolvableDocLink } from './publicDocLinks' import type { DomainId } from './types' + const domainIds = Object.keys(domainManifests) as DomainId[] +/** CauseStarter routes are eager wallet-backed pages, not the lazyRoute samples these crawls assume. */ +const crawledDomainIds = domainIds.filter((id) => id !== 'causestarter') const publicDocModules = import.meta.glob('../../../docs/end-user/**/*.md', { query: '?raw', import: 'default', eager: true }) as Record const routeParamSamples: Record = { @@ -16,6 +19,10 @@ const routeParamSamples: Record = { roundAddress: '0x0000000000000000000000000000000000000003', statementCid: 'bafybeigdyrztktxq5mkrl3zpnczqtyse534w7y576guthacdf5uloxx3za', channelId: 'demo-channel', + owner: '0x0000000000000000000000000000000000000004', + slugPart: 'demo-cause', + causeId: 'demo-cause-id', + draftId: 'draft-1', } type CrawledPage = { @@ -132,7 +139,7 @@ afterEach(() => { describe('cross-link crawler for rendered UI and public docs', () => { it('renders every public domain route sample without React console errors', () => { - for (const domainId of domainIds) { + for (const domainId of crawledDomainIds) { for (const routePath of extractRoutePaths(domainManifests[domainId].routes)) { cleanup() const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) @@ -144,7 +151,7 @@ describe('cross-link crawler for rendered UI and public docs', () => { }) it('crawls rendered route samples and only finds resolvable internal app links', () => { - const pages: CrawledPage[] = domainIds.flatMap(domainId => + const pages: CrawledPage[] = crawledDomainIds.flatMap(domainId => extractRoutePaths(domainManifests[domainId].routes).map(routePath => ({ domainId, path: samplePath(routePath), @@ -165,7 +172,7 @@ describe('cross-link crawler for rendered UI and public docs', () => { }) it('keeps rendered route-sample external links on intentionally allowed hosts', () => { - for (const domainId of domainIds) { + for (const domainId of crawledDomainIds) { for (const routePath of extractRoutePaths(domainManifests[domainId].routes)) { cleanup() const path = samplePath(routePath) diff --git a/ui/src/domains/DomainDeepLinksSmoke.test.tsx b/ui/src/domains/DomainDeepLinksSmoke.test.tsx index 832a2508..044ffd00 100644 --- a/ui/src/domains/DomainDeepLinksSmoke.test.tsx +++ b/ui/src/domains/DomainDeepLinksSmoke.test.tsx @@ -19,6 +19,10 @@ const sampleParamValues: Record = { projectAddress: '0x2222222222222222222222222222222222222222', roundAddress: '0x3333333333333333333333333333333333333333', statementCid: 'bafybeigdyrzt', + owner: '0x1111111111111111111111111111111111111111', + slugPart: 'demo-cause', + causeId: 'demo-cause-id', + draftId: 'draft-1', } const representativeDocsPath = '/docs/why-trust-it' @@ -120,6 +124,7 @@ async function expectPathToRender(domainId: DomainId, path: string) { describe('domain representative deep links', () => { it('renders every declared domain route pattern with representative params', async () => { for (const [domainId, manifest] of Object.entries(domainManifests) as [DomainId, typeof domainManifests[DomainId]][]) { + if (domainId === 'causestarter') continue for (const routePattern of extractRoutePaths(manifest.routes)) { cleanup() const path = samplePathForRoutePattern(routePattern) diff --git a/ui/src/domains/ExternalLinksAllowlist.test.tsx b/ui/src/domains/ExternalLinksAllowlist.test.tsx index a4bf5a3e..f618ea47 100644 --- a/ui/src/domains/ExternalLinksAllowlist.test.tsx +++ b/ui/src/domains/ExternalLinksAllowlist.test.tsx @@ -70,6 +70,7 @@ describe('external link allowlist', () => { it('keeps rendered landing-page external links on intentionally allowed hosts', () => { for (const domainId of domainIds) { + if (domainId === 'causestarter') continue cleanup() renderDomainLanding(domainId) for (const link of screen.queryAllByRole('link')) { diff --git a/ui/src/domains/causestarter/manifest.tsx b/ui/src/domains/causestarter/manifest.tsx new file mode 100644 index 00000000..97aa0b0d --- /dev/null +++ b/ui/src/domains/causestarter/manifest.tsx @@ -0,0 +1,92 @@ +import type { ReactNode } from 'react' +import { Navigate, Route } from 'react-router-dom' +import type { DomainManifest } from '../types' +import { lazyRoute } from '../lazyRoute' +import { WelcomePage } from '../../causestarter/pages/WelcomePage' + +const routes: ReactNode = ( + <> + import('../../causestarter/pages/HomePage'), 'HomePage')} /> + import('../../causestarter/pages/PersonalDashboardPage'), 'PersonalDashboardPage')} /> + import('../../causestarter/pages/WelcomePage'), 'WelcomePage')} /> + import('../../causestarter/pages/StartCauseRedirect'), 'StartCauseRedirect')} /> + import('../../causestarter/pages/StartBridgeRedirect'), 'StartBridgeRedirect')} /> + import('../../causestarter/pages/BridgeTriplePage'), 'BridgeTriplePage')} /> + import('../../causestarter/pages/BridgeClusterPage'), 'BridgeClusterPage')} /> + import('../../causestarter/pages/BridgeClusterPage'), 'BridgeClusterPage')} /> + import('../../causestarter/pages/CausesPage'), 'CausesPage')} /> + import('../../causestarter/pages/StatementsPage'), 'StatementsPage')} /> + } /> + import('../../causestarter/pages/DelegationPages'), 'MyNotesPage')} /> + import('../../causestarter/pages/DelegationPages'), 'DepositPage')} /> + import('../../causestarter/pages/DelegationPages'), 'NoteDetailPage')} /> + import('../../causestarter/pages/DelegationPages'), 'DelegateProfilePage')} /> + import('../../causestarter/pages/DelegationPages'), 'DelegateProfilePage')} /> + import('../../causestarter/pages/CauseContentBoardPage'), 'CauseContentBoardPage')} /> + import('../../causestarter/pages/CauseContentBoardPage'), 'CauseContentBoardPage')} /> + import('../../causestarter/pages/CauseFundingPage'), 'CauseFundingPage')} /> + import('../../causestarter/pages/CauseFundingPage'), 'CauseFundingPage')} /> + import('../../causestarter/pages/CauseBoardLeaderboardPage'), 'CauseBoardLeaderboardPage')} /> + import('../../causestarter/pages/CauseBoardLeaderboardPage'), 'CauseBoardLeaderboardPage')} /> + import('../../causestarter/pages/CauseMediatorPage'), 'CauseMediatorPage')} /> + import('../../causestarter/pages/CauseMediatorPage'), 'CauseMediatorPage')} /> + import('../../causestarter/pages/CauseDetailPage'), 'CauseDetailEditPage')} /> + import('../../causestarter/pages/CauseDetailPage'), 'CauseDetailEditPage')} /> + import('../../causestarter/pages/CauseDetailPage'), 'CauseDetailPage')} /> + import('../../causestarter/pages/CauseDetailPage'), 'CauseDetailPage')} /> + import('../../causestarter/pages/StatementPage'), 'StatementPage')} /> + import('../../causestarter/pages/StatementBoardRedirect'), 'StatementBoardRedirect')} /> + import('../../causestarter/pages/StatementBoardLeaderboardPage'), 'StatementBoardLeaderboardPage')} /> + import('../../causestarter/pages/CreateProjectPage'), 'CreateProjectPage')} /> + import('../../causestarter/pages/ProjectDetailPage'), 'ProjectLeaderboardPage')} /> + import('../../causestarter/pages/ProjectDetailPage'), 'ProjectDetailPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingLandingPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingAboutPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingCreatorsPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingStartContractPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingCreatorDashboardPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingContractPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingBrowsePage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingChannelPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingCreateContractPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingMaterializeFutureContentPage')} /> + import('../../causestarter/pages/ContentFundingPages'), 'ContentFundingExploreKindsPage')} /> + import('../../causestarter/pages/DocsPage'), 'DocsPage')} /> + import('../../causestarter/pages/DocsPage'), 'DocsPage')} /> + } /> + import('../../causestarter/pages/SettingsPage'), 'SettingsPage')} /> + +) + +export const causestarterManifest: DomainManifest = { + id: 'causestarter', + branding: { + name: 'CauseStarter', + tagline: 'Organize a cause, enroll people, fund the work.', + }, + shell: { + primaryNavigation: [ + { label: 'Cause boards', path: '/causes' }, + { label: 'Docs', path: '/docs' }, + ], + secondaryNavigation: [ + { label: 'Settings', path: '/settings' }, + ], + footerText: 'CauseStarter is a lens: it renders a cause you already have a link to. It does not rank or directory causes.', + }, + features: { + conceptspace: true, + lazyGiving: true, + fundingportal: true, + delegation: true, + mutablerefs: true, + contentFunding: true, + docs: true, + }, + basePath: '/', + routes, + useCauseShell: true, + LandingPage: WelcomePage, +} + +export const causestarterRoutes: ReactNode = routes diff --git a/ui/src/domains/index.ts b/ui/src/domains/index.ts index f22e913c..62418743 100644 --- a/ui/src/domains/index.ts +++ b/ui/src/domains/index.ts @@ -7,6 +7,7 @@ import { contentFundingManifest } from './content-funding/manifest.tsx' import { civilityManifest } from './civility/manifest.tsx' import { commonSenseMajorityManifest } from './common-sense-majority/manifest.tsx' import { conceptspaceManifest } from './conceptspace/manifest.tsx' +import { causestarterManifest } from './causestarter/manifest.tsx' export * from './types' @@ -19,6 +20,7 @@ export const domainManifests: Record = { civility: civilityManifest, 'common-sense-majority': commonSenseMajorityManifest, conceptspace: conceptspaceManifest, + causestarter: causestarterManifest, } export function getDomainManifest(domainId: DomainId): DomainManifest { @@ -42,7 +44,8 @@ function getDomainIdFromEnv(): DomainId { envDomain === 'content-funding' || envDomain === 'civility' || envDomain === 'common-sense-majority' || - envDomain === 'conceptspace' + envDomain === 'conceptspace' || + envDomain === 'causestarter' ) { return envDomain } @@ -57,3 +60,4 @@ export { contentFundingManifest } export { civilityManifest } export { commonSenseMajorityManifest } export { conceptspaceManifest } +export { causestarterManifest } diff --git a/ui/src/domains/lazyRoute.tsx b/ui/src/domains/lazyRoute.tsx index 9e1e6e06..10d51356 100644 --- a/ui/src/domains/lazyRoute.tsx +++ b/ui/src/domains/lazyRoute.tsx @@ -1,11 +1,11 @@ import { lazy, Suspense, type ComponentType, type ReactElement } from 'react' -type PageModule = Record +type PageModule = Record export function lazyRoute(loadModule: () => Promise, exportName: string): ReactElement { const Page = lazy(async () => { const module = await loadModule() - return { default: module[exportName] } + return { default: module[exportName] as ComponentType } }) return ( diff --git a/ui/src/domains/types.ts b/ui/src/domains/types.ts index c24a54c6..5bbf2ffa 100644 --- a/ui/src/domains/types.ts +++ b/ui/src/domains/types.ts @@ -32,6 +32,8 @@ export interface DomainManifest { basePath: string routes: ReactNode LandingPage?: () => ReactNode + /** CauseStarter keeps its own bottom-nav shell instead of AppShell. */ + useCauseShell?: boolean } // `DomainId` lives in `shared/routing/domainUrls` (cross-brand URL resolution is a cross-cutting diff --git a/ui/src/shared/config/runtimeConfig.ts b/ui/src/shared/config/runtimeConfig.ts index 46c7802f..d22bf094 100644 --- a/ui/src/shared/config/runtimeConfig.ts +++ b/ui/src/shared/config/runtimeConfig.ts @@ -56,6 +56,7 @@ export type RuntimeConfigKey = | 'VITE_CIVILITY_URL' | 'VITE_COMMON_SENSE_MAJORITY_URL' | 'VITE_CONCEPTSPACE_URL' + | 'VITE_CAUSESTARTER_URL' const buildTimeConfig: UiRuntimeConfig = { VITE_EVENT_CACHE_URL: import.meta.env.VITE_EVENT_CACHE_URL, @@ -112,6 +113,7 @@ const buildTimeConfig: UiRuntimeConfig = { VITE_CIVILITY_URL: import.meta.env.VITE_CIVILITY_URL, VITE_COMMON_SENSE_MAJORITY_URL: import.meta.env.VITE_COMMON_SENSE_MAJORITY_URL, VITE_CONCEPTSPACE_URL: import.meta.env.VITE_CONCEPTSPACE_URL, + VITE_CAUSESTARTER_URL: import.meta.env.VITE_CAUSESTARTER_URL, } let runtimeConfig: UiRuntimeConfig = stripEmptyValues(buildTimeConfig) diff --git a/ui/src/shared/hooks/useCachedProject.test.ts b/ui/src/shared/hooks/useCachedProject.test.ts index f7335138..889db06b 100644 --- a/ui/src/shared/hooks/useCachedProject.test.ts +++ b/ui/src/shared/hooks/useCachedProject.test.ts @@ -13,11 +13,13 @@ vi.mock('./useMachinery', () => ({ })) const mockGetProject = vi.fn() +const mockGetProjectFold = vi.fn() vi.mock('@commonality/sdk/lazy-giving', async () => { const actual = await vi.importActual('@commonality/sdk/lazy-giving') return { ...actual, getProject: mockGetProject, + getProjectFold: mockGetProjectFold, PROJECT_FOLD_VERSION: 1, } }) @@ -115,83 +117,93 @@ describe('loadProjectWithCache', () => { it('fetches from SDK when no cache exists', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue(null) - mockGetProject.mockResolvedValue(mockProject) + mockGetProjectFold.mockResolvedValue({ project: mockProject, accumulator: mockAccumulator }) const result = await loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions) expect(result).toEqual(mockProject) expect(loadCachedProjectAccumulator).toHaveBeenCalled() - expect(mockGetProject).toHaveBeenCalledWith(mockMachinery, '0xProject') + expect(mockGetProjectFold).toHaveBeenCalledWith(mockMachinery, '0xProject') expect(saveCachedProjectAccumulator).toHaveBeenCalled() }) - it('saves to cache after fresh fetch', async () => { + it('saves the fold accumulator after a fresh fetch', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue(null) - mockGetProject.mockResolvedValue(mockProject) + mockGetProjectFold.mockResolvedValue({ project: mockProject, accumulator: mockAccumulator }) await loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions) expect(saveCachedProjectAccumulator).toHaveBeenCalledWith( expect.objectContaining({ address: '0xProject' }), - expect.objectContaining({ id: '1', totalReceived: 500n }), + mockAccumulator, '100' ) }) - it('refetches from SDK when cached accumulator is available', async () => { + it('resumes from the cached accumulator instead of replaying every event', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue({ accumulator: mockAccumulator, blockNumber: '100', }) - mockGetProject.mockResolvedValue(mockProject) + mockGetProjectFold.mockResolvedValue({ project: mockProject, accumulator: mockAccumulator }) const result = await loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions) expect(result).toEqual(mockProject) - expect(mockGetProject).toHaveBeenCalledWith(mockMachinery, '0xProject') + expect(mockGetProjectFold).toHaveBeenCalledWith(mockMachinery, '0xProject', { + initialAccumulator: mockAccumulator, + blockNumber_gte: '100', + }) expect(saveCachedProjectAccumulator).toHaveBeenCalledWith( expect.objectContaining({ address: '0xProject' }), - expect.objectContaining({ id: '1', totalReceived: 500n }), + mockAccumulator, '100' ) }) - it('updates cache when block number changes', async () => { + it('updates cache when the resumed fold advances the cursor', async () => { const updatedProject = { ...mockProject, blockNumber: '200', totalReceived: '800' } + const updatedAccumulator = { + ...mockAccumulator, + blockNumber: '200', + lastEventBlockNumber: '200', + lastEventLogIndex: 3, + totalReceived: 800n, + } ;(loadCachedProjectAccumulator as any).mockResolvedValue({ accumulator: mockAccumulator, blockNumber: '100', }) - mockGetProject.mockResolvedValue(updatedProject) + mockGetProjectFold.mockResolvedValue({ project: updatedProject, accumulator: updatedAccumulator }) await loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions) expect(saveCachedProjectAccumulator).toHaveBeenCalledWith( expect.objectContaining({ address: '0xProject' }), - expect.objectContaining({ blockNumber: '200' }), + updatedAccumulator, '200' ) }) - it('rewrites cache after a cached refetch even when block number is unchanged', async () => { + it('rewrites cache after a resumed fold even when the cursor is unchanged', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue({ accumulator: mockAccumulator, blockNumber: '100', }) - mockGetProject.mockResolvedValue(mockProject) + mockGetProjectFold.mockResolvedValue({ project: mockProject, accumulator: mockAccumulator }) await loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions) expect(saveCachedProjectAccumulator).toHaveBeenCalledWith( expect.objectContaining({ address: '0xProject' }), - expect.objectContaining({ blockNumber: '100', totalReceived: 500n }), + mockAccumulator, '100' ) }) it('returns null when SDK returns null and no cache', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue(null) - mockGetProject.mockResolvedValue(null) + mockGetProjectFold.mockResolvedValue(null) const result = await loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions) @@ -199,21 +211,21 @@ describe('loadProjectWithCache', () => { expect(saveCachedProjectAccumulator).not.toHaveBeenCalled() }) - it('returns cached result when SDK returns null but cache exists', async () => { + it('still returns the resumed project when no new events arrive', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue({ accumulator: mockAccumulator, blockNumber: '100', }) - mockGetProject.mockResolvedValue(null) + mockGetProjectFold.mockResolvedValue({ project: mockProject, accumulator: mockAccumulator }) const result = await loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions) - expect(result).toBeNull() + expect(result).toEqual(mockProject) }) - it('normalizes address to lowercase in cache key', async () => { + it('passes the original address into the cache key options', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue(null) - mockGetProject.mockResolvedValue(mockProject) + mockGetProjectFold.mockResolvedValue({ project: mockProject, accumulator: mockAccumulator }) await loadProjectWithCache(mockMachinery, '0xPROJECT', mockCacheOptions) @@ -224,7 +236,7 @@ describe('loadProjectWithCache', () => { it('throws when SDK call fails', async () => { ;(loadCachedProjectAccumulator as any).mockResolvedValue(null) - mockGetProject.mockRejectedValue(new Error('Network error')) + mockGetProjectFold.mockRejectedValue(new Error('Network error')) await expect(loadProjectWithCache(mockMachinery, '0xProject', mockCacheOptions)).rejects.toThrow('Network error') }) diff --git a/ui/src/shared/hooks/useCachedProject.ts b/ui/src/shared/hooks/useCachedProject.ts index 05c85597..e3d6051d 100644 --- a/ui/src/shared/hooks/useCachedProject.ts +++ b/ui/src/shared/hooks/useCachedProject.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { PROJECT_FOLD_VERSION, type Project, type ProjectAccumulator } from '@commonality/sdk/lazy-giving'; +import type { Project, ProjectAccumulator } from '@commonality/sdk/lazy-giving'; import type { SDKMachinery } from '@commonality/sdk/machinery'; import { loadCachedProjectAccumulator, @@ -20,20 +20,21 @@ interface UseCachedProjectOptions { cacheOptions: Omit; } -function projectToAccumulator(project: Project): ProjectAccumulator { +/** IndexedDB key material shared by every project-fold cache caller. */ +export function projectFoldCacheOptions( + machinery: SDKMachinery, +): Omit | null { + const factory = machinery.contractAddresses?.assuranceContractFactory + if (!machinery.eventCacheUrl || !factory) return null return { - foldVersion: PROJECT_FOLD_VERSION, - id: project.id, - erc1155Address: project.erc1155Address, - recipient: project.recipient, - conditionAddress: project.conditionAddress, - metadataCid: project.metadataCid, - createdAt: project.createdAt, - blockNumber: project.blockNumber, - lastEventBlockNumber: undefined, - lastEventLogIndex: undefined, - totalReceived: BigInt(project.totalReceived), - }; + eventCacheUrl: machinery.eventCacheUrl, + contractAddresses: { assuranceContractFactory: factory }, + foldType: 'project', + } +} + +function resumeFromBlock(accumulator: ProjectAccumulator, fallbackBlock: string): string { + return accumulator.lastEventBlockNumber ?? accumulator.blockNumber ?? fallbackBlock } export async function loadProjectWithCache( @@ -41,7 +42,7 @@ export async function loadProjectWithCache( projectAddress: string, cacheOptions: Omit ): Promise { - const { getProject } = await import('@commonality/sdk/lazy-giving'); + const { getProject, getProjectFold } = await import('@commonality/sdk/lazy-giving'); if (!projectAddress) { return null; @@ -61,27 +62,26 @@ export async function loadProjectWithCache( }; const cached = await loadCachedProjectAccumulator(cacheKeyOptions); - if (cached) { - const project = await getProject(machinery, projectAddress); - if (project) { - await saveCachedProjectAccumulator( - cacheKeyOptions, - projectToAccumulator(project), - project.blockNumber ?? cached.blockNumber - ); - } - return project; - } + const folded = cached + ? await getProjectFold(machinery, projectAddress, { + initialAccumulator: cached.accumulator, + blockNumber_gte: resumeFromBlock(cached.accumulator, cached.blockNumber), + }) + : await getProjectFold(machinery, projectAddress); - const project = await getProject(machinery, projectAddress); - if (project) { + if (folded) { await saveCachedProjectAccumulator( cacheKeyOptions, - projectToAccumulator(project), - project.blockNumber ?? '0' + folded.accumulator, + folded.accumulator.lastEventBlockNumber + ?? folded.project.blockNumber + ?? cached?.blockNumber + ?? '0' ); + return folded.project; } - return project; + + return null; } export function useCachedProject({ diff --git a/ui/src/shared/index.ts b/ui/src/shared/index.ts index cc906a07..be6c933b 100644 --- a/ui/src/shared/index.ts +++ b/ui/src/shared/index.ts @@ -103,7 +103,7 @@ export { } from './hooks/useBeatAgentTrustPolicy' export type { BeatAgentTrustPolicy } from './hooks/useBeatAgentTrustPolicy' -export { useCachedProject } from './hooks/useCachedProject' +export { loadProjectWithCache, projectFoldCacheOptions, useCachedProject } from './hooks/useCachedProject' export { useCachedProjects } from './hooks/useCachedProjects' export { getEventCacheUrl, getIpfsApiUrl, useMachinery } from './hooks/useMachinery' export { useMutedNudgers } from './hooks/useMutedNudgers' diff --git a/ui/src/shared/routing/domainUrls.ts b/ui/src/shared/routing/domainUrls.ts index 9cdd6b7f..e184e691 100644 --- a/ui/src/shared/routing/domainUrls.ts +++ b/ui/src/shared/routing/domainUrls.ts @@ -14,6 +14,7 @@ export type DomainId = | 'civility' | 'common-sense-majority' | 'conceptspace' + | 'causestarter' type DomainUrlRuntimeConfigKey = | 'VITE_COMMONALITY_URL' @@ -24,6 +25,7 @@ type DomainUrlRuntimeConfigKey = | 'VITE_CIVILITY_URL' | 'VITE_COMMON_SENSE_MAJORITY_URL' | 'VITE_CONCEPTSPACE_URL' + | 'VITE_CAUSESTARTER_URL' const domainUrlKeys: Record = { commonality: 'VITE_COMMONALITY_URL', @@ -34,6 +36,7 @@ const domainUrlKeys: Record = { civility: 'VITE_CIVILITY_URL', 'common-sense-majority': 'VITE_COMMON_SENSE_MAJORITY_URL', conceptspace: 'VITE_CONCEPTSPACE_URL', + causestarter: 'VITE_CAUSESTARTER_URL', } const domainHostLabels: Record = { @@ -45,6 +48,7 @@ const domainHostLabels: Record = { civility: 'civility', 'common-sense-majority': 'common-sense-majority', conceptspace: 'conceptspace', + causestarter: 'causestarter', } const knownDomainHostLabels = new Set(Object.values(domainHostLabels)) diff --git a/ui/src/test/setup.ts b/ui/src/test/setup.ts index 5b8f1760..880a4c31 100644 --- a/ui/src/test/setup.ts +++ b/ui/src/test/setup.ts @@ -14,6 +14,7 @@ const envVarsThatShouldNotLeakFromLocalDeployment = [ 'VITE_CIVILITY_URL', 'VITE_COMMON_SENSE_MAJORITY_URL', 'VITE_CONCEPTSPACE_URL', + 'VITE_CAUSESTARTER_URL', 'VITE_DEFAULT_TRUSTED_ATTESTERS', 'VITE_DEFAULT_TRUSTED_CONTENT_ATTESTERS', 'VITE_DEFAULT_TRUSTED_BEAT_AGENTS', diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json index 04005d46..a36f2395 100644 --- a/ui/tsconfig.app.json +++ b/ui/tsconfig.app.json @@ -22,7 +22,10 @@ "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "paths": { + "@ui/*": ["./src/*"] + } }, "include": ["src"], "exclude": ["src/**/*.test.tsx", "src/**/*.test.ts", "src/test"] diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 0ae9d656..756f060b 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -30,6 +30,7 @@ export default defineConfig(({ mode }) => { // notice SDK rebuilds while the dev server is running. One alias per SDK // subpath (the package has no flat barrel). ...sdkSourceAliases(), + '@ui': path.resolve(process.cwd(), 'src'), events: 'events', }, }, @@ -53,6 +54,16 @@ export default defineConfig(({ mode }) => { '/conceptspace': indexerUrl, // /status is polled by waitForIndexerToSyncToTxHash in E2E tests '/status': indexerUrl, + '/api/cause-assist': { + target: process.env.CAUSE_ASSIST_URL ?? 'http://localhost:3002', + changeOrigin: true, + rewrite: (proxyPath: string) => proxyPath.replace(/^\/api\/cause-assist/, ''), + }, + '/api/implication-attester': { + target: process.env.IMPLICATION_ATTESTER_URL ?? 'http://localhost:3006/implication-attester', + changeOrigin: true, + rewrite: (proxyPath: string) => proxyPath.replace(/^\/api\/implication-attester/, ''), + }, '/api': indexerUrl, // Proxy platform-api-service requests (runs at localhost:3001) '/api/platform-api': 'http://localhost:3001', @@ -161,6 +172,7 @@ function buildRuntimeConfig(env: Record) { 'VITE_NONINFLAMMATORY_URL', 'VITE_CSM_URL', 'VITE_CONCEPTSPACE_URL', + 'VITE_CAUSESTARTER_URL', ] return Object.fromEntries(keys.flatMap(key => env[key] ? [[key, env[key]]] : [])) } @@ -175,6 +187,7 @@ function resolveDomain(value: string | undefined) { case 'civility': case 'common-sense-majority': case 'conceptspace': + case 'causestarter': return value default: return 'commonality' diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts index a685a305..2d5c5f19 100644 --- a/ui/vitest.config.ts +++ b/ui/vitest.config.ts @@ -14,6 +14,11 @@ const repoRoot = fileURLToPath(new URL('..', import.meta.url)) export default defineConfig({ plugins: [react(), endUserDocsPlugin({ domain: 'commonality', includeAll: true })], + resolve: { + alias: { + '@ui': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, server: { fs: { allow: [repoRoot], From d0cd9c696ca717deae6a2470a0e8683cff56feda Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 17:01:06 -0400 Subject: [PATCH 09/13] Restore local Hardhat wallets after the CauseStarter fold. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The folded SPA boots ui wagmi config, which skipped CauseStarter's unlocked Hardhat connectors. ui/.env also sets VITE_E2E=true, so localhost got the mock connector and the Connect menu listed disabled accounts. Local loopback now registers Hardhat #0–#9; Playwright can still swap in _setupTestWallet. --- ui/src/causestarter/wagmi.ts | 128 +++-------------------------------- ui/src/main.tsx | 5 +- ui/src/wagmi.ts | 52 ++++++++++---- 3 files changed, 51 insertions(+), 134 deletions(-) diff --git a/ui/src/causestarter/wagmi.ts b/ui/src/causestarter/wagmi.ts index 82ab8684..fa1b58b8 100644 --- a/ui/src/causestarter/wagmi.ts +++ b/ui/src/causestarter/wagmi.ts @@ -1,119 +1,9 @@ -import { http, createConfig } from 'wagmi' -import { mainnet, base, baseSepolia, hardhat } from 'wagmi/chains' -import { getDefaultConfig, getDefaultConnectors } from 'connectkit' -import { injected, mock } from 'wagmi/connectors' -import { isAddress } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' -import type { MockParameters } from 'wagmi/connectors' -import { HARDHAT_DEV_ACCOUNTS, isLocalDevHost } from './lib/hardhatAccounts' -import { hardhatLocalConnector } from './lib/hardhatLocalConnector' - -export const walletConnectProjectId = (import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || '').trim() -export const isE2E = import.meta.env.VITE_E2E === 'true' -export const useLocalHardhatWallets = !isE2E && isLocalDevHost() - -const mainnetRpcUrl = import.meta.env.VITE_MAINNET_RPC_URL || 'https://ethereum-rpc.publicnode.com' -const baseRpcUrl = import.meta.env.VITE_BASE_RPC_URL || 'https://mainnet.base.org' -const baseSepoliaRpcUrl = import.meta.env.VITE_BASE_SEPOLIA_RPC_URL || 'https://baseSepolia.base.org' -const hardhatRpcUrl = import.meta.env.VITE_ETH_RPC_URL || 'http://127.0.0.1:8545' - -export const wagmiChains = [mainnet, base, baseSepolia, hardhat] as const - -export const wagmiTransports = { - [mainnet.id]: http(mainnetRpcUrl), - [base.id]: http(baseRpcUrl), - [baseSepolia.id]: http(baseSepoliaRpcUrl), - [hardhat.id]: http(hardhatRpcUrl), -} - -export function createMockConfig( - addressOrPkey: `0x${string}` = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', - features?: MockParameters['features'], -) { - const account = isAddress(addressOrPkey) - ? addressOrPkey - : privateKeyToAccount(addressOrPkey) - - const address = typeof account === 'string' ? account : account.address - - return createConfig({ - chains: [hardhat, mainnet, baseSepolia], - transports: wagmiTransports, - connectors: [mock({ accounts: [address], features })], - }) -} - -/** - * Localhost: only Hardhat #0–#9 connectors (no MetaMask / WalletConnect). - * Matches the local Docker stack on chain 31337. - */ -function buildLocalHardhatConfig() { - return createConfig({ - chains: [hardhat], - transports: { - [hardhat.id]: http(hardhatRpcUrl), - }, - connectors: HARDHAT_DEV_ACCOUNTS.map((account) => hardhatLocalConnector(account)), - multiInjectedProviderDiscovery: false, - ssr: false, - }) -} - -/** - * Build ConnectKit/wagmi config for non-local browser use. - * - * Recent ConnectKit defaults enable an Aave Account connector (`enableAaveAccount: true`) - * and only MetaMask as a *named* injected target. That combination has caused Connect - * modal failures and missing browser wallets in local CauseStarter deploys. - * - * We: - * - disable Aave Account - * - use ConnectKit defaults for Coinbase / WalletConnect (when project id present) - * - prepend a generic `injected()` connector so any browser extension wallet works - */ -function buildWagmiConfig() { - if (!walletConnectProjectId && typeof console !== 'undefined') { - console.warn( - '[CauseStarter] VITE_WALLETCONNECT_PROJECT_ID is not set. ' - + 'Browser-injected wallets still work; WalletConnect QR / mobile wallets will not. ' - + 'Get a free id at https://cloud.reown.com and rebuild with it set.', - ) - } - - const defaultConnectors = getDefaultConnectors({ - app: { - name: 'CauseStarter', - description: 'Do the part you’d do anyway — cooperate on agreement without a committee.', - url: typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8090', - }, - // Empty string → ConnectKit skips the WalletConnect connector (no broken project id). - walletConnectProjectId: walletConnectProjectId || '', - enableAaveAccount: false, - }) - - // Keep a generic injected connector first so any browser extension wallet works - // (ConnectKit defaults only target MetaMask by name). - const connectors = [ - injected({ shimDisconnect: true }), - ...defaultConnectors, - ] - - return createConfig( - getDefaultConfig({ - chains: wagmiChains, - transports: wagmiTransports, - connectors: connectors as never, - walletConnectProjectId: walletConnectProjectId || '', - appName: 'CauseStarter', - appDescription: 'Do the part you’d do anyway — cooperate on agreement without a committee.', - appUrl: typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8090', - enableAaveAccount: false, - }), - ) -} - -export const config = isE2E - ? createMockConfig() - : useLocalHardhatWallets - ? buildLocalHardhatConfig() - : buildWagmiConfig() +export { + config, + createMockConfig, + isE2E, + useLocalHardhatWallets, + wagmiChains, + wagmiTransports, + walletConnectProjectId, +} from '../wagmi' diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 3ea18a2b..255e0fe6 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -149,7 +149,8 @@ declare global { export function Root() { const [mode, setMode] = useState(getInitialColorMode) - const [wagmiConfig, setWagmiConfig] = useState(config) + const [testWagmiConfig, setTestWagmiConfig] = useState(null) + const wagmiConfig = testWagmiConfig ?? config const theme = useMemo(() => createAppTheme(mode), [mode]) const themeModeContextValue = useMemo(() => ({ @@ -166,7 +167,7 @@ export function Root() { const setupTestWallet = useCallback( (...args: Parameters) => { const newConfig = createMockConfig(...args) - setWagmiConfig(newConfig) + setTestWagmiConfig(newConfig) return newConfig }, [] diff --git a/ui/src/wagmi.ts b/ui/src/wagmi.ts index b8773f3b..a94bbc24 100644 --- a/ui/src/wagmi.ts +++ b/ui/src/wagmi.ts @@ -6,6 +6,8 @@ import { isAddress } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import type { MockParameters } from 'wagmi/connectors' import { isPrivySmartWalletEnabled } from './privy/config' +import { HARDHAT_DEV_ACCOUNTS, isLocalDevHost } from './causestarter/lib/hardhatAccounts' +import { hardhatLocalConnector } from './causestarter/lib/hardhatLocalConnector' export const walletConnectProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || '' export const isE2E = import.meta.env.VITE_E2E === 'true' @@ -13,7 +15,17 @@ export const privyAppId = import.meta.env.VITE_PRIVY_APP_ID?.trim() || '' export const privyClientId = import.meta.env.VITE_PRIVY_CLIENT_ID?.trim() || undefined export const privySmartWalletBundlerUrl = import.meta.env.VITE_PRIVY_SMART_WALLET_BUNDLER_URL?.trim() || '' export const privySmartWalletPaymasterUrl = import.meta.env.VITE_PRIVY_SMART_WALLET_PAYMASTER_URL?.trim() || undefined -export const isPrivyEnabled = !isE2E && privyAppId.length > 0 +/** + * Local Docker / vite: unlocked Hardhat accounts instead of browser wallets. + * + * Takes precedence over `VITE_E2E`. The ui package's `.env` sets `VITE_E2E=true` + * for Playwright, but that would otherwise install only the mock connector and + * leave the CauseStarter Hardhat account menu disabled. Playwright tests that + * need a specific account still call `window._setupTestWallet`. + */ +export const useLocalHardhatWallets = isLocalDevHost() + +export const isPrivyEnabled = !isE2E && !useLocalHardhatWallets && privyAppId.length > 0 export { isPrivySmartWalletEnabled } const mainnetRpcUrl = import.meta.env.VITE_MAINNET_RPC_URL || 'https://ethereum-rpc.publicnode.com' @@ -61,15 +73,29 @@ export function createMockConfig( }) } -export const config = isE2E - ? createMockConfig() - : createConfig( - getDefaultConfig({ - chains: wagmiChains, - transports: wagmiTransports, - walletConnectProjectId, - appName: 'Commonality', - appDescription: 'Fund projects and content around shared values', - appUrl: 'https://commonality.app', - }), - ) +function buildLocalHardhatConfig() { + return createConfig({ + chains: [hardhat], + transports: { + [hardhat.id]: http(hardhatRpcUrl), + }, + connectors: HARDHAT_DEV_ACCOUNTS.map((account) => hardhatLocalConnector(account)), + multiInjectedProviderDiscovery: false, + ssr: false, + }) +} + +export const config = useLocalHardhatWallets + ? buildLocalHardhatConfig() + : isE2E + ? createMockConfig() + : createConfig( + getDefaultConfig({ + chains: wagmiChains, + transports: wagmiTransports, + walletConnectProjectId, + appName: 'Commonality', + appDescription: 'Fund projects and content around shared values', + appUrl: 'https://commonality.app', + }), + ) From 04b91c41cca80b79536d632ef9b8560debcb92a2 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 17:19:11 -0400 Subject: [PATCH 10/13] Paint last cause-board snapshot on reload while folds revalidate. Keep metrics and aligned-project rows in the browser fold cache so a refresh shows the previous board with a corner spinner instead of a blank load. --- specs/tech/indexer/README.md | 10 +- .../components/AlignedProjectsList.test.tsx | 49 ++++- .../components/AlignedProjectsList.tsx | 67 ++++++- .../components/CauseBoard.test.tsx | 173 ++++++++++++++++++ .../fundingportals/components/CauseBoard.tsx | 74 +++++++- ui/src/shared/index.ts | 14 ++ ui/src/shared/stores/foldCache.test.ts | 67 +++++++ ui/src/shared/stores/foldCache.ts | 167 ++++++++++++++++- 8 files changed, 596 insertions(+), 25 deletions(-) create mode 100644 ui/src/fundingportals/components/CauseBoard.test.tsx diff --git a/specs/tech/indexer/README.md b/specs/tech/indexer/README.md index 140947c5..c9c8868b 100644 --- a/specs/tech/indexer/README.md +++ b/specs/tech/indexer/README.md @@ -69,13 +69,15 @@ All contract event emitters share the single event cache, including Conceptspace ## Fold Versioning and Upgrades -### Current state: accumulators exist, storage doesn't +### Current state: per-project accumulators are stored in the browser; most queries still fold from scratch The fold functions are designed for resumable folding. `foldProject` and `foldContributionsFromEvents` accept an optional `initialAccumulator` parameter and return the updated accumulator alongside the result. The intent is: store the accumulator + the block number it's current through, then on the next query fetch only new events and pass the saved accumulator in. -**However, no code currently stores or retrieves these accumulators.** The query layer (`getProject`, etc.) always folds from scratch. The resumable-fold infrastructure is there but unconnected. +The UI persists **per-project** accumulators in IndexedDB (`ui/src/shared/stores/foldCache.ts`) and `loadProjectWithCache` resumes from that cursor when present. Cause-board pages also persist the last successful **view snapshot** (metrics + aligned-project rows) so a reload can paint immediately while a live fold revalidates. -For now this is fine — folds are fast and entities are small. It becomes worth wiring up if fold latency becomes noticeable (e.g. a project with tens of thousands of contributions). +The default query helpers (`getProject`, `getAllAlignedProjectsForCause`, etc.) still fold from scratch unless a caller passes an accumulator. Alignment/implication walks are **not** resumable yet; that waits until entities are large enough that from-scratch folds hurt. + +For now this is fine at expected MVP scale. Wire more fold types into storage if fold latency becomes noticeable (e.g. a project with tens of thousands of contributions). ### If/when we store accumulators: versioning is required @@ -107,7 +109,7 @@ The only case where raw event interpretation changes is if a contract's ABI chan | Change | Impact | Action needed | |--------|--------|---------------| -| Fold logic fix (no accumulator shape change) | None while accumulators aren't stored | Once stored: bump `foldVersion` | +| Fold logic fix (no accumulator shape change) | Stored project accumulators invalid | Bump `foldVersion`; clients discard and re-fold | | Accumulator shape change | Stored accumulators invalid | Bump `foldVersion`; clients discard and re-fold | | New event type added to contract | None for old clients | Deploy new SDK to handle new events | | Event ABI breaking change | Raw events misinterpreted | Update decoder; rebuild event cache if needed | diff --git a/ui/src/fundingportals/components/AlignedProjectsList.test.tsx b/ui/src/fundingportals/components/AlignedProjectsList.test.tsx index ddd2a267..ffe7cad1 100644 --- a/ui/src/fundingportals/components/AlignedProjectsList.test.tsx +++ b/ui/src/fundingportals/components/AlignedProjectsList.test.tsx @@ -55,6 +55,18 @@ vi.mock('@commonality/sdk/lazy-giving', async () => { return { ...actual, getProject: vi.fn(), + getProjectFold: vi.fn(), + } +}) + +vi.mock('../../shared/stores/foldCache', async () => { + const actual = await vi.importActual( + '../../shared/stores/foldCache', + ) + return { + ...actual, + loadAlignedListSnapshot: vi.fn(async () => null), + saveAlignedListSnapshot: vi.fn(async () => {}), } }) @@ -94,7 +106,7 @@ import { getProject } from '@commonality/sdk/lazy-giving' import { createSDKMachinery } from '@commonality/sdk/machinery' import { readProjectMetadata } from './projectMetadata' import { useAccount } from 'wagmi' -import { getDomainUrl, isDomainConfigured, useTrustedSet } from '../../shared' +import { getDomainUrl, isDomainConfigured, loadAlignedListSnapshot, useTrustedSet } from '../../shared' import { useContentFundingState } from '../../content-funding' const mockMachinery = {} as any @@ -714,4 +726,39 @@ describe('AlignedProjectsList', () => { expect(seeAll).toHaveTextContent(/see all 3 projects/i) }) }) + + describe('Cached snapshot', () => { + it('paints the last list immediately and shows a corner spinner until the live fold returns', async () => { + vi.mocked(createSDKMachinery).mockReturnValue({ + eventCacheUrl: 'http://localhost:42069/api', + contractAddresses: { + assuranceContractFactory: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }, + } as any) + vi.mocked(loadAlignedListSnapshot).mockResolvedValue({ + snapshotVersion: 1, + projects: [makeProject({ projectAddress: ADDR_A })], + metadata: { [ADDR_A]: { name: 'Cached Garden' } }, + }) + let resolveLive: (value: ReturnType[]) => void = () => {} + vi.mocked(getAllAlignedProjectsForCause).mockReturnValue( + new Promise((resolve) => { + resolveLive = resolve + }), + ) + + render() + + expect(await screen.findByText('Cached Garden')).toBeInTheDocument() + expect(screen.getByTestId('trust-network-refresh')).toHaveAttribute( + 'aria-label', + 'Updating aligned projects from the latest events.', + ) + + resolveLive([makeProject({ projectAddress: ADDR_B })]) + await waitFor(() => { + expect(screen.queryByText('Cached Garden')).toBeNull() + }) + }) + }) }) diff --git a/ui/src/fundingportals/components/AlignedProjectsList.tsx b/ui/src/fundingportals/components/AlignedProjectsList.tsx index d3fd123c..7d2383eb 100644 --- a/ui/src/fundingportals/components/AlignedProjectsList.tsx +++ b/ui/src/fundingportals/components/AlignedProjectsList.tsx @@ -16,7 +16,19 @@ import SortIcon from '@mui/icons-material/Sort' import { getAllAlignedProjectsForCause } from '@commonality/sdk/fundingportals' import { getProject } from '@commonality/sdk/lazy-giving' import { ETH_CURRENCY, type IpfsCidV1 } from '@commonality/sdk/utils' -import { getDomainUrl, isDomainConfigured, useMachinery, useTrustedContentAttesters, useTrustedSet, TrustNetworkRefreshIndicator } from '../../shared' +import { + boardSnapshotCacheOptions, + getDomainUrl, + isDomainConfigured, + loadAlignedListSnapshot, + loadProjectWithCache, + projectFoldCacheOptions, + saveAlignedListSnapshot, + useMachinery, + useTrustedContentAttesters, + useTrustedSet, + TrustNetworkRefreshIndicator, +} from '../../shared' import { selectAlignedContentContracts, useContentFundingState } from '../../content-funding' import { getProjectStatus } from '../../lazy-giving' import { @@ -120,6 +132,7 @@ export function AlignedProjectsList({ const [projects, setProjects] = useState([]) const [metadata, setMetadata] = useState>({}) const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) const [error, setError] = useState(null) const [sortBy, setSortBy] = useState('latest') const [statusFilter, setStatusFilter] = useState(statusFilterLock ?? 'all') @@ -127,12 +140,32 @@ export function AlignedProjectsList({ useEffect(() => { let cancelled = false + const loadCids = cidsKey ? cidsKey.split('\0').filter(Boolean) : [] + const snapshotOptions = boardSnapshotCacheOptions(machinery, { + kind: 'aligned-list', + statementCids: loadCids, + implicationTrustKey, + alignmentTrustKey, + contentTrustKey, + }) async function load() { - keepPainted.beginLoad(setLoading) + const cached = snapshotOptions + ? await loadAlignedListSnapshot(snapshotOptions) + : null + if (cancelled) return + if (cached) { + setProjects(cached.projects as AlignedProject[]) + setMetadata(cached.metadata as Record) + keepPainted.markResolved() + setLoading(false) + setRefreshing(true) + } else { + keepPainted.beginLoad(setLoading) + setRefreshing(false) + } setError(null) try { - const loadCids = cidsKey ? cidsKey.split('\0') : [] const implicationForLoad = implicationTrustKey ? implicationTrustKey.split(',') : undefined @@ -171,9 +204,12 @@ export function AlignedProjectsList({ // Load metadata for every displayed row, including content-funding // contracts that never appear in the aligned-project query. + const projectCacheOptions = projectFoldCacheOptions(machinery) const metadataEntries = await Promise.all( displayed.map(async (p) => { - const fullProject = await getProject(machinery, p.projectAddress).catch(() => null) + const fullProject = projectCacheOptions + ? await loadProjectWithCache(machinery, p.projectAddress, projectCacheOptions).catch(() => null) + : await getProject(machinery, p.projectAddress).catch(() => null) if (!fullProject?.metadataCid) return [p.projectAddress, null] as const const data = await readProjectMetadata(machinery, fullProject.metadataCid as IpfsCidV1).catch(() => null) return [p.projectAddress, data] as const @@ -186,15 +222,24 @@ export function AlignedProjectsList({ if (data) newMetadata[addr] = data } setMetadata(newMetadata) + if (snapshotOptions) { + await saveAlignedListSnapshot(snapshotOptions, { + projects: displayed, + metadata: newMetadata, + }) + } } catch (err) { if (!cancelled) { console.error('Error loading aligned projects:', err) - setError(err instanceof Error ? err.message : 'Failed to load aligned projects') + if (!cached) { + setError(err instanceof Error ? err.message : 'Failed to load aligned projects') + } } } finally { if (!cancelled) { keepPainted.markResolved() setLoading(false) + setRefreshing(false) } } } @@ -259,12 +304,16 @@ export function AlignedProjectsList({ )} - {address && trustedSetLoading && trustedAlignmentAttesters === undefined && ( + {(refreshing || (address && trustedSetLoading && trustedAlignmentAttesters === undefined)) && ( )} diff --git a/ui/src/fundingportals/components/CauseBoard.test.tsx b/ui/src/fundingportals/components/CauseBoard.test.tsx new file mode 100644 index 00000000..538d5676 --- /dev/null +++ b/ui/src/fundingportals/components/CauseBoard.test.tsx @@ -0,0 +1,173 @@ +import { render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CauseBoard } from './CauseBoard' + +vi.mock('react-router-dom', () => { + function Link({ to, children, ...props }: any) { + return {children} + } + return { Link, RouterLink: Link } +}) + +vi.mock('wagmi', () => ({ + useAccount: vi.fn(), +})) + +vi.mock('@commonality/sdk/conceptspace', async () => { + const actual = await vi.importActual('@commonality/sdk/conceptspace') + return { + ...actual, + getStatementWithContent: vi.fn(), + } +}) + +vi.mock('@commonality/sdk/delegation', async () => { + const actual = await vi.importActual('@commonality/sdk/delegation') + return { + ...actual, + getMonthlyPledgedByCause: vi.fn(), + } +}) + +vi.mock('@commonality/sdk/fundingportals', async () => { + const actual = await vi.importActual('@commonality/sdk/fundingportals') + return { + ...actual, + getAllAlignedProjectsForCause: vi.fn(), + foldAlignedProjectFunding: vi.fn(), + } +}) + +vi.mock('../../shared/hooks/useMachinery', () => ({ + useMachinery: vi.fn(), +})) + +vi.mock('../../shared/hooks/useTrustedSet', () => ({ + useTrustedSet: vi.fn(), +})) + +vi.mock('../../shared/hooks/useTrustedAttesters', () => ({ + useTrustedAttesters: vi.fn(), +})) + +vi.mock('../../shared/hooks/useTrustedContentAttesters', () => ({ + useTrustedContentAttesters: vi.fn(() => []), +})) + +vi.mock('../../shared/stores/foldCache', async () => { + const actual = await vi.importActual( + '../../shared/stores/foldCache', + ) + return { + ...actual, + loadBoardMetricsSnapshot: vi.fn(async () => null), + saveBoardMetricsSnapshot: vi.fn(async () => {}), + } +}) + +vi.mock('../../content-funding', async () => { + const actual = await vi.importActual('../../content-funding') + return { + ...actual, + useContentFundingState: vi.fn(() => ({ + state: null, + channels: [], + contentAttestations: new Map(), + loading: false, + })), + } +}) + +vi.mock('./AlignedProjectsList', () => ({ + AlignedProjectsList: vi.fn(() =>
Aligned Projects List
), +})) + +vi.mock('./SuccessfulProjectsTab', () => ({ + SuccessfulProjectsTab: vi.fn(() =>
Successful Projects Tab
), +})) + +vi.mock('./AttestAlignmentForm', () => ({ + AttestAlignmentForm: vi.fn(() =>
Attest Alignment Form
), +})) + +import { getStatementWithContent } from '@commonality/sdk/conceptspace' +import { getMonthlyPledgedByCause } from '@commonality/sdk/delegation' +import { foldAlignedProjectFunding, getAllAlignedProjectsForCause } from '@commonality/sdk/fundingportals' +import { loadBoardMetricsSnapshot, useMachinery, useTrustedAttesters, useTrustedSet } from '../../shared' +import { useAccount } from 'wagmi' + +const USER_ADDRESS = '0x1111111111111111111111111111111111111111' + +const mockMachinery = { + eventCacheUrl: 'http://localhost:42069/api', + contractAddresses: { + recurringPledges: '0x9999999999999999999999999999999999999999', + assuranceContractFactory: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }, +} as any + +describe('CauseBoard', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(useAccount).mockReturnValue({ address: USER_ADDRESS } as any) + vi.mocked(useMachinery).mockReturnValue(mockMachinery) + vi.mocked(useTrustedSet).mockReturnValue({ + trustedSet: new Set([USER_ADDRESS]), + isLoading: false, + } as any) + vi.mocked(useTrustedAttesters).mockReturnValue([]) + vi.mocked(loadBoardMetricsSnapshot).mockResolvedValue(null) + vi.mocked(getAllAlignedProjectsForCause).mockResolvedValue([]) + vi.mocked(foldAlignedProjectFunding).mockResolvedValue({ + totalRaisedAcrossProjects: [], + remainingToThreshold: [], + totalUnreimbursed: [], + projectCount: 0, + } as any) + vi.mocked(getMonthlyPledgedByCause).mockResolvedValue(new Map()) + vi.mocked(getStatementWithContent).mockResolvedValue({ + statement: { title: 'Live title', excerpt: 'Live excerpt' }, + content: null, + } as any) + }) + + it('paints cached metrics immediately while the live fold is in flight', async () => { + vi.mocked(loadBoardMetricsSnapshot).mockResolvedValue({ + snapshotVersion: 1, + title: 'Cached cause', + summary: 'From last visit', + totalRaised: [], + remainingToThreshold: [], + totalUnreimbursed: [], + monthlyPledged: '0', + projectCount: 4, + }) + let resolveFold: (value: unknown) => void = () => {} + vi.mocked(foldAlignedProjectFunding).mockReturnValue( + new Promise((resolve) => { + resolveFold = resolve + }) as any, + ) + + render() + + expect(await screen.findByText('Cached cause')).toBeInTheDocument() + expect(screen.getByText('4')).toBeInTheDocument() + expect(screen.getByTestId('trust-network-refresh')).toHaveAttribute( + 'aria-label', + 'Updating this fundable-projects board from the latest events.', + ) + + resolveFold({ + totalRaisedAcrossProjects: [], + remainingToThreshold: [], + totalUnreimbursed: [], + projectCount: 1, + }) + + await waitFor(() => { + expect(screen.getByText('Live title')).toBeInTheDocument() + expect(screen.getByText('1')).toBeInTheDocument() + }) + }) +}) diff --git a/ui/src/fundingportals/components/CauseBoard.tsx b/ui/src/fundingportals/components/CauseBoard.tsx index 5e9c5a83..cb372ae4 100644 --- a/ui/src/fundingportals/components/CauseBoard.tsx +++ b/ui/src/fundingportals/components/CauseBoard.tsx @@ -25,10 +25,13 @@ import type { IpfsCidV1 } from '@commonality/sdk/utils' import { useMachinery } from '../../shared' import { DEFAULT_PAYMENT_CURRENCY, + boardSnapshotCacheOptions, formatCurrencyAmount, formatCurrencyTotals, getConfiguredPaymentCurrency, getDomainUrl, + loadBoardMetricsSnapshot, + saveBoardMetricsSnapshot, useTrustedSet, useTrustedAttesters, useTrustedContentAttesters, @@ -218,6 +221,7 @@ export function CauseBoard({ const [projectTab, setProjectTab] = useState< 'aligned' | 'successful' | 'reimbursed' | 'failed' >('aligned') + const [refreshing, setRefreshing] = useState(false) const keepPainted = useKeepPaintedWhileRefreshing() useEffect(() => { @@ -229,6 +233,13 @@ export function CauseBoard({ const attestersForLoad = trustedAttestersKey ? trustedAttestersKey.split(',') : undefined + const snapshotOptions = boardSnapshotCacheOptions(machinery, { + kind: 'board-metrics', + statementCids: loadCids, + implicationTrustKey: trustedAttestersKey, + alignmentTrustKey: trustedSetKey, + contentTrustKey, + }) async function load() { if (preview) { @@ -241,7 +252,25 @@ export function CauseBoard({ setError('No statement specified.') return } - keepPainted.beginLoad(setLoading) + const cached = snapshotOptions + ? await loadBoardMetricsSnapshot(snapshotOptions) + : null + if (cancelled) return + if (cached) { + setTitle(cached.title) + setSummary(cached.summary) + setTotalRaised(cached.totalRaised as typeof totalRaised) + setRemainingToThreshold(cached.remainingToThreshold as typeof remainingToThreshold) + setTotalUnreimbursed(cached.totalUnreimbursed as typeof totalUnreimbursed) + setMonthlyPledged(BigInt(cached.monthlyPledged || '0')) + setProjectCount(cached.projectCount) + keepPainted.markResolved() + setLoading(false) + setRefreshing(true) + } else { + keepPainted.beginLoad(setLoading) + setRefreshing(false) + } setError(null) try { const cid = loadCids[0]! @@ -284,15 +313,21 @@ export function CauseBoard({ if (cancelled) return - if (stmtResult) { - const t = + const nextTitle = stmtResult + ? ( stmtResult.statement.title ?? (stmtResult.content?.content ? stmtResult.content.content.split('\n')[0].replace(/^#+\s*/, '').trim() : null) ?? `Statement ${cid.slice(0, 12)}...` - setTitle(t) - setSummary(stmtResult.statement.excerpt ?? null) + ) + : (cached?.title ?? null) + const nextSummary = stmtResult + ? (stmtResult.statement.excerpt ?? null) + : (cached?.summary ?? null) + if (stmtResult) { + setTitle(nextTitle) + setSummary(nextSummary) } setTotalRaised(fundingMetrics.totalRaisedAcrossProjects) @@ -309,15 +344,30 @@ export function CauseBoard({ monthly += monthlyTotals.get(plankCid) ?? 0n } setMonthlyPledged(monthly) + + if (snapshotOptions) { + await saveBoardMetricsSnapshot(snapshotOptions, { + title: nextTitle, + summary: nextSummary, + totalRaised: fundingMetrics.totalRaisedAcrossProjects, + remainingToThreshold: fundingMetrics.remainingToThreshold, + totalUnreimbursed: fundingMetrics.totalUnreimbursed, + monthlyPledged: monthly.toString(), + projectCount: fundingMetrics.projectCount, + }) + } } catch (err) { if (!cancelled) { console.error('Error loading cause board:', err) - setError(err instanceof Error ? err.message : 'Failed to load fundable-projects board') + if (!cached) { + setError(err instanceof Error ? err.message : 'Failed to load fundable-projects board') + } } } finally { if (!cancelled) { keepPainted.markResolved() setLoading(false) + setRefreshing(false) } } } @@ -524,12 +574,16 @@ export function CauseBoard({ {headerExtra} - {address && trustedSetLoading && trustedAlignmentAttesters === undefined && ( + {(refreshing || (address && trustedSetLoading && trustedAlignmentAttesters === undefined)) && ( )} diff --git a/ui/src/shared/index.ts b/ui/src/shared/index.ts index be6c933b..04f14edf 100644 --- a/ui/src/shared/index.ts +++ b/ui/src/shared/index.ts @@ -85,6 +85,20 @@ export type { BridgeLabels, MediatorBridgeAnchor, MediatorBridgeCard } from './m // === stores/ — client-side persistence (contacts; folded-state cache via hooks) === export { addContact, getContacts } from './stores/contactStore' export type { ContactKind, SavedContact } from './stores/contactStore' +export { + BOARD_SNAPSHOT_VERSION, + boardSnapshotCacheOptions, + loadAlignedListSnapshot, + loadBoardMetricsSnapshot, + saveAlignedListSnapshot, + saveBoardMetricsSnapshot, +} from './stores/foldCache' +export type { + AlignedListSnapshot, + BoardMetricsSnapshot, + BoardSnapshotKeyOptions, + BoardSnapshotKind, +} from './stores/foldCache' // === trust/ — subjectiv trust network (computation + cache + worker live behind hooks) === export { notifySubjectivTrustNetworkInvalidated } from './trust/subjectivTrust' diff --git a/ui/src/shared/stores/foldCache.test.ts b/ui/src/shared/stores/foldCache.test.ts index db22167f..da158e7d 100644 --- a/ui/src/shared/stores/foldCache.test.ts +++ b/ui/src/shared/stores/foldCache.test.ts @@ -223,6 +223,73 @@ describe('foldCache', () => { ).resolves.toBeNull(); }); + it('round-trips cause-board snapshots isolated by trust fingerprint', async () => { + const { + boardSnapshotCacheOptions, + loadAlignedListSnapshot, + loadBoardMetricsSnapshot, + saveAlignedListSnapshot, + saveBoardMetricsSnapshot, + } = await import('./foldCache'); + + const machinery = { + eventCacheUrl: 'http://localhost:42069/api/board', + contractAddresses: { + assuranceContractFactory: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }, + }; + const metricsKey = boardSnapshotCacheOptions(machinery as never, { + kind: 'board-metrics', + statementCids: ['QmB', 'QmA'], + implicationTrustKey: '0x1', + alignmentTrustKey: '0x2', + contentTrustKey: '0x3', + }); + const listKey = boardSnapshotCacheOptions(machinery as never, { + kind: 'aligned-list', + statementCids: ['QmA', 'QmB'], + implicationTrustKey: '0x1', + alignmentTrustKey: '0x2', + contentTrustKey: '0x3', + }); + expect(metricsKey).not.toBeNull(); + expect(listKey).not.toBeNull(); + + await saveBoardMetricsSnapshot(metricsKey!, { + title: 'Garden', + summary: 'Grow food', + totalRaised: [{ symbol: 'ETH', amount: '1' }], + remainingToThreshold: [], + totalUnreimbursed: [], + monthlyPledged: '5', + projectCount: 2, + }); + await saveAlignedListSnapshot(listKey!, { + projects: [{ projectAddress: '0xabc', totalReceived: '1' }], + metadata: { '0xabc': { name: 'Beds' } }, + }); + + await expect(loadBoardMetricsSnapshot(metricsKey!)).resolves.toMatchObject({ + title: 'Garden', + projectCount: 2, + monthlyPledged: '5', + }); + await expect(loadAlignedListSnapshot(listKey!)).resolves.toMatchObject({ + projects: [{ projectAddress: '0xabc', totalReceived: '1' }], + metadata: { '0xabc': { name: 'Beds' } }, + }); + + const otherTrust = boardSnapshotCacheOptions(machinery as never, { + kind: 'board-metrics', + statementCids: ['QmA', 'QmB'], + implicationTrustKey: '0x1', + alignmentTrustKey: '0xother', + contentTrustKey: '0x3', + }); + await expect(loadBoardMetricsSnapshot(otherTrust!)).resolves.toBeNull(); + await expect(loadAlignedListSnapshot(metricsKey!)).resolves.toBeNull(); + }); + it('returns null when foldVersion mismatches', async () => { const { loadCachedProjectAccumulator, saveCachedProjectAccumulator } = await import( './foldCache' diff --git a/ui/src/shared/stores/foldCache.ts b/ui/src/shared/stores/foldCache.ts index 5081ea23..8b52ef60 100644 --- a/ui/src/shared/stores/foldCache.ts +++ b/ui/src/shared/stores/foldCache.ts @@ -1,11 +1,12 @@ import type { ProjectAccumulator } from '@commonality/sdk/lazy-giving'; -import type { ContractAddresses } from '@commonality/sdk/machinery'; +import type { ContractAddresses, SDKMachinery } from '@commonality/sdk/machinery'; const FOLD_CACHE_DB_NAME = 'commonality-fold-cache'; const FOLD_CACHE_DB_VERSION = 1; const FOLD_CACHE_STORE_NAME = 'fold-accumulators'; const FOLD_CACHE_VERSION = 'v2'; const CURRENT_PROJECT_FOLD_VERSION = 1; +export const BOARD_SNAPSHOT_VERSION = 1; export interface FoldCacheRecord { cacheKey: string; @@ -22,6 +23,43 @@ export interface FoldCacheOptions { foldType: 'project'; } +export type BoardSnapshotKind = 'board-metrics' | 'aligned-list'; + +export interface BoardSnapshotKeyOptions { + eventCacheUrl: string; + contractAddresses: Pick; + kind: BoardSnapshotKind; + statementCids: string[]; + implicationTrustKey: string; + alignmentTrustKey: string; + contentTrustKey: string; +} + +export interface BoardMetricsSnapshot { + snapshotVersion: number; + title: string | null; + summary: string | null; + totalRaised: unknown; + remainingToThreshold: unknown; + totalUnreimbursed: unknown; + monthlyPledged: string; + projectCount: number; +} + +export interface AlignedListSnapshot { + snapshotVersion: number; + projects: unknown[]; + metadata: Record; +} + +interface BoardSnapshotRecord { + cacheKey: string; + snapshotVersion: number; + kind: BoardSnapshotKind; + payload: unknown; + updatedAt: number; +} + let openDatabasePromise: Promise | null = null; function getCacheKey({ @@ -39,6 +77,51 @@ function getCacheKey({ ].join('::'); } +function getBoardSnapshotCacheKey({ + eventCacheUrl, + contractAddresses, + kind, + statementCids, + implicationTrustKey, + alignmentTrustKey, + contentTrustKey, +}: BoardSnapshotKeyOptions): string { + return [ + FOLD_CACHE_VERSION, + 'board-snapshot', + String(BOARD_SNAPSHOT_VERSION), + kind, + eventCacheUrl, + contractAddresses.assuranceContractFactory.toLowerCase(), + [...statementCids].sort().join('\0'), + implicationTrustKey, + alignmentTrustKey, + contentTrustKey, + ].join('::'); +} + +function toJsonValue(value: unknown): unknown { + return JSON.parse( + JSON.stringify(value, (_, entry) => + typeof entry === 'bigint' ? entry.toString() : entry, + ), + ); +} + +/** IndexedDB key material for a cause-board snapshot, or null when cache is unavailable. */ +export function boardSnapshotCacheOptions( + machinery: SDKMachinery, + rest: Omit, +): BoardSnapshotKeyOptions | null { + const factory = machinery.contractAddresses?.assuranceContractFactory; + if (!machinery.eventCacheUrl || !factory) return null; + return { + eventCacheUrl: machinery.eventCacheUrl, + contractAddresses: { assuranceContractFactory: factory }, + ...rest, + }; +} + function waitForRequest(request: IDBRequest): Promise { return new Promise((resolve, reject) => { request.addEventListener('success', () => { @@ -149,3 +232,85 @@ export async function saveCachedProjectAccumulator( await waitForRequest(store.put(record)); await waitForTransaction(transaction); } + +async function loadBoardSnapshotRecord( + options: BoardSnapshotKeyOptions, +): Promise { + try { + const database = await openFoldCacheDatabase(); + const transaction = database.transaction(FOLD_CACHE_STORE_NAME, 'readonly'); + const store = transaction.objectStore(FOLD_CACHE_STORE_NAME); + const record = await waitForRequest( + store.get(getBoardSnapshotCacheKey(options)) as IDBRequest, + ); + await waitForTransaction(transaction); + if (!record || record.snapshotVersion !== BOARD_SNAPSHOT_VERSION) { + return null; + } + return record; + } catch { + return null; + } +} + +async function saveBoardSnapshotRecord( + options: BoardSnapshotKeyOptions, + payload: unknown, +): Promise { + try { + const database = await openFoldCacheDatabase(); + const transaction = database.transaction(FOLD_CACHE_STORE_NAME, 'readwrite'); + const store = transaction.objectStore(FOLD_CACHE_STORE_NAME); + const record: BoardSnapshotRecord = { + cacheKey: getBoardSnapshotCacheKey(options), + snapshotVersion: BOARD_SNAPSHOT_VERSION, + kind: options.kind, + payload: toJsonValue(payload), + updatedAt: Date.now(), + }; + await waitForRequest(store.put(record)); + await waitForTransaction(transaction); + } catch { + // IndexedDB is a best-effort paint cache; a miss just shows the spinner. + } +} + +export async function loadBoardMetricsSnapshot( + options: BoardSnapshotKeyOptions, +): Promise { + const record = await loadBoardSnapshotRecord(options); + if (!record || record.kind !== 'board-metrics') return null; + const payload = record.payload as BoardMetricsSnapshot | null; + if (!payload || payload.snapshotVersion !== BOARD_SNAPSHOT_VERSION) return null; + return payload; +} + +export async function saveBoardMetricsSnapshot( + options: BoardSnapshotKeyOptions, + snapshot: Omit, +): Promise { + await saveBoardSnapshotRecord(options, { + ...snapshot, + snapshotVersion: BOARD_SNAPSHOT_VERSION, + }); +} + +export async function loadAlignedListSnapshot( + options: BoardSnapshotKeyOptions, +): Promise { + const record = await loadBoardSnapshotRecord(options); + if (!record || record.kind !== 'aligned-list') return null; + const payload = record.payload as AlignedListSnapshot | null; + if (!payload || payload.snapshotVersion !== BOARD_SNAPSHOT_VERSION) return null; + return payload; +} + +export async function saveAlignedListSnapshot( + options: BoardSnapshotKeyOptions, + snapshot: Omit, +): Promise { + await saveBoardSnapshotRecord(options, { + ...snapshot, + snapshotVersion: BOARD_SNAPSHOT_VERSION, + }); +} From c0b31ef8bf1688d0dca96546e5673035686c2edc Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 17:35:55 -0400 Subject: [PATCH 11/13] Drop unused CauseStarter route table and fix personal-board empty CTA. Live routing is the domain manifest. Local project empty states now point at /projects/new, IPFS still overlays causestarter/.env, and founder-e2e paths follow the ui/ fold. --- TODO.md | 5 + causestarter/README.md | 2 +- docs/end-user/causestarter/the-jobs.md | 2 +- scripts/publish-ui-to-ipfs.mjs | 3 + scripts/seed-causestarter-vite-env.py | 10 +- tsconfig.json | 2 - ui/src/causestarter/App.tsx | 118 ------------------ .../components/YourDashboard.test.tsx | 2 +- .../causestarter/components/YourDashboard.tsx | 17 --- .../pages/PersonalDashboardPage.tsx | 1 - .../components/AlignedProjectsList.test.tsx | 16 ++- .../components/AlignedProjectsList.tsx | 23 ++-- .../review/founder-e2e-causestarter.def.json | 17 ++- verifier/checks/review/founder-e2e.mjs | 2 +- 14 files changed, 56 insertions(+), 164 deletions(-) delete mode 100644 ui/src/causestarter/App.tsx diff --git a/TODO.md b/TODO.md index bff04719..753719b0 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,11 @@ When an item from this page is done and no longer needs an LLM implementor's att ---- +- **(Tell)** After folding CauseStarter into `ui/src/causestarter/`, leftover package + glue still talks as if `causestarter/src` is the SPA: `causestarter/vite.config.ts` + is unused, Compose/Docker docs mix `:8090` and `:5174`, and some verifier prompts + may still cite deleted paths. Sweep when touching local-dev docs. + - Add a fresh-stack integration test for the alignment-trust bootstrap: publish an alignment vouch from a previously unknown wallet, observe the service's `TrustSet(..., 100)`, confirm a wallet with no personal graph sees that vouch diff --git a/causestarter/README.md b/causestarter/README.md index ce828414..51ae0754 100644 --- a/causestarter/README.md +++ b/causestarter/README.md @@ -44,7 +44,7 @@ as main `CreateStatementForm`), not browser → Kubo API upload. 1. Start (or leave running) the local stack: `./scripts/services.sh --start` (or at least hardhat + indexer + **cause-assist** + gateway). `cause-assist` is a Compose service (loopback **:3002**). You do **not** need a separate `npm run cause-assist:*` process for normal UI work. -2. Seed `causestarter/.env` from the running Docker SPA config (contract addresses + tool domain URLs): +2. Seed `ui/.env` (and optionally overlay `causestarter/.env`) from the running Docker SPA config (contract addresses + tool domain URLs). Vite HMR on :5174 reads `ui/.env`. IPFS/local publish merges `ui/.env` then `causestarter/.env`, then root contract-address mappings: ```bash python3 scripts/seed-causestarter-vite-env.py diff --git a/docs/end-user/causestarter/the-jobs.md b/docs/end-user/causestarter/the-jobs.md index 77053b5a..9845c7a9 100644 --- a/docs/end-user/causestarter/the-jobs.md +++ b/docs/end-user/causestarter/the-jobs.md @@ -31,7 +31,7 @@ The cause board’s **Fundable Projects** list (the fundable-projects board) is - **…I can’t afford to pay for it myself.** Publish the project and let others pledge. Funds release if the threshold is met; otherwise everyone is refunded. - **…I don’t even know who to go to for funding.** You do not need a grant officer. Post the project, talk to a friend who is a bit better-connected in the trust graph, and get an **alignment** vouch onto a statement people already watch. The project then shows up on those cause boards. -Start from **Create Project** on the home page, or from a cause’s project list. +Start from **Start project** on a cause board, or from `/projects/new`. Occupied home is a watch list, not the create form. ## Wording diff --git a/scripts/publish-ui-to-ipfs.mjs b/scripts/publish-ui-to-ipfs.mjs index ae5273c8..a208f76e 100644 --- a/scripts/publish-ui-to-ipfs.mjs +++ b/scripts/publish-ui-to-ipfs.mjs @@ -93,6 +93,9 @@ async function loadUiBuildEnvFromFiles() { const rootEnv = await loadEnvFile(path.join(rootDir, '.env')) const uiEnv = await loadEnvFile(path.join(rootDir, 'ui', '.env')) const env = { ...uiEnv } + if (buildDomain === 'causestarter') { + Object.assign(env, await loadEnvFile(path.join(rootDir, 'causestarter', '.env'))) + } for (const [sourceKey, viteKey] of Object.entries(UI_ENV_ADDRESS_MAPPINGS)) { if (rootEnv[sourceKey]) { diff --git a/scripts/seed-causestarter-vite-env.py b/scripts/seed-causestarter-vite-env.py index 5eee3afa..c0ddf71b 100755 --- a/scripts/seed-causestarter-vite-env.py +++ b/scripts/seed-causestarter-vite-env.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Seed causestarter/.env from the running Docker SPA config.json for Vite HMR. +"""Seed ui/.env and causestarter/.env from Docker CauseStarter config.json. Requires CauseStarter Docker on http://localhost:8090 (or CAUSESTARTER_CONFIG_URL). Keeps the compose backends; only the SPA host switches to Vite (:5174). @@ -13,7 +13,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -OUT = ROOT / "causestarter" / ".env" +OUTS = (ROOT / "ui" / ".env", ROOT / "causestarter" / ".env") CONFIG_URL = os.environ.get("CAUSESTARTER_CONFIG_URL", "http://localhost:8090/config.json") PREFERRED = [ @@ -102,8 +102,10 @@ def main() -> int: if wc: lines.append(f"VITE_WALLETCONNECT_PROJECT_ID={wc}") - OUT.write_text("\n".join(lines) + "\n") - print(f"Wrote {OUT}") + text = "\n".join(lines) + "\n" + for out in OUTS: + out.write_text(text) + print(f"Wrote {out}") return 0 diff --git a/tsconfig.json b/tsconfig.json index 3f932253..632ccd6a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,8 +27,6 @@ "integration-tests/src/**/*.ts", "ui/src/**/*.ts", "ui/src/**/*.tsx", - "causestarter/src/**/*.ts", - "causestarter/src/**/*.tsx", "cause-assist/src/**/*.ts" ], "exclude": ["node_modules", "dist", ".turbo"] diff --git a/ui/src/causestarter/App.tsx b/ui/src/causestarter/App.tsx deleted file mode 100644 index ce56abb0..00000000 --- a/ui/src/causestarter/App.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { lazy, Suspense, type ReactNode } from 'react' -import { Navigate, Route } from 'react-router-dom' -import { CircularProgress, Stack } from '@mui/material' -import { HomePage } from './pages/HomePage' -import { PersonalDashboardPage } from './pages/PersonalDashboardPage' -import { WelcomePage } from './pages/WelcomePage' -import { StartCauseRedirect } from './pages/StartCauseRedirect' -import { StartBridgeRedirect } from './pages/StartBridgeRedirect' -import { BridgeClusterPage } from './pages/BridgeClusterPage' -import { BridgeTriplePage } from './pages/BridgeTriplePage' -import { CausesPage } from './pages/CausesPage' -import { CauseDetailPage } from './pages/CauseDetailPage' -import { CauseMediatorPage } from './pages/CauseMediatorPage' -import { CauseBoardLeaderboardPage } from './pages/CauseBoardLeaderboardPage' -import { StatementBoardLeaderboardPage } from './pages/StatementBoardLeaderboardPage' -import { StatementBoardRedirect } from './pages/StatementBoardRedirect' -import { StatementPage } from './pages/StatementPage' -import { StatementsPage } from './pages/StatementsPage' -import { SettingsPage } from './pages/SettingsPage' -import { ProjectDetailPage, ProjectLeaderboardPage } from './pages/ProjectDetailPage' -import { CreateProjectPage } from './pages/CreateProjectPage' -import { CauseContentBoardPage } from './pages/CauseContentBoardPage' -import { CauseFundingPage } from './pages/CauseFundingPage' -import { - ContentFundingAboutPage, - ContentFundingBrowsePage, - ContentFundingChannelPage, - ContentFundingContractPage, - ContentFundingCreateContractPage, - ContentFundingCreatorDashboardPage, - ContentFundingCreatorsPage, - ContentFundingExploreKindsPage, - ContentFundingLandingPage, - ContentFundingMaterializeFutureContentPage, - ContentFundingStartContractPage, -} from './pages/ContentFundingPages' -import { - DelegateProfilePage, - DepositPage, - MyNotesPage, - NoteDetailPage, -} from './pages/DelegationPages' - -const DocsPage = lazy(() => import('./pages/DocsPage').then((mod) => ({ default: mod.DocsPage }))) - -function DocsFallback() { - return ( - - - - ) -} - -function docsRoute(): ReactNode { - return ( - }> - - - ) -} - -/** CauseStarter route table composed into `VITE_DOMAIN=causestarter`. */ -export const causeStarterRoutes: ReactNode = ( - <> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - } /> - } /> - -) diff --git a/ui/src/causestarter/components/YourDashboard.test.tsx b/ui/src/causestarter/components/YourDashboard.test.tsx index aa338a02..8f8d1e39 100644 --- a/ui/src/causestarter/components/YourDashboard.test.tsx +++ b/ui/src/causestarter/components/YourDashboard.test.tsx @@ -110,7 +110,7 @@ describe('YourDashboard', () => {
, ) expect(screen.getByTestId('fundable-projects')).toHaveTextContent('preview:3:/dashboard:bafy1,bafy2') - expect(screen.getByTestId('home-dashboard-see-all')).toHaveAttribute('href', '/dashboard') + expect(screen.queryByTestId('home-dashboard-see-all')).toBeNull() }) it('renders the uncapped board on the dedicated page', () => { diff --git a/ui/src/causestarter/components/YourDashboard.tsx b/ui/src/causestarter/components/YourDashboard.tsx index b6a64c32..4a83dc35 100644 --- a/ui/src/causestarter/components/YourDashboard.tsx +++ b/ui/src/causestarter/components/YourDashboard.tsx @@ -1,5 +1,4 @@ import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material' -import { Link as RouterLink } from 'react-router-dom' import { CauseBoard } from '@ui/fundingportals' import { TrustNetworkRefreshIndicator } from '@ui/shared' import { AlignmentTrustGate } from './AlignmentTrustGate' @@ -14,12 +13,6 @@ const sectionHeadingSx = { fontWeight: 800, fontSize: { xs: '1.6rem', sm: '2rem' export const PERSONAL_DASHBOARD_PATH = '/dashboard' const HOME_PREVIEW_LIMIT = 3 -/** - * Personal fundable-projects board: union of work on statements this wallet signed. - * Not a cause board. Spec: specs/product/personal-dashboard.md - * - * `preview` (home): a few compact rows + link. `page`: the full board. - */ export function YourDashboard({ layout = 'preview', }: { @@ -47,16 +40,6 @@ export function YourDashboard({ title="Work vouched as advancing statements this wallet has signed — not membership in a cause board. Organizer pages stay for mixes someone else circulated." label="About your fundable-projects board" /> - {preview && connected && statementCids.length > 0 && ( - - )} Projects on statements you’ve signed. diff --git a/ui/src/causestarter/pages/PersonalDashboardPage.tsx b/ui/src/causestarter/pages/PersonalDashboardPage.tsx index 7edfbc80..877b1d2c 100644 --- a/ui/src/causestarter/pages/PersonalDashboardPage.tsx +++ b/ui/src/causestarter/pages/PersonalDashboardPage.tsx @@ -1,6 +1,5 @@ import { YourDashboard } from '../components/YourDashboard' -/** Full personal fundable-projects board (not a cause board). */ export function PersonalDashboardPage() { return } diff --git a/ui/src/fundingportals/components/AlignedProjectsList.test.tsx b/ui/src/fundingportals/components/AlignedProjectsList.test.tsx index ffe7cad1..7bf51581 100644 --- a/ui/src/fundingportals/components/AlignedProjectsList.test.tsx +++ b/ui/src/fundingportals/components/AlignedProjectsList.test.tsx @@ -307,17 +307,29 @@ describe('AlignedProjectsList', () => { expect(screen.queryByRole('link', { name: 'Browse all projects' })).not.toBeInTheDocument() }) - it('explains missing LazyGiving config instead of path-only create links', async () => { + it('creates locally when projectLinks is local', async () => { vi.mocked(getAllAlignedProjectsForCause).mockResolvedValue([]) vi.mocked(isDomainConfigured).mockReturnValue(false) render() + await waitFor(() => { + expect(screen.getByText(/No aligned projects yet/)).toBeInTheDocument() + }) + expect(screen.getByRole('link', { name: /Create a project/i })).toHaveAttribute('href', '/projects/new') + }) + + it('explains missing LazyGiving config instead of path-only create links', async () => { + vi.mocked(getAllAlignedProjectsForCause).mockResolvedValue([]) + vi.mocked(isDomainConfigured).mockReturnValue(false) + + render() + await waitFor(() => { expect(screen.getByText(/No aligned projects yet/)).toBeInTheDocument() }) expect(screen.queryByRole('link', { name: /Create a project/i })).not.toBeInTheDocument() - expect(screen.getByText(/Project creation still happens on LazyGiving/i)).toBeInTheDocument() + expect(screen.getByText(/Configure VITE_LAZYGIVING_URL/i)).toBeInTheDocument() }) it('shows "No projects match" message when all projects are filtered out', async () => { diff --git a/ui/src/fundingportals/components/AlignedProjectsList.tsx b/ui/src/fundingportals/components/AlignedProjectsList.tsx index 7d2383eb..29a2df31 100644 --- a/ui/src/fundingportals/components/AlignedProjectsList.tsx +++ b/ui/src/fundingportals/components/AlignedProjectsList.tsx @@ -368,28 +368,37 @@ export function AlignedProjectsList({ {projects.length === 0 - ? 'No aligned projects yet. Create one for this cause to get started.' + ? (fullPageTo + ? 'No aligned projects on the statements you have signed yet.' + : 'No aligned projects yet. Create one for this cause to get started.') : 'No projects match the current filters.'} {projects.length === 0 && ( - isDomainConfigured('lazyGiving') ? ( + projectLinks === 'local' ? ( + + + + ) : isDomainConfigured('lazyGiving') ? ( ) : ( - {projectLinks === 'local' - ? 'Project creation still happens on LazyGiving once its domain URL is configured.' - : 'Configure VITE_LAZYGIVING_URL to create a project for this cause.'} + Configure VITE_LAZYGIVING_URL to create a project for this cause. ) )} diff --git a/verifier/checks/review/founder-e2e-causestarter.def.json b/verifier/checks/review/founder-e2e-causestarter.def.json index 0ffea6d4..fcd072e7 100644 --- a/verifier/checks/review/founder-e2e-causestarter.def.json +++ b/verifier/checks/review/founder-e2e-causestarter.def.json @@ -23,15 +23,14 @@ "founderBriefFile": "checks/review/founder-e2e/causestarter.md", "startingPoints": [ "causestarter/README.md", - "causestarter/src/App.tsx", - "causestarter/src/pages/HomePage.tsx", - "causestarter/src/pages/StartCauseRedirect.tsx", - "causestarter/src/pages/CauseDetailPage.tsx", - "causestarter/src/pages/StatementBoardPage.tsx", - "causestarter/src/pages/StatementPage.tsx", - "causestarter/src/pages/ProjectDetailPage.tsx", - "causestarter/src/pages/CausesPage.tsx", - "causestarter/src/components/CauseViewStrip.tsx" + "ui/src/domains/causestarter/manifest.tsx", + "ui/src/causestarter/pages/HomePage.tsx", + "ui/src/causestarter/pages/StartCauseRedirect.tsx", + "ui/src/causestarter/pages/CauseDetailPage.tsx", + "ui/src/causestarter/pages/StatementPage.tsx", + "ui/src/causestarter/pages/ProjectDetailPage.tsx", + "ui/src/causestarter/pages/CausesPage.tsx", + "ui/src/causestarter/components/CauseViewStrip.tsx" ] } } diff --git a/verifier/checks/review/founder-e2e.mjs b/verifier/checks/review/founder-e2e.mjs index eb66ed7a..7373f1ae 100644 --- a/verifier/checks/review/founder-e2e.mjs +++ b/verifier/checks/review/founder-e2e.mjs @@ -44,7 +44,7 @@ function buildPrompt({ surface, founderBrief, startingPoints }) { The description is written by the founder in their own words. Do NOT reword, summarize, or "improve" it into a different spec. Treat the verbatim text as the checklist. Docs and code comments are evidence about the product, not a replacement for this brief.` })} Where to look: -- CauseStarter lives in \`causestarter/\` (not the eight-domain \`ui/\` package). Start from \`causestarter/README.md\` and the pages under \`causestarter/src/pages/\`. +- CauseStarter SPA source lives in \`ui/src/causestarter/\` (\`VITE_DOMAIN=causestarter\`). Glue/backlog stay in \`causestarter/\`. Start from \`causestarter/README.md\` and \`ui/src/domains/causestarter/manifest.tsx\`. - Local CauseStarter is typically http://localhost:5174 (Vite) or http://localhost:8090 (Docker SPA). You do not have a browser tool in this run; judge from source and docs unless a later run gives you live access. - Suggested starting points (open these first, then follow the product wherever it leads): ${hints.length > 0 ? hints.map((p) => ` - \`${p}\``).join("\n") : " - (none specified — locate the surface yourself from the README)"} From b725186dfe214f1f51e4c24aecd1c71fc6f5fda3 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 17:38:29 -0400 Subject: [PATCH 12/13] Tighten personal-board heading copy. Drop the duplicate subtitle and the club-vs-board lecture in the tooltip. --- ui/src/causestarter/components/YourDashboard.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ui/src/causestarter/components/YourDashboard.tsx b/ui/src/causestarter/components/YourDashboard.tsx index 4a83dc35..350889f5 100644 --- a/ui/src/causestarter/components/YourDashboard.tsx +++ b/ui/src/causestarter/components/YourDashboard.tsx @@ -34,16 +34,13 @@ export function YourDashboard({ - Your fundable projects + Fundable projects - - Projects on statements you’ve signed. - {!connected && ( From 269710c016547c99190e5bf26c4700ef5a46d879 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Mon, 24 Aug 2026 17:48:33 -0400 Subject: [PATCH 13/13] Tighten cause-board cards and personal-list copy. Compact CauseCard rows, denser lists, and clearer wallet/bookmark wording. --- ui/src/causestarter/components/CauseCard.tsx | 44 ++++++++++--------- ui/src/causestarter/components/YourCauses.tsx | 4 +- .../causestarter/components/YourDashboard.tsx | 4 +- .../components/YourProjects.test.tsx | 2 +- .../causestarter/components/YourProjects.tsx | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/ui/src/causestarter/components/CauseCard.tsx b/ui/src/causestarter/components/CauseCard.tsx index 08fd81b0..51fdbae0 100644 --- a/ui/src/causestarter/components/CauseCard.tsx +++ b/ui/src/causestarter/components/CauseCard.tsx @@ -21,8 +21,9 @@ export function CauseCard({ cause }: CauseCardProps) { elevation={0} sx={{ display: 'block', - p: 2.5, - borderRadius: 3, + px: 1.5, + py: 1, + borderRadius: 2, border: '1px solid', borderColor: 'divider', textDecoration: 'none', @@ -31,33 +32,34 @@ export function CauseCard({ cause }: CauseCardProps) { '&:active': { transform: 'scale(0.99)' }, '&:hover': { borderColor: 'primary.main', - boxShadow: (theme) => `0 8px 24px ${theme.palette.mode === 'light' ? 'rgba(15,118,110,0.12)' : 'rgba(0,0,0,0.35)'}`, + boxShadow: (theme) => `0 4px 12px ${theme.palette.mode === 'light' ? 'rgba(15,118,110,0.10)' : 'rgba(0,0,0,0.28)'}`, }, }} > - + - + {causeTitle(cause)} - {!isLive(cause) && ( - - )} + + {planks.length > 0 && ( + + {planks.length} statement{planks.length === 1 ? '' : 's'} + {publishedCount < planks.length && ` · ${publishedCount} published`} + + )} + {!isLive(cause) && ( + + )} + - - {planks.length > 0 && ( - - {planks.length} statement{planks.length === 1 ? '' : 's'} - {publishedCount < planks.length && ` · ${publishedCount} published`} - - )} ) } diff --git a/ui/src/causestarter/components/YourCauses.tsx b/ui/src/causestarter/components/YourCauses.tsx index a5f72e76..d96e56c1 100644 --- a/ui/src/causestarter/components/YourCauses.tsx +++ b/ui/src/causestarter/components/YourCauses.tsx @@ -86,7 +86,7 @@ export function YourCauses({ label="About bookmarked cause boards" /> - + {launched.map((cause) => ( ))} @@ -105,7 +105,7 @@ export function YourCauses({ label="About cause board drafts" /> - + {drafts.map((cause) => ( ))} diff --git a/ui/src/causestarter/components/YourDashboard.tsx b/ui/src/causestarter/components/YourDashboard.tsx index 350889f5..e2ea179c 100644 --- a/ui/src/causestarter/components/YourDashboard.tsx +++ b/ui/src/causestarter/components/YourDashboard.tsx @@ -37,14 +37,14 @@ export function YourDashboard({ Fundable projects {!connected && ( - Connect a wallet to see projects on statements you have signed. + Connect a wallet to see fundable projects vouched for as advancing statements you have signed. )} diff --git a/ui/src/causestarter/components/YourProjects.test.tsx b/ui/src/causestarter/components/YourProjects.test.tsx index 9ce30f32..1b8860e1 100644 --- a/ui/src/causestarter/components/YourProjects.test.tsx +++ b/ui/src/causestarter/components/YourProjects.test.tsx @@ -27,7 +27,7 @@ describe('YourProjects', () => { , ) - expect(screen.getByRole('heading', { name: 'Projects' })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Bookmarked projects' })).toBeInTheDocument() expect(screen.getByTestId('home-create-project')).toBeInTheDocument() expect(screen.getByText(/No projects yet/)).toBeInTheDocument() }) diff --git a/ui/src/causestarter/components/YourProjects.tsx b/ui/src/causestarter/components/YourProjects.tsx index 244ee7d0..1eacdf76 100644 --- a/ui/src/causestarter/components/YourProjects.tsx +++ b/ui/src/causestarter/components/YourProjects.tsx @@ -21,7 +21,7 @@ export function YourProjects() { > - Projects + Bookmarked projects