901 lines
16 KiB
Markdown
901 lines
16 KiB
Markdown
---
|
|
name: policyengine-app
|
|
description: PolicyEngine React web application - the user interface at policyengine.org
|
|
---
|
|
|
|
# PolicyEngine App
|
|
|
|
The PolicyEngine App is the React-based web application that users interact with at policyengine.org.
|
|
|
|
## For Users 👥
|
|
|
|
### What is the App?
|
|
|
|
The app at policyengine.org provides:
|
|
- Interactive household calculator
|
|
- Policy reform creator
|
|
- Population impact analysis
|
|
- Blog and research hub
|
|
|
|
**Access:** https://policyengine.org
|
|
|
|
### App Features
|
|
|
|
**Calculator:**
|
|
- Enter household details
|
|
- See tax and benefit calculations
|
|
- Visualize marginal tax rates
|
|
- Compare scenarios
|
|
|
|
**Policy designer:**
|
|
- Browse all parameters
|
|
- Create custom reforms
|
|
- Share via URL
|
|
- Download charts
|
|
|
|
**Research hub:**
|
|
- Read policy analysis
|
|
- Explore modeled programs
|
|
- Access documentation
|
|
|
|
## For Analysts 📊
|
|
|
|
### Understanding App URLs
|
|
|
|
Reform URLs encode all policy changes in the query string, allowing sharing and reproducibility.
|
|
|
|
**Example URL:**
|
|
```
|
|
policyengine.org/us/policy?
|
|
focus=policyOutput.policyBreakdown&
|
|
reform=67696&
|
|
region=enhanced_us&
|
|
timePeriod=2025&
|
|
baseline=2
|
|
```
|
|
|
|
**Parameters:**
|
|
- `focus` - Which section to display
|
|
- `reform` - Reform ID from database
|
|
- `region` - Geographic scope (enhanced_us, CA, congressional districts)
|
|
- `timePeriod` - Year of analysis
|
|
- `baseline` - Baseline policy ID
|
|
|
|
### Embedding PolicyEngine
|
|
|
|
**iFrame integration:**
|
|
```html
|
|
<iframe
|
|
src="https://policyengine.org/us/household?embedded=true"
|
|
width="100%"
|
|
height="800">
|
|
</iframe>
|
|
```
|
|
|
|
**Parameter:**
|
|
- `embedded=true` - Removes navigation, optimizes for embedding
|
|
|
|
### URL Structure
|
|
|
|
**Household calculator:**
|
|
```
|
|
/us/household?household=12345
|
|
/uk/household?household=67890
|
|
```
|
|
|
|
**Policy page:**
|
|
```
|
|
/us/policy?reform=12345
|
|
/uk/policy?reform=67890
|
|
```
|
|
|
|
**Research/blog:**
|
|
```
|
|
/us/research/article-slug
|
|
/uk/research/article-slug
|
|
```
|
|
|
|
## For Contributors 💻
|
|
|
|
### Repository
|
|
|
|
**Location:** PolicyEngine/policyengine-app
|
|
|
|
**Clone:**
|
|
```bash
|
|
git clone https://github.com/PolicyEngine/policyengine-app
|
|
cd policyengine-app
|
|
```
|
|
|
|
### Current Architecture
|
|
|
|
**To see current structure:**
|
|
```bash
|
|
tree src/ -L 2
|
|
|
|
# Key directories:
|
|
ls src/
|
|
# - pages/ - Page components
|
|
# - applets/ - Reusable UI modules
|
|
# - api/ - API integration
|
|
# - controls/ - Form controls
|
|
# - layout/ - Layout components
|
|
# - posts/ - Blog posts
|
|
# - routing/ - Routing configuration
|
|
# - hooks/ - Custom React hooks
|
|
# - data/ - Static data
|
|
```
|
|
|
|
### Technology Stack
|
|
|
|
**Current dependencies:**
|
|
```bash
|
|
# See package.json for versions
|
|
cat package.json
|
|
|
|
# Key dependencies:
|
|
# - React 18
|
|
# - React Router v6
|
|
# - Plotly.js
|
|
# - Ant Design
|
|
# - axios
|
|
```
|
|
|
|
### React Patterns (Critical)
|
|
|
|
**✅ Functional components only (no classes):**
|
|
```javascript
|
|
// CORRECT
|
|
import { useState, useEffect } from "react";
|
|
|
|
export default function TaxCalculator({ income }) {
|
|
const [tax, setTax] = useState(0);
|
|
|
|
useEffect(() => {
|
|
calculateTax(income).then(setTax);
|
|
}, [income]);
|
|
|
|
return <div>Tax: ${tax}</div>;
|
|
}
|
|
```
|
|
|
|
**❌ Class components forbidden:**
|
|
```javascript
|
|
// WRONG - Don't use class components
|
|
class TaxCalculator extends Component {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
**To find component examples:**
|
|
```bash
|
|
# Reference components
|
|
ls src/pages/
|
|
ls src/applets/
|
|
|
|
# See a complete page
|
|
cat src/pages/HouseholdPage.jsx
|
|
```
|
|
|
|
### State Management
|
|
|
|
**No global state (Redux, Context) - lift state up:**
|
|
|
|
```javascript
|
|
// Parent manages state
|
|
function PolicyPage() {
|
|
const [reform, setReform] = useState({});
|
|
const [impact, setImpact] = useState(null);
|
|
|
|
return (
|
|
<>
|
|
<PolicyEditor reform={reform} onChange={setReform} />
|
|
<ImpactDisplay impact={impact} />
|
|
</>
|
|
);
|
|
}
|
|
```
|
|
|
|
**To see state patterns:**
|
|
```bash
|
|
# Find useState usage
|
|
grep -r "useState" src/pages/ | head -20
|
|
```
|
|
|
|
### API Integration
|
|
|
|
**To see current API patterns:**
|
|
```bash
|
|
cat src/api/call.js # Base API caller
|
|
cat src/api/variables.js # Variable metadata
|
|
cat src/api/parameters.js # Parameter metadata
|
|
```
|
|
|
|
**Standard pattern:**
|
|
```javascript
|
|
import { api call } from "api/call";
|
|
|
|
// Fetch data
|
|
const result = await call(
|
|
`/us/calculate`,
|
|
{ household: householdData },
|
|
"POST"
|
|
);
|
|
```
|
|
|
|
### Routing
|
|
|
|
**To see current routing:**
|
|
```bash
|
|
cat src/routing/routes.js
|
|
|
|
# Routes defined with React Router v6
|
|
# See examples:
|
|
grep -r "useNavigate" src/
|
|
grep -r "useSearchParams" src/
|
|
```
|
|
|
|
**URL parameters:**
|
|
```javascript
|
|
import { useSearchParams } from "react-router-dom";
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
// Read
|
|
const reformId = searchParams.get("reform");
|
|
|
|
// Update
|
|
setSearchParams({ ...Object.fromEntries(searchParams), reform: newId });
|
|
```
|
|
|
|
### Custom Hooks
|
|
|
|
**To see PolicyEngine-specific hooks:**
|
|
```bash
|
|
ls src/hooks/
|
|
# - useCountryId.js - Current country
|
|
# - useDisplayCategory.js
|
|
# - etc.
|
|
```
|
|
|
|
**Usage:**
|
|
```javascript
|
|
import { useCountryId } from "hooks/useCountryId";
|
|
|
|
function Component() {
|
|
const [countryId, setCountryId] = useCountryId();
|
|
// countryId = "us", "uk", or "ca"
|
|
}
|
|
```
|
|
|
|
### Charts and Visualization
|
|
|
|
**Plotly integration:**
|
|
```bash
|
|
# See chart components
|
|
ls src/pages/policy/output/
|
|
|
|
# Reference implementation
|
|
cat src/pages/policy/output/EconomyOutput.jsx
|
|
```
|
|
|
|
**Standard Plotly pattern:**
|
|
```javascript
|
|
import Plot from "react-plotly.js";
|
|
|
|
const layout = {
|
|
font: { family: "Roboto Serif" },
|
|
plot_bgcolor: "white",
|
|
// PolicyEngine branding
|
|
};
|
|
|
|
<Plot
|
|
data={traces}
|
|
layout={layout}
|
|
config={{ displayModeBar: false }}
|
|
/>;
|
|
```
|
|
|
|
### Blog Posts
|
|
|
|
**To see blog post structure:**
|
|
```bash
|
|
ls src/posts/articles/
|
|
|
|
# Read a recent post
|
|
cat src/posts/articles/harris-eitc.md
|
|
```
|
|
|
|
**Blog posts:**
|
|
- Written in Markdown
|
|
- Stored in `src/posts/articles/`
|
|
- Include metadata (title, date, authors)
|
|
- Follow policyengine-writing-skill style
|
|
|
|
**Adding a post:**
|
|
```bash
|
|
# Create new file
|
|
# src/posts/articles/my-analysis.md
|
|
|
|
# Add to index (if needed)
|
|
# See existing posts for format
|
|
```
|
|
|
|
### Styling
|
|
|
|
**Current styling approach:**
|
|
```bash
|
|
# See style configuration
|
|
ls src/style/
|
|
|
|
# Colors
|
|
cat src/style/colors.js
|
|
|
|
# Ant Design theme
|
|
cat src/style/theme.js
|
|
```
|
|
|
|
**PolicyEngine colors:**
|
|
- Teal: `#39C6C0` (primary accent)
|
|
- Blue: `#2C6496` (charts, links)
|
|
- Dark gray: `#616161` (text)
|
|
|
|
### Testing
|
|
|
|
**To see current tests:**
|
|
```bash
|
|
ls src/__tests__/
|
|
|
|
# Run tests
|
|
make test
|
|
|
|
# Test pattern
|
|
cat src/__tests__/example.test.js
|
|
```
|
|
|
|
**Testing libraries:**
|
|
- Jest (test runner)
|
|
- React Testing Library (component testing)
|
|
- User-centric testing (not implementation details)
|
|
|
|
### Development Server
|
|
|
|
**Start locally:**
|
|
```bash
|
|
make debug
|
|
# Opens http://localhost:3000
|
|
```
|
|
|
|
**Environment:**
|
|
```bash
|
|
# Environment variables
|
|
cat .env.example
|
|
|
|
# Config
|
|
ls src/config/
|
|
```
|
|
|
|
### Building and Deployment
|
|
|
|
**Build:**
|
|
```bash
|
|
make build
|
|
# Creates optimized production build
|
|
```
|
|
|
|
**Deployment:**
|
|
```bash
|
|
# See deployment config
|
|
cat netlify.toml # or appropriate hosting config
|
|
```
|
|
|
|
## Component Patterns
|
|
|
|
### Standard Component Structure
|
|
|
|
**To see well-structured components:**
|
|
```bash
|
|
# Example page
|
|
cat src/pages/HouseholdPage.jsx
|
|
|
|
# Example applet
|
|
cat src/applets/PolicySearch.jsx
|
|
```
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
import { useState, useEffect } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import { useCountryId } from "hooks/useCountryId";
|
|
|
|
export default function MyComponent({ prop1, prop2 }) {
|
|
// 1. Hooks first
|
|
const [state, setState] = useState(initialValue);
|
|
const [countryId] = useCountryId();
|
|
const [searchParams] = useSearchParams();
|
|
|
|
// 2. Effects
|
|
useEffect(() => {
|
|
// Side effects
|
|
}, [dependencies]);
|
|
|
|
// 3. Event handlers
|
|
const handleClick = () => {
|
|
setState(newValue);
|
|
};
|
|
|
|
// 4. Render
|
|
return (
|
|
<div>
|
|
{/* JSX */}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Component Size Limit
|
|
|
|
**Keep components under 150 lines after formatting.**
|
|
|
|
**If component is too large:**
|
|
1. Extract sub-components
|
|
2. Move logic to custom hooks
|
|
3. Split into multiple files
|
|
|
|
**To find large components:**
|
|
```bash
|
|
# Find files >150 lines
|
|
find src/ -name "*.jsx" -exec wc -l {} \; | sort -rn | head -20
|
|
```
|
|
|
|
### File Naming
|
|
|
|
**Components:** PascalCase.jsx
|
|
- `HouseholdPage.jsx`
|
|
- `PolicySearch.jsx`
|
|
- `ImpactChart.jsx`
|
|
|
|
**Utilities:** camelCase.js
|
|
- `formatCurrency.js`
|
|
- `apiUtils.js`
|
|
- `chartHelpers.js`
|
|
|
|
**Hooks:** camelCase.js with 'use' prefix
|
|
- `useCountryId.js`
|
|
- `usePolicy.js`
|
|
|
|
## Common Development Tasks
|
|
|
|
### Task 1: Add New Page
|
|
|
|
1. **See page structure:**
|
|
```bash
|
|
cat src/pages/HouseholdPage.jsx
|
|
```
|
|
|
|
2. **Create new page:**
|
|
```javascript
|
|
// src/pages/MyNewPage.jsx
|
|
export default function MyNewPage() {
|
|
return <div>Content</div>;
|
|
}
|
|
```
|
|
|
|
3. **Add route:**
|
|
```bash
|
|
# See routing
|
|
cat src/routing/routes.js
|
|
|
|
# Add your route following the pattern
|
|
```
|
|
|
|
### Task 2: Add New Chart
|
|
|
|
1. **See chart examples:**
|
|
```bash
|
|
ls src/pages/policy/output/
|
|
cat src/pages/policy/output/DistributionalImpact.jsx
|
|
```
|
|
|
|
2. **Create chart component:**
|
|
```javascript
|
|
import Plot from "react-plotly.js";
|
|
|
|
export default function MyChart({ data }) {
|
|
return (
|
|
<Plot
|
|
data={traces}
|
|
layout={{
|
|
font: { family: "Roboto Serif" },
|
|
plot_bgcolor: "white"
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Task 3: Add Blog Post
|
|
|
|
1. **See post structure:**
|
|
```bash
|
|
cat src/posts/articles/harris-eitc.md
|
|
```
|
|
|
|
2. **Create post:**
|
|
```bash
|
|
# Create markdown file
|
|
# src/posts/articles/my-analysis.md
|
|
|
|
# Follow policyengine-writing-skill for style
|
|
```
|
|
|
|
3. **Images:**
|
|
```bash
|
|
# Store in public/images/posts/
|
|
# Reference in markdown
|
|
```
|
|
|
|
## API Integration Patterns
|
|
|
|
### Fetching Data
|
|
|
|
**To see API call patterns:**
|
|
```bash
|
|
cat src/api/call.js
|
|
```
|
|
|
|
**Standard pattern:**
|
|
```javascript
|
|
import { call } from "api/call";
|
|
|
|
const fetchData = async () => {
|
|
const result = await call(
|
|
`/us/calculate`,
|
|
{ household: data },
|
|
"POST"
|
|
);
|
|
return result;
|
|
};
|
|
```
|
|
|
|
### Loading States
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const [data, setData] = useState(null);
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
fetchData()
|
|
.then(setData)
|
|
.catch(setError)
|
|
.finally(() => setLoading(false));
|
|
}, [dependencies]);
|
|
|
|
if (loading) return <Spin />;
|
|
if (error) return <Error message={error} />;
|
|
return <Data data={data} />;
|
|
```
|
|
|
|
## Performance Patterns
|
|
|
|
### Code Splitting
|
|
|
|
**To see code splitting:**
|
|
```bash
|
|
grep -r "React.lazy" src/
|
|
```
|
|
|
|
**Pattern:**
|
|
```javascript
|
|
import { lazy, Suspense } from "react";
|
|
|
|
const HeavyComponent = lazy(() => import("./HeavyComponent"));
|
|
|
|
function Page() {
|
|
return (
|
|
<Suspense fallback={<Spin />}>
|
|
<HeavyComponent />
|
|
</Suspense>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Memoization
|
|
|
|
**Use React.memo for expensive components:**
|
|
```javascript
|
|
import { memo } from "react";
|
|
|
|
const ExpensiveChart = memo(function ExpensiveChart({ data }) {
|
|
// Only re-renders if data changes
|
|
return <Plot data={data} />;
|
|
});
|
|
```
|
|
|
|
## Accessibility
|
|
|
|
**Requirements:**
|
|
- Semantic HTML elements
|
|
- ARIA labels for complex widgets
|
|
- Keyboard navigation
|
|
- Color contrast (WCAG AA)
|
|
|
|
**To see accessibility patterns:**
|
|
```bash
|
|
grep -r "aria-" src/
|
|
grep -r "role=" src/
|
|
```
|
|
|
|
## Country-Specific Features
|
|
|
|
### Country Switching
|
|
|
|
**To see country switching:**
|
|
```bash
|
|
cat src/hooks/useCountryId.js
|
|
```
|
|
|
|
**Usage:**
|
|
```javascript
|
|
import { useCountryId } from "hooks/useCountryId";
|
|
|
|
function Component() {
|
|
const [countryId] = useCountryId(); // "us", "uk", or "ca"
|
|
|
|
// Load country-specific data
|
|
const data = countryId === "us" ? usData : ukData;
|
|
}
|
|
```
|
|
|
|
### Country-Specific Content
|
|
|
|
**Conditional rendering:**
|
|
```javascript
|
|
{countryId === "us" && <USSpecificComponent />}
|
|
{countryId === "uk" && <UKSpecificComponent />}
|
|
```
|
|
|
|
**To find country-specific code:**
|
|
```bash
|
|
grep -r "countryId ===" src/
|
|
```
|
|
|
|
## Development Workflow
|
|
|
|
### Local Development
|
|
|
|
**Start dev server:**
|
|
```bash
|
|
make debug
|
|
# App runs on http://localhost:3000
|
|
# Connects to production API by default
|
|
```
|
|
|
|
**Connect to local API:**
|
|
```bash
|
|
# See environment configuration
|
|
cat src/config/environment.js
|
|
|
|
# Or set environment variable
|
|
REACT_APP_API_URL=http://localhost:5000 make debug
|
|
```
|
|
|
|
### Testing
|
|
|
|
**Run tests:**
|
|
```bash
|
|
make test
|
|
```
|
|
|
|
**Watch mode:**
|
|
```bash
|
|
npm test -- --watch
|
|
```
|
|
|
|
**Coverage:**
|
|
```bash
|
|
npm test -- --coverage
|
|
```
|
|
|
|
### Linting and Formatting
|
|
|
|
**Format code (critical before committing):**
|
|
```bash
|
|
make format
|
|
|
|
# Or manually
|
|
npm run lint -- --fix
|
|
npx prettier --write .
|
|
```
|
|
|
|
**Check linting (CI check):**
|
|
```bash
|
|
npm run lint -- --max-warnings=0
|
|
```
|
|
|
|
## Current Implementation Reference
|
|
|
|
### Component Structure
|
|
|
|
**To see current page structure:**
|
|
```bash
|
|
ls src/pages/
|
|
# - HouseholdPage.jsx
|
|
# - PolicyPage.jsx
|
|
# - HomePage.jsx
|
|
# - etc.
|
|
```
|
|
|
|
**To see a complete page:**
|
|
```bash
|
|
cat src/pages/PolicyPage.jsx
|
|
```
|
|
|
|
### API Call Patterns
|
|
|
|
**To see current API integration:**
|
|
```bash
|
|
cat src/api/call.js # Base caller
|
|
cat src/api/variables.js # Variable metadata fetching
|
|
cat src/api/parameters.js # Parameter metadata fetching
|
|
```
|
|
|
|
### Routing Configuration
|
|
|
|
**To see current routes:**
|
|
```bash
|
|
cat src/routing/routes.js
|
|
```
|
|
|
|
### Form Controls
|
|
|
|
**To see PolicyEngine-specific form controls:**
|
|
```bash
|
|
ls src/controls/
|
|
# - InputField.jsx
|
|
# - SearchParamControl.jsx
|
|
# - etc.
|
|
```
|
|
|
|
### Chart Components
|
|
|
|
**To see chart implementations:**
|
|
```bash
|
|
ls src/pages/policy/output/
|
|
# - BudgetaryImpact.jsx
|
|
# - DistributionalImpact.jsx
|
|
# - PovertyImpact.jsx
|
|
# - etc.
|
|
```
|
|
|
|
**Reference chart:**
|
|
```bash
|
|
cat src/pages/policy/output/DistributionalImpact.jsx
|
|
```
|
|
|
|
## Multi-Repository Integration
|
|
|
|
### How App Relates to Other Repos
|
|
|
|
```
|
|
policyengine-core (engine)
|
|
↓
|
|
policyengine-us, policyengine-uk (country models)
|
|
↓
|
|
policyengine-api (backend)
|
|
↓
|
|
policyengine-app (you are here)
|
|
```
|
|
|
|
**Understanding the stack:**
|
|
- See `policyengine-core-skill` for engine concepts
|
|
- See `policyengine-us-skill` for what variables/parameters mean
|
|
- See `policyengine-api-skill` for API endpoints the app calls
|
|
|
|
### Blog Posts Reference Country Models
|
|
|
|
**Blog posts often reference variables:**
|
|
```bash
|
|
# Posts reference variables like "income_tax", "ctc"
|
|
# See policyengine-us-skill for variable definitions
|
|
cat src/posts/articles/harris-eitc.md
|
|
```
|
|
|
|
## Common Development Tasks
|
|
|
|
### Task 1: Add New Parameter to UI
|
|
|
|
1. **Understand parameter:**
|
|
```bash
|
|
# See parameter in country model
|
|
cd ../policyengine-us
|
|
cat policyengine_us/parameters/gov/irs/credits/ctc/amount/base_amount.yaml
|
|
```
|
|
|
|
2. **Find similar parameter in app:**
|
|
```bash
|
|
cd ../policyengine-app
|
|
grep -r "ctc.*amount" src/pages/policy/
|
|
```
|
|
|
|
3. **Add UI control following pattern**
|
|
|
|
### Task 2: Add New Chart
|
|
|
|
1. **See existing charts:**
|
|
```bash
|
|
cat src/pages/policy/output/DistributionalImpact.jsx
|
|
```
|
|
|
|
2. **Create new chart component**
|
|
|
|
3. **Add to policy output page**
|
|
|
|
### Task 3: Fix Bug in Calculator
|
|
|
|
1. **Find relevant component:**
|
|
```bash
|
|
# Search for the feature
|
|
grep -r "keyword" src/pages/
|
|
```
|
|
|
|
2. **Read component code**
|
|
|
|
3. **Make fix following React patterns**
|
|
|
|
4. **Test with dev server:**
|
|
```bash
|
|
make debug
|
|
```
|
|
|
|
## Build and Deployment
|
|
|
|
**Production build:**
|
|
```bash
|
|
make build
|
|
# Creates optimized bundle in build/
|
|
```
|
|
|
|
**Deployment:**
|
|
```bash
|
|
# See deployment configuration
|
|
cat netlify.toml # or appropriate config
|
|
```
|
|
|
|
**Environment variables:**
|
|
```bash
|
|
# React env vars must have REACT_APP_ prefix
|
|
REACT_APP_API_URL=https://api.policyengine.org
|
|
|
|
# Or use config file pattern (recommended)
|
|
cat src/config/environment.js
|
|
```
|
|
|
|
## Style Guide
|
|
|
|
**Follow policyengine-standards-skill for:**
|
|
- ESLint configuration
|
|
- Prettier formatting
|
|
- Component size limits
|
|
- File organization
|
|
|
|
**Follow policyengine-writing-skill for:**
|
|
- Blog post content
|
|
- Documentation
|
|
- UI copy
|
|
|
|
## Resources
|
|
|
|
**Repository:** https://github.com/PolicyEngine/policyengine-app
|
|
**Live app:** https://policyengine.org
|
|
**Staging:** https://staging.policyengine.org (if applicable)
|
|
|
|
**Related skills:**
|
|
- **policyengine-api-skill** - Understanding the backend
|
|
- **policyengine-us-skill** - Understanding variables/parameters
|
|
- **policyengine-writing-skill** - Blog post style
|
|
- **policyengine-standards-skill** - Code quality
|