Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Commonality is a system for decentralized crowdfunding of public goods: people can fund projects aligned with shared values without needing a central organization to coordinate them. See [What is Commonality?](./specs/README.md#what-is-commonality) for the product overview.

- [Current focus](./focus.md) — the one or two kinds of work we are concentrating on right now (not a to-do list)
- [AI continuity notes](./CONTINUITY.md)
- [Fake / seed data plan](./fake-data-generation/PLAN.md) — tiny local world vs real Conceptspace statements vs mass fake activity; **next step for a fresh LLM**
- [To-do list](./TODO.md) — where LLMs file new one-shot tasks. Tag each with its [autonomy tier](./workflow/task-tiers.md) (Ask / Tell / Trust); untagged means Ask.
Expand Down
8 changes: 4 additions & 4 deletions causestarter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,10 @@ 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 / `/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` | Occupied-home teaser of the personal fundable-projects board |
| `home-dashboard-see-all` | Occupied home → `/dashboard` (full personal list) |
| `home-landing` | Root landing (first-visit pitch plus role cards) |
| `home-dashboard` | Role-card grid on the home landing |
| `home-dashboard-board` | Preview layout of the personal fundable-projects board (unused on home now) |
| `nav-profile` | Header icon → `/profile` |
| `personal-dashboard-page` | Full personal fundable-projects board at `/dashboard` |
| `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 board” coach copy here) |
Expand Down
9 changes: 5 additions & 4 deletions causestarter/e2e/connect-and-start.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,16 @@ test.describe('CauseStarter agent smoke', () => {
await page.goto(appPath('/'))
})

test('occupied home shows the personal fundable-projects board after connect', async ({ page }) => {
test('personal fundable-projects board lives under Fund 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()
await page.getByTestId('nav-fund').click()
await expect(page.getByTestId('personal-dashboard-page')).toBeVisible({ timeout: 15_000 })
await expect(page.getByRole('heading', { name: /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()
await expect(page.getByTestId('home-landing')).toBeVisible()
await connectHardhat0(page)
await startCause(page)

Expand Down
17 changes: 17 additions & 0 deletions focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Current focus

High-level work we are actually concentrating on right now. Not a to-do list — those live in [TODO.md](./TODO.md), [causestarter/TODO.md](./causestarter/TODO.md), and the [testnet working plan](./workflow/testnet-working-plan.md). At most three items. Keep this file current.

## 1. Make testnet usable

The MVP is implemented in code; the live testnet stack is not yet a credible shared lab. Goal: Adam and Sam can use the deployed testnet without fighting ops, config, or broken paths.

Ordered work is in [`workflow/testnet-working-plan.md`](./workflow/testnet-working-plan.md). Do the next unchecked item there. Mass fake activity is **not** part of this focus.

Related: [project status](./workflow/project-status.md), [MVP](./specs/product/mvp.md), verifier reports.

## 2. Finish the CauseStarter UI

CauseStarter is the founder-first surface and the intended primary entry. Goal: it is complete enough as a product UI that remaining items are known leftovers, not “the app isn’t there yet.”

Backlog: [`causestarter/TODO.md`](./causestarter/TODO.md). Product/tech domain notes: [product UI domains](./specs/product/ui-domains.md), [technical UI domains](./specs/tech/ui-domains.md).
59 changes: 59 additions & 0 deletions ui/src/causestarter/hooks/useUserAlignments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useCallback, useEffect, useState } from 'react'
import { useAccount } from 'wagmi'
import { getAlignmentsByAttester, type AlignmentAttestation } from '@commonality/sdk/fundingportals'
import { useMachinery } from '../../shared'

export function useUserAlignments(): {
attestations: AlignmentAttestation[]
loading: boolean
connected: boolean
error: string | null
refresh: () => void
} {
const machinery = useMachinery()
const { address } = useAccount()
const [attestations, setAttestations] = useState<AlignmentAttestation[]>([])
const [loading, setLoading] = useState(Boolean(address))
const [error, setError] = useState<string | null>(null)
const [tick, setTick] = useState(0)

const refresh = useCallback(() => setTick((n) => n + 1), [])

useEffect(() => {
let cancelled = false

const run = async () => {
if (!address) {
if (!cancelled) {
setAttestations([])
setError(null)
setLoading(false)
}
return
}

if (!cancelled) setLoading(true)
try {
const next = await getAlignmentsByAttester(machinery, address)
if (!cancelled) {
setAttestations(next)
setError(null)
}
} catch (cause) {
if (!cancelled) {
setAttestations([])
setError(cause instanceof Error ? cause.message : 'Could not load alignment attestations')
}
} finally {
if (!cancelled) setLoading(false)
}
}

void run()
return () => {
cancelled = true
}
}, [machinery, address, tick])

return { attestations, loading, connected: Boolean(address), error, refresh }
}
10 changes: 8 additions & 2 deletions ui/src/causestarter/pages/HomePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,23 @@ function renderHome({ connected = false, statements = 0, pledges = 0, notes = 0
render(<MemoryRouter><HomePage /></MemoryRouter>)
}

describe('HomePage role launcher', () => {
describe('HomePage landing', () => {
afterEach(cleanup)

it('gives every visitor the focused role cards', () => {
it('explains the product to a first-time visitor and still offers the role cards', () => {
renderHome()
expect(screen.getByTestId('home-landing')).toBeInTheDocument()
expect(screen.getByRole('heading', { name: 'There are enough of us. We just couldn’t work together.' })).toBeInTheDocument()
expect(screen.getByText('A cause board is a bulletin of crowdfundable projects', { exact: false })).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Start a cause board' })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Read the short version' })).toHaveAttribute('href', '/docs')
expect(screen.getByRole('heading', { name: 'What would you like to do?' })).toBeInTheDocument()
expect(screen.getByTestId('home-role-sign')).toHaveAttribute('href', '/statements')
expect(screen.getByTestId('home-role-donate')).toHaveAttribute('href', '/donate')
expect(screen.getByTestId('home-role-fund')).toHaveAttribute('href', '/dashboard')
expect(screen.getByTestId('home-role-work')).toHaveAttribute('href', '/work')
expect(screen.getByTestId('home-role-organize')).toHaveAttribute('href', '/causes')
expect(screen.getByTestId('home-profile-link')).toHaveAttribute('href', '/profile')
expect(screen.getByText('Signing does not commit money.', { exact: false })).toBeInTheDocument()
})

Expand Down
181 changes: 121 additions & 60 deletions ui/src/causestarter/pages/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'
import { Box, Card, CardActionArea, CardContent, CircularProgress, Stack, Typography } from '@mui/material'
import { Box, Button, Card, CardActionArea, CardContent, CircularProgress, Paper, Stack, Typography } from '@mui/material'
import type { ReactNode } from 'react'
import { Link as RouterLink } from 'react-router-dom'
import { useAccount } from 'wagmi'
Expand Down Expand Up @@ -51,70 +51,131 @@ export function HomePage() {
const fundingBoard = readPersonalFundingBoard(address)

return (
<Stack spacing={{ xs: 3, sm: 4 }} data-testid="home-dashboard">
<Box sx={{ maxWidth: 680 }}>
<Typography variant="h3" component="h1" sx={{ fontWeight: 850, fontSize: { xs: '2rem', sm: '2.7rem' }, letterSpacing: '-0.035em' }}>
<Stack spacing={{ xs: 3, sm: 4 }} data-testid="home-landing">
<Paper
elevation={0}
sx={{
p: { xs: 2.5, sm: 3.5 },
borderRadius: 4,
border: '1px solid',
borderColor: 'divider',
background: (theme) =>
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%)',
}}
>
<Typography variant="overline" sx={{ letterSpacing: '0.14em', fontWeight: 700, color: 'primary.main' }}>
CauseStarter
</Typography>
<Typography
variant="h3"
component="h1"
sx={{
mt: 0.5,
fontWeight: 800,
letterSpacing: '-0.03em',
fontSize: { xs: '1.85rem', sm: '2.35rem' },
lineHeight: 1.15,
}}
>
There are enough of us. We just couldn’t work together.
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mt: 1.5, maxWidth: 640 }}>
A cause board is a bulletin of crowdfundable projects: raise a specific
amount by a date to do a specific piece of work. If the crowd doesn’t
show, contributors get their money back. That is a third way besides
“government does it” and “a big charity does it.”
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mt: 1.25, maxWidth: 640 }}>
You do not have to watch the board — pledge a monthly amount to someone
you already trust. You do not have to bet on pitches — reimburse work
that already delivered. Signing a statement does not spend money.
</Typography>

<Button
component={RouterLink}
to="/docs"
sx={{ mt: 2, px: 0, textTransform: 'none', fontWeight: 700 }}
>
Read the short version
</Button>
</Paper>

<Box>
<Typography variant="h4" component="h2" sx={{ fontWeight: 850, fontSize: { xs: '1.45rem', sm: '1.75rem' }, letterSpacing: '-0.03em' }}>
What would you like to do?
</Typography>
<Typography color="text.secondary" sx={{ mt: 1.25, fontSize: { sm: '1.05rem' } }}>
Choose a job. Each workspace stays focused, and you can come back here whenever you want to switch roles.
<Typography color="text.secondary" sx={{ mt: 1, mb: 2, maxWidth: 680, fontSize: { sm: '1.05rem' } }}>
Money, judgment, skilled work, and organizing do not have to arrive in
one organization. Pick the job you would do anyway. Come back here to
switch.
</Typography>
</Box>

<Box sx={{ display: 'grid', gap: 2, gridTemplateColumns: { xs: '1fr', md: 'repeat(2, minmax(0, 1fr))' } }}>
<RoleCard
title="Sign"
to="/statements"
description="Express what you believe, improve the wording, and discover common ground. Signing does not commit money."
loading={isConnected && statementsLoading}
summary={isConnected && statements.length > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(statements.length, 'signed statement')}</Typography>
: <Typography variant="body2" color="text.secondary">Find a statement worth standing behind.</Typography>}
action={statements.length > 0 ? 'Continue signing' : 'Explore statements'}
/>
<RoleCard
title="Donate"
to="/donate"
description="Pledge money to a cause, entrust it to someone you trust, and check what your money has done."
loading={isConnected && donation.loading}
summary={hasDonateActivity
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(donation.activePledgeCount, 'monthly pledge')} · {countLabel(donation.activeNoteCount, 'active fund')}</Typography>
: <Typography variant="body2" color="text.secondary">Set up giving that does not need your daily attention.</Typography>}
action={hasDonateActivity ? 'Manage donations' : 'Set up a donation'}
/>
<RoleCard
title="Fund"
to="/dashboard"
description="Review relevant projects and actively decide where available money should go."
loading={isConnected && statementsLoading}
summary={fundingBoard
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(fundingBoard.statementCids.length, 'statement')} in your board{fundingBoard.geographicWithin?.length ? ` · ${fundingBoard.geographicWithin.join(', ')}` : ''}</Typography>
: statements.length > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(statements.length, 'signed statement')} (default board)</Typography>
: <Typography variant="body2" color="text.secondary">Set the scope of your personal funding board.</Typography>}
action={fundingBoard || statements.length > 0 ? 'Review projects' : 'Set up your funding board'}
/>
<RoleCard
title="Work"
to="/work"
description="Create a project, follow the ones you started, and keep a shared bookmark list of work you care about."
loading={isConnected && projectsLoading}
summary={createdProjects > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(createdProjects, 'created project')}</Typography>
: <Typography variant="body2" color="text.secondary">Publish a piece of work people can fund.</Typography>}
action={createdProjects > 0 ? 'Continue your work' : 'Start a project'}
/>
<RoleCard
title="Organize"
to="/causes"
description="Publish cause boards and help people coordinate around shared statements."
loading={causesLoading}
summary={causes.length > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(causes.length, 'cause board')}</Typography>
: <Typography variant="body2" color="text.secondary">Turn a useful mix of statements into a board people can share.</Typography>}
action={causes.length > 0 ? 'Continue organizing' : 'Start organizing'}
/>
<Box sx={{ display: 'grid', gap: 2, gridTemplateColumns: { xs: '1fr', md: 'repeat(2, minmax(0, 1fr))' } }} data-testid="home-dashboard">
<RoleCard
title="Sign"
to="/statements"
description="Express what you believe, improve the wording, and discover common ground. Signing does not commit money."
loading={isConnected && statementsLoading}
summary={isConnected && statements.length > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(statements.length, 'signed statement')}</Typography>
: <Typography variant="body2" color="text.secondary">Find a statement worth standing behind.</Typography>}
action={statements.length > 0 ? 'Continue signing' : 'Explore statements'}
/>
<RoleCard
title="Donate"
to="/donate"
description="Pledge money to a cause, entrust it to someone you trust, and check what your money has done."
loading={isConnected && donation.loading}
summary={hasDonateActivity
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(donation.activePledgeCount, 'monthly pledge')} · {countLabel(donation.activeNoteCount, 'active fund')}</Typography>
: <Typography variant="body2" color="text.secondary">Set up giving that does not need your daily attention.</Typography>}
action={hasDonateActivity ? 'Manage donations' : 'Set up a donation'}
/>
<RoleCard
title="Fund"
to="/dashboard"
description="Review relevant projects and actively decide where available money should go."
loading={isConnected && statementsLoading}
summary={fundingBoard
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(fundingBoard.statementCids.length, 'statement')} in your board{fundingBoard.geographicWithin?.length ? ` · ${fundingBoard.geographicWithin.join(', ')}` : ''}</Typography>
: statements.length > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(statements.length, 'signed statement')} (default board)</Typography>
: <Typography variant="body2" color="text.secondary">Set the scope of your personal funding board.</Typography>}
action={fundingBoard || statements.length > 0 ? 'Review projects' : 'Set up your funding board'}
/>
<RoleCard
title="Work"
to="/work"
description="Create a project, follow the ones you started, and keep a shared bookmark list of work you care about."
loading={isConnected && projectsLoading}
summary={createdProjects > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(createdProjects, 'created project')}</Typography>
: <Typography variant="body2" color="text.secondary">Publish a piece of work people can fund.</Typography>}
action={createdProjects > 0 ? 'Continue your work' : 'Start a project'}
/>
<RoleCard
title="Organize"
to="/causes"
description="Publish cause boards and help people coordinate around shared statements."
loading={causesLoading}
summary={causes.length > 0
? <Typography variant="body2" sx={{ fontWeight: 700 }}>{countLabel(causes.length, 'cause board')}</Typography>
: <Typography variant="body2" color="text.secondary">Turn a useful mix of statements into a board people can share.</Typography>}
action={causes.length > 0 ? 'Continue organizing' : 'Start organizing'}
/>
</Box>
</Box>

<Button
component={RouterLink}
to="/profile"
data-testid="home-profile-link"
sx={{ alignSelf: 'flex-start', px: 0, textTransform: 'none', fontWeight: 700 }}
>
See what you’ve done
</Button>
</Stack>
)
}
Loading
Loading