John Bowyer commited on
Commit
5acadf5
·
1 Parent(s): facae46

Replace with org-card app - simpler React app for organization page

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +4 -0
  2. .gitignore +5 -0
  3. Dockerfile +14 -30
  4. README.md +6 -74
  5. frontend/.env.example +0 -15
  6. frontend/.eslintrc.cjs +0 -18
  7. frontend/.gitignore +0 -21
  8. frontend/README.md +0 -166
  9. frontend/index.html +0 -25
  10. frontend/policy-dashboards/.gitignore +0 -23
  11. frontend/policy-dashboards/README.md +0 -251
  12. frontend/policy-dashboards/package-lock.json +0 -0
  13. frontend/policy-dashboards/package.json +0 -36
  14. frontend/policy-dashboards/public/communityone_logo.jpg +0 -0
  15. frontend/policy-dashboards/public/communityone_logo.svg +0 -22
  16. frontend/policy-dashboards/public/index.html +0 -17
  17. frontend/policy-dashboards/src/App.jsx +0 -1355
  18. frontend/policy-dashboards/src/App.jsx.backup +0 -1
  19. frontend/policy-dashboards/src/App.jsx.corrupted +0 -553
  20. frontend/policy-dashboards/src/components/EndlessStudyLoop.jsx +0 -162
  21. frontend/policy-dashboards/src/components/HomePage.jsx +0 -291
  22. frontend/policy-dashboards/src/components/ImpactDashboard.jsx +0 -280
  23. frontend/policy-dashboards/src/components/NonprofitCard.jsx +0 -290
  24. frontend/policy-dashboards/src/components/SplitScreenView.jsx +0 -375
  25. frontend/policy-dashboards/src/components/Summary.jsx +0 -183
  26. frontend/policy-dashboards/src/components/TopicNavigation.jsx +0 -511
  27. frontend/policy-dashboards/src/components/WhereMoneyWent.jsx +0 -162
  28. frontend/policy-dashboards/src/components/WhoIsInCharge.jsx +0 -162
  29. frontend/policy-dashboards/src/components/WordsVsDollars.jsx +0 -151
  30. frontend/policy-dashboards/src/components/shared/BarMeter.jsx +0 -34
  31. frontend/policy-dashboards/src/components/shared/Compare.jsx +0 -58
  32. frontend/policy-dashboards/src/components/shared/DashboardTile.jsx +0 -171
  33. frontend/policy-dashboards/src/components/shared/DecisionCard.jsx +0 -253
  34. frontend/policy-dashboards/src/components/shared/FilterPanel.jsx +0 -240
  35. frontend/policy-dashboards/src/components/shared/InsightBox.jsx +0 -37
  36. frontend/policy-dashboards/src/components/shared/MetricCard.jsx +0 -36
  37. frontend/policy-dashboards/src/data/dashboardData.js +0 -321
  38. frontend/policy-dashboards/src/index.css +0 -31
  39. frontend/policy-dashboards/src/index.js +0 -11
  40. frontend/public/communityone_logo.jpg +0 -0
  41. frontend/public/communityone_logo.png +0 -0
  42. frontend/public/communityone_logo.svg +0 -22
  43. frontend/public/communityone_logo_64.png +0 -0
  44. frontend/public/favicon.ico +0 -0
  45. frontend/public/privacyfacebook.html +0 -276
  46. frontend/src/App.tsx +0 -66
  47. frontend/src/components/AddressLookup.tsx +0 -671
  48. frontend/src/components/FollowButton.tsx +0 -160
  49. frontend/src/components/JurisdictionDiscovery.tsx +0 -268
  50. frontend/src/components/Layout.tsx +0 -498
.dockerignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .env
4
+ *.log
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .env
4
+ *.log
5
+ .DS_Store
Dockerfile CHANGED
@@ -1,40 +1,24 @@
1
- # Simple React App Deployment for HuggingFace Spaces
2
- FROM node:20-slim AS builder
3
 
4
  WORKDIR /app
5
 
6
- # Copy frontend source
7
- COPY frontend/package*.json ./
8
- RUN npm ci
9
 
10
- COPY frontend/ ./
 
11
 
12
- # Build React app for production
13
- ENV VITE_CANONICAL_DOMAIN=communityone-readme.hf.space
14
- ENV VITE_API_URL=https://www.communityone.com/api
15
- RUN npm run build
16
-
17
- # Production stage with nginx
18
- FROM nginx:alpine
19
 
20
- # Copy built React app
21
- COPY --from=builder /app/dist /usr/share/nginx/html
22
 
23
- # Create nginx config for SPA
24
- RUN echo 'server { \
25
- listen 7860; \
26
- root /usr/share/nginx/html; \
27
- index index.html; \
28
- location / { \
29
- try_files $uri $uri/ /index.html; \
30
- } \
31
- }' > /etc/nginx/conf.d/default.conf
32
 
 
33
  EXPOSE 7860
34
 
35
- CMD ["nginx", "-g", "daemon off;"]
36
- ENV LOG_LEVEL=INFO
37
- ENV HF_SPACES=1
38
-
39
- # Use supervisor to run all services
40
- CMD ["/app/start.sh"]
 
1
+ FROM node:20-slim
 
2
 
3
  WORKDIR /app
4
 
5
+ # Copy package files
6
+ COPY package*.json ./
 
7
 
8
+ # Install dependencies
9
+ RUN npm ci --prefer-offline --no-audit
10
 
11
+ # Copy source code
12
+ COPY . .
 
 
 
 
 
13
 
14
+ # Build the app
15
+ RUN npm run build
16
 
17
+ # Install a simple static server
18
+ RUN npm install -g serve
 
 
 
 
 
 
 
19
 
20
+ # Expose port 7860 (HuggingFace Spaces default)
21
  EXPOSE 7860
22
 
23
+ # Serve the built app
24
+ CMD ["serve", "-s", "dist", "-l", "7860"]
 
 
 
 
README.md CHANGED
@@ -1,86 +1,18 @@
1
  ---
2
  title: CommunityOne
3
- emoji: 🏘️
4
  colorFrom: blue
5
  colorTo: green
6
- sdk: static
7
  pinned: false
 
8
  ---
9
 
10
- # 🏘️ CommunityOne
11
 
12
  **The open path to everything local**
13
 
14
- Building tools to help communities understand and engage with local government, nonprofits, and civic processes.
15
 
16
- ## 🚀 What We Build
17
 
18
- ### [Open Navigator for Engagement](https://www.communityone.com)
19
- An open-source platform for exploring and tracking:
20
-
21
- - **90,000+ jurisdictions** (cities, counties, states)
22
- - **3M+ nonprofit organizations** (complete IRS registry)
23
- - **7,300+ state legislators** (all 50 states + DC + PR)
24
- - **100,000+ state bills** (legislative tracking with full text)
25
- - **Government meetings** with AI-extracted insights
26
- - **Budget analysis** (municipal, county, state spending)
27
- - **Public opinion data** (survey questions from Roper Center)
28
-
29
- ## 📊 Open Datasets
30
-
31
- All our data is freely available on HuggingFace:
32
-
33
- - **[one-nonprofits-organizations](https://huggingface.co/datasets/CommunityOne/one-nonprofits-organizations)** - 3.9M nonprofits (IRS EO-BMF + ProPublica)
34
- - **[one-nonprofits-financials](https://huggingface.co/datasets/CommunityOne/one-nonprofits-financials)** - Form 990 financial data
35
- - **[one-meetings-calendar](https://huggingface.co/datasets/CommunityOne/one-meetings-calendar)** - Government meeting schedules
36
- - **[reference-jurisdictions-cities](https://huggingface.co/datasets/CommunityOne/reference-jurisdictions-cities)** - 19K+ U.S. cities
37
- - **[reference-jurisdictions-counties](https://huggingface.co/datasets/CommunityOne/reference-jurisdictions-counties)** - 3,144 U.S. counties
38
-
39
- [View all 60+ datasets →](https://huggingface.co/CommunityOne/datasets)
40
-
41
- ## 🛠️ Technology Stack
42
-
43
- - **Frontend:** React + Vite + Tailwind CSS
44
- - **Backend:** FastAPI + PostgreSQL + Delta Lake
45
- - **AI/ML:** LangChain, OpenAI GPT-4, Anthropic Claude
46
- - **Data:** Apache Spark, DuckDB, Polars, Pandas
47
- - **Deployment:** HuggingFace Spaces, Docker
48
-
49
- ## 🎯 Use Cases
50
-
51
- ### For Advocates & Organizers
52
- - Track legislation on your issue (healthcare, education, housing)
53
- - Find nonprofits working in your community
54
- - Monitor government meetings and budgets
55
- - Identify engagement opportunities
56
-
57
- ### For Researchers & Journalists
58
- - Analyze policy trends across states
59
- - Study nonprofit sector patterns
60
- - Track government spending priorities
61
- - Map community infrastructure
62
-
63
- ### For Developers
64
- - Access clean, standardized civic data
65
- - Build on open APIs and datasets
66
- - Fork and extend the platform
67
- - Contribute to open source tools
68
-
69
- ## 🔗 Links
70
-
71
- - **🌐 Live Application:** [www.communityone.com](https://www.communityone.com)
72
- - **📖 Documentation:** [www.communityone.com/docs](https://www.communityone.com/docs)
73
- - **💻 GitHub:** [getcommunityone/open-navigator-for-engagement](https://github.com/getcommunityone/open-navigator-for-engagement)
74
- - **📊 Datasets:** [All CommunityOne datasets](https://huggingface.co/CommunityOne/datasets)
75
-
76
- ## 📝 License
77
-
78
- MIT License - Free to use, modify, and distribute
79
-
80
- ## 🤝 Contributing
81
-
82
- We welcome contributions! Check out our [GitHub repository](https://github.com/getcommunityone/open-navigator-for-engagement) to get started.
83
-
84
- ---
85
-
86
- **Focus Areas:** Civic Tech • Open Data • Government Transparency • Nonprofit Sector • Legislative Tracking • Community Engagement
 
1
  ---
2
  title: CommunityOne
3
+ emoji: 🌐
4
  colorFrom: blue
5
  colorTo: green
6
+ sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
  ---
10
 
11
+ # 🌐 CommunityOne
12
 
13
  **The open path to everything local**
14
 
15
+ This is the official organization card for CommunityOne, showcasing our civic engagement platform and open datasets.
16
 
17
+ Visit the live platform at **https://www.communityone.com**
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.env.example DELETED
@@ -1,15 +0,0 @@
1
- # Example environment variables for frontend development
2
- # Copy to .env.development.local to customize
3
-
4
- # Documentation URL (Docusaurus runs on port 3000 in development)
5
- # Default: http://localhost:3000
6
- # VITE_DOCS_URL=http://localhost:3000
7
-
8
- # API URL (FastAPI runs on port 8000)
9
- # Default: http://localhost:8000
10
- # VITE_API_URL=http://localhost:8000
11
-
12
- # Google Analytics Measurement ID (same as docs site)
13
- # Get from: https://analytics.google.com (GA4 property)
14
- # Format: G-XXXXXXXXXX
15
- VITE_GOOGLE_ANALYTICS_ID=G-5EQV815915
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.eslintrc.cjs DELETED
@@ -1,18 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- env: { browser: true, es2020: true },
4
- extends: [
5
- 'eslint:recommended',
6
- 'plugin:@typescript-eslint/recommended',
7
- 'plugin:react-hooks/recommended',
8
- ],
9
- ignorePatterns: ['dist', '.eslintrc.cjs'],
10
- parser: '@typescript-eslint/parser',
11
- plugins: ['react-refresh'],
12
- rules: {
13
- 'react-refresh/only-export-components': [
14
- 'warn',
15
- { allowConstantExport: true },
16
- ],
17
- },
18
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.gitignore DELETED
@@ -1,21 +0,0 @@
1
- # Frontend dependencies
2
- node_modules/
3
- dist/
4
- *.log
5
- .DS_Store
6
-
7
- # Build output
8
- api/static/
9
-
10
- # Environment
11
- .env
12
- .env.local
13
-
14
- # IDE
15
- .vscode/
16
- .idea/
17
- *.swp
18
- *.swo
19
-
20
- # Testing
21
- coverage/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/README.md DELETED
@@ -1,166 +0,0 @@
1
- # Open Navigator for Engagement - Frontend
2
-
3
- React + TypeScript web interface for the Open Navigator for Engagement application.
4
-
5
- ## Projects
6
-
7
- ### Main Application
8
- React + TypeScript web interface with maps, charts, and data visualization.
9
-
10
- **Location:** `frontend/` (this directory)
11
-
12
- ### Policy Accountability Dashboards
13
- Evidence-based accountability dashboards for policy advocacy.
14
-
15
- **Location:** `frontend/policy-dashboards/`
16
- **Documentation:** [Policy Dashboards README](policy-dashboards/README.md)
17
-
18
- ## Tech Stack
19
-
20
- - **React 18.2** - UI framework
21
- - **TypeScript** - Type safety
22
- - **Vite** - Build tool
23
- - **Tailwind CSS** - Styling
24
- - **React Router** - Navigation
25
- - **TanStack Query** - Data fetching
26
- - **Recharts** - Charts
27
- - **Leaflet** - Interactive maps
28
-
29
- ## Development
30
-
31
- ```bash
32
- # Install dependencies
33
- npm install
34
-
35
- # Run dev server (hot reload)
36
- npm run dev
37
- # Opens http://localhost:3000
38
-
39
- # Build for production
40
- npm run build
41
- # Outputs to ../api/static/
42
-
43
- # Type check
44
- npm run type-check
45
-
46
- # Lint
47
- npm run lint
48
- ```
49
-
50
- ## Project Structure
51
-
52
- ```
53
- src/
54
- ├── components/
55
- │ └── Layout.tsx # Main layout with sidebar
56
- ├── pages/
57
- │ ├── Dashboard.tsx # Statistics and charts
58
- │ ├── Heatmap.tsx # Interactive map
59
- │ ├── Documents.tsx # Document browser
60
- │ ├── Opportunities.tsx # Opportunity manager
61
- │ └── Settings.tsx # Configuration
62
- ├── App.tsx # Root component with routing
63
- ├── main.tsx # Entry point
64
- └── index.css # Global styles
65
- ```
66
-
67
- ## Pages
68
-
69
- ### Dashboard (`/`)
70
- - Total documents, opportunities, states monitored
71
- - Topic distribution charts (bar and pie)
72
- - Recent opportunities table
73
-
74
- ### Heatmap (`/heatmap`)
75
- - Interactive Leaflet map
76
- - Color-coded urgency markers
77
- - State and topic filters
78
- - Popup details
79
-
80
- ### Documents (`/documents`)
81
- - Searchable document list
82
- - Pagination
83
- - Topic tags
84
- - Source links
85
-
86
- ### Opportunities (`/opportunities`)
87
- - Filterable by urgency
88
- - Generate advocacy emails
89
- - Talking points display
90
- - Confidence scores
91
-
92
- ### Settings (`/settings`)
93
- - Target state selection
94
- - Policy topic configuration
95
- - Notification preferences
96
- - Agent status monitoring
97
-
98
- ## Environment
99
-
100
- The frontend proxies API requests to the backend:
101
-
102
- ```typescript
103
- // vite.config.ts
104
- server: {
105
- proxy: {
106
- '/api': {
107
- target: 'http://localhost:8000',
108
- changeOrigin: true,
109
- },
110
- },
111
- }
112
- ```
113
-
114
- ## Building
115
-
116
- Production builds are output to `../api/static/` so the FastAPI backend can serve them:
117
-
118
- ```typescript
119
- // vite.config.ts
120
- build: {
121
- outDir: '../api/static',
122
- emptyOutDir: true,
123
- }
124
- ```
125
-
126
- ## Styling
127
-
128
- Uses Tailwind CSS with custom utility classes:
129
-
130
- ```css
131
- /* index.css */
132
- .card { @apply bg-white rounded-lg shadow-md p-6; }
133
- .btn-primary { @apply bg-primary-600 hover:bg-primary-700 text-white ... }
134
- .btn-secondary { @apply bg-gray-200 hover:bg-gray-300 text-gray-800 ... }
135
- ```
136
-
137
- ## Data Fetching
138
-
139
- Uses TanStack Query for server state management:
140
-
141
- ```typescript
142
- const { data, isLoading } = useQuery({
143
- queryKey: ['opportunities', state, topic],
144
- queryFn: async () => {
145
- const response = await axios.get('/api/opportunities', { params: { state, topic } })
146
- return response.data
147
- },
148
- })
149
- ```
150
-
151
- ## Deployment
152
-
153
- The built frontend is automatically included when deploying to Databricks Apps:
154
-
155
- ```bash
156
- # Build frontend
157
- npm run build
158
-
159
- # Deploy entire app
160
- cd ..
161
- ./scripts/deploy-databricks-app.sh
162
- ```
163
-
164
- ## License
165
-
166
- MIT License - See LICENSE file for details
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/index.html DELETED
@@ -1,25 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/x-icon" href="/favicon.ico" />
6
- <link rel="icon" type="image/png" sizes="64x64" href="/communityone_logo_64.png" />
7
- <link rel="icon" type="image/svg+xml" href="/communityone_logo.svg" />
8
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
- <meta name="description" content="CommunityOne: The open path to everything local" />
10
- <title>Open Navigator for Engagement</title>
11
-
12
- <!-- Google tag (gtag.js) -->
13
- <script async src="https://www.googletagmanager.com/gtag/js?id=G-5EQV815915"></script>
14
- <script>
15
- window.dataLayer = window.dataLayer || [];
16
- function gtag(){dataLayer.push(arguments);}
17
- gtag('js', new Date());
18
- gtag('config', 'G-5EQV815915');
19
- </script>
20
- </head>
21
- <body>
22
- <div id="root"></div>
23
- <script type="module" src="/src/main.tsx"></script>
24
- </body>
25
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/.gitignore DELETED
@@ -1,23 +0,0 @@
1
- # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
-
3
- # dependencies
4
- /node_modules
5
- /.pnp
6
- .pnp.js
7
-
8
- # testing
9
- /coverage
10
-
11
- # production
12
- /build
13
-
14
- # misc
15
- .DS_Store
16
- .env.local
17
- .env.development.local
18
- .env.test.local
19
- .env.production.local
20
-
21
- npm-debug.log*
22
- yarn-debug.log*
23
- yarn-error.log*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/README.md DELETED
@@ -1,251 +0,0 @@
1
- # Policy Accountability Dashboards
2
-
3
- **Evidence-based accountability dashboards for local government policy advocacy**
4
-
5
- Interactive web dashboards that expose gaps between rhetoric and reality, deferral tactics, and power imbalances in local government decision-making.
6
-
7
- ## Quick Start
8
-
9
- ```bash
10
- # Install dependencies
11
- cd frontend/policy-dashboards
12
- npm install
13
-
14
- # Start development server
15
- npm start
16
- ```
17
-
18
- The app will open at `http://localhost:3000`
19
-
20
- ## Overview
21
-
22
- This React application visualizes accountability data from local government meetings and budgets. It presents four key dashboards:
23
-
24
- 1. **They cut health spending while praising wellness** - Rhetoric vs. reality gap
25
- 2. **Delayed 6 months and counting** - Sequential deferral patterns
26
- 3. **What got funded instead** - Budget displacement analysis
27
- 4. **One memo beat 240 residents** - Influence power mapping
28
-
29
- Plus a **Summary** page that provides an overview and strategic guidance.
30
-
31
- ## Data Integration
32
-
33
- ### Automatic Data Export from Python
34
-
35
- The Python accountability analysis automatically exports data for the frontend:
36
-
37
- ```bash
38
- # Run the full analysis (includes frontend export)
39
- python examples/tuscaloosa_accountability_report.py
40
- ```
41
-
42
- This generates: `frontend/policy-dashboards/src/data/dashboardData.js`
43
-
44
- ### Manual Data Updates
45
-
46
- To update dashboard data manually, edit `src/data/dashboardData.js`:
47
-
48
- ```javascript
49
- export const rhetoricGapData = {
50
- sentimentScore: 92, // Update this
51
- budgetDelta: -120000, // And this
52
- // ... more fields
53
- };
54
- ```
55
-
56
- ## Project Structure
57
-
58
- ```
59
- frontend/policy-dashboards/
60
- ├── public/
61
- │ └── index.html
62
- ├── src/
63
- │ ├── components/
64
- │ │ ├── shared/ # Reusable components
65
- │ │ │ ├── BarMeter.jsx
66
- │ │ │ ├── MetricCard.jsx
67
- │ │ │ ├── Compare.jsx
68
- │ │ │ └── InsightBox.jsx
69
- │ │ ├── Summary.jsx # Summary dashboard
70
- │ │ ├── WordsVsDollars.jsx # Dashboard 1
71
- │ │ ├── EndlessStudyLoop.jsx # Dashboard 2
72
- │ │ ├── WhereMoneyWent.jsx # Dashboard 3
73
- │ │ └── WhoIsInCharge.jsx # Dashboard 4
74
- │ ├── data/
75
- │ │ └── dashboardData.js # All dashboard data (UPDATE THIS)
76
- │ ├── App.jsx
77
- │ └── index.js
78
- ├── package.json
79
- └── README.md
80
- ```
81
-
82
- ## Features
83
-
84
- ### Per-Capita Benchmarks
85
-
86
- Each dashboard includes comparisons to:
87
- - This District
88
- - Republican Districts Average
89
- - Democratic Districts Average
90
- - National Average
91
-
92
- This framing shows the issue transcends partisan politics.
93
-
94
- ### Interactive Navigation
95
-
96
- - Click dashboard titles in tabs to switch views
97
- - Click summary findings to jump to detailed dashboards
98
- - Clean, print-friendly design
99
-
100
- ### Export to PDF
101
-
102
- Add PDF export capability:
103
-
104
- ```bash
105
- npm install html2canvas jspdf
106
- ```
107
-
108
- Then add a download button (see component code for implementation).
109
-
110
- ## Customization
111
-
112
- ### Change Colors
113
-
114
- Edit the color variables in each component:
115
-
116
- ```javascript
117
- const colors = {
118
- positive: "#1D9E75", // Green
119
- negative: "#D85A30", // Red/orange
120
- neutral: "#222" // Dark gray
121
- };
122
- ```
123
-
124
- ### Add New Benchmarks
125
-
126
- Update the `benchmarks` object in `dashboardData.js`:
127
-
128
- ```javascript
129
- benchmarks: {
130
- thisDistrict: { perStudent: 41, label: "This District" },
131
- // Add more comparison groups here
132
- }
133
- ```
134
-
135
- ### Customize Dashboard Names
136
-
137
- Edit the `tabs` array in `src/App.jsx`:
138
-
139
- ```javascript
140
- const tabs = [
141
- { id: 1, label: 'Your Custom Title Here', component: WordsVsDollars },
142
- // ...
143
- ];
144
- ```
145
-
146
- ## Deployment
147
-
148
- ### Build for Production
149
-
150
- ```bash
151
- npm run build
152
- ```
153
-
154
- This creates an optimized build in `build/` folder.
155
-
156
- ### Deploy to GitHub Pages
157
-
158
- ```bash
159
- # Install GitHub Pages package
160
- npm install --save-dev gh-pages
161
-
162
- # Add to package.json scripts:
163
- "predeploy": "npm run build",
164
- "deploy": "gh-pages -d build"
165
-
166
- # Deploy
167
- npm run deploy
168
- ```
169
-
170
- ### Deploy to Netlify/Vercel
171
-
172
- 1. Connect your repository to Netlify or Vercel
173
- 2. Set build command: `npm run build`
174
- 3. Set publish directory: `build`
175
- 4. Deploy!
176
-
177
- ## Presentation Mode
178
-
179
- Add URL parameter support for presentation mode (stacks all dashboards vertically):
180
-
181
- ```javascript
182
- // In App.jsx
183
- const urlParams = new URLSearchParams(window.location.search);
184
- const presentMode = urlParams.get('mode') === 'present';
185
- ```
186
-
187
- Then visit: `http://localhost:3000?mode=present`
188
-
189
- ## Data Sources
190
-
191
- The accountability dashboards use data from:
192
-
193
- - **NCES Common Core of Data (CCD)** - School district financials
194
- - **ASTDD State Oral Health Programs** - Program tracking
195
- - **NCES F-33 Survey** - Capital outlay by function
196
- - **National Association of School Nurses** - Liability data
197
- - **ADA Health Policy Institute** - Dental health metrics
198
-
199
- All sources are cited in dashboard components.
200
-
201
- ## Strategic Usage
202
-
203
- ### For Board Meetings
204
-
205
- 1. Start with **Summary** to set context
206
- 2. Move to **specific dashboard** based on board response
207
- 3. Use the "Ask them" boxes for direct questions
208
-
209
- ### For Media
210
-
211
- Lead with **Dashboard 4** (Influence Radar) - it's the headline:
212
- > "Risk Manager's One Memo Outweighed 240 Citizen Testimonies"
213
-
214
- ### For Advocacy Training
215
-
216
- Show all four to demonstrate:
217
- - Data-driven approach
218
- - Understanding of political dynamics
219
- - Ability to counter deflection tactics
220
-
221
- ## Development
222
-
223
- ### Available Scripts
224
-
225
- - `npm start` - Development server
226
- - `npm run build` - Production build
227
- - `npm test` - Run tests
228
- - `npm run eject` - Eject from Create React App
229
-
230
- ### Adding New Dashboards
231
-
232
- 1. Create component in `src/components/`
233
- 2. Add data to `src/data/dashboardData.js`
234
- 3. Import and add to tabs in `src/App.jsx`
235
- 4. Update Python export function
236
-
237
- ## Support
238
-
239
- For questions or issues:
240
-
241
- - **Documentation**: See `docs/ACCOUNTABILITY_DASHBOARD_STRATEGY.md`
242
- - **Python Backend**: See `extraction/accountability_dashboards.py`
243
- - **Examples**: See `examples/tuscaloosa_accountability_report.py`
244
-
245
- ## License
246
-
247
- Part of the Open Navigator for Engagement project.
248
-
249
- ---
250
-
251
- **Built with React** | **Designed for Policy Advocacy** | **Evidence-Based Accountability**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
frontend/policy-dashboards/package.json DELETED
@@ -1,36 +0,0 @@
1
- {
2
- "name": "policy-accountability-dashboards",
3
- "version": "1.0.0",
4
- "private": true,
5
- "description": "Evidence-based accountability dashboards for local government policy advocacy",
6
- "dependencies": {
7
- "react": "^18.2.0",
8
- "react-dom": "^18.2.0",
9
- "react-scripts": "5.0.1",
10
- "recharts": "^2.5.0",
11
- "lucide-react": "^0.263.1"
12
- },
13
- "scripts": {
14
- "start": "react-scripts start",
15
- "build": "react-scripts build",
16
- "test": "react-scripts test",
17
- "eject": "react-scripts eject"
18
- },
19
- "eslintConfig": {
20
- "extends": [
21
- "react-app"
22
- ]
23
- },
24
- "browserslist": {
25
- "production": [
26
- ">0.2%",
27
- "not dead",
28
- "not op_mini all"
29
- ],
30
- "development": [
31
- "last 1 chrome version",
32
- "last 1 firefox version",
33
- "last 1 safari version"
34
- ]
35
- }
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/public/communityone_logo.jpg DELETED
Binary file (2.59 kB)
 
frontend/policy-dashboards/public/communityone_logo.svg DELETED
frontend/policy-dashboards/public/index.html DELETED
@@ -1,17 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <meta name="theme-color" content="#000000" />
7
- <meta
8
- name="description"
9
- content="Policy Accountability Dashboards - Evidence-based advocacy for local government transparency"
10
- />
11
- <title>Policy Accountability Dashboards</title>
12
- </head>
13
- <body>
14
- <noscript>You need to enable JavaScript to run this app.</noscript>
15
- <div id="root"></div>
16
- </body>
17
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/App.jsx DELETED
@@ -1,1355 +0,0 @@
1
- import React, { useState } from 'react';
2
- import HomePage from './components/HomePage';
3
- import ImpactDashboard from './components/ImpactDashboard';
4
- import TopicNavigation from './components/TopicNavigation';
5
- import DecisionCard from './components/shared/DecisionCard';
6
- import SplitScreenView from './components/SplitScreenView';
7
- import NonprofitCard from './components/NonprofitCard';
8
- import { metadata } from './data/dashboardData';
9
- import { Home, Grid, Building2, Heart, Church, Search, X, Calendar, Filter, MapPin, Edit2, Lightbulb } from 'lucide-react';
10
-
11
- export default function App() {
12
- const [viewMode, setViewMode] = useState('home'); // 'home', 'impact', 'browse', 'split-screen'
13
- const [exploreMode, setExploreMode] = useState('decisions'); // 'decisions', 'organizations', or 'causes'
14
- const [sectorView, setSectorView] = useState('all'); // 'all', 'public', 'nonprofits', 'churches'
15
- const [selectedPersona, setSelectedPersona] = useState(null);
16
- const [selectedTopic, setSelectedTopic] = useState(null);
17
- const [selectedDecision, setSelectedDecision] = useState(null);
18
- const [selectedTopics, setSelectedTopics] = useState([]);
19
- const [selectedPatterns, setSelectedPatterns] = useState([]);
20
- const [selectedResources, setSelectedResources] = useState([]);
21
- const [startDate, setStartDate] = useState(null);
22
- const [endDate, setEndDate] = useState(null);
23
- const [searchQuery, setSearchQuery] = useState('');
24
- const [quickDateFilter, setQuickDateFilter] = useState('all'); // 'all', '7days', '30days', '90days'
25
- const [quickTopicFilter, setQuickTopicFilter] = useState('all'); // 'all', 'health', 'education', 'infrastructure'
26
- const [jurisdictionType, setJurisdictionType] = useState('city'); // 'nation', 'state', 'county', 'city', 'school-district'
27
- const [jurisdictionName, setJurisdictionName] = useState('Tuscaloosa');
28
- const [jurisdictionState, setJurisdictionState] = useState('AL');
29
- const [showLocationModal, setShowLocationModal] = useState(false);
30
- const [tempJurisdictionType, setTempJurisdictionType] = useState('city');
31
- const [tempJurisdictionName, setTempJurisdictionName] = useState('');
32
- const [tempJurisdictionState, setTempJurisdictionState] = useState('');
33
-
34
- const handlePersonaSelect = (persona, topic) => {
35
- setSelectedPersona(persona);
36
- setSelectedTopic(topic);
37
- setViewMode('impact');
38
- };
39
-
40
- const handleTopicSelect = (topicId) => {
41
- setSelectedTopics([topicId]);
42
- setViewMode('browse');
43
- };
44
-
45
- const handleTopicToggle = (topicId) => {
46
- setSelectedTopics(prev =>
47
- prev.includes(topicId)
48
- ? prev.filter(t => t !== topicId)
49
- : [...prev, topicId]
50
- );
51
- };
52
-
53
- const handlePatternToggle = (patternId) => {
54
- setSelectedPatterns(prev =>
55
- prev.includes(patternId)
56
- ? prev.filter(p => p !== patternId)
57
- : [...prev, patternId]
58
- );
59
- };
60
-
61
- const handleResourceToggle = (resourceId) => {
62
- setSelectedResources(prev =>
63
- prev.includes(resourceId)
64
- ? prev.filter(r => r !== resourceId)
65
- : [...prev, resourceId]
66
- );
67
- };
68
-
69
- const handleClearFilters = () => {
70
- setSelectedTopics([]);
71
- setSelectedPatterns([]);
72
- setSelectedResources([]);
73
- setStartDate(null);
74
- setEndDate(null);
75
- setSearchQuery('');
76
- setQuickDateFilter('all');
77
- setQuickTopicFilter('all');
78
- };
79
-
80
- const handleQuickDateFilter = (filter) => {
81
- setQuickDateFilter(filter);
82
- const today = new Date();
83
-
84
- switch(filter) {
85
- case '7days':
86
- const sevenDaysAgo = new Date(today);
87
- sevenDaysAgo.setDate(today.getDate() - 7);
88
- setStartDate(sevenDaysAgo.toISOString().split('T')[0]);
89
- setEndDate(today.toISOString().split('T')[0]);
90
- break;
91
- case '30days':
92
- const thirtyDaysAgo = new Date(today);
93
- thirtyDaysAgo.setDate(today.getDate() - 30);
94
- setStartDate(thirtyDaysAgo.toISOString().split('T')[0]);
95
- setEndDate(today.toISOString().split('T')[0]);
96
- break;
97
- case '90days':
98
- const ninetyDaysAgo = new Date(today);
99
- ninetyDaysAgo.setDate(today.getDate() - 90);
100
- setStartDate(ninetyDaysAgo.toISOString().split('T')[0]);
101
- setEndDate(today.toISOString().split('T')[0]);
102
- break;
103
- case 'all':
104
- default:
105
- setStartDate(null);
106
- setEndDate(null);
107
- break;
108
- }
109
- };
110
-
111
- const handleQuickTopicFilter = (filter) => {
112
- setQuickTopicFilter(filter);
113
-
114
- if (filter === 'all') {
115
- setSelectedTopics([]);
116
- } else {
117
- setSelectedTopics([filter]);
118
- }
119
- };
120
-
121
- const handleBackToHome = () => {
122
- setViewMode('home');
123
- setSelectedPersona(null);
124
- setSelectedTopic(null);
125
- setSelectedDecision(null);
126
- };
127
-
128
- const handleDecisionClick = (decision) => {
129
- setSelectedDecision(decision);
130
- setViewMode('split-screen');
131
- };
132
-
133
- const handleSectorSelect = (sector) => {
134
- setViewMode('browse');
135
-
136
- // Set explore mode and sector based on selection
137
- if (sector === 'public') {
138
- setExploreMode('decisions');
139
- setSectorView('public');
140
- } else if (sector === 'nonprofits' || sector === 'churches') {
141
- setExploreMode('organizations');
142
- setSectorView(sector);
143
- } else if (sector === 'all') {
144
- setExploreMode('organizations');
145
- setSectorView('all');
146
- }
147
- };
148
-
149
- // Example decision data - would come from Python export
150
- const exampleDecisions = [
151
- {
152
- decision_summary: "Approval of $850,000 athletic turf replacement project",
153
- outcome: "Approved",
154
- primary_rationale: "Athletic facilities are essential for student engagement and community pride. The current turf is beyond its useful life and poses safety concerns.",
155
- supporters: [
156
- { name: "Board Member Johnson", role: "Board Member" },
157
- { name: "Athletic Director Smith", role: "Staff" }
158
- ],
159
- opponents: [
160
- { name: "Parent Coalition for Health", role: "Public" }
161
- ],
162
- vote_result: "6-1",
163
- meeting_date: "2026-03-15",
164
- tradeoffs_discussed: ["Athletic facilities vs. health screening programs"],
165
- evidence_cited: [{ source: "Athletic Department Report" }],
166
- policy_domain: "facilities",
167
- ntee_code: "N20" // Recreation, Sports, and Athletics
168
- },
169
- {
170
- decision_summary: "Tabled decision on dental screening partnership with West Alabama Dental Clinic",
171
- outcome: "Tabled for further study",
172
- primary_rationale: "Risk management concerns require additional legal review and liability analysis before proceeding with external health partnerships.",
173
- supporters: [
174
- { name: "Patricia Johnson, Risk Manager", role: "Staff" }
175
- ],
176
- opponents: [
177
- { name: "Dr. Sarah Martinez", role: "Public" },
178
- { name: "Parent Teacher Association", role: "Public" },
179
- { name: "Board Member Williams", role: "Board Member" }
180
- ],
181
- vote_result: "5-2 to table",
182
- meeting_date: "2026-01-18",
183
- tradeoffs_discussed: ["Preventive care vs. perceived liability risk"],
184
- evidence_cited: [{ source: "Risk Management Memo" }],
185
- policy_domain: "health",
186
- ntee_code: "E32", // School-Based Health Care
187
- community_gap: {
188
- description: "100% of surveyed parents want dental screenings for students",
189
- nonprofit_filling_gap: true
190
- }
191
- },
192
- {
193
- decision_summary: "Reduction of nursing staff from 5 FTE to 3 FTE",
194
- outcome: "Approved",
195
- primary_rationale: "Budget constraints necessitate cost reductions. Nursing positions will be restructured to focus on emergency response rather than preventive services.",
196
- supporters: [
197
- { name: "Superintendent Brown", role: "Staff" },
198
- { name: "Board Chair Thompson", role: "Board Member" }
199
- ],
200
- opponents: [
201
- { name: "School Nurses Association", role: "Public" },
202
- { name: "Board Member Lee", role: "Board Member" }
203
- ],
204
- vote_result: "4-3",
205
- meeting_date: "2026-02-20",
206
- tradeoffs_discussed: ["Cost savings vs. preventive health services"],
207
- evidence_cited: [{ source: "FY2026 Budget Proposal" }],
208
- policy_domain: "budget",
209
- ntee_code: "E40", // Health - General and Rehabilitative
210
- community_gap: {
211
- description: "Students lost access to preventive health services",
212
- nonprofit_filling_gap: true
213
- }
214
- }
215
- ];
216
-
217
- // Example nonprofit data - would come from IRS/GuideStar API
218
- const exampleNonprofits = [
219
- {
220
- name: "West Alabama Dental Initiative",
221
- ein: "63-1234567",
222
- ntee_code: "E32",
223
- ntee_description: "School-Based Health Care",
224
- mission: "Providing free dental screenings and preventive care to underserved students in West Alabama",
225
- services: [
226
- "Mobile dental unit visits to schools",
227
- "Free toothbrush and fluoride kits",
228
- "Dental education workshops for parents"
229
- ],
230
- annual_budget: 125000,
231
- students_served: 2400,
232
- contact: {
233
- website: "https://wadaldentalinitiative.org",
234
- email: "info@wadaldentalinitiative.org",
235
- phone: "(205) 555-0123"
236
- },
237
- volunteer_opportunities: true,
238
- accepting_board_members: true
239
- },
240
- {
241
- name: "Tuscaloosa Family Health Network",
242
- ein: "63-7654321",
243
- ntee_code: "E40",
244
- ntee_description: "Health - General and Rehabilitative",
245
- mission: "Connecting low-income families with preventive health services and wellness programs",
246
- services: [
247
- "Health screenings at community centers",
248
- "Nutrition education programs",
249
- "Mental health counseling referrals"
250
- ],
251
- annual_budget: 280000,
252
- families_served: 850,
253
- contact: {
254
- website: "https://tuscfamilyhealth.org",
255
- email: "contact@tuscfamilyhealth.org",
256
- phone: "(205) 555-0456"
257
- },
258
- volunteer_opportunities: true,
259
- accepting_board_members: false
260
- },
261
- {
262
- name: "After School Champions",
263
- ein: "63-9876543",
264
- ntee_code: "O50",
265
- ntee_description: "Youth Development Programs",
266
- mission: "Providing safe, enriching after-school programs that support academic success and healthy development",
267
- services: [
268
- "Homework help and tutoring",
269
- "Healthy snacks and meals",
270
- "Physical activity and sports",
271
- "Health and wellness workshops"
272
- ],
273
- annual_budget: 340000,
274
- youth_served: 450,
275
- contact: {
276
- website: "https://afterschoolchamps.org",
277
- email: "info@afterschoolchamps.org",
278
- phone: "(205) 555-0789"
279
- },
280
- volunteer_opportunities: true,
281
- accepting_board_members: true
282
- },
283
- {
284
- name: "First Baptist Church Tuscaloosa - Health Ministry",
285
- ein: "63-2345678",
286
- ntee_code: "X20",
287
- ntee_description: "Christian",
288
- mission: "Faith-based health outreach serving Tuscaloosa families through free dental kits, health screenings, and nutrition education",
289
- services: [
290
- "Free dental hygiene kits distribution",
291
- "Health screenings after Sunday service",
292
- "Nutrition education classes",
293
- "Mobile health unit partnership"
294
- ],
295
- annual_budget: 45000,
296
- families_served: 450,
297
- contact: {
298
- website: "https://fbctuscaloosa.org/health",
299
- email: "health@fbctuscaloosa.org",
300
- phone: "(205) 555-0200"
301
- },
302
- volunteer_opportunities: true,
303
- accepting_board_members: false
304
- },
305
- {
306
- name: "Tuscaloosa County Interfaith Dental Initiative",
307
- ein: "63-3456789",
308
- ntee_code: "X20",
309
- ntee_description: "Christian",
310
- mission: "Multi-faith collaboration providing free dental care to low-income students across Tuscaloosa County schools",
311
- services: [
312
- "Mobile dental unit serving Title I schools",
313
- "Free toothbrush and fluoride programs",
314
- "Parent education workshops",
315
- "Dental emergency fund for families"
316
- ],
317
- annual_budget: 180000,
318
- students_served: 1600,
319
- contact: {
320
- website: "https://tuscaloosainterfaithdental.org",
321
- email: "contact@tuscaloosainterfaithdental.org",
322
- phone: "(205) 555-0300"
323
- },
324
- volunteer_opportunities: true,
325
- accepting_board_members: true
326
- },
327
- {
328
- name: "Catholic Social Services - Dental Outreach",
329
- ein: "63-4567890",
330
- ntee_code: "X20",
331
- ntee_description: "Christian",
332
- mission: "Diocese of Birmingham outreach providing dental care and health services to underserved communities",
333
- services: [
334
- "Quarterly dental clinics at parish hall",
335
- "School dental screening partnerships",
336
- "Dental supply distribution",
337
- "Financial assistance for dental emergencies"
338
- ],
339
- annual_budget: 95000,
340
- families_served: 320,
341
- contact: {
342
- website: "https://cssalabama.org/dental",
343
- email: "dental@cssalabama.org",
344
- phone: "(205) 555-0400"
345
- },
346
- volunteer_opportunities: true,
347
- accepting_board_members: false
348
- }
349
- ];
350
-
351
- // Filter decisions based on search, topics, patterns, resources, and date range
352
- const filteredDecisions = exampleDecisions.filter(decision => {
353
- const matchesSearch = searchQuery === '' ||
354
- decision.decision_summary.toLowerCase().includes(searchQuery.toLowerCase()) ||
355
- decision.primary_rationale.toLowerCase().includes(searchQuery.toLowerCase());
356
-
357
- // Map policy_domain to topic IDs
358
- const topicMap = {
359
- 'health': 'public-health',
360
- 'facilities': 'infrastructure',
361
- 'budget': 'education' // Example mapping
362
- };
363
- const decisionTopic = topicMap[decision.policy_domain] || decision.policy_domain;
364
- const matchesTopic = selectedTopics.length === 0 || selectedTopics.includes(decisionTopic);
365
-
366
- // Date filtering
367
- const decisionDate = new Date(decision.meeting_date);
368
- const matchesStartDate = !startDate || decisionDate >= new Date(startDate);
369
- const matchesEndDate = !endDate || decisionDate <= new Date(endDate);
370
-
371
- // TODO: Add pattern matching based on decision.patterns field
372
- // TODO: Add resource matching based on decision.available_resources field
373
-
374
- return matchesSearch && matchesTopic && matchesStartDate && matchesEndDate;
375
- });
376
-
377
- // Filter nonprofits and churches based on search and topic
378
- const filteredNonprofits = exampleNonprofits.filter(org => {
379
- const matchesSearch = searchQuery === '' ||
380
- org.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
381
- org.mission.toLowerCase().includes(searchQuery.toLowerCase()) ||
382
- org.services.some(service => service.toLowerCase().includes(searchQuery.toLowerCase())) ||
383
- org.ntee_description.toLowerCase().includes(searchQuery.toLowerCase());
384
-
385
- // Map NTEE codes to topic IDs
386
- const nteeTopicMap = {
387
- 'E': 'public-health', // Health
388
- 'F': 'public-health', // Mental Health
389
- 'K': 'public-health', // Food/Nutrition
390
- 'O': 'education', // Youth Development
391
- 'P': 'education', // Human Services
392
- 'X': 'public-health' // Religious (often health ministries)
393
- };
394
- const orgTopic = nteeTopicMap[org.ntee_code?.[0]] || 'public-health';
395
- const matchesTopic = selectedTopics.length === 0 || selectedTopics.includes(orgTopic);
396
-
397
- return matchesSearch && matchesTopic;
398
- });
399
-
400
- return (
401
- <div>
402
- {/* Sticky Header */}
403
- <div style={{
404
- position: 'sticky',
405
- top: 0,
406
- zIndex: 1000,
407
- background: 'white',
408
- borderBottom: '1px solid #eee',
409
- boxShadow: '0 2px 8px rgba(0,0,0,0.05)'
410
- }}>
411
- <div style={{ maxWidth: 1200, margin: '0 auto', padding: '16px 24px' }}>
412
- {/* Logo and Title Row */}
413
- <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 12 }}>
414
- {/* CommunityOne Logo */}
415
- <img
416
- src="/communityone_logo.svg"
417
- alt="CommunityOne"
418
- onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
419
- style={{
420
- height: 100,
421
- cursor: 'pointer',
422
- objectFit: 'contain'
423
- }}
424
- />
425
-
426
- {/* User Location Display */}
427
- <div style={{
428
- display: 'flex',
429
- alignItems: 'center',
430
- gap: 8,
431
- padding: '8px 16px',
432
- background: '#f5f5f5',
433
- borderRadius: 8,
434
- border: '1px solid #e0e0e0'
435
- }}>
436
- <MapPin size={18} style={{ color: '#059669' }} />
437
- <div>
438
- <div style={{ fontSize: 14, fontWeight: 600, color: '#111' }}>
439
- {jurisdictionType === 'nation' ? 'United States' :
440
- jurisdictionType === 'state' ? jurisdictionState :
441
- jurisdictionType === 'county' ? `${jurisdictionName} County, ${jurisdictionState}` :
442
- jurisdictionType === 'school-district' ? `${jurisdictionName} (${jurisdictionState})` :
443
- `${jurisdictionName}, ${jurisdictionState}`}
444
- </div>
445
- <div style={{ fontSize: 12, color: '#666', textTransform: 'capitalize' }}>
446
- {jurisdictionType === 'school-district' ? 'School District' : jurisdictionType}
447
- </div>
448
- </div>
449
- <button
450
- onClick={() => {
451
- setTempJurisdictionType(jurisdictionType);
452
- setTempJurisdictionName(jurisdictionName);
453
- setTempJurisdictionState(jurisdictionState);
454
- setShowLocationModal(true);
455
- }}
456
- style={{
457
- background: 'none',
458
- border: 'none',
459
- cursor: 'pointer',
460
- padding: 4,
461
- display: 'flex',
462
- alignItems: 'center',
463
- color: '#059669'
464
- }}
465
- title="Change location"
466
- >
467
- <Edit2 size={16} />
468
- </button>
469
- </div>
470
-
471
- <div style={{ flex: 1 }}>
472
- <h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 4, color: '#111' }}>
473
- {metadata.title}
474
- </h1>
475
- <p style={{ color: '#666', fontSize: 14, lineHeight: 1.4, margin: 0 }}>
476
- {metadata.description}
477
- </p>
478
- </div>
479
- </div>
480
-
481
- {/* Global Search Bar */}
482
- <div style={{
483
- background: 'white',
484
- border: '1px solid #eee',
485
- borderRadius: 8,
486
- padding: 12,
487
- marginBottom: 12
488
- }}>
489
- <div style={{
490
- position: 'relative',
491
- display: 'flex',
492
- alignItems: 'center'
493
- }}>
494
- <Search
495
- size={16}
496
- style={{
497
- position: 'absolute',
498
- left: 12,
499
- color: '#999'
500
- }}
501
- />
502
- <input
503
- type="text"
504
- placeholder="Search decisions, nonprofits, churches, or topics..."
505
- value={searchQuery}
506
- onChange={(e) => setSearchQuery(e.target.value)}
507
- style={{
508
- width: '100%',
509
- padding: '10px 40px 10px 40px',
510
- fontSize: 14,
511
- border: '1px solid #ddd',
512
- borderRadius: 6,
513
- outline: 'none',
514
- transition: 'border-color 0.2s'
515
- }}
516
- onFocus={(e) => e.target.style.borderColor = '#059669'}
517
- onBlur={(e) => e.target.style.borderColor = '#ddd'}
518
- />
519
- {searchQuery && (
520
- <button
521
- onClick={() => setSearchQuery('')}
522
- style={{
523
- position: 'absolute',
524
- right: 12,
525
- background: 'none',
526
- border: 'none',
527
- cursor: 'pointer',
528
- color: '#999',
529
- display: 'flex',
530
- alignItems: 'center',
531
- padding: 4
532
- }}
533
- >
534
- <X size={16} />
535
- </button>
536
- )}
537
- </div>
538
- </div>
539
-
540
- {/* View Mode Toggle */}
541
- <div style={{
542
- display: 'flex',
543
- justifyContent: 'space-between',
544
- alignItems: 'center'
545
- }}>
546
- <div style={{ display: 'flex', gap: 6 }}>
547
- <button
548
- onClick={() => setViewMode('home')}
549
- style={{
550
- padding: '10px 20px',
551
- borderRadius: 8,
552
- fontSize: 15,
553
- cursor: 'pointer',
554
- border: '1px solid',
555
- borderColor: viewMode === 'home' ? '#888' : '#ddd',
556
- background: viewMode === 'home' ? '#f5f5f2' : 'white',
557
- fontWeight: viewMode === 'home' ? 500 : 400,
558
- color: viewMode === 'home' ? '#111' : '#666',
559
- display: 'flex',
560
- alignItems: 'center',
561
- gap: 6
562
- }}
563
- >
564
- <Home size={14} />
565
- Home
566
- </button>
567
- <button
568
- onClick={() => {
569
- setViewMode('browse');
570
- setExploreMode('causes');
571
- setSectorView('all');
572
- }}
573
- style={{
574
- padding: '10px 20px',
575
- borderRadius: 8,
576
- fontSize: 15,
577
- cursor: 'pointer',
578
- border: '1px solid',
579
- borderColor: (viewMode === 'browse' && exploreMode === 'causes') ? '#888' : '#ddd',
580
- background: (viewMode === 'browse' && exploreMode === 'causes') ? '#f5f5f2' : 'white',
581
- fontWeight: (viewMode === 'browse' && exploreMode === 'causes') ? 500 : 400,
582
- color: (viewMode === 'browse' && exploreMode === 'causes') ? '#111' : '#666',
583
- display: 'flex',
584
- alignItems: 'center',
585
- gap: 6
586
- }}
587
- >
588
- <Lightbulb size={14} />
589
- Explore Causes
590
- </button>
591
- <button
592
- onClick={() => {
593
- setViewMode('browse');
594
- setExploreMode('decisions');
595
- setSectorView('public');
596
- }}
597
- style={{
598
- padding: '10px 20px',
599
- borderRadius: 8,
600
- fontSize: 15,
601
- cursor: 'pointer',
602
- border: '1px solid',
603
- borderColor: (viewMode === 'browse' && exploreMode === 'decisions') ? '#888' : '#ddd',
604
- background: (viewMode === 'browse' && exploreMode === 'decisions') ? '#f5f5f2' : 'white',
605
- fontWeight: (viewMode === 'browse' && exploreMode === 'decisions') ? 500 : 400,
606
- color: (viewMode === 'browse' && exploreMode === 'decisions') ? '#111' : '#666',
607
- display: 'flex',
608
- alignItems: 'center',
609
- gap: 6
610
- }}
611
- >
612
- <Building2 size={14} />
613
- Explore Decisions
614
- </button>
615
- <button
616
- onClick={() => {
617
- setViewMode('browse');
618
- setExploreMode('organizations');
619
- setSectorView('all');
620
- }}
621
- style={{
622
- padding: '10px 20px',
623
- borderRadius: 8,
624
- fontSize: 15,
625
- cursor: 'pointer',
626
- border: '1px solid',
627
- borderColor: (viewMode === 'browse' && exploreMode === 'organizations') ? '#888' : '#ddd',
628
- background: (viewMode === 'browse' && exploreMode === 'organizations') ? '#f5f5f2' : 'white',
629
- fontWeight: (viewMode === 'browse' && exploreMode === 'organizations') ? 500 : 400,
630
- color: (viewMode === 'browse' && exploreMode === 'organizations') ? '#111' : '#666',
631
- display: 'flex',
632
- alignItems: 'center',
633
- gap: 6
634
- }}
635
- >
636
- <Heart size={14} />
637
- Explore Organizations
638
- </button>
639
- </div>
640
- </div>
641
- </div>
642
-
643
- {/* Main Content Area */}
644
- <div style={{ maxWidth: 1200, margin: '0 auto', padding: 24 }}>
645
-
646
- {/* Search Bar & Quick Filters (show in browse view) */}
647
- {viewMode === 'browse' && (
648
- <div style={{
649
- background: 'white',
650
- border: '1px solid #eee',
651
- borderRadius: 12,
652
- padding: 16,
653
- marginBottom: 16,
654
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
655
- }}>
656
- {/* Quick Filters */}
657
- <div style={{
658
- display: 'flex',
659
- gap: 16,
660
- flexWrap: 'wrap',
661
- alignItems: 'center'
662
- }}>
663
- {/* Date Range Quick Filter */}
664
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
665
- <Calendar size={14} style={{ color: '#666' }} />
666
- <span style={{ fontSize: 13, color: '#666', fontWeight: 500 }}>Date:</span>
667
- <div style={{ display: 'flex', gap: 4 }}>
668
- {['all', '7days', '30days', '90days'].map(filter => (
669
- <button
670
- key={filter}
671
- onClick={() => handleQuickDateFilter(filter)}
672
- style={{
673
- padding: '6px 12px',
674
- fontSize: 12,
675
- fontWeight: 500,
676
- border: '1px solid',
677
- borderColor: quickDateFilter === filter ? '#059669' : '#ddd',
678
- background: quickDateFilter === filter ? '#dcfce7' : 'white',
679
- color: quickDateFilter === filter ? '#059669' : '#666',
680
- borderRadius: 6,
681
- cursor: 'pointer',
682
- transition: 'all 0.2s'
683
- }}
684
- >
685
- {filter === 'all' ? 'All Time' :
686
- filter === '7days' ? 'Last 7 Days' :
687
- filter === '30days' ? 'Last 30 Days' : 'Last 90 Days'}
688
- </button>
689
- ))}
690
- </div>
691
- </div>
692
-
693
- {/* Topic Quick Filter */}
694
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
695
- <Filter size={14} style={{ color: '#666' }} />
696
- <span style={{ fontSize: 13, color: '#666', fontWeight: 500 }}>Topic:</span>
697
- <div style={{ display: 'flex', gap: 4 }}>
698
- {[
699
- { id: 'all', label: 'All' },
700
- { id: 'public-health', label: 'Health' },
701
- { id: 'education', label: 'Education' },
702
- { id: 'infrastructure', label: 'Infrastructure' },
703
- { id: 'safety', label: 'Safety' }
704
- ].map(topic => (
705
- <button
706
- key={topic.id}
707
- onClick={() => handleQuickTopicFilter(topic.id)}
708
- style={{
709
- padding: '6px 12px',
710
- fontSize: 12,
711
- fontWeight: 500,
712
- border: '1px solid',
713
- borderColor: quickTopicFilter === topic.id ? '#059669' : '#ddd',
714
- background: quickTopicFilter === topic.id ? '#dcfce7' : 'white',
715
- color: quickTopicFilter === topic.id ? '#059669' : '#666',
716
- borderRadius: 6,
717
- cursor: 'pointer',
718
- transition: 'all 0.2s'
719
- }}
720
- >
721
- {topic.label}
722
- </button>
723
- ))}
724
- </div>
725
- </div>
726
-
727
- {/* Clear All Filters */}
728
- {(searchQuery || quickDateFilter !== 'all' || quickTopicFilter !== 'all' ||
729
- selectedPatterns.length > 0 || selectedResources.length > 0) && (
730
- <button
731
- onClick={handleClearFilters}
732
- style={{
733
- padding: '6px 12px',
734
- fontSize: 12,
735
- fontWeight: 500,
736
- border: '1px solid #D85A30',
737
- background: 'white',
738
- color: '#D85A30',
739
- borderRadius: 6,
740
- cursor: 'pointer',
741
- marginLeft: 'auto'
742
- }}
743
- >
744
- Clear All
745
- </button>
746
- )}
747
- </div>
748
- </div>
749
- )}
750
-
751
- {/* Filters (show in browse view for decisions only) */}
752
- {viewMode === 'browse' && exploreMode === 'decisions' && (
753
- <TopicNavigation
754
- selectedTopics={selectedTopics}
755
- selectedPatterns={selectedPatterns}
756
- selectedResources={selectedResources}
757
- startDate={startDate}
758
- endDate={endDate}
759
- onTopicToggle={handleTopicToggle}
760
- onPatternToggle={handlePatternToggle}
761
- onResourceToggle={handleResourceToggle}
762
- onStartDateChange={setStartDate}
763
- onEndDateChange={setEndDate}
764
- onClearAll={handleClearFilters}
765
- />
766
- )}
767
-
768
- {/* Content */}
769
- {viewMode === 'home' ? (
770
- <HomePage
771
- onPersonaSelect={handlePersonaSelect}
772
- onTopicSelect={handleTopicSelect}
773
- onSectorSelect={handleSectorSelect}
774
- decisionsCount={exampleDecisions.length}
775
- nonprofitsCount={exampleNonprofits.filter(org => !org.ntee_code.startsWith('X')).length}
776
- churchesCount={exampleNonprofits.filter(org => org.ntee_code.startsWith('X')).length}
777
- />
778
- ) : viewMode === 'impact' ? (
779
- <div style={{
780
- background: 'white',
781
- border: '1px solid #eee',
782
- borderRadius: 12,
783
- padding: '1.5rem',
784
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
785
- }}>
786
- <button
787
- onClick={handleBackToHome}
788
- style={{
789
- fontSize: 13,
790
- color: '#666',
791
- background: 'none',
792
- border: 'none',
793
- cursor: 'pointer',
794
- marginBottom: 16,
795
- display: 'flex',
796
- alignItems: 'center',
797
- gap: 4
798
- }}
799
- >
800
- ← Back to Home
801
- </button>
802
- <ImpactDashboard persona={selectedPersona} topic={selectedTopic} />
803
- </div>
804
- ) : viewMode === 'split-screen' ? (
805
- <div style={{
806
- background: 'white',
807
- border: '1px solid #eee',
808
- borderRadius: 12,
809
- padding: '1.5rem',
810
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
811
- }}>
812
- <button
813
- onClick={() => setViewMode('browse')}
814
- style={{
815
- fontSize: 13,
816
- color: '#666',
817
- background: 'none',
818
- border: 'none',
819
- cursor: 'pointer',
820
- marginBottom: 16,
821
- display: 'flex',
822
- alignItems: 'center',
823
- gap: 4
824
- }}
825
- >
826
- ← Back to Decisions
827
- </button>
828
- <div style={{
829
- fontSize: 12,
830
- fontWeight: 600,
831
- color: '#059669',
832
- textTransform: 'uppercase',
833
- letterSpacing: '0.05em',
834
- marginBottom: 8
835
- }}>
836
- Split-Screen Analysis
837
- </div>
838
- <h2 style={{
839
- fontSize: 22,
840
- fontWeight: 600,
841
- color: '#111',
842
- marginBottom: 16
843
- }}>
844
- Government Decision ↔ Community Response
845
- </h2>
846
- <SplitScreenView
847
- decision={selectedDecision}
848
- nonprofits={exampleNonprofits}
849
- />
850
- </div>
851
- ) : (
852
- <div style={{
853
- background: 'white',
854
- border: '1px solid #eee',
855
- borderRadius: 12,
856
- padding: '1.5rem',
857
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
858
- }}>
859
- <h2 style={{
860
- fontSize: 20,
861
- fontWeight: 500,
862
- marginBottom: 16
863
- }}>
864
- {exploreMode === 'decisions' ? 'Explore Decisions' : exploreMode === 'organizations' ? 'Explore Organizations' : 'Explore Causes'}
865
- {sectorView !== 'all' && sectorView !== 'public' && exploreMode !== 'causes' && (
866
- <span style={{ color: '#999', fontWeight: 400 }}>
867
- {' '}› {sectorView === 'nonprofits' ? 'Nonprofits' : 'Churches'}
868
- </span>
869
- )}
870
- </h2>
871
-
872
- {/* Sector Filter Buttons */}
873
- {exploreMode === 'decisions' && (
874
- <div style={{
875
- display: 'flex',
876
- gap: 8,
877
- marginBottom: 20,
878
- padding: 16,
879
- background: '#f9fafb',
880
- borderRadius: 8,
881
- flexWrap: 'wrap'
882
- }}>
883
- <button
884
- onClick={() => setSectorView('public')}
885
- style={{
886
- padding: '10px 16px',
887
- borderRadius: 6,
888
- fontSize: 14,
889
- fontWeight: 500,
890
- cursor: 'pointer',
891
- border: '1px solid',
892
- borderColor: sectorView === 'public' ? '#185FA5' : '#ddd',
893
- background: sectorView === 'public' ? '#EFF6FF' : 'white',
894
- color: sectorView === 'public' ? '#185FA5' : '#666',
895
- display: 'flex',
896
- alignItems: 'center',
897
- gap: 6
898
- }}
899
- >
900
- <Building2 size={14} />
901
- Public Sector ({filteredDecisions.length})
902
- </button>
903
- </div>
904
- )}
905
-
906
- {exploreMode === 'organizations' && (
907
- <div style={{
908
- display: 'flex',
909
- gap: 8,
910
- marginBottom: 20,
911
- padding: 16,
912
- background: '#f9fafb',
913
- borderRadius: 8,
914
- flexWrap: 'wrap'
915
- }}>
916
- <button
917
- onClick={() => setSectorView('all')}
918
- style={{
919
- padding: '10px 16px',
920
- borderRadius: 6,
921
- fontSize: 14,
922
- fontWeight: 500,
923
- cursor: 'pointer',
924
- border: '1px solid',
925
- borderColor: sectorView === 'all' ? '#059669' : '#ddd',
926
- background: sectorView === 'all' ? '#dcfce7' : 'white',
927
- color: sectorView === 'all' ? '#059669' : '#666',
928
- display: 'flex',
929
- alignItems: 'center',
930
- gap: 6
931
- }}
932
- >
933
- <Grid size={14} />
934
- All Organizations
935
- </button>
936
- <button
937
- onClick={() => setSectorView('nonprofits')}
938
- style={{
939
- padding: '10px 16px',
940
- borderRadius: 6,
941
- fontSize: 14,
942
- fontWeight: 500,
943
- cursor: 'pointer',
944
- border: '1px solid',
945
- borderColor: sectorView === 'nonprofits' ? '#059669' : '#ddd',
946
- background: sectorView === 'nonprofits' ? '#dcfce7' : 'white',
947
- color: sectorView === 'nonprofits' ? '#059669' : '#666',
948
- display: 'flex',
949
- alignItems: 'center',
950
- gap: 6
951
- }}
952
- >
953
- <Heart size={14} />
954
- Nonprofits ({filteredNonprofits.filter(org => !org.ntee_code.startsWith('X')).length})
955
- </button>
956
- <button
957
- onClick={() => setSectorView('churches')}
958
- style={{
959
- padding: '10px 16px',
960
- borderRadius: 6,
961
- fontSize: 14,
962
- fontWeight: 500,
963
- cursor: 'pointer',
964
- border: '1px solid',
965
- borderColor: sectorView === 'churches' ? '#A855F7' : '#ddd',
966
- background: sectorView === 'churches' ? '#FAF5FF' : 'white',
967
- color: sectorView === 'churches' ? '#A855F7' : '#666',
968
- display: 'flex',
969
- alignItems: 'center',
970
- gap: 6
971
- }}
972
- >
973
- <Church size={14} />
974
- Churches ({filteredNonprofits.filter(org => org.ntee_code.startsWith('X')).length})
975
- </button>
976
- </div>
977
- )}
978
-
979
- {/* Public Sector Decisions */}
980
- {exploreMode === 'decisions' && (
981
- <div>
982
- {filteredDecisions.length === 0 ? (
983
- <div style={{
984
- textAlign: 'center',
985
- padding: '2rem',
986
- color: '#999',
987
- background: '#f9fafb',
988
- borderRadius: 8
989
- }}>
990
- <p style={{ fontSize: 15, marginBottom: 8 }}>
991
- No government decisions match your filters
992
- </p>
993
- <button
994
- onClick={handleClearFilters}
995
- style={{
996
- fontSize: 14,
997
- color: '#D85A30',
998
- background: 'none',
999
- border: 'none',
1000
- cursor: 'pointer',
1001
- textDecoration: 'underline'
1002
- }}
1003
- >
1004
- Clear all filters
1005
- </button>
1006
- </div>
1007
- ) : (
1008
- <>
1009
- <div style={{ fontSize: 14, color: '#888', marginBottom: 12 }}>
1010
- Showing {filteredDecisions.length} decision{filteredDecisions.length !== 1 ? 's' : ''}
1011
- </div>
1012
- {filteredDecisions.map((decision, i) => (
1013
- <DecisionCard
1014
- key={i}
1015
- decision={decision}
1016
- onClick={() => handleDecisionClick(decision)}
1017
- />
1018
- ))}
1019
- </>
1020
- )}
1021
- </div>
1022
- )}
1023
-
1024
- {/* Nonprofits Section */}
1025
- {exploreMode === 'organizations' && (sectorView === 'all' || sectorView === 'nonprofits') && (
1026
- <div style={{ marginBottom: sectorView === 'all' ? 32 : 0 }}>
1027
- {sectorView === 'all' && (
1028
- <h3 style={{
1029
- fontSize: 18,
1030
- fontWeight: 600,
1031
- color: '#059669',
1032
- marginBottom: 16,
1033
- display: 'flex',
1034
- alignItems: 'center',
1035
- gap: 8
1036
- }}>
1037
- <Heart size={20} />
1038
- Nonprofit Organizations
1039
- </h3>
1040
- )}
1041
- {filteredNonprofits.filter(org => !org.ntee_code.startsWith('X')).length === 0 ? (
1042
- <div style={{
1043
- textAlign: 'center',
1044
- padding: '2rem',
1045
- color: '#999',
1046
- background: '#f9fafb',
1047
- borderRadius: 8
1048
- }}>
1049
- <p style={{ fontSize: 15 }}>
1050
- No nonprofits match your search
1051
- </p>
1052
- </div>
1053
- ) : (
1054
- filteredNonprofits
1055
- .filter(org => !org.ntee_code.startsWith('X'))
1056
- .map((nonprofit, i) => (
1057
- <NonprofitCard key={i} nonprofit={nonprofit} isChurch={false} />
1058
- ))
1059
- )}
1060
- </div>
1061
- )}
1062
-
1063
- {/* Churches Section */}
1064
- {exploreMode === 'organizations' && (sectorView === 'all' || sectorView === 'churches') && (
1065
- <div>
1066
- {sectorView === 'all' && (
1067
- <h3 style={{
1068
- fontSize: 18,
1069
- fontWeight: 600,
1070
- color: '#A855F7',
1071
- marginBottom: 16,
1072
- display: 'flex',
1073
- alignItems: 'center',
1074
- gap: 8
1075
- }}>
1076
- <Church size={20} />
1077
- Faith-Based Organizations
1078
- </h3>
1079
- )}
1080
- {filteredNonprofits.filter(org => org.ntee_code.startsWith('X')).length === 0 ? (
1081
- <div style={{
1082
- textAlign: 'center',
1083
- padding: '2rem',
1084
- color: '#999',
1085
- background: '#f9fafb',
1086
- borderRadius: 8
1087
- }}>
1088
- <p style={{ fontSize: 15 }}>
1089
- No churches match your search
1090
- </p>
1091
- </div>
1092
- ) : (
1093
- filteredNonprofits
1094
- .filter(org => org.ntee_code.startsWith('X'))
1095
- .map((church, i) => (
1096
- <NonprofitCard key={i} nonprofit={church} isChurch={true} />
1097
- ))
1098
- )}
1099
- </div>
1100
- )}
1101
-
1102
- {/* Causes Section */}
1103
- {exploreMode === 'causes' && (
1104
- <div>
1105
- <div style={{
1106
- textAlign: 'center',
1107
- padding: '3rem 2rem',
1108
- color: '#666',
1109
- background: '#f9fafb',
1110
- borderRadius: 12,
1111
- border: '2px dashed #ddd'
1112
- }}>
1113
- <Lightbulb size={48} style={{ color: '#D85A30', marginBottom: 16 }} />
1114
- <h3 style={{
1115
- fontSize: 20,
1116
- fontWeight: 600,
1117
- color: '#111',
1118
- marginBottom: 12
1119
- }}>
1120
- Explore Root Causes
1121
- </h3>
1122
- <p style={{ fontSize: 15, lineHeight: 1.6, maxWidth: 600, margin: '0 auto', marginBottom: 16 }}>
1123
- Discover the underlying factors driving policy decisions in your community.
1124
- Understand the connections between social determinants, community needs, and government actions.
1125
- </p>
1126
- <p style={{ fontSize: 14, color: '#999', fontStyle: 'italic' }}>
1127
- Coming soon: Interactive cause analysis and trend mapping
1128
- </p>
1129
- </div>
1130
- </div>
1131
- )}
1132
- </div>
1133
- )}
1134
- </div>
1135
- </div>
1136
-
1137
- {/* Location Selection Modal */}
1138
- {showLocationModal && (
1139
- <div style={{
1140
- position: 'fixed',
1141
- top: 0,
1142
- left: 0,
1143
- right: 0,
1144
- bottom: 0,
1145
- background: 'rgba(0,0,0,0.5)',
1146
- display: 'flex',
1147
- alignItems: 'center',
1148
- justifyContent: 'center',
1149
- zIndex: 2000
1150
- }}>
1151
- <div style={{
1152
- background: 'white',
1153
- borderRadius: 12,
1154
- padding: 32,
1155
- maxWidth: 500,
1156
- width: '90%',
1157
- boxShadow: '0 10px 40px rgba(0,0,0,0.2)'
1158
- }}>
1159
- <h2 style={{
1160
- fontSize: 22,
1161
- fontWeight: 600,
1162
- marginBottom: 8,
1163
- color: '#111'
1164
- }}>
1165
- Select Your Jurisdiction
1166
- </h2>
1167
- <p style={{
1168
- color: '#666',
1169
- fontSize: 14,
1170
- marginBottom: 24
1171
- }}>
1172
- Choose the level and location to see relevant decisions and organizations.
1173
- </p>
1174
-
1175
- <div style={{ marginBottom: 16 }}>
1176
- <label style={{
1177
- display: 'block',
1178
- fontSize: 14,
1179
- fontWeight: 500,
1180
- marginBottom: 8,
1181
- color: '#111'
1182
- }}>
1183
- Jurisdiction Level
1184
- </label>
1185
- <select
1186
- value={tempJurisdictionType}
1187
- onChange={(e) => setTempJurisdictionType(e.target.value)}
1188
- style={{
1189
- width: '100%',
1190
- padding: '10px 12px',
1191
- fontSize: 15,
1192
- border: '1px solid #ddd',
1193
- borderRadius: 6,
1194
- outline: 'none',
1195
- cursor: 'pointer'
1196
- }}
1197
- onFocus={(e) => e.target.style.borderColor = '#059669'}
1198
- onBlur={(e) => e.target.style.borderColor = '#ddd'}
1199
- >
1200
- <option value="nation">Nation</option>
1201
- <option value="state">State</option>
1202
- <option value="county">County</option>
1203
- <option value="city">City</option>
1204
- <option value="school-district">School District</option>
1205
- </select>
1206
- </div>
1207
-
1208
- {tempJurisdictionType !== 'nation' && (
1209
- <div style={{ marginBottom: 16 }}>
1210
- <label style={{
1211
- display: 'block',
1212
- fontSize: 14,
1213
- fontWeight: 500,
1214
- marginBottom: 8,
1215
- color: '#111'
1216
- }}>
1217
- {tempJurisdictionType === 'state' ? 'State' :
1218
- tempJurisdictionType === 'county' ? 'County Name' :
1219
- tempJurisdictionType === 'school-district' ? 'School District Name' :
1220
- 'City Name'}
1221
- </label>
1222
- <input
1223
- type="text"
1224
- value={tempJurisdictionName}
1225
- onChange={(e) => setTempJurisdictionName(e.target.value)}
1226
- placeholder={
1227
- tempJurisdictionType === 'state' ? 'e.g., Alabama' :
1228
- tempJurisdictionType === 'county' ? 'e.g., Tuscaloosa' :
1229
- tempJurisdictionType === 'school-district' ? 'e.g., Tuscaloosa City Schools' :
1230
- 'e.g., Tuscaloosa'
1231
- }
1232
- style={{
1233
- width: '100%',
1234
- padding: '10px 12px',
1235
- fontSize: 15,
1236
- border: '1px solid #ddd',
1237
- borderRadius: 6,
1238
- outline: 'none'
1239
- }}
1240
- onFocus={(e) => e.target.style.borderColor = '#059669'}
1241
- onBlur={(e) => e.target.style.borderColor = '#ddd'}
1242
- />
1243
- </div>
1244
- )}
1245
-
1246
- {tempJurisdictionType !== 'nation' && tempJurisdictionType !== 'state' && (
1247
- <div style={{ marginBottom: 24 }}>
1248
- <label style={{
1249
- display: 'block',
1250
- fontSize: 14,
1251
- fontWeight: 500,
1252
- marginBottom: 8,
1253
- color: '#111'
1254
- }}>
1255
- State
1256
- </label>
1257
- <input
1258
- type="text"
1259
- value={tempJurisdictionState}
1260
- onChange={(e) => setTempJurisdictionState(e.target.value)}
1261
- placeholder="e.g., AL"
1262
- maxLength="2"
1263
- style={{
1264
- width: '100%',
1265
- padding: '10px 12px',
1266
- fontSize: 15,
1267
- border: '1px solid #ddd',
1268
- borderRadius: 6,
1269
- outline: 'none',
1270
- textTransform: 'uppercase'
1271
- }}
1272
- onFocus={(e) => e.target.style.borderColor = '#059669'}
1273
- onBlur={(e) => e.target.style.borderColor = '#ddd'}
1274
- />
1275
- </div>
1276
- )}
1277
-
1278
- {tempJurisdictionType === 'state' && (
1279
- <div style={{ marginBottom: 24 }}>
1280
- <label style={{
1281
- display: 'block',
1282
- fontSize: 14,
1283
- fontWeight: 500,
1284
- marginBottom: 8,
1285
- color: '#111'
1286
- }}>
1287
- State Code
1288
- </label>
1289
- <input
1290
- type="text"
1291
- value={tempJurisdictionState}
1292
- onChange={(e) => setTempJurisdictionState(e.target.value)}
1293
- placeholder="e.g., AL"
1294
- maxLength="2"
1295
- style={{
1296
- width: '100%',
1297
- padding: '10px 12px',
1298
- fontSize: 15,
1299
- border: '1px solid #ddd',
1300
- borderRadius: 6,
1301
- outline: 'none',
1302
- textTransform: 'uppercase'
1303
- }}
1304
- onFocus={(e) => e.target.style.borderColor = '#059669'}
1305
- onBlur={(e) => e.target.style.borderColor = '#ddd'}
1306
- />
1307
- </div>
1308
- )}
1309
-
1310
- <div style={{
1311
- display: 'flex',
1312
- gap: 12,
1313
- justifyContent: 'flex-end'
1314
- }}>
1315
- <button
1316
- onClick={() => setShowLocationModal(false)}
1317
- style={{
1318
- padding: '10px 20px',
1319
- fontSize: 15,
1320
- border: '1px solid #ddd',
1321
- background: 'white',
1322
- borderRadius: 6,
1323
- cursor: 'pointer',
1324
- color: '#666'
1325
- }}
1326
- >
1327
- Cancel
1328
- </button>
1329
- <button
1330
- onClick={() => {
1331
- setJurisdictionType(tempJurisdictionType);
1332
- setJurisdictionName(tempJurisdictionName);
1333
- setJurisdictionState(tempJurisdictionState);
1334
- setShowLocationModal(false);
1335
- }}
1336
- style={{
1337
- padding: '10px 20px',
1338
- fontSize: 15,
1339
- border: 'none',
1340
- background: '#059669',
1341
- color: 'white',
1342
- borderRadius: 6,
1343
- cursor: 'pointer',
1344
- fontWeight: 500
1345
- }}
1346
- >
1347
- Save Location
1348
- </button>
1349
- </div>
1350
- </div>
1351
- </div>
1352
- )}
1353
- </div>
1354
- );
1355
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/App.jsx.backup DELETED
@@ -1 +0,0 @@
1
- // Backup of corrupted file - see App.jsx for clean version
 
 
frontend/policy-dashboards/src/App.jsx.corrupted DELETED
@@ -1,553 +0,0 @@
1
- import React, { useState } from 'react';
2
- import HomePage from './components/HomePage';
3
- import ImpactDashboard from './components/ImpactDashboard';
4
- import TopicNavigation from './components/TopicNavigation';
5
- import Summary from './components/Summary';
6
- import WordsVsDollars from './components/WordsVsDollars';
7
- import EndlessStudyLoop from './components/EndlessStudyLoop';
8
- import WhereMoneyWent from './components/WhereMoneyWent';
9
- import WhoIsInCharge from './components/WhoIsInCharge';
10
- import FilterPanel from './components/shared/FilterPanel';
11
- import DecisionCard from './components/shared/DecisionCard';
12
- import { metadata } from './data/dashboardData';
13
- import { Home, Layout, Grid, List, BarChart3 } from 'lucide-react';
14
-
15
- const tabs = [
16
- { id: 0, label: 'Summary', component: Summary },
17
- { id: 1, label: 'They cut health spending while praising wellness', component: WordsVsDollars },
18
- { id: 2, label: 'Delayed 6 months and counting', component: EndlessStudyLoop },
19
- { id: 3, label: 'What got funded instead', component: WhereMoneyWent },
20
- { id: 4, label: 'One memo beat 240 residents', component: WhoIsInCharge },
21
- ];
22
-
23
- export default function App() {
24
- const [active, setActive] = useState(0);
25
- const [viewMode, setViewMode] = useState('home'); // 'home', 'impact', 'dashboards', 'decisions', 'browse'
26
- const [selectedPersona, setSelectedPersona] = useState(null);
27
- const [selectedTopic, setSelectedTopic] = useState(null);
28
- const [selectedDomains, setSelectedDomains] = useState([]);
29
- const [selectedTopics, setSelectedTopics] = useState([]);
30
- const [selectedPatterns, setSelectedPatterns] = useState([]);
31
- const [selectedResources, setSelectedResources] = useState([]);
32
- const [searchQuery, setSearchQuery] = useState('');
33
-
34
- const handlePersonaSelect = (persona, topic) => {
35
- setSelectedPersona(persona);
36
- setSelectedTopic(topic);
37
- setViewMode('impact');
38
- };
39
-
40
- const handleTopicSelect = (topicId) => {
41
- setSelectedTopics([topicId]);
42
- setViewMode('browse');
43
- };
44
-
45
- const handleDomainToggle = (domainId) => {
46
- setSelectedDomains(prev =>
47
- prev.includes(domainId)
48
- ? prev.filter(d => d !== domainId)
49
- : [...prev, domainId]
50
- );
51
- };
52
-
53
- const handleTopicToggle = (topicId) => {
54
- setSelectedTopics(prev =>
55
- prev.includes(topicId)
56
- ? prev.filter(t => t !== topicId)
57
- : [...prev, topicId]
58
- );
59
- };
60
-
61
- const handlePatternToggle = (patternId) => {
62
- setSelectedPatterns(prev =>
63
- prev.includes(patternId)
64
- ? prev.filter(p => p !== patternId)
65
- : [...prev, patternId]
66
- );
67
- };
68
-
69
- const handleResourceToggle = (resourceId) => {
70
- setSelectedResources(prev =>
71
- prev.includes(resourceId)
72
- ? prev.filter(r => r !== resourceId)
73
- : [...prev, resourceId]
74
- );
75
- };
76
-
77
- const handleClearFilters = () => {
78
- setSelectedDomains([]);
79
- setSelectedTopics([]);
80
- setSelectedPatterns([]);
81
- setSelectedResources([]);
82
- setSearchQuery('');
83
- };
84
-
85
- const handleBackToHome = () => {
86
- setViewMode('home');
87
- setSelectedPersona(null);
88
- setSelectedTopic(null);
89
- };
90
-
91
- // Example decision data - would come from Python export
92
- const exampleDecisions = [
93
- {
94
- decision_summary: "Approval of $850,000 athletic turf replacement project",
95
- outcome: "Approved",
96
- primary_rationale: "Athletic facilities are essential for student engagement and community pride. The current turf is beyond its useful life and poses safety concerns.",
97
- supporters: [
98
- { name: "Board Member Johnson", role: "Board Member" },
99
- { name: "Athletic Director Smith", role: "Staff" }
100
- ],
101
- opponents: [
102
- { name: "Parent Coalition Representative", role: "Public Comment" }
103
- ],
104
- vote_result: "5-2",
105
- meeting_date: "2026-03-15",
106
- tradeoffs_discussed: ["Capital funding vs. operational needs", "Visibility vs. academic priorities"],
107
- evidence_cited: [{ source: "Facility Assessment Report 2025" }],
108
- policy_domain: "facilities"
109
- },
110
- {
111
- decision_summary: "Tabled: West Alabama Community Dental Clinic partnership proposal",
112
- outcome: "Deferred",
113
- primary_rationale: "Need additional time to review liability implications and insurance coverage requirements before making a final decision.",
114
- supporters: [
115
- { name: "Health Department Representative", role: "External Partner" },
116
- { name: "240+ citizen comments", role: "Public Comment" }
117
- ],
118
- opponents: [],
119
- vote_result: "Motion to table: 6-1",
120
- meeting_date: "2026-04-10",
121
- tradeoffs_discussed: ["Liability concerns vs. student health needs"],
122
- evidence_cited: [
123
- { source: "Risk Manager Patricia Johnson memo" },
124
- { source: "Insurance policy documentation" }
125
- ],
126
- policy_domain: "health"
127
- },
128
- {
129
- decision_summary: "Contracted health services budget reduction of $120,000",
130
- outcome: "Approved as part of budget",
131
- primary_rationale: "Administrative cost increases require reallocation from non-critical service contracts. Health screenings can be covered through existing nurse staff.",
132
- supporters: [
133
- { name: "CFO Thompson", role: "Staff" },
134
- { name: "Budget Committee", role: "Committee" }
135
- ],
136
- opponents: [
137
- { name: "School Nurses Association", role: "Public Comment" }
138
- ],
139
- vote_result: "7-0",
140
- meeting_date: "2026-02-20",
141
- tradeoffs_discussed: ["Cost savings vs. preventive health services"],
142
- evidence_cited: [{ source: "FY2026 Budget Proposal" }],
143
- policy_domain: "budget"
144
- }
145
- ];
146
-
147
- // Filter decisions based on search and domains
148
- const filteredDecisions = exampleDecisions.filter(decision => {
149
- const matchesSearch = searchQuery === '' ||
150
- decision.decision_summary.toLowerCase().includes(searchQuery.toLowerCase());
151
-
152
- const matchesDomain = selectedDomains.length === 0 ||
153
- selectedDomains.includes(decision.policy_domain);
154
-
155
- return matchesSearch && matchesDomain;
156
- });
157
-
158
- const ActiveComponent = tabs[active].component;
159
-
160
- return (
161
- <div style={{ maxWidth: 1200, margin: '0 auto', padding: 24 }}>
162
- {/* Header */}
163
- <div style={{ marginBottom: 24 }}>
164
- <h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 4, color: '#111' }}>
165
- {metadata.title}
166
- </h1>
167
- <p style={{ color: '#666', fontSize: 14, lineHeight: 1.5 }}>
168
- {metadata.description}
169
- </p>
170
- </div>
171
-
172
- {/* View Mode Toggle */}
173
- <div style={{
174
- display: 'flex',
175
- justifyContent: 'space-between',
176
- alignItems: 'center',
177
- marginBottom: 16
178
- }}>
179
- <div style={{ display: 'flex', gap: 6 }}>
180
- <button
181
- onClick={() => setViewMode('home')}
182
- style={{
183
- padding: '8px 16px',
184
- borderRadius: 8,
185
- fontSize: 13,
186
- cursor: 'pointer',
187
- border: '1px solid',
188
- borderColor: viewMode === 'home' ? '#888' : '#ddd',
189
- background: viewMode === 'home' ? '#f5f5f2' : 'white',
190
- fontWeight: viewMode === 'home' ? 500 : 400,
191
- color: viewMode === 'home' ? '#111' : '#666',
192
- display: 'flex',
193
- alignItems: 'center',
194
- gap: 6
195
- }}
196
- >
197
- <Home size={14} />
198
- Home
199
- </button>
200
- <button
201
- onClick={() => setViewMode('browse')}
202
- style={{
203
- padding: '8px 16px',
204
- borderRadius: 8,
205
- fontSize: 13,
206
- cursor: 'pointer',
207
- border: '1px solid',
208
- borderColor: viewMode === 'browse' ? '#888' : '#ddd',
209
- background: viewMode === 'browse' ? '#f5f5f2' : 'white',
210
- fontWeight: viewMode === 'browse' ? 500 : 400,
211
- color: viewMode === 'browse' ? '#111' : '#666',
212
- display: 'flex',
213
- alignItems: 'center',
214
- gap: 6
215
- }}
216
- >
217
- <Grid size={14} />
218
- Browse by Topic
219
- </button>
220
- <button
221
- onClick={() => setViewMode('dashboards')}
222
- style={{
223
- padding: '8px 16px',
224
- borderRadius: 8,
225
- fontSize: 13,
226
- cursor: 'pointer',
227
- border: '1px solid',
228
- borderColor: viewMode === 'dashboards' ? '#888' : '#ddd',
229
- background: viewMode === 'dashboards' ? '#f5f5f2' : 'white',
230
- fontWeight: viewMode === 'dashboards' ? 500 : 400,
231
- color: viewMode === 'dashboards' ? '#111' : '#666',
232
- display: 'flex',
233
- alignItems: 'center',
234
- gap: 6
235
- }}
236
- >
237
- <BarChart3 size={14} />
238
- Analysis Dashboards
239
- </button>
240
- <button
241
- onClick={() => setViewMode('decisions')}
242
- style={{
243
- padding: '8px 16px',
244
- borderRadius: 8,
245
- fontSize: 13,
246
- cursor: 'pointer',
247
- border: '1px solid',
248
- borderColor: viewMode === 'decisions' ? '#888' : '#ddd',
249
- background: viewMode === 'decisions' ? '#f5f5f2' : 'white',
250
- fontWeight: viewMode === 'decisions' ? 500 : 400,
251
- color: viewMode === 'decisions' ? '#111' : '#666',
252
- display: 'flex',
253
- alignItems: 'center',
254
- gap: 6
255
- }}
256
- >
257
- <List size={14} />
258
- Alx',
259
- justifyContent: 'space-between',
260
- alignItems: 'center',
261
- marginBottom: 16
262
- }}>
263
- <div style={{ display: 'flex', gap: 6 }}>
264
- <button
265
- onClick={() => setViewMode('dashboards')}
266
- style={{
267
- Topic Navigation (show in browse view) */}
268
- {viewMode === 'browse' && (
269
- <TopicNavigation
270
- selectedTopics={selectedTopics}
271
- selectedPatterns={selectedPatterns}
272
- selectedResources={selectedResources}
273
- onTopicToggle={handleTopicToggle}
274
- onPatternToggle={handlePatternToggle}
275
- onResourceToggle={handleResourceToggle}
276
- onClearAll={handleClearFilters}
277
- />
278
- )}
279
-
280
- {/* padding: '8px 16px',
281
- borderRadius: 8,
282
- fontSize: 13,
283
- cursor: 'pointer',
284
- border: '1px solid',
285
- borderColor: viewMode === 'dashboards' ? '#888' : '#ddd',
286
- background: viewMode === 'dashboards' ? '#f5f5f2' : 'white',
287
- fontWeight: viewMode === 'dashboards' ? 500 : 400,
288
- color: viewMode === 'dashboards' ? '#111' : '#666',
289
- display: 'flex',
290
- alignItems: 'center',
291
- gap: 6
292
- }}
293
- >
294
- <Layout size={14} />
295
- Dashboards
296
- </button>
297
- <button
298
- onClick={() => setViewMode('decisions')}
299
- style={{
300
- padding: '8px 16px',
301
- borderRadius: 8,
302
- fontSize: 13,
303
- cursor: 'pointer',
304
- border: '1px solid',
305
- borderColor: viewMode === 'decisions' ? '#888' : '#ddd',
306
- background: viewMode === 'decisions' ? '#f5f5f2' : 'white',
307
- fontWeight: viewMode === 'decisions' ? 500 : 400,
308
- color: viewMode === 'decisions' ? '#111' : '#666',
309
- display: 'flex',
310
- alignIthome' ? (
311
- <HomePage
312
- onPersonaSelect={handlePersonaSelect}
313
- onTopicSelect={handleTopicSelect}
314
- />
315
- ) : viewMode === 'impact' ? (
316
- <div style={{
317
- background: 'white',
318
- border: '1px solid #eee',
319
- borderRadius: 12,
320
- padding: '1.5rem',
321
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
322
- }}>
323
- <button
324
- onClick={handleBackToHome}
325
- style={{
326
- fontSize: 13,
327
- color: '#666',
328
- background: 'none',
329
- border: 'none',
330
- cursor: 'pointer',
331
- marginBottom: 16,
332
- display: 'flex',
333
- alignItems: 'center',
334
- gap: 4
335
- }}
336
- >
337
- ← Back to Home
338
- </button>
339
- <ImpactDashboard persona={selectedPersona} topic={selectedTopic} />
340
- </div>
341
- ) : viewMode === 'browse' ? (
342
- <div style={{
343
- background: 'white',
344
- border: '1px solid #eee',
345
- borderRadius: 12,
346
- padding: '1.5rem',
347
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
348
- }}>
349
- <h2 style={{
350
- fontSize: 16,
351
- fontWeight: 500,
352
- marginBottom: 16
353
- }}>
354
- Browse Decisions by Topic
355
- </h2>
356
-
357
- {filteredDecisions.length === 0 ? (
358
- <div style={{
359
- textAlign: 'center',
360
- padding: '3rem',
361
- color: '#999'
362
- }}>
363
- <p style={{ fontSize: 14, marginBottom: 8 }}>
364
- No decisions match your filters
365
- </p>
366
- <button
367
- onClick={handleClearFilters}
368
- style={{
369
- fontSize: 13,
370
- color: '#D85A30',
371
- background: 'none',
372
- border: 'none',
373
- cursor: 'pointer',
374
- textDecoration: 'underline'
375
- }}
376
- >
377
- Clear filters
378
- </button>
379
- </div>
380
- ) : (
381
- <>
382
- <div style={{ fontSize: 13, color: '#888', marginBottom: 16 }}>
383
- {filteredDecisions.length} decision{filteredDecisions.length !== 1 ? 's' : ''} found
384
- </div>
385
- {filteredDecisions.map((decision, i) => (
386
- <DecisionCard
387
- key={i}
388
- decision={decision}
389
- onClick={() => console.log('View decision:', decision)}
390
- />
391
- ))}
392
- </>
393
- )}
394
- </div>
395
- ) : viewMode === 'ems: 'center',
396
- gap: 6
397
- }}
398
- >
399
- <List size={14} />
400
- Individual Decisions
401
- </button>
402
- </div>
403
- </div>
404
-
405
- {/* Filters (show in decisions view) */}
406
- {viewMode === 'decisions' && (
407
- <FilterPanel
408
- selectedDomains={selectedDomains}
409
- onDomainToggle={handleDomainToggle}
410
- searchQuery={searchQuery}
411
- onSearchChange={setSearchQuery}
412
- onClear={handleClearFilters}
413
- />
414
- )}
415
-
416
- {/* Tab Navigation (show in dashboard view) */}
417
- {viewMode === 'dashboards' && (
418
- <div style={{
419
- display: 'flex',
420
- gap: 6,
421
- flexWrap: 'wrap',
422
- marginBottom: 24
423
- }}>
424
- {tabs.map((t) => (
425
- <button
426
- key={t.id}
427
- onClick={() => setActive(t.id)}
428
- style={{
429
- padding: '6px 14px',
430
- borderRadius: 8,
431
- fontSize: 13,
432
- cursor: 'pointer',
433
- border: '1px solid',
434
- borderColor: active === t.id ? '#888' : '#ddd',
435
- background: active === t.id ? '#f5f5f2' : 'white',
436
- fontWeight: active === t.id ? 500 : 400,
437
- color: active === t.id ? '#111' : '#666',
438
- transition: 'all 0.2s ease'
439
- }}
440
- >
441
- {t.label}
442
- </button>
443
- ))}
444
- </div>
445
- )}
446
-
447
- {/* Content */}
448
- {viewMode === 'dashboards' ? (
449
- <div style={{
450
- background: 'white',
451
- border: '1px solid #eee',
452
- borderRadius: 12,
453
- padding: '1.5rem',
454
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
455
- }}>
456
- <h2 style={{
457
- fontSize: 14,
458
- textTransform: 'uppercase',
459
- letterSpacing: '0.07em',
460
- color: '#aaa',
461
- marginBottom: 16
462
- }}>
463
- {tabs[active].label}
464
- </h2>
465
-
466
- {active === 0 ? (
467
- <ActiveComponent onNavigate={handleNavigate} />
468
- ) : (
469
- <ActiveComponent />
470
- )}
471
- </div>
472
- ) : (
473
- <div style={{
474
- background: 'white',
475
- border: '1px solid #eee',
476
- borderRadius: 12,
477
- padding: '1.5rem',
478
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
479
- }}>
480
- <div style={{
481
- display: 'flex',
482
- justifyContent: 'space-between',
483
- alignItems: 'center',
484
- marginBottom: 16
485
- }}>
486
- <h2 style={{
487
- fontSize: 14,
488
- textTransform: 'uppercase',
489
- letterSpacing: '0.07em',
490
- color: '#aaa'
491
- }}>
492
- Individual Decisions
493
- </h2>
494
- <div style={{ fontSize: 13, color: '#888' }}>
495
- {filteredDecisions.length} decision{filteredDecisions.length !== 1 ? 's' : ''}
496
- </div>
497
- </div>
498
-
499
- {filteredDecisions.length === 0 ? (
500
- <div style={{
501
- textAlign: 'center',
502
- padding: '3rem',
503
- color: '#999'
504
- }}>
505
- <p style={{ fontSize: 14, marginBottom: 8 }}>
506
- No decisions match your filters
507
- </p>
508
- <button
509
- onClick={handleClearFilters}
510
- style={{
511
- fontSize: 13,
512
- color: '#D85A30',
513
- background: 'none',
514
- border: 'none',
515
- cursor: 'pointer',
516
- textDecoration: 'underline'
517
- }}
518
- >
519
- Clear filters
520
- </button>
521
- </div>
522
- ) : (
523
- filteredDecisions.map((decision, i) => (
524
- <DecisionCard
525
- key={i}
526
- decision={decision}
527
- onClick={() => {
528
- // Could open modal or navigate to detail view
529
- console.log('View decision details:', decision);
530
- }}
531
- />
532
- ))
533
- )}
534
- </div>
535
- )}
536
-
537
- {/* Footer */}
538
- <div style={{
539
- marginTop: 24,
540
- textAlign: 'center',
541
- fontSize: 11,
542
- color: '#999'
543
- }}>
544
- <p>
545
- Generated by <strong>Oral Health Policy Pulse</strong> — Evidence-based advocacy toolkit
546
- </p>
547
- <p style={{ marginTop: 4 }}>
548
- Methodology: Accountability dashboards using public meeting records and budget data
549
- </p>
550
- </div>
551
- </div>
552
- );
553
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/EndlessStudyLoop.jsx DELETED
@@ -1,162 +0,0 @@
1
- import React from 'react';
2
- import MetricCard from './shared/MetricCard';
3
- import Compare from './shared/Compare';
4
- import InsightBox from './shared/InsightBox';
5
- import { logicChainData as d } from '../data/dashboardData';
6
-
7
- /**
8
- * Dashboard 2: Delayed 6 months and counting
9
- * (Logic Chain / Sequential Deferral)
10
- */
11
- export default function EndlessStudyLoop() {
12
- return (
13
- <div>
14
- <p style={{
15
- fontSize: 14,
16
- color: '#555',
17
- borderLeft: '2px solid #D85A30',
18
- paddingLeft: 12,
19
- marginBottom: 20
20
- }}>
21
- {d.conclusion}
22
- </p>
23
-
24
- {/* Topic */}
25
- <div style={{
26
- background: '#f5f5f2',
27
- padding: 12,
28
- borderRadius: 8,
29
- marginBottom: 16
30
- }}>
31
- <div style={{ fontSize: 11, color: '#999', textTransform: 'uppercase', marginBottom: 4 }}>
32
- Policy Decision
33
- </div>
34
- <div style={{ fontSize: 14, fontWeight: 500, color: '#222' }}>
35
- {d.topic}
36
- </div>
37
- </div>
38
-
39
- {/* Key Metrics */}
40
- <div style={{
41
- display: 'grid',
42
- gridTemplateColumns: '1fr 1fr',
43
- gap: 12,
44
- marginBottom: 20
45
- }}>
46
- <MetricCard
47
- value={`${d.totalDeferrals}×`}
48
- label={`Times deferred in ${d.monthsInLimbo} months`}
49
- tone="negative"
50
- />
51
- <MetricCard
52
- value={new Set(d.justifications.map(j => j.reason)).size}
53
- label="Distinct justifications used"
54
- />
55
- </div>
56
-
57
- {/* The "Study" Loop Timeline */}
58
- <div style={{ marginBottom: 20 }}>
59
- <h4 style={{
60
- fontSize: 12,
61
- textTransform: 'uppercase',
62
- letterSpacing: '0.05em',
63
- color: '#999',
64
- marginBottom: 12
65
- }}>
66
- Shifting Justifications
67
- </h4>
68
-
69
- <div style={{ position: 'relative', paddingLeft: 24 }}>
70
- {/* Vertical timeline line */}
71
- <div style={{
72
- position: 'absolute',
73
- left: 7,
74
- top: 8,
75
- bottom: 8,
76
- width: 1,
77
- background: '#ddd'
78
- }} />
79
-
80
- {d.justifications.map((item, i) => (
81
- <div key={i} style={{ position: 'relative', marginBottom: 18 }}>
82
- {/* Timeline dot */}
83
- <div style={{
84
- position: 'absolute',
85
- left: -20,
86
- top: 4,
87
- width: 8,
88
- height: 8,
89
- borderRadius: '50%',
90
- background: item.status === 'deferred' ? '#D85A30' : '#BA7517',
91
- border: '1.5px solid',
92
- borderColor: item.status === 'deferred' ? '#D85A30' : '#BA7517'
93
- }} />
94
-
95
- {/* Timeline content */}
96
- <div style={{ fontSize: 11, color: '#999', marginBottom: 2 }}>
97
- {item.month} — <strong>{item.status}</strong>
98
- </div>
99
- <div style={{ fontSize: 13, color: '#555' }}>
100
- "{item.reason}"
101
- </div>
102
- {item.speaker && item.speaker !== 'N/A' && (
103
- <div style={{ fontSize: 11, color: '#888', fontStyle: 'italic', marginTop: 2 }}>
104
- — {item.speaker}
105
- </div>
106
- )}
107
- </div>
108
- ))}
109
- </div>
110
- </div>
111
-
112
- {/* Benchmark Comparison */}
113
- <div style={{ marginTop: 20 }}>
114
- <h4 style={{
115
- fontSize: 12,
116
- textTransform: 'uppercase',
117
- letterSpacing: '0.05em',
118
- color: '#999',
119
- marginBottom: 10
120
- }}>
121
- School-Linked Dental Programs by State Type
122
- </h4>
123
- <Compare
124
- benchmarks={d.benchmarks}
125
- metric="activePrograms"
126
- prefix=""
127
- suffix=" states"
128
- />
129
- <p style={{
130
- fontSize: 11,
131
- color: '#888',
132
- marginTop: 8,
133
- textAlign: 'center'
134
- }}>
135
- Source: ASTDD State Oral Health Program Database, 2025
136
- </p>
137
- </div>
138
-
139
- {/* The Logic */}
140
- <InsightBox type="critical">
141
- <strong>{d.patternType}:</strong> {d.inference}
142
- </InsightBox>
143
-
144
- {/* Question for the Room */}
145
- <div style={{
146
- marginTop: 16,
147
- padding: 14,
148
- background: '#fff',
149
- border: '2px solid #D85A30',
150
- borderRadius: 8
151
- }}>
152
- <strong style={{ fontSize: 13, color: '#D85A30' }}>Ask them:</strong>
153
- <p style={{ fontSize: 13, color: '#222', marginTop: 6 }}>
154
- "This proposal has been 'under review' for {d.monthsInLimbo} months with {d.totalDeferrals} deferrals.
155
- Each time, you give a different reason. {d.benchmarks.republicanAvg.activePrograms} Republican-led states
156
- and {d.benchmarks.democraticAvg.activePrograms} Democratic-led states already have active programs.
157
- What analysis are you waiting for that 35 states haven't already completed?"
158
- </p>
159
- </div>
160
- </div>
161
- );
162
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/HomePage.jsx DELETED
@@ -1,291 +0,0 @@
1
- import React from 'react';
2
- import { TrendingUp, AlertCircle, Users, Scale, Droplet, Building2, Heart, Church, Grid } from 'lucide-react';
3
-
4
- /**
5
- * HomePage Component - Policy Accountability Platform
6
- * Sector-based landing page for citizens
7
- */
8
- export default function HomePage({ onPersonaSelect, onTopicSelect, onSectorSelect }) {
9
- return (
10
- <div>
11
- {/* Top Section: Explore by Sector */}
12
- <div style={{
13
- background: 'linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%)',
14
- color: 'white',
15
- borderRadius: 12,
16
- padding: '2rem',
17
- marginBottom: 24
18
- }}>
19
- <div style={{ marginBottom: 16 }}>
20
- <div style={{ fontSize: 13, color: '#aaa', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8 }}>
21
- Policy Accountability Platform
22
- </div>
23
- <h1 style={{ fontSize: 28, fontWeight: 500, marginBottom: 12, lineHeight: 1.3 }}>
24
- Track Government Decisions & Community Solutions
25
- </h1>
26
- <p style={{ fontSize: 16, color: '#ccc', lineHeight: 1.6 }}>
27
- Explore policy decisions, track accountability, and discover community resources
28
- </p>
29
- </div>
30
-
31
- {/* Sector Navigation Cards */}
32
- <div style={{
33
- display: 'grid',
34
- gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
35
- gap: 12,
36
- marginTop: 24
37
- }}>
38
- <SectorCard
39
- icon={<Grid size={24} />}
40
- title="All Sectors"
41
- description="View everything together"
42
- color="#059669"
43
- onClick={() => onSectorSelect('all')}
44
- />
45
- <SectorCard
46
- icon={<Building2 size={24} />}
47
- title="Public Sector"
48
- description="Government decisions"
49
- color="#185FA5"
50
- onClick={() => onSectorSelect('public')}
51
- />
52
- <SectorCard
53
- icon={<Heart size={24} />}
54
- title="Nonprofits"
55
- description="Community organizations"
56
- color="#059669"
57
- onClick={() => onSectorSelect('nonprofits')}
58
- />
59
- <SectorCard
60
- icon={<Church size={24} />}
61
- title="Churches"
62
- description="Faith-based ministries"
63
- color="#A855F7"
64
- onClick={() => onSectorSelect('churches')}
65
- />
66
- </div>
67
- </div>
68
-
69
- {/* Middle Section: Find Your Impact (Persona Filters) */}
70
- <div style={{ marginBottom: 24 }}>
71
- <h2 style={{ fontSize: 22, fontWeight: 500, marginBottom: 8 }}>
72
- Find Your Impact
73
- </h2>
74
- <p style={{ fontSize: 16, color: '#666', marginBottom: 16 }}>
75
- How are these decisions affecting you?
76
- </p>
77
-
78
- <div style={{
79
- display: 'grid',
80
- gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
81
- gap: 16
82
- }}>
83
- <PersonaCard
84
- icon="🏠"
85
- persona="Parent"
86
- concern="Student Dental Health"
87
- action="The Learning Barrier Map"
88
- onClick={() => onPersonaSelect('parent', 'dental-health')}
89
- />
90
- <PersonaCard
91
- icon="📢"
92
- persona="Advocate"
93
- concern="Transparency & Vetoes"
94
- action="The Influence Radar"
95
- onClick={() => onPersonaSelect('advocate', 'transparency')}
96
- />
97
- <PersonaCard
98
- icon="🚰"
99
- persona="Resident"
100
- concern="Water & Infrastructure"
101
- action="The Lifetime Health Tax"
102
- onClick={() => onPersonaSelect('resident', 'water-infrastructure')}
103
- />
104
- </div>
105
- </div>
106
-
107
- {/* Bottom Section: Topic Navigation */}
108
- <div>
109
- <h2 style={{ fontSize: 18, fontWeight: 500, marginBottom: 16 }}>
110
- Browse by Topic
111
- </h2>
112
-
113
- <div style={{
114
- display: 'grid',
115
- gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
116
- gap: 12
117
- }}>
118
- <TopicCard
119
- icon={<Scale size={24} />}
120
- title="Public Health"
121
- subtitle="Dental, Water, Mental Health"
122
- count={24}
123
- color="#1D9E75"
124
- onClick={() => onTopicSelect('public-health')}
125
- />
126
- <TopicCard
127
- icon={<Users size={24} />}
128
- title="Education & Youth"
129
- subtitle="School Board, Pre-K"
130
- count={18}
131
- color="#185FA5"
132
- onClick={() => onTopicSelect('education')}
133
- />
134
- <TopicCard
135
- icon={<TrendingUp size={24} />}
136
- title="Infrastructure"
137
- subtitle="Roads, Utilities, Construction"
138
- count={32}
139
- color="#BA7517"
140
- onClick={() => onTopicSelect('infrastructure')}
141
- />
142
- <TopicCard
143
- icon={<Droplet size={24} />}
144
- title="Public Safety"
145
- subtitle="Police, Fire, EMS"
146
- count={15}
147
- color="#E24B4A"
148
- onClick={() => onTopicSelect('public-safety')}
149
- />
150
- </div>
151
- </div>
152
- </div>
153
- );
154
- }
155
-
156
- function SectorCard({ icon, title, description, color, onClick }) {
157
- return (
158
- <div
159
- onClick={onClick}
160
- style={{
161
- background: 'rgba(255, 255, 255, 0.1)',
162
- border: '1px solid rgba(255, 255, 255, 0.2)',
163
- borderRadius: 8,
164
- padding: 20,
165
- cursor: 'pointer',
166
- transition: 'all 0.2s ease',
167
- textAlign: 'center'
168
- }}
169
- onMouseEnter={(e) => {
170
- e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
171
- e.currentTarget.style.borderColor = color;
172
- e.currentTarget.style.transform = 'translateY(-4px)';
173
- }}
174
- onMouseLeave={(e) => {
175
- e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
176
- e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.2)';
177
- e.currentTarget.style.transform = 'translateY(0)';
178
- }}
179
- >
180
- <div style={{ color: color, marginBottom: 12 }}>
181
- {icon}
182
- </div>
183
- <div style={{ fontSize: 16, fontWeight: 600, marginBottom: 4, color: 'white' }}>
184
- {title}
185
- </div>
186
- <div style={{ fontSize: 13, color: '#aaa' }}>
187
- {description}
188
- </div>
189
- </div>
190
- );
191
- }
192
-
193
- function PersonaCard({ icon, persona, concern, action, onClick }) {
194
- return (
195
- <div
196
- onClick={onClick}
197
- style={{
198
- background: 'white',
199
- border: '2px solid #eee',
200
- borderRadius: 12,
201
- padding: 20,
202
- cursor: 'pointer',
203
- transition: 'all 0.2s ease'
204
- }}
205
- onMouseEnter={(e) => {
206
- e.currentTarget.style.borderColor = '#D85A30';
207
- e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
208
- }}
209
- onMouseLeave={(e) => {
210
- e.currentTarget.style.borderColor = '#eee';
211
- e.currentTarget.style.boxShadow = 'none';
212
- }}
213
- >
214
- <div style={{ fontSize: 32, marginBottom: 12 }}>{icon}</div>
215
- <div style={{ marginBottom: 8 }}>
216
- <div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}>
217
- I am a...
218
- </div>
219
- <div style={{ fontSize: 16, fontWeight: 500, color: '#222' }}>
220
- {persona}
221
- </div>
222
- </div>
223
- <div style={{ marginBottom: 12 }}>
224
- <div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}>
225
- I care about...
226
- </div>
227
- <div style={{ fontSize: 14, color: '#555' }}>
228
- {concern}
229
- </div>
230
- </div>
231
- <div style={{
232
- fontSize: 13,
233
- color: '#D85A30',
234
- fontWeight: 500,
235
- display: 'flex',
236
- alignItems: 'center',
237
- gap: 6
238
- }}>
239
- Show me → {action}
240
- </div>
241
- </div>
242
- );
243
- }
244
-
245
- function TopicCard({ icon, title, subtitle, count, color, onClick }) {
246
- return (
247
- <div
248
- onClick={onClick}
249
- style={{
250
- background: 'white',
251
- border: '1px solid #eee',
252
- borderRadius: 8,
253
- padding: 16,
254
- cursor: 'pointer',
255
- transition: 'all 0.2s ease',
256
- display: 'flex',
257
- alignItems: 'center',
258
- gap: 12
259
- }}
260
- onMouseEnter={(e) => {
261
- e.currentTarget.style.borderColor = color;
262
- e.currentTarget.style.transform = 'translateY(-2px)';
263
- e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
264
- }}
265
- onMouseLeave={(e) => {
266
- e.currentTarget.style.borderColor = '#eee';
267
- e.currentTarget.style.transform = 'translateY(0)';
268
- e.currentTarget.style.boxShadow = 'none';
269
- }}
270
- >
271
- <div style={{ color }}>{icon}</div>
272
- <div style={{ flex: 1 }}>
273
- <div style={{ fontSize: 14, fontWeight: 500, color: '#222', marginBottom: 2 }}>
274
- {title}
275
- </div>
276
- <div style={{ fontSize: 11, color: '#888' }}>
277
- {subtitle}
278
- </div>
279
- </div>
280
- <div style={{
281
- fontSize: 18,
282
- fontWeight: 500,
283
- color,
284
- minWidth: 32,
285
- textAlign: 'right'
286
- }}>
287
- {count}
288
- </div>
289
- </div>
290
- );
291
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/ImpactDashboard.jsx DELETED
@@ -1,280 +0,0 @@
1
- import React from 'react';
2
- import { XCircle, AlertTriangle, MapPin } from 'lucide-react';
3
-
4
- /**
5
- * ImpactDashboard Component
6
- * Visual impact story for specific persona + topic combination
7
- */
8
- export default function ImpactDashboard({ persona, topic }) {
9
- // Example: Parent + Dental Health
10
- if (persona === 'parent' && topic === 'dental-health') {
11
- return <DentalHealthImpact />;
12
- }
13
-
14
- // Example: Advocate + Transparency
15
- if (persona === 'advocate' && topic === 'transparency') {
16
- return <TransparencyImpact />;
17
- }
18
-
19
- // Default fallback
20
- return <GenericImpact persona={persona} topic={topic} />;
21
- }
22
-
23
- function DentalHealthImpact() {
24
- return (
25
- <div>
26
- {/* Header */}
27
- <div style={{ marginBottom: 24 }}>
28
- <div style={{ fontSize: 11, color: '#999', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>
29
- Impact Story: Parent → Student Dental Health
30
- </div>
31
- <h2 style={{ fontSize: 20, fontWeight: 500, color: '#222', marginBottom: 8 }}>
32
- The School Dental Screening Veto
33
- </h2>
34
- <p style={{ fontSize: 14, color: '#D85A30', fontWeight: 500 }}>
35
- A legal "Risk Rationale" is blocking healthcare for 5,000 students
36
- </p>
37
- </div>
38
-
39
- {/* The Visual Split */}
40
- <div style={{
41
- display: 'grid',
42
- gridTemplateColumns: '1fr 1fr',
43
- gap: 20,
44
- marginBottom: 24
45
- }}>
46
- {/* Left: The Human Cost */}
47
- <div style={{
48
- background: 'white',
49
- border: '2px solid #eee',
50
- borderRadius: 12,
51
- padding: 20
52
- }}>
53
- <div style={{
54
- display: 'flex',
55
- alignItems: 'center',
56
- gap: 8,
57
- marginBottom: 16
58
- }}>
59
- <MapPin size={20} color="#D85A30" />
60
- <h3 style={{ fontSize: 16, fontWeight: 500, margin: 0 }}>
61
- The Human Cost
62
- </h3>
63
- </div>
64
-
65
- {/* School Map Placeholder */}
66
- <div style={{
67
- background: '#f5f5f2',
68
- borderRadius: 8,
69
- padding: 16,
70
- marginBottom: 16,
71
- minHeight: 200,
72
- display: 'flex',
73
- flexDirection: 'column',
74
- justifyContent: 'center',
75
- alignItems: 'center',
76
- color: '#888'
77
- }}>
78
- <MapPin size={32} color="#D85A30" style={{ marginBottom: 8 }} />
79
- <div style={{ fontSize: 13, textAlign: 'center' }}>
80
- Map of Tuscaloosa Schools
81
- </div>
82
- <div style={{ fontSize: 11, textAlign: 'center', marginTop: 4 }}>
83
- Red = High dental pain absence rates
84
- </div>
85
- <div style={{ fontSize: 11, textAlign: 'center', color: '#185FA5' }}>
86
- Blue dots = Mobile clinics (currently: 0)
87
- </div>
88
- </div>
89
-
90
- {/* Stats */}
91
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
92
- <div style={{ background: '#ffe6e6', borderRadius: 6, padding: 12 }}>
93
- <div style={{ fontSize: 20, fontWeight: 500, color: '#D85A30' }}>
94
- 48%
95
- </div>
96
- <div style={{ fontSize: 11, color: '#666' }}>
97
- Students with no dental visit last year
98
- </div>
99
- </div>
100
- <div style={{ background: '#fff4e6', borderRadius: 6, padding: 12 }}>
101
- <div style={{ fontSize: 20, fontWeight: 500, color: '#BA7517' }}>
102
- 127
103
- </div>
104
- <div style={{ fontSize: 11, color: '#666' }}>
105
- Days missed due to dental pain (2025)
106
- </div>
107
- </div>
108
- </div>
109
- </div>
110
-
111
- {/* Right: The Veto */}
112
- <div style={{
113
- background: 'white',
114
- border: '2px solid #eee',
115
- borderRadius: 12,
116
- padding: 20
117
- }}>
118
- <div style={{
119
- display: 'flex',
120
- alignItems: 'center',
121
- gap: 8,
122
- marginBottom: 16
123
- }}>
124
- <XCircle size={20} color="#E24B4A" />
125
- <h3 style={{ fontSize: 16, fontWeight: 500, margin: 0 }}>
126
- The Veto Chain
127
- </h3>
128
- </div>
129
-
130
- {/* Flowchart */}
131
- <div style={{ position: 'relative' }}>
132
- {/* Step 1: Public Demand */}
133
- <div style={{
134
- background: '#e6f4f1',
135
- border: '2px solid #1D9E75',
136
- borderRadius: 8,
137
- padding: 12,
138
- marginBottom: 16
139
- }}>
140
- <div style={{ fontSize: 12, fontWeight: 500, color: '#1D9E75', marginBottom: 4 }}>
141
- ✓ Public Demand
142
- </div>
143
- <div style={{ fontSize: 13, color: '#222' }}>
144
- 1,200 Signed Petitions
145
- </div>
146
- <div style={{ fontSize: 11, color: '#666', marginTop: 4 }}>
147
- 240+ Testimonies at board meetings
148
- </div>
149
- </div>
150
-
151
- {/* Arrow */}
152
- <div style={{
153
- textAlign: 'center',
154
- margin: '-8px 0',
155
- fontSize: 20,
156
- color: '#D85A30',
157
- fontWeight: 'bold'
158
- }}>
159
- ↓ BLOCKED
160
- </div>
161
-
162
- {/* Step 2: The Blocker */}
163
- <div style={{
164
- background: '#ffe6e6',
165
- border: '2px solid #E24B4A',
166
- borderRadius: 8,
167
- padding: 12,
168
- marginBottom: 16,
169
- marginTop: 8
170
- }}>
171
- <div style={{ fontSize: 12, fontWeight: 500, color: '#E24B4A', marginBottom: 4 }}>
172
- ✗ The Blocker
173
- </div>
174
- <div style={{ fontSize: 13, color: '#222', fontWeight: 500 }}>
175
- Risk Management Memo
176
- </div>
177
- <div style={{ fontSize: 11, color: '#666', marginTop: 4 }}>
178
- From: Patricia Johnson, District Legal Office
179
- </div>
180
- <div style={{ fontSize: 11, color: '#666' }}>
181
- Concern: "Insurance Liability"
182
- </div>
183
- </div>
184
-
185
- {/* Arrow */}
186
- <div style={{
187
- textAlign: 'center',
188
- margin: '-8px 0',
189
- fontSize: 20,
190
- color: '#888'
191
- }}>
192
-
193
- </div>
194
-
195
- {/* Step 3: The Result */}
196
- <div style={{
197
- background: '#f5f5f2',
198
- border: '2px solid #888',
199
- borderRadius: 8,
200
- padding: 12,
201
- marginTop: 8
202
- }}>
203
- <div style={{ fontSize: 12, fontWeight: 500, color: '#888', marginBottom: 4 }}>
204
- The Result
205
- </div>
206
- <div style={{ fontSize: 13, color: '#222' }}>
207
- Board votes to "Table" initiative
208
- </div>
209
- <div style={{ fontSize: 11, color: '#D85A30', marginTop: 4 }}>
210
- Status: Deferred for 152 days
211
- </div>
212
- </div>
213
- </div>
214
- </div>
215
- </div>
216
-
217
- {/* Bottom: Key Fact */}
218
- <div style={{
219
- background: '#fff4e6',
220
- border: '2px solid #BA7517',
221
- borderRadius: 12,
222
- padding: 16,
223
- display: 'flex',
224
- alignItems: 'flex-start',
225
- gap: 12
226
- }}>
227
- <AlertTriangle size={24} color="#BA7517" />
228
- <div>
229
- <div style={{ fontSize: 14, fontWeight: 500, color: '#222', marginBottom: 4 }}>
230
- Key Finding
231
- </div>
232
- <div style={{ fontSize: 13, color: '#555' }}>
233
- Zero successful liability lawsuits in any of the 35 states with active school dental screening programs.
234
- The "risk" cited in the memo has no empirical basis.
235
- </div>
236
- </div>
237
- </div>
238
- </div>
239
- );
240
- }
241
-
242
- function TransparencyImpact() {
243
- return (
244
- <div>
245
- <div style={{ marginBottom: 16 }}>
246
- <div style={{ fontSize: 11, color: '#999', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>
247
- Impact Story: Advocate → Transparency & Vetoes
248
- </div>
249
- <h2 style={{ fontSize: 20, fontWeight: 500, color: '#222', marginBottom: 8 }}>
250
- Who Really Decides?
251
- </h2>
252
- <p style={{ fontSize: 14, color: '#D85A30', fontWeight: 500 }}>
253
- Unelected staff have veto power that outweighs public input
254
- </p>
255
- </div>
256
-
257
- <div style={{ fontSize: 13, color: '#666', marginBottom: 16 }}>
258
- See the Influence Radar dashboard for detailed analysis →
259
- </div>
260
- </div>
261
- );
262
- }
263
-
264
- function GenericImpact({ persona, topic }) {
265
- return (
266
- <div>
267
- <div style={{ marginBottom: 16 }}>
268
- <div style={{ fontSize: 11, color: '#999', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>
269
- Impact Story: {persona} → {topic}
270
- </div>
271
- <h2 style={{ fontSize: 20, fontWeight: 500, color: '#222', marginBottom: 8 }}>
272
- Coming Soon
273
- </h2>
274
- <p style={{ fontSize: 14, color: '#666' }}>
275
- This impact story is being developed. Check back soon or browse other topics.
276
- </p>
277
- </div>
278
- </div>
279
- );
280
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/NonprofitCard.jsx DELETED
@@ -1,290 +0,0 @@
1
- import React from 'react';
2
- import { Heart, Mail, Phone, Globe, Users, DollarSign } from 'lucide-react';
3
-
4
- /**
5
- * NonprofitCard Component
6
- * Displays individual nonprofit or church organization
7
- */
8
- export default function NonprofitCard({ nonprofit, isChurch = false }) {
9
- const {
10
- name,
11
- ein,
12
- ntee_code,
13
- ntee_description,
14
- mission,
15
- services = [],
16
- annual_budget,
17
- students_served,
18
- families_served,
19
- youth_served,
20
- contact,
21
- volunteer_opportunities,
22
- accepting_board_members
23
- } = nonprofit;
24
-
25
- return (
26
- <div style={{
27
- background: 'white',
28
- border: '1px solid',
29
- borderColor: isChurch ? '#A855F7' : '#10b981',
30
- borderLeft: `4px solid ${isChurch ? '#A855F7' : '#10b981'}`,
31
- borderRadius: 12,
32
- padding: 20,
33
- marginBottom: 16,
34
- boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
35
- }}>
36
- {/* Header */}
37
- <div style={{ marginBottom: 16 }}>
38
- <div style={{
39
- display: 'flex',
40
- justifyContent: 'space-between',
41
- alignItems: 'flex-start',
42
- marginBottom: 8
43
- }}>
44
- <h3 style={{
45
- fontSize: 18,
46
- fontWeight: 600,
47
- color: '#111',
48
- lineHeight: 1.4
49
- }}>
50
- {name}
51
- </h3>
52
- {isChurch && (
53
- <span style={{
54
- padding: '4px 10px',
55
- borderRadius: 12,
56
- background: '#F3E8FF',
57
- color: '#A855F7',
58
- fontSize: 11,
59
- fontWeight: 500
60
- }}>
61
- ⛪ Faith-Based
62
- </span>
63
- )}
64
- </div>
65
-
66
- <div style={{
67
- fontSize: 12,
68
- color: '#666',
69
- marginBottom: 8
70
- }}>
71
- NTEE {ntee_code}: {ntee_description}
72
- {ein && <span style={{ marginLeft: 8, color: '#999' }}>• EIN: {ein}</span>}
73
- </div>
74
-
75
- <div style={{
76
- fontSize: 15,
77
- color: '#444',
78
- lineHeight: 1.6,
79
- marginBottom: 16
80
- }}>
81
- {mission}
82
- </div>
83
- </div>
84
-
85
- {/* Services */}
86
- {services.length > 0 && (
87
- <div style={{ marginBottom: 16 }}>
88
- <div style={{
89
- fontSize: 13,
90
- fontWeight: 500,
91
- color: isChurch ? '#A855F7' : '#059669',
92
- marginBottom: 8
93
- }}>
94
- Services Provided:
95
- </div>
96
- <ul style={{
97
- margin: 0,
98
- paddingLeft: 20,
99
- fontSize: 14,
100
- color: '#555',
101
- lineHeight: 1.8
102
- }}>
103
- {services.map((service, i) => (
104
- <li key={i}>{service}</li>
105
- ))}
106
- </ul>
107
- </div>
108
- )}
109
-
110
- {/* Impact Metrics */}
111
- <div style={{
112
- display: 'grid',
113
- gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
114
- gap: 12,
115
- marginBottom: 16,
116
- padding: 16,
117
- background: isChurch ? '#FAF5FF' : '#f0fdf4',
118
- borderRadius: 8
119
- }}>
120
- {students_served > 0 && (
121
- <div>
122
- <div style={{ fontSize: 11, color: '#666', marginBottom: 4 }}>Students Served</div>
123
- <div style={{
124
- fontSize: 18,
125
- fontWeight: 600,
126
- color: isChurch ? '#A855F7' : '#059669',
127
- display: 'flex',
128
- alignItems: 'center',
129
- gap: 6
130
- }}>
131
- <Users size={16} />
132
- {students_served.toLocaleString()}
133
- </div>
134
- </div>
135
- )}
136
- {families_served > 0 && (
137
- <div>
138
- <div style={{ fontSize: 11, color: '#666', marginBottom: 4 }}>Families Served</div>
139
- <div style={{
140
- fontSize: 18,
141
- fontWeight: 600,
142
- color: isChurch ? '#A855F7' : '#059669',
143
- display: 'flex',
144
- alignItems: 'center',
145
- gap: 6
146
- }}>
147
- <Heart size={16} />
148
- {families_served.toLocaleString()}
149
- </div>
150
- </div>
151
- )}
152
- {youth_served > 0 && (
153
- <div>
154
- <div style={{ fontSize: 11, color: '#666', marginBottom: 4 }}>Youth Served</div>
155
- <div style={{
156
- fontSize: 18,
157
- fontWeight: 600,
158
- color: isChurch ? '#A855F7' : '#059669',
159
- display: 'flex',
160
- alignItems: 'center',
161
- gap: 6
162
- }}>
163
- <Users size={16} />
164
- {youth_served.toLocaleString()}
165
- </div>
166
- </div>
167
- )}
168
- <div>
169
- <div style={{ fontSize: 11, color: '#666', marginBottom: 4 }}>Annual Budget</div>
170
- <div style={{
171
- fontSize: 18,
172
- fontWeight: 600,
173
- color: isChurch ? '#A855F7' : '#059669',
174
- display: 'flex',
175
- alignItems: 'center',
176
- gap: 6
177
- }}>
178
- <DollarSign size={16} />
179
- {(annual_budget / 1000).toFixed(0)}K
180
- </div>
181
- </div>
182
- </div>
183
-
184
- {/* Contact & Actions */}
185
- <div style={{
186
- display: 'flex',
187
- flexWrap: 'wrap',
188
- gap: 8,
189
- marginBottom: 12
190
- }}>
191
- {contact?.website && (
192
- <a
193
- href={contact.website}
194
- target="_blank"
195
- rel="noopener noreferrer"
196
- style={{
197
- fontSize: 13,
198
- padding: '8px 16px',
199
- background: isChurch ? '#A855F7' : '#059669',
200
- color: 'white',
201
- borderRadius: 6,
202
- textDecoration: 'none',
203
- display: 'flex',
204
- alignItems: 'center',
205
- gap: 6,
206
- fontWeight: 500
207
- }}
208
- >
209
- <Globe size={14} />
210
- Website
211
- </a>
212
- )}
213
- {contact?.email && (
214
- <a
215
- href={`mailto:${contact.email}`}
216
- style={{
217
- fontSize: 13,
218
- padding: '8px 16px',
219
- background: 'white',
220
- color: isChurch ? '#A855F7' : '#059669',
221
- border: `1px solid ${isChurch ? '#A855F7' : '#059669'}`,
222
- borderRadius: 6,
223
- textDecoration: 'none',
224
- display: 'flex',
225
- alignItems: 'center',
226
- gap: 6,
227
- fontWeight: 500
228
- }}
229
- >
230
- <Mail size={14} />
231
- Email
232
- </a>
233
- )}
234
- {contact?.phone && (
235
- <a
236
- href={`tel:${contact.phone}`}
237
- style={{
238
- fontSize: 13,
239
- padding: '8px 16px',
240
- background: 'white',
241
- color: isChurch ? '#A855F7' : '#059669',
242
- border: `1px solid ${isChurch ? '#A855F7' : '#059669'}`,
243
- borderRadius: 6,
244
- textDecoration: 'none',
245
- display: 'flex',
246
- alignItems: 'center',
247
- gap: 6,
248
- fontWeight: 500
249
- }}
250
- >
251
- <Phone size={14} />
252
- Call
253
- </a>
254
- )}
255
- </div>
256
-
257
- {/* Opportunities */}
258
- <div style={{
259
- display: 'flex',
260
- gap: 8,
261
- flexWrap: 'wrap'
262
- }}>
263
- {volunteer_opportunities && (
264
- <span style={{
265
- fontSize: 12,
266
- padding: '6px 12px',
267
- background: isChurch ? '#FAF5FF' : '#dcfce7',
268
- color: isChurch ? '#A855F7' : '#059669',
269
- borderRadius: 6,
270
- fontWeight: 500
271
- }}>
272
- ✓ Accepting Volunteers
273
- </span>
274
- )}
275
- {accepting_board_members && (
276
- <span style={{
277
- fontSize: 12,
278
- padding: '6px 12px',
279
- background: '#fef3c7',
280
- color: '#d97706',
281
- borderRadius: 6,
282
- fontWeight: 500
283
- }}>
284
- ⭐ Board Seats Available
285
- </span>
286
- )}
287
- </div>
288
- </div>
289
- );
290
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/SplitScreenView.jsx DELETED
@@ -1,375 +0,0 @@
1
- import React from 'react';
2
- import { ExternalLink, Users, DollarSign, Heart, Mail, Phone, Globe } from 'lucide-react';
3
-
4
- /**
5
- * SplitScreenView Component
6
- * Shows government decisions on the left, community nonprofits on the right
7
- * Demonstrates the accountability gap and community response
8
- */
9
- export default function SplitScreenView({ decision, nonprofits = [] }) {
10
- // Find nonprofits matching this decision's NTEE code
11
- const matchingNonprofits = nonprofits.filter(np =>
12
- np.ntee_code === decision.ntee_code ||
13
- (decision.ntee_code && np.ntee_code?.startsWith(decision.ntee_code[0])) // Match category
14
- );
15
-
16
- const hasGap = decision.community_gap?.nonprofit_filling_gap;
17
-
18
- return (
19
- <div style={{
20
- display: 'grid',
21
- gridTemplateColumns: '1fr 1fr',
22
- gap: 24,
23
- marginTop: 20
24
- }}>
25
- {/* LEFT RAIL: The Public Sector (Government Decision) */}
26
- <div style={{
27
- background: 'white',
28
- border: '1px solid #eee',
29
- borderRadius: 12,
30
- padding: 24
31
- }}>
32
- <div style={{
33
- fontSize: 12,
34
- fontWeight: 600,
35
- color: '#666',
36
- textTransform: 'uppercase',
37
- letterSpacing: '0.05em',
38
- marginBottom: 16
39
- }}>
40
- 🏛️ Public Sector Decision
41
- </div>
42
-
43
- <h3 style={{
44
- fontSize: 18,
45
- fontWeight: 600,
46
- color: '#111',
47
- marginBottom: 12,
48
- lineHeight: 1.4
49
- }}>
50
- {decision.decision_summary}
51
- </h3>
52
-
53
- <div style={{
54
- background: '#f9f9f7',
55
- borderRadius: 8,
56
- padding: 16,
57
- marginBottom: 16
58
- }}>
59
- <div style={{
60
- fontSize: 13,
61
- fontWeight: 500,
62
- color: '#BA7517',
63
- marginBottom: 8
64
- }}>
65
- Official Rationale:
66
- </div>
67
- <div style={{
68
- fontSize: 15,
69
- color: '#444',
70
- lineHeight: 1.5
71
- }}>
72
- "{decision.primary_rationale}"
73
- </div>
74
- </div>
75
-
76
- {hasGap && (
77
- <div style={{
78
- background: '#FFF5F0',
79
- border: '1px solid #D85A30',
80
- borderRadius: 8,
81
- padding: 16,
82
- marginBottom: 16
83
- }}>
84
- <div style={{
85
- fontSize: 14,
86
- fontWeight: 500,
87
- color: '#D85A30',
88
- marginBottom: 6,
89
- display: 'flex',
90
- alignItems: 'center',
91
- gap: 6
92
- }}>
93
- ⚠️ The Accountability Gap
94
- </div>
95
- <div style={{
96
- fontSize: 14,
97
- color: '#666',
98
- lineHeight: 1.5
99
- }}>
100
- {decision.community_gap.description}
101
- </div>
102
- </div>
103
- )}
104
-
105
- <div style={{
106
- fontSize: 13,
107
- color: '#888',
108
- paddingTop: 12,
109
- borderTop: '1px solid #eee'
110
- }}>
111
- <div>Outcome: <strong>{decision.outcome}</strong></div>
112
- <div>Vote: {decision.vote_result}</div>
113
- <div>Date: {new Date(decision.meeting_date).toLocaleDateString()}</div>
114
- </div>
115
- </div>
116
-
117
- {/* RIGHT RAIL: The Community Sector (Nonprofits) */}
118
- <div style={{
119
- background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)',
120
- border: '1px solid #10b981',
121
- borderRadius: 12,
122
- padding: 24
123
- }}>
124
- <div style={{
125
- fontSize: 12,
126
- fontWeight: 600,
127
- color: '#059669',
128
- textTransform: 'uppercase',
129
- letterSpacing: '0.05em',
130
- marginBottom: 16
131
- }}>
132
- 🤝 Community Sector Response
133
- </div>
134
-
135
- {matchingNonprofits.length === 0 ? (
136
- <div style={{
137
- textAlign: 'center',
138
- padding: '2rem',
139
- color: '#666'
140
- }}>
141
- <p style={{ fontSize: 15, marginBottom: 12 }}>
142
- No nonprofits found filling this gap yet.
143
- </p>
144
- <p style={{ fontSize: 13, color: '#888' }}>
145
- NTEE Code: {decision.ntee_code || 'Not classified'}
146
- </p>
147
- </div>
148
- ) : (
149
- <>
150
- <div style={{
151
- fontSize: 15,
152
- fontWeight: 500,
153
- color: '#059669',
154
- marginBottom: 16
155
- }}>
156
- {matchingNonprofits.length} organization{matchingNonprofits.length !== 1 ? 's' : ''} filling this gap:
157
- </div>
158
-
159
- <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
160
- {matchingNonprofits.map((nonprofit, i) => (
161
- <NonprofitCard key={i} nonprofit={nonprofit} />
162
- ))}
163
- </div>
164
-
165
- <div style={{
166
- marginTop: 20,
167
- padding: 16,
168
- background: 'white',
169
- borderRadius: 8,
170
- fontSize: 13,
171
- color: '#666'
172
- }}>
173
- <div style={{ fontWeight: 500, color: '#059669', marginBottom: 8 }}>
174
- 💡 Bridge the Gap
175
- </div>
176
- <ul style={{ margin: 0, paddingLeft: 20, lineHeight: 1.6 }}>
177
- <li>Support these organizations with donations or volunteering</li>
178
- <li>Join their boards to influence systemic change</li>
179
- <li>Cite their work in public meetings to show solutions exist</li>
180
- </ul>
181
- </div>
182
- </>
183
- )}
184
- </div>
185
- </div>
186
- );
187
- }
188
-
189
- function NonprofitCard({ nonprofit }) {
190
- return (
191
- <div style={{
192
- background: 'white',
193
- borderRadius: 8,
194
- padding: 16,
195
- border: '1px solid #10b981'
196
- }}>
197
- <div style={{ marginBottom: 12 }}>
198
- <h4 style={{
199
- fontSize: 16,
200
- fontWeight: 600,
201
- color: '#111',
202
- marginBottom: 4
203
- }}>
204
- {nonprofit.name}
205
- </h4>
206
- <div style={{
207
- fontSize: 12,
208
- color: '#666',
209
- marginBottom: 8
210
- }}>
211
- NTEE {nonprofit.ntee_code}: {nonprofit.ntee_description}
212
- </div>
213
- <div style={{
214
- fontSize: 14,
215
- color: '#444',
216
- lineHeight: 1.5,
217
- marginBottom: 12
218
- }}>
219
- {nonprofit.mission}
220
- </div>
221
- </div>
222
-
223
- {/* Services */}
224
- <div style={{ marginBottom: 12 }}>
225
- <div style={{
226
- fontSize: 12,
227
- fontWeight: 500,
228
- color: '#059669',
229
- marginBottom: 6
230
- }}>
231
- Services Provided:
232
- </div>
233
- <ul style={{
234
- margin: 0,
235
- paddingLeft: 20,
236
- fontSize: 13,
237
- color: '#666',
238
- lineHeight: 1.6
239
- }}>
240
- {nonprofit.services.slice(0, 3).map((service, i) => (
241
- <li key={i}>{service}</li>
242
- ))}
243
- </ul>
244
- </div>
245
-
246
- {/* Impact */}
247
- <div style={{
248
- display: 'grid',
249
- gridTemplateColumns: '1fr 1fr',
250
- gap: 12,
251
- marginBottom: 12,
252
- padding: 12,
253
- background: '#f0fdf4',
254
- borderRadius: 6
255
- }}>
256
- {nonprofit.students_served && (
257
- <div>
258
- <div style={{ fontSize: 11, color: '#666' }}>Impact</div>
259
- <div style={{ fontSize: 15, fontWeight: 600, color: '#059669' }}>
260
- <Users size={14} style={{ display: 'inline', marginRight: 4 }} />
261
- {nonprofit.students_served.toLocaleString()} students
262
- </div>
263
- </div>
264
- )}
265
- {nonprofit.families_served && (
266
- <div>
267
- <div style={{ fontSize: 11, color: '#666' }}>Impact</div>
268
- <div style={{ fontSize: 15, fontWeight: 600, color: '#059669' }}>
269
- <Heart size={14} style={{ display: 'inline', marginRight: 4 }} />
270
- {nonprofit.families_served.toLocaleString()} families
271
- </div>
272
- </div>
273
- )}
274
- {nonprofit.youth_served && (
275
- <div>
276
- <div style={{ fontSize: 11, color: '#666' }}>Impact</div>
277
- <div style={{ fontSize: 15, fontWeight: 600, color: '#059669' }}>
278
- <Users size={14} style={{ display: 'inline', marginRight: 4 }} />
279
- {nonprofit.youth_served.toLocaleString()} youth
280
- </div>
281
- </div>
282
- )}
283
- <div>
284
- <div style={{ fontSize: 11, color: '#666' }}>Annual Budget</div>
285
- <div style={{ fontSize: 15, fontWeight: 600, color: '#059669' }}>
286
- <DollarSign size={14} style={{ display: 'inline', marginRight: 2 }} />
287
- {(nonprofit.annual_budget / 1000).toFixed(0)}K
288
- </div>
289
- </div>
290
- </div>
291
-
292
- {/* Contact & Actions */}
293
- <div style={{
294
- display: 'flex',
295
- flexWrap: 'wrap',
296
- gap: 8,
297
- marginBottom: 12
298
- }}>
299
- {nonprofit.contact.website && (
300
- <a
301
- href={nonprofit.contact.website}
302
- target="_blank"
303
- rel="noopener noreferrer"
304
- style={{
305
- fontSize: 12,
306
- padding: '6px 12px',
307
- background: '#059669',
308
- color: 'white',
309
- borderRadius: 6,
310
- textDecoration: 'none',
311
- display: 'flex',
312
- alignItems: 'center',
313
- gap: 4
314
- }}
315
- >
316
- <Globe size={12} />
317
- Website
318
- </a>
319
- )}
320
- {nonprofit.contact.email && (
321
- <a
322
- href={`mailto:${nonprofit.contact.email}`}
323
- style={{
324
- fontSize: 12,
325
- padding: '6px 12px',
326
- background: 'white',
327
- color: '#059669',
328
- border: '1px solid #059669',
329
- borderRadius: 6,
330
- textDecoration: 'none',
331
- display: 'flex',
332
- alignItems: 'center',
333
- gap: 4
334
- }}
335
- >
336
- <Mail size={12} />
337
- Email
338
- </a>
339
- )}
340
- </div>
341
-
342
- {/* Opportunities */}
343
- <div style={{
344
- display: 'flex',
345
- gap: 8,
346
- flexWrap: 'wrap'
347
- }}>
348
- {nonprofit.volunteer_opportunities && (
349
- <span style={{
350
- fontSize: 11,
351
- padding: '4px 8px',
352
- background: '#dcfce7',
353
- color: '#059669',
354
- borderRadius: 4,
355
- fontWeight: 500
356
- }}>
357
- ✓ Accepting Volunteers
358
- </span>
359
- )}
360
- {nonprofit.accepting_board_members && (
361
- <span style={{
362
- fontSize: 11,
363
- padding: '4px 8px',
364
- background: '#fef3c7',
365
- color: '#d97706',
366
- borderRadius: 4,
367
- fontWeight: 500
368
- }}>
369
- ⭐ Board Seats Available
370
- </span>
371
- )}
372
- </div>
373
- </div>
374
- );
375
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/Summary.jsx DELETED
@@ -1,183 +0,0 @@
1
- import React from 'react';
2
- import { summaryData as d } from '../data/dashboardData';
3
- import { DashboardGrid } from './shared/DashboardTile';
4
-
5
- /**
6
- * Summary Dashboard
7
- * Overview of all four findings with tile-based navigation
8
- */
9
- export default function Summary({ onNavigate }) {
10
- return (
11
- <div>
12
- {/* Headline */}
13
- <div style={{ marginBottom: 24 }}>
14
- <h2 style={{ fontSize: 18, fontWeight: 500, color: '#222', marginBottom: 8 }}>
15
- {d.headline}
16
- </h2>
17
- <p style={{ fontSize: 14, color: '#666' }}>
18
- {d.subheadline}
19
- </p>
20
- </div>
21
-
22
- {/* Dashboard Tiles */}
23
- <DashboardGrid
24
- dashboards={d.findings}
25
- onNavigate={onNavigate}
26
- />
27
-
28
- {/* Legacy Finding Cards - Keep for compact view */}
29
- <details style={{ marginBottom: 24 }}>
30
- <summary style={{
31
- cursor: 'pointer',
32
- fontSize: 13,
33
- color: '#888',
34
- marginBottom: 12
35
- }}>
36
- Show compact list view
37
- </summary>
38
- <div style={{
39
- display: 'grid',
40
- gap: 12
41
- }}>
42
- {d.findings.map((finding, i) => (
43
- <div
44
- key={finding.id}
45
- onClick={() => onNavigate && onNavigate(finding.id)}
46
- style={{
47
- background: '#fff',
48
- border: '1px solid #eee',
49
- borderLeft: `4px solid ${finding.discomfort >= 9 ? '#D85A30' : '#BA7517'}`,
50
- borderRadius: 8,
51
- padding: 16,
52
- cursor: onNavigate ? 'pointer' : 'default',
53
- transition: 'all 0.2s ease'
54
- }}
55
- onMouseEnter={(e) => {
56
- if (onNavigate) {
57
- e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
58
- e.currentTarget.style.borderLeftWidth = '6px';
59
- }
60
- }}
61
- onMouseLeave={(e) => {
62
- if (onNavigate) {
63
- e.currentTarget.style.boxShadow = 'none';
64
- e.currentTarget.style.borderLeftWidth = '4px';
65
- }
66
- }}
67
- >
68
- <div style={{
69
- display: 'flex',
70
- justifyContent: 'space-between',
71
- alignItems: 'flex-start',
72
- marginBottom: 8
73
- }}>
74
- <div>
75
- <div style={{
76
- fontSize: 11,
77
- color: '#999',
78
- textTransform: 'uppercase',
79
- letterSpacing: '0.05em',
80
- marginBottom: 4
81
- }}>
82
- Dashboard {finding.id}
83
- </div>
84
- <div style={{ fontSize: 15, fontWeight: 500, color: '#222' }}>
85
- {finding.title}
86
- </div>
87
- </div>
88
- <div style={{ textAlign: 'right' }}>
89
- <div style={{
90
- fontSize: 18,
91
- fontWeight: 500,
92
- color: finding.discomfort >= 9 ? '#D85A30' : '#BA7517'
93
- }}>
94
- {finding.metric}
95
- </div>
96
- <div style={{ fontSize: 11, color: '#888' }}>
97
- {finding.context}
98
- </div>
99
- </div>
100
- </div>
101
- <p style={{ fontSize: 13, color: '#666', lineHeight: 1.5 }}>
102
- {finding.summary}
103
- </p>
104
- {finding.discomfort >= 9 && (
105
- <div style={{
106
- marginTop: 8,
107
- fontSize: 11,
108
- color: '#D85A30',
109
- fontWeight: 500
110
- }}>
111
- ⚠️ High accountability impact
112
- </div>
113
- )}
114
- </div>
115
- ))}
116
- </div>
117
- </details>
118
-
119
- {/* How to Use Section */}
120
- <div style={{
121
- background: '#f5f5f2',
122
- borderRadius: 8,
123
- padding: 18,
124
- marginBottom: 20
125
- }}>
126
- <h3 style={{
127
- fontSize: 14,
128
- fontWeight: 500,
129
- color: '#222',
130
- marginBottom: 12
131
- }}>
132
- {d.howToUse.title}
133
- </h3>
134
-
135
- <div style={{ display: 'grid', gap: 12 }}>
136
- {d.howToUse.strategies.map((strategy, i) => (
137
- <div key={i} style={{
138
- display: 'grid',
139
- gridTemplateColumns: '1fr 1fr',
140
- gap: 12
141
- }}>
142
- <div>
143
- <div style={{
144
- fontSize: 11,
145
- color: '#E24B4A',
146
- fontWeight: 500,
147
- marginBottom: 4
148
- }}>
149
- ❌ DON'T: {strategy.dont}
150
- </div>
151
- </div>
152
- <div>
153
- <div style={{
154
- fontSize: 11,
155
- color: '#1D9E75',
156
- fontWeight: 500,
157
- marginBottom: 4
158
- }}>
159
- ✅ DO: {strategy.do}
160
- </div>
161
- </div>
162
- </div>
163
- ))}
164
- </div>
165
- </div>
166
-
167
- {/* Bottom Navigation Hint */}
168
- {onNavigate && (
169
- <div style={{
170
- textAlign: 'center',
171
- fontSize: 12,
172
- color: '#888',
173
- padding: 12,
174
- background: '#fff',
175
- border: '1px dashed #ddd',
176
- borderRadius: 8
177
- }}>
178
- 💡 Click any finding above to see the detailed dashboard
179
- </div>
180
- )}
181
- </div>
182
- );
183
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/TopicNavigation.jsx DELETED
@@ -1,511 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Filter, Video, FileText, DollarSign, BarChart3 } from 'lucide-react';
3
-
4
- /**
5
- * TopicNavigation Component
6
- * Primary and secondary filters for browsing decisions
7
- */
8
- export default function TopicNavigation({
9
- selectedTopics = [],
10
- selectedPatterns = [],
11
- selectedResources = [],
12
- startDate,
13
- endDate,
14
- onTopicToggle,
15
- onPatternToggle,
16
- onResourceToggle,
17
- onStartDateChange,
18
- onEndDateChange,
19
- onClearAll
20
- }) {
21
- const [showFilters, setShowFilters] = useState(true);
22
-
23
- const topics = [
24
- { id: 'public-health', label: 'Public Health', sublabel: 'Dental, Water, Mental Health', color: '#1D9E75' },
25
- { id: 'education', label: 'Education & Youth', sublabel: 'School Board, Pre-K', color: '#185FA5' },
26
- { id: 'infrastructure', label: 'Infrastructure', sublabel: 'Roads, Utilities, Construction', color: '#BA7517' },
27
- { id: 'public-safety', label: 'Public Safety', sublabel: 'Police, Fire, EMS', color: '#E24B4A' }
28
- ];
29
-
30
- const patterns = [
31
- { id: 'technocratic-veto', label: 'Technocratic Veto', description: 'Legal/risk managers blocking decisions' },
32
- { id: 'sequential-deferral', label: 'Sequential Deferral', description: 'Repeated "tabling for study"' },
33
- { id: 'performance-rationale', label: 'Performance Rationale', description: 'Rhetoric not matching funding' }
34
- ];
35
-
36
- const resources = [
37
- { id: 'video', label: 'Video Recap', icon: Video },
38
- { id: 'budget', label: 'Budget PDF', icon: DollarSign },
39
- { id: 'dashboard', label: 'Impact Dashboard', icon: BarChart3 },
40
- { id: 'summary', label: 'Summary Notes', icon: FileText }
41
- ];
42
-
43
- const hasActiveFilters = selectedTopics.length > 0 ||
44
- selectedPatterns.length > 0 ||
45
- selectedResources.length > 0 ||
46
- startDate !== null ||
47
- endDate !== null;
48
-
49
- return (
50
- <div style={{ marginBottom: 20 }}>
51
- {/* Header */}
52
- <div style={{
53
- display: 'flex',
54
- justifyContent: 'space-between',
55
- alignItems: 'center',
56
- marginBottom: 12
57
- }}>
58
- <button
59
- onClick={() => setShowFilters(!showFilters)}
60
- style={{
61
- padding: '10px 18px',
62
- border: '1px solid',
63
- borderColor: showFilters ? '#888' : '#ddd',
64
- borderRadius: 8,
65
- background: showFilters ? '#f5f5f2' : 'white',
66
- cursor: 'pointer',
67
- fontSize: 15,
68
- fontWeight: 500,
69
- display: 'flex',
70
- alignItems: 'center',
71
- gap: 6,
72
- color: showFilters ? '#222' : '#666'
73
- }}
74
- >
75
- <Filter size={14} />
76
- Filter & Browse
77
- {hasActiveFilters && (
78
- <span style={{
79
- background: '#D85A30',
80
- color: 'white',
81
- borderRadius: '50%',
82
- width: 18,
83
- height: 18,
84
- display: 'flex',
85
- alignItems: 'center',
86
- justifyContent: 'center',
87
- fontSize: 10,
88
- fontWeight: 600
89
- }}>
90
- {selectedTopics.length + selectedPatterns.length + selectedResources.length}
91
- </span>
92
- )}
93
- </button>
94
-
95
- {hasActiveFilters && (
96
- <button
97
- onClick={onClearAll}
98
- style={{
99
- padding: '10px 18px',
100
- border: '1px solid #ddd',
101
- borderRadius: 8,
102
- background: 'white',
103
- cursor: 'pointer',
104
- fontSize: 15,
105
- color: '#D85A30'
106
- }}
107
- >
108
- Clear All Filters
109
- </button>
110
- )}
111
- </div>
112
-
113
- {/* Filter Panel */}
114
- {showFilters && (
115
- <div style={{
116
- background: '#f5f5f2',
117
- borderRadius: 12,
118
- padding: 18,
119
- display: 'grid',
120
- gridTemplateColumns: '2fr 1fr 1fr 1fr',
121
- gap: 20
122
- }}>
123
- {/* Primary Navigation: Topics */}
124
- <div>
125
- <div style={{
126
- fontSize: 12,
127
- fontWeight: 500,
128
- color: '#666',
129
- textTransform: 'uppercase',
130
- letterSpacing: '0.05em',
131
- marginBottom: 12
132
- }}>
133
- Primary Topic / Domain
134
- </div>
135
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
136
- {topics.map(topic => {
137
- const isSelected = selectedTopics.includes(topic.id);
138
- return (
139
- <button
140
- key={topic.id}
141
- onClick={() => onTopicToggle(topic.id)}
142
- style={{
143
- padding: '12px 16px',
144
- borderRadius: 8,
145
- border: '2px solid',
146
- borderColor: isSelected ? topic.color : '#ddd',
147
- background: isSelected ? `${topic.color}15` : 'white',
148
- color: '#222',
149
- fontSize: 15,
150
- cursor: 'pointer',
151
- textAlign: 'left',
152
- fontWeight: isSelected ? 500 : 400,
153
- transition: 'all 0.2s ease'
154
- }}
155
- >
156
- <div style={{ fontWeight: 500 }}>{topic.label}</div>
157
- <div style={{ fontSize: 13, color: '#888', marginTop: 2 }}>
158
- {topic.sublabel}
159
- </div>
160
- </button>
161
- );
162
- })}
163
- </div>
164
- </div>
165
-
166
- {/* Secondary Filter: Patterns */}
167
- <div>
168
- <div style={{
169
- fontSize: 12,
170
- fontWeight: 500,
171
- color: '#666',
172
- textTransform: 'uppercase',
173
- letterSpacing: '0.05em',
174
- marginBottom: 12
175
- }}>
176
- Filter by Pattern
177
- </div>
178
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
179
- {patterns.map(pattern => {
180
- const isSelected = selectedPatterns.includes(pattern.id);
181
- return (
182
- <label
183
- key={pattern.id}
184
- style={{
185
- display: 'flex',
186
- alignItems: 'flex-start',
187
- gap: 8,
188
- padding: 8,
189
- borderRadius: 6,
190
- background: isSelected ? 'white' : 'transparent',
191
- cursor: 'pointer',
192
- transition: 'background 0.2s ease'
193
- }}
194
- >
195
- <input
196
- type="checkbox"
197
- checked={isSelected}
198
- onChange={() => onPatternToggle(pattern.id)}
199
- style={{ marginTop: 2 }}
200
- />
201
- <div style={{ flex: 1 }}>
202
- <div style={{ fontSize: 12, fontWeight: 500, color: '#222' }}>
203
- {pattern.label}
204
- </div>
205
- <div style={{ fontSize: 10, color: '#888', marginTop: 2 }}>
206
- {pattern.description}
207
- </div>
208
- </div>
209
- </label>
210
- );
211
- })}
212
- </div>
213
- </div>
214
-
215
- {/* Tertiary Filter: Resources */}
216
- <div>
217
- <div style={{
218
- fontSize: 12,
219
- fontWeight: 500,
220
- color: '#666',
221
- textTransform: 'uppercase',
222
- letterSpacing: '0.05em',
223
- marginBottom: 12
224
- }}>
225
- Filter by Resource
226
- </div>
227
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
228
- {resources.map(resource => {
229
- const isSelected = selectedResources.includes(resource.id);
230
- const Icon = resource.icon;
231
- return (
232
- <label
233
- key={resource.id}
234
- style={{
235
- display: 'flex',
236
- alignItems: 'center',
237
- gap: 8,
238
- padding: 8,
239
- borderRadius: 6,
240
- background: isSelected ? 'white' : 'transparent',
241
- cursor: 'pointer',
242
- transition: 'background 0.2s ease'
243
- }}
244
- >
245
- <input
246
- type="checkbox"
247
- checked={isSelected}
248
- onChange={() => onResourceToggle(resource.id)}
249
- />
250
- <Icon size={16} color={isSelected ? '#D85A30' : '#888'} />
251
- <div style={{
252
- fontSize: 14,
253
- color: isSelected ? '#222' : '#666',
254
- fontWeight: isSelected ? 500 : 400
255
- }}>
256
- {resource.label}
257
- </div>
258
- </label>
259
- );
260
- })}
261
- </div>
262
- </div>
263
-
264
- {/* Time Window Filter */}
265
- <div>
266
- <div style={{
267
- fontSize: 12,
268
- fontWeight: 500,
269
- color: '#666',
270
- textTransform: 'uppercase',
271
- letterSpacing: '0.05em',
272
- marginBottom: 12
273
- }}>
274
- Time Window
275
- </div>
276
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
277
- <div>
278
- <label style={{
279
- fontSize: 10,
280
- color: '#888',
281
- display: 'block',
282
- marginBottom: 4
283
- }}>
284
- From
285
- </label>
286
- <input
287
- type="date"
288
- value={startDate || ''}
289
- onChange={(e) => onStartDateChange(e.target.value || null)}
290
- style={{
291
- width: '100%',
292
- padding: '6px 8px',
293
- borderRadius: 6,
294
- border: '1px solid',
295
- borderColor: startDate ? '#D85A30' : '#ddd',
296
- fontSize: 12,
297
- background: 'white'
298
- }}
299
- />
300
- </div>
301
- <div>
302
- <label style={{
303
- fontSize: 10,
304
- color: '#888',
305
- display: 'block',
306
- marginBottom: 4
307
- }}>
308
- To
309
- </label>
310
- <input
311
- type="date"
312
- value={endDate || ''}
313
- onChange={(e) => onEndDateChange(e.target.value || null)}
314
- style={{
315
- width: '100%',
316
- padding: '6px 8px',
317
- borderRadius: 6,
318
- border: '1px solid',
319
- borderColor: endDate ? '#D85A30' : '#ddd',
320
- fontSize: 12,
321
- background: 'white'
322
- }}
323
- />
324
- </div>
325
- </div>
326
- </div>
327
- </div>
328
- )}
329
-
330
- {/* Active Filters Display */}
331
- {hasActiveFilters && (
332
- <div style={{
333
- marginTop: 12,
334
- display: 'flex',
335
- flexWrap: 'wrap',
336
- gap: 6,
337
- alignItems: 'center'
338
- }}>
339
- <span style={{ fontSize: 11, color: '#888' }}>Active filters:</span>
340
- {selectedTopics.map(topicId => {
341
- const topic = topics.find(t => t.id === topicId);
342
- return (
343
- <span
344
- key={topicId}
345
- style={{
346
- fontSize: 11,
347
- padding: '4px 10px',
348
- borderRadius: 12,
349
- background: topic.color,
350
- color: 'white',
351
- fontWeight: 500,
352
- display: 'flex',
353
- alignItems: 'center',
354
- gap: 4
355
- }}
356
- >
357
- {topic.label}
358
- <button
359
- onClick={() => onTopicToggle(topicId)}
360
- style={{
361
- background: 'none',
362
- border: 'none',
363
- color: 'white',
364
- cursor: 'pointer',
365
- padding: 0,
366
- fontSize: 14,
367
- lineHeight: 1
368
- }}
369
- >
370
- ×
371
- </button>
372
- </span>
373
- );
374
- })}
375
- {selectedPatterns.map(patternId => {
376
- const pattern = patterns.find(p => p.id === patternId);
377
- return (
378
- <span
379
- key={patternId}
380
- style={{
381
- fontSize: 11,
382
- padding: '4px 10px',
383
- borderRadius: 12,
384
- background: '#BA7517',
385
- color: 'white',
386
- fontWeight: 500,
387
- display: 'flex',
388
- alignItems: 'center',
389
- gap: 4
390
- }}
391
- >
392
- {pattern.label}
393
- <button
394
- onClick={() => onPatternToggle(patternId)}
395
- style={{
396
- background: 'none',
397
- border: 'none',
398
- color: 'white',
399
- cursor: 'pointer',
400
- padding: 0,
401
- fontSize: 14,
402
- lineHeight: 1
403
- }}
404
- >
405
- ×
406
- </button>
407
- </span>
408
- );
409
- })}
410
- {selectedResources.map(resourceId => {
411
- const resource = resources.find(r => r.id === resourceId);
412
- return (
413
- <span
414
- key={resourceId}
415
- style={{
416
- fontSize: 11,
417
- padding: '4px 10px',
418
- borderRadius: 12,
419
- background: '#185FA5',
420
- color: 'white',
421
- fontWeight: 500,
422
- display: 'flex',
423
- alignItems: 'center',
424
- gap: 4
425
- }}
426
- >
427
- {resource.label}
428
- <button
429
- onClick={() => onResourceToggle(resourceId)}
430
- style={{
431
- background: 'none',
432
- border: 'none',
433
- color: 'white',
434
- cursor: 'pointer',
435
- padding: 0,
436
- fontSize: 14,
437
- lineHeight: 1
438
- }}
439
- >
440
- ×
441
- </button>
442
- </span>
443
- );
444
- })}
445
- {startDate && (
446
- <span
447
- style={{
448
- fontSize: 11,
449
- padding: '4px 10px',
450
- borderRadius: 12,
451
- background: '#666',
452
- color: 'white',
453
- fontWeight: 500,
454
- display: 'flex',
455
- alignItems: 'center',
456
- gap: 4
457
- }}
458
- >
459
- From: {new Date(startDate).toLocaleDateString()}
460
- <button
461
- onClick={() => onStartDateChange(null)}
462
- style={{
463
- background: 'none',
464
- border: 'none',
465
- color: 'white',
466
- cursor: 'pointer',
467
- padding: 0,
468
- fontSize: 14,
469
- lineHeight: 1
470
- }}
471
- >
472
- ×
473
- </button>
474
- </span>
475
- )}
476
- {endDate && (
477
- <span
478
- style={{
479
- fontSize: 11,
480
- padding: '4px 10px',
481
- borderRadius: 12,
482
- background: '#666',
483
- color: 'white',
484
- fontWeight: 500,
485
- display: 'flex',
486
- alignItems: 'center',
487
- gap: 4
488
- }}
489
- >
490
- To: {new Date(endDate).toLocaleDateString()}
491
- <button
492
- onClick={() => onEndDateChange(null)}
493
- style={{
494
- background: 'none',
495
- border: 'none',
496
- color: 'white',
497
- cursor: 'pointer',
498
- padding: 0,
499
- fontSize: 14,
500
- lineHeight: 1
501
- }}
502
- >
503
- ×
504
- </button>
505
- </span>
506
- )}
507
- </div>
508
- )}
509
- </div>
510
- );
511
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/WhereMoneyWent.jsx DELETED
@@ -1,162 +0,0 @@
1
- import React from 'react';
2
- import Compare from './shared/Compare';
3
- import InsightBox from './shared/InsightBox';
4
- import { displacementData as d } from '../data/dashboardData';
5
-
6
- /**
7
- * Dashboard 3: What got funded instead
8
- * (Displacement Matrix)
9
- */
10
- export default function WhereMoneyWent() {
11
- return (
12
- <div>
13
- <p style={{
14
- fontSize: 14,
15
- color: '#555',
16
- borderLeft: '2px solid #D85A30',
17
- paddingLeft: 12,
18
- marginBottom: 20
19
- }}>
20
- {d.conclusion}
21
- </p>
22
-
23
- {/* Topic */}
24
- <div style={{
25
- background: '#f5f5f2',
26
- padding: 12,
27
- borderRadius: 8,
28
- marginBottom: 16
29
- }}>
30
- <div style={{ fontSize: 11, color: '#999', textTransform: 'uppercase', marginBottom: 4 }}>
31
- Budget Cycle
32
- </div>
33
- <div style={{ fontSize: 14, fontWeight: 500, color: '#222' }}>
34
- {d.topic}
35
- </div>
36
- </div>
37
-
38
- {/* The Matrix Table */}
39
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13, marginBottom: 20 }}>
40
- <thead>
41
- <tr>
42
- {['Funded (winner)', 'Stagnant (loser)', 'Trade-off factor'].map(h => (
43
- <th key={h} style={{
44
- fontSize: 11,
45
- textTransform: 'uppercase',
46
- letterSpacing: '0.07em',
47
- color: '#999',
48
- padding: '8px 10px',
49
- borderBottom: '1px solid #eee',
50
- textAlign: 'left'
51
- }}>
52
- {h}
53
- </th>
54
- ))}
55
- </tr>
56
- </thead>
57
- <tbody>
58
- {d.displacements.map((row, i) => (
59
- <tr key={i}>
60
- <td style={{
61
- padding: '10px 10px',
62
- borderBottom: '1px solid #f0f0ee',
63
- color: '#1D9E75',
64
- fontWeight: 500
65
- }}>
66
- {row.winner}
67
- {row.winnerAmount && ` — $${(row.winnerAmount / 1000).toFixed(0)}k`}
68
- </td>
69
- <td style={{
70
- padding: '10px 10px',
71
- borderBottom: '1px solid #f0f0ee',
72
- color: '#D85A30',
73
- fontWeight: 500
74
- }}>
75
- {row.loser}
76
- {row.loserAmount > 0 && ` — $${(row.loserAmount / 1000).toFixed(0)}k`}
77
- </td>
78
- <td style={{ padding: '10px 10px', borderBottom: '1px solid #f0f0ee' }}>
79
- <span style={{
80
- fontSize: 11,
81
- padding: '2px 8px',
82
- borderRadius: 99,
83
- background: '#f0f0ee',
84
- color: '#666'
85
- }}>
86
- {row.tradeoffFactor}
87
- </span>
88
- </td>
89
- </tr>
90
- ))}
91
- </tbody>
92
- </table>
93
-
94
- {/* Per-Student Spending Breakdown */}
95
- <div style={{ marginBottom: 20 }}>
96
- <h4 style={{
97
- fontSize: 12,
98
- textTransform: 'uppercase',
99
- letterSpacing: '0.05em',
100
- color: '#999',
101
- marginBottom: 10
102
- }}>
103
- Health Capital Spending (Per Student)
104
- </h4>
105
- <Compare
106
- benchmarks={d.benchmarks}
107
- metric="healthCapital"
108
- prefix="$"
109
- suffix="/student"
110
- />
111
- </div>
112
-
113
- <div style={{ marginBottom: 20 }}>
114
- <h4 style={{
115
- fontSize: 12,
116
- textTransform: 'uppercase',
117
- letterSpacing: '0.05em',
118
- color: '#999',
119
- marginBottom: 10
120
- }}>
121
- Athletic Capital Spending (Per Student)
122
- </h4>
123
- <Compare
124
- benchmarks={d.benchmarks}
125
- metric="athleticCapital"
126
- prefix="$"
127
- suffix="/student"
128
- />
129
- <p style={{
130
- fontSize: 11,
131
- color: '#888',
132
- marginTop: 8,
133
- textAlign: 'center'
134
- }}>
135
- Source: NCES F-33 Survey, Capital Outlay by Function, FY2025
136
- </p>
137
- </div>
138
-
139
- {/* The Logic */}
140
- <InsightBox type="critical">
141
- <strong>{d.priorityPattern}:</strong> {d.inference}
142
- </InsightBox>
143
-
144
- {/* Question for the Room */}
145
- <div style={{
146
- marginTop: 16,
147
- padding: 14,
148
- background: '#fff',
149
- border: '2px solid #D85A30',
150
- borderRadius: 8
151
- }}>
152
- <strong style={{ fontSize: 13, color: '#D85A30' }}>Ask them:</strong>
153
- <p style={{ fontSize: 13, color: '#222', marginTop: 6 }}>
154
- "This budget year, you spent $
155
- {(d.displacements[0].winnerAmount / 1000).toFixed(0)}k on {d.displacements[0].winner.toLowerCase()}
156
- and $0 on {d.displacements[0].loser.toLowerCase()}. Can you explain why turf is worth more than
157
- the dental health of 5,000 students?"
158
- </p>
159
- </div>
160
- </div>
161
- );
162
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/WhoIsInCharge.jsx DELETED
@@ -1,162 +0,0 @@
1
- import React from 'react';
2
- import BarMeter from './shared/BarMeter';
3
- import MetricCard from './shared/MetricCard';
4
- import Compare from './shared/Compare';
5
- import InsightBox from './shared/InsightBox';
6
- import { influenceData as d } from '../data/dashboardData';
7
-
8
- const colors = { blocker: '#E24B4A', public: '#185FA5' };
9
-
10
- /**
11
- * Dashboard 4: One memo beat 240 residents
12
- * (Influence Radar)
13
- */
14
- export default function WhoIsInCharge() {
15
- return (
16
- <div>
17
- <p style={{
18
- fontSize: 14,
19
- color: '#555',
20
- borderLeft: '2px solid #D85A30',
21
- paddingLeft: 12,
22
- marginBottom: 20
23
- }}>
24
- {d.conclusion}
25
- </p>
26
-
27
- {/* Topic */}
28
- <div style={{
29
- background: '#f5f5f2',
30
- padding: 12,
31
- borderRadius: 8,
32
- marginBottom: 16
33
- }}>
34
- <div style={{ fontSize: 11, color: '#999', textTransform: 'uppercase', marginBottom: 4 }}>
35
- Policy Decision
36
- </div>
37
- <div style={{ fontSize: 14, fontWeight: 500, color: '#222' }}>
38
- {d.topic}
39
- </div>
40
- </div>
41
-
42
- {/* Influence Bars */}
43
- <div style={{ marginBottom: 20 }}>
44
- <h4 style={{
45
- fontSize: 12,
46
- textTransform: 'uppercase',
47
- letterSpacing: '0.05em',
48
- color: '#999',
49
- marginBottom: 12
50
- }}>
51
- Influence on Final Decision
52
- </h4>
53
-
54
- {d.actors.map((item, i) => (
55
- <div key={i} style={{ marginBottom: 12 }}>
56
- <BarMeter
57
- label={item.actor}
58
- value={item.influence}
59
- color={colors[item.type]}
60
- />
61
- <div style={{
62
- fontSize: 11,
63
- color: '#888',
64
- marginLeft: 12,
65
- marginTop: -8,
66
- marginBottom: 8
67
- }}>
68
- {item.contactName && `Contact: ${item.contactName}`}
69
- </div>
70
- </div>
71
- ))}
72
- </div>
73
-
74
- {/* Key Metrics */}
75
- <div style={{
76
- display: 'grid',
77
- gridTemplateColumns: '1fr 1fr',
78
- gap: 12,
79
- marginBottom: 20
80
- }}>
81
- <MetricCard
82
- value={`${d.publicComments}+`}
83
- label="Public comments in support"
84
- />
85
- <MetricCard
86
- value="1"
87
- label="Memo that blocked the policy"
88
- tone="negative"
89
- />
90
- </div>
91
-
92
- {/* Veto Holder Callout */}
93
- <div style={{
94
- background: '#ffe6e6',
95
- border: '2px solid #E24B4A',
96
- borderRadius: 8,
97
- padding: 14,
98
- marginBottom: 20
99
- }}>
100
- <div style={{ fontSize: 11, color: '#999', textTransform: 'uppercase', marginBottom: 4 }}>
101
- Effective Veto Holder
102
- </div>
103
- <div style={{ fontSize: 16, fontWeight: 500, color: '#E24B4A' }}>
104
- {d.vetoHolder}
105
- </div>
106
- <div style={{ fontSize: 12, color: '#666', marginTop: 6 }}>
107
- One liability memo had {d.actors.find(a => a.type === 'blocker').influence}% influence
108
- despite {d.publicComments}+ citizen testimonies
109
- </div>
110
- </div>
111
-
112
- {/* Liability Benchmark */}
113
- <div style={{ marginTop: 20 }}>
114
- <h4 style={{
115
- fontSize: 12,
116
- textTransform: 'uppercase',
117
- letterSpacing: '0.05em',
118
- color: '#999',
119
- marginBottom: 10
120
- }}>
121
- Successful Liability Suits in States with Screening Programs
122
- </h4>
123
- <Compare
124
- benchmarks={d.benchmarks}
125
- metric="liabilitySuits"
126
- prefix=""
127
- suffix=""
128
- />
129
- <p style={{
130
- fontSize: 11,
131
- color: '#888',
132
- marginTop: 8,
133
- textAlign: 'center'
134
- }}>
135
- Source: National Association of School Nurses, ADA Health Policy Institute
136
- </p>
137
- </div>
138
-
139
- {/* The Logic */}
140
- <InsightBox type="critical">
141
- <strong>{d.powerStructure}:</strong> {d.inference}
142
- </InsightBox>
143
-
144
- {/* Question for the Room */}
145
- <div style={{
146
- marginTop: 16,
147
- padding: 14,
148
- background: '#fff',
149
- border: '2px solid #D85A30',
150
- borderRadius: 8
151
- }}>
152
- <strong style={{ fontSize: 13, color: '#D85A30' }}>Ask them:</strong>
153
- <p style={{ fontSize: 13, color: '#222', marginTop: 6 }}>
154
- "{d.vetoHolder}, can you please stand and explain to these {d.publicComments} citizens
155
- why your one memo expressing 'liability concerns' outweighed their collective voice?
156
- And can you cite a single successful lawsuit in any of the 35 states with active
157
- school dental screening programs?"
158
- </p>
159
- </div>
160
- </div>
161
- );
162
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/WordsVsDollars.jsx DELETED
@@ -1,151 +0,0 @@
1
- import React from 'react';
2
- import BarMeter from './shared/BarMeter';
3
- import MetricCard from './shared/MetricCard';
4
- import Compare from './shared/Compare';
5
- import InsightBox from './shared/InsightBox';
6
- import { rhetoricGapData as d } from '../data/dashboardData';
7
-
8
- /**
9
- * Dashboard 1: They cut health spending while praising wellness
10
- * (Rhetoric Gap Monitor)
11
- */
12
- export default function WordsVsDollars() {
13
- return (
14
- <div>
15
- <p style={{
16
- fontSize: 14,
17
- color: '#555',
18
- borderLeft: '2px solid #D85A30',
19
- paddingLeft: 12,
20
- marginBottom: 20
21
- }}>
22
- {d.conclusion}
23
- </p>
24
-
25
- {/* Key Metrics */}
26
- <div style={{
27
- display: 'grid',
28
- gridTemplateColumns: '1fr 1fr',
29
- gap: 12,
30
- marginBottom: 20
31
- }}>
32
- <MetricCard
33
- value={`${d.sentimentScore}%`}
34
- label='Positive sentiment re: "wellness"'
35
- tone="positive"
36
- />
37
- <MetricCard
38
- value={`-$${(Math.abs(d.budgetDelta) / 1000).toFixed(0)}k`}
39
- label="Budget delta: contracted dental/vision"
40
- tone="negative"
41
- />
42
- </div>
43
-
44
- {/* What They SAY */}
45
- <div style={{ marginBottom: 16 }}>
46
- <h4 style={{
47
- fontSize: 12,
48
- textTransform: 'uppercase',
49
- letterSpacing: '0.05em',
50
- color: '#999',
51
- marginBottom: 10
52
- }}>
53
- What They SAY
54
- </h4>
55
- <BarMeter
56
- label="Wellness language in meeting minutes"
57
- value={d.sentimentScore}
58
- color="#1D9E75"
59
- />
60
- <div style={{
61
- fontSize: 12,
62
- color: '#888',
63
- fontStyle: 'italic',
64
- marginTop: 8,
65
- paddingLeft: 12,
66
- borderLeft: '2px solid #f0f0ee'
67
- }}>
68
- <p style={{ marginBottom: 6 }}>Sample quotes ({d.totalMentions} total mentions):</p>
69
- {d.sampleQuotes.slice(0, 2).map((quote, i) => (
70
- <p key={i} style={{ marginBottom: 4 }}>"{quote}"</p>
71
- ))}
72
- </div>
73
- </div>
74
-
75
- {/* What They FUND */}
76
- <div style={{ marginBottom: 16 }}>
77
- <h4 style={{
78
- fontSize: 12,
79
- textTransform: 'uppercase',
80
- letterSpacing: '0.05em',
81
- color: '#999',
82
- marginBottom: 10
83
- }}>
84
- What They FUND
85
- </h4>
86
- <BarMeter
87
- label={`${d.budgetCategory} vs. prior year`}
88
- value={100 + d.budgetDeltaPercent}
89
- max={100}
90
- color="#D85A30"
91
- suffix="% of last year"
92
- />
93
- <BarMeter
94
- label="Admin cost growth (same period)"
95
- value={d.adminCostGrowth}
96
- max={100}
97
- color="#BA7517"
98
- suffix="% increase"
99
- />
100
- </div>
101
-
102
- {/* Benchmark Comparison */}
103
- <div style={{ marginTop: 20 }}>
104
- <h4 style={{
105
- fontSize: 12,
106
- textTransform: 'uppercase',
107
- letterSpacing: '0.05em',
108
- color: '#999',
109
- marginBottom: 10
110
- }}>
111
- Per-Student Health Spending Comparison
112
- </h4>
113
- <Compare
114
- benchmarks={d.benchmarks}
115
- metric="perStudent"
116
- prefix="$"
117
- suffix="/student"
118
- />
119
- <p style={{
120
- fontSize: 11,
121
- color: '#888',
122
- marginTop: 8,
123
- textAlign: 'center'
124
- }}>
125
- Source: NCES Common Core of Data (CCD), FY2025
126
- </p>
127
- </div>
128
-
129
- {/* The Logic */}
130
- <InsightBox type="critical">
131
- {d.inference}
132
- </InsightBox>
133
-
134
- {/* Question for the Room */}
135
- <div style={{
136
- marginTop: 16,
137
- padding: 14,
138
- background: '#fff',
139
- border: '2px solid #D85A30',
140
- borderRadius: 8
141
- }}>
142
- <strong style={{ fontSize: 13, color: '#D85A30' }}>Ask them:</strong>
143
- <p style={{ fontSize: 13, color: '#222', marginTop: 6 }}>
144
- "You've praised student wellness {d.totalMentions} times this year with {d.sentimentScore}%
145
- positive sentiment. But you cut the health budget by ${Math.abs(d.budgetDelta).toLocaleString()}.
146
- Which statement is true: your words or your wallet?"
147
- </p>
148
- </div>
149
- </div>
150
- );
151
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/shared/BarMeter.jsx DELETED
@@ -1,34 +0,0 @@
1
- import React from 'react';
2
-
3
- /**
4
- * BarMeter Component
5
- * Reusable horizontal bar chart for showing metrics
6
- */
7
- export default function BarMeter({ label, value, max = 100, color = "#D85A30", suffix = "%" }) {
8
- const pct = Math.min((value / max) * 100, 100);
9
-
10
- return (
11
- <div style={{ marginBottom: 14 }}>
12
- <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 5 }}>
13
- <span style={{ fontSize: 13, color: "#555" }}>{label}</span>
14
- <span style={{ fontSize: 13, fontWeight: 500 }}>
15
- {value}{suffix}
16
- </span>
17
- </div>
18
- <div style={{
19
- height: 8,
20
- background: "#f0f0ee",
21
- borderRadius: 99,
22
- overflow: "hidden"
23
- }}>
24
- <div style={{
25
- height: "100%",
26
- width: `${pct}%`,
27
- background: color,
28
- borderRadius: 99,
29
- transition: "width 0.6s ease"
30
- }} />
31
- </div>
32
- </div>
33
- );
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/shared/Compare.jsx DELETED
@@ -1,58 +0,0 @@
1
- import React from 'react';
2
-
3
- /**
4
- * Compare Component
5
- * Four-column comparison: This District → Republican → Democratic → National
6
- */
7
- export default function Compare({ benchmarks, metric = "value", prefix = "$", suffix = "" }) {
8
- const buckets = [
9
- { key: 'thisDistrict', color: '#D85A30' },
10
- { key: 'republicanAvg', color: '#BA7517' },
11
- { key: 'democraticAvg', color: '#185FA5' },
12
- { key: 'nationalAvg', color: '#1D9E75' }
13
- ];
14
-
15
- return (
16
- <div style={{
17
- display: 'grid',
18
- gridTemplateColumns: 'repeat(4, 1fr)',
19
- gap: 10,
20
- marginTop: 16
21
- }}>
22
- {buckets.map(bucket => {
23
- const data = benchmarks[bucket.key];
24
- const value = typeof data === 'object' ? data[metric] : data;
25
- const label = typeof data === 'object' ? data.label : bucket.key;
26
-
27
- return (
28
- <div
29
- key={bucket.key}
30
- style={{
31
- background: '#f5f5f2',
32
- borderRadius: 8,
33
- padding: 12,
34
- borderTop: `3px solid ${bucket.color}`
35
- }}
36
- >
37
- <div style={{
38
- fontSize: 18,
39
- fontWeight: 500,
40
- color: bucket.color,
41
- marginBottom: 4
42
- }}>
43
- {prefix}{value}{suffix}
44
- </div>
45
- <div style={{
46
- fontSize: 10,
47
- color: '#888',
48
- textTransform: 'uppercase',
49
- letterSpacing: '0.05em'
50
- }}>
51
- {label}
52
- </div>
53
- </div>
54
- );
55
- })}
56
- </div>
57
- );
58
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/shared/DashboardTile.jsx DELETED
@@ -1,171 +0,0 @@
1
- import React from 'react';
2
- import { ArrowRight, TrendingUp, Clock, DollarSign, Users } from 'lucide-react';
3
-
4
- /**
5
- * DashboardTile Component
6
- * Tile-based navigation for dashboards
7
- */
8
- export default function DashboardTile({
9
- id,
10
- title,
11
- metric,
12
- context,
13
- summary,
14
- discomfort,
15
- icon: Icon,
16
- onClick
17
- }) {
18
- const getDiscomfortColor = (score) => {
19
- if (score >= 9) return '#D85A30';
20
- if (score >= 7) return '#BA7517';
21
- return '#888';
22
- };
23
-
24
- return (
25
- <div
26
- onClick={() => onClick(id)}
27
- style={{
28
- background: 'white',
29
- border: '1px solid #eee',
30
- borderRadius: 12,
31
- padding: 18,
32
- cursor: 'pointer',
33
- transition: 'all 0.2s ease',
34
- position: 'relative',
35
- overflow: 'hidden'
36
- }}
37
- onMouseEnter={(e) => {
38
- e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
39
- e.currentTarget.style.transform = 'translateY(-2px)';
40
- }}
41
- onMouseLeave={(e) => {
42
- e.currentTarget.style.boxShadow = 'none';
43
- e.currentTarget.style.transform = 'translateY(0)';
44
- }}
45
- >
46
- {/* Discomfort indicator */}
47
- <div style={{
48
- position: 'absolute',
49
- top: 0,
50
- right: 0,
51
- width: 60,
52
- height: 60,
53
- background: getDiscomfortColor(discomfort),
54
- borderBottomLeft: '60px solid transparent',
55
- opacity: 0.1
56
- }} />
57
-
58
- {/* Icon */}
59
- {Icon && (
60
- <div style={{
61
- width: 40,
62
- height: 40,
63
- borderRadius: 8,
64
- background: '#f5f5f2',
65
- display: 'flex',
66
- alignItems: 'center',
67
- justifyContent: 'center',
68
- marginBottom: 12,
69
- color: getDiscomfortColor(discomfort)
70
- }}>
71
- <Icon size={20} />
72
- </div>
73
- )}
74
-
75
- {/* Title */}
76
- <h3 style={{
77
- fontSize: 15,
78
- fontWeight: 500,
79
- color: '#222',
80
- marginBottom: 8,
81
- lineHeight: 1.3
82
- }}>
83
- {title}
84
- </h3>
85
-
86
- {/* Metrics */}
87
- <div style={{
88
- display: 'flex',
89
- gap: 12,
90
- marginBottom: 10
91
- }}>
92
- <div>
93
- <div style={{
94
- fontSize: 20,
95
- fontWeight: 500,
96
- color: getDiscomfortColor(discomfort)
97
- }}>
98
- {metric}
99
- </div>
100
- <div style={{
101
- fontSize: 11,
102
- color: '#888'
103
- }}>
104
- {context}
105
- </div>
106
- </div>
107
- </div>
108
-
109
- {/* Summary */}
110
- <p style={{
111
- fontSize: 12,
112
- color: '#666',
113
- lineHeight: 1.5,
114
- marginBottom: 12
115
- }}>
116
- {summary}
117
- </p>
118
-
119
- {/* Footer */}
120
- <div style={{
121
- display: 'flex',
122
- justifyContent: 'space-between',
123
- alignItems: 'center'
124
- }}>
125
- <div style={{
126
- fontSize: 11,
127
- color: getDiscomfortColor(discomfort),
128
- fontWeight: 500
129
- }}>
130
- {discomfort >= 9 ? '⚠️ High Impact' : discomfort >= 7 ? '⚡ Medium Impact' : '📊 Analysis'}
131
- </div>
132
- <div style={{
133
- fontSize: 12,
134
- color: '#666',
135
- display: 'flex',
136
- alignItems: 'center',
137
- gap: 4
138
- }}>
139
- View Details
140
- <ArrowRight size={14} />
141
- </div>
142
- </div>
143
- </div>
144
- );
145
- }
146
-
147
- /**
148
- * DashboardGrid Component
149
- * Grid layout for dashboard tiles
150
- */
151
- export function DashboardGrid({ dashboards, onNavigate }) {
152
- const icons = [TrendingUp, Clock, DollarSign, Users];
153
-
154
- return (
155
- <div style={{
156
- display: 'grid',
157
- gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
158
- gap: 16,
159
- marginBottom: 24
160
- }}>
161
- {dashboards.map((dashboard, i) => (
162
- <DashboardTile
163
- key={dashboard.id}
164
- {...dashboard}
165
- icon={icons[i % icons.length]}
166
- onClick={onNavigate}
167
- />
168
- ))}
169
- </div>
170
- );
171
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/shared/DecisionCard.jsx DELETED
@@ -1,253 +0,0 @@
1
- import React from 'react';
2
- import { Users, MessageSquare, FileText, Calendar, CheckCircle, XCircle } from 'lucide-react';
3
-
4
- /**
5
- * DecisionCard Component
6
- * Shows individual decision with speakers, rationale, and details
7
- */
8
- export default function DecisionCard({ decision, onClick }) {
9
- const {
10
- decision_summary,
11
- outcome,
12
- primary_rationale,
13
- supporters = [],
14
- opponents = [],
15
- vote_result,
16
- meeting_date,
17
- tradeoffs_discussed = [],
18
- evidence_cited = [],
19
- policy_domain = 'general',
20
- community_gap,
21
- ntee_code
22
- } = decision;
23
-
24
- const domainColors = {
25
- health: '#1D9E75',
26
- education: '#185FA5',
27
- facilities: '#BA7517',
28
- budget: '#D85A30',
29
- personnel: '#6B4C9A',
30
- safety: '#E24B4A',
31
- community: '#2C7A7B',
32
- policy: '#744210',
33
- general: '#888'
34
- };
35
-
36
- const isApproved = outcome?.toLowerCase().includes('approved') ||
37
- outcome?.toLowerCase().includes('passed');
38
-
39
- return (
40
- <div
41
- onClick={onClick}
42
- style={{
43
- background: 'white',
44
- border: '1px solid #eee',
45
- borderLeft: `4px solid ${domainColors[policy_domain] || '#888'}`,
46
- borderRadius: 8,
47
- padding: 20,
48
- cursor: 'pointer',
49
- transition: 'all 0.2s ease',
50
- marginBottom: 12
51
- }}
52
- onMouseEnter={(e) => {
53
- e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
54
- e.currentTarget.style.borderLeftWidth = '6px';
55
- }}
56
- onMouseLeave={(e) => {
57
- e.currentTarget.style.boxShadow = 'none';
58
- e.currentTarget.style.borderLeftWidth = '4px';
59
- }}
60
- >
61
- {/* Header */}
62
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
63
- <div style={{ flex: 1 }}>
64
- <div style={{ fontSize: 16, fontWeight: 500, color: '#222', lineHeight: 1.5, marginBottom: 8 }}>
65
- {decision_summary}
66
- </div>
67
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
68
- <span style={{
69
- fontSize: 13,
70
- color: '#888',
71
- display: 'flex',
72
- alignItems: 'center',
73
- gap: 4
74
- }}>
75
- <Calendar size={14} />
76
- {meeting_date ? new Date(meeting_date).toLocaleDateString() : 'Date unknown'}
77
- </span>
78
- {vote_result && (
79
- <span style={{ fontSize: 13, color: '#888' }}>
80
- Vote: {vote_result}
81
- </span>
82
- )}
83
- </div>
84
- </div>
85
-
86
- <div style={{
87
- display: 'flex',
88
- alignItems: 'center',
89
- gap: 6,
90
- padding: '4px 10px',
91
- borderRadius: 12,
92
- background: isApproved ? '#e6f4f1' : '#fff4e6',
93
- color: isApproved ? '#1D9E75' : '#BA7517',
94
- fontSize: 11,
95
- fontWeight: 500
96
- }}>
97
- {isApproved ? <CheckCircle size={12} /> : <XCircle size={12} />}
98
- {outcome || 'Unknown'}
99
- </div>
100
- </div>
101
-
102
- {/* Rationale */}
103
- {primary_rationale && (
104
- <div style={{
105
- background: '#f5f5f2',
106
- borderRadius: 6,
107
- padding: 10,
108
- marginBottom: 10
109
- }}>
110
- <div style={{
111
- fontSize: 10,
112
- color: '#999',
113
- textTransform: 'uppercase',
114
- letterSpacing: '0.05em',
115
- marginBottom: 4,
116
- display: 'flex',
117
- alignItems: 'center',
118
- gap: 4
119
- }}>
120
- <MessageSquare size={10} />
121
- Primary Rationale
122
- </div>
123
- <div style={{ fontSize: 12, color: '#555', lineHeight: 1.5 }}>
124
- "{primary_rationale}"
125
- </div>
126
- </div>
127
- )}
128
-
129
- {/* Speakers */}
130
- {(supporters.length > 0 || opponents.length > 0) && (
131
- <div style={{ display: 'flex', gap: 12, marginBottom: 10 }}>
132
- {supporters.length > 0 && (
133
- <div style={{ flex: 1 }}>
134
- <div style={{
135
- fontSize: 10,
136
- color: '#1D9E75',
137
- fontWeight: 500,
138
- marginBottom: 4,
139
- display: 'flex',
140
- alignItems: 'center',
141
- gap: 4
142
- }}>
143
- <Users size={10} />
144
- Supporters ({supporters.length})
145
- </div>
146
- {supporters.slice(0, 2).map((supporter, i) => (
147
- <div key={i} style={{ fontSize: 11, color: '#666', marginBottom: 2 }}>
148
- • {typeof supporter === 'string' ? supporter : supporter.name || 'Unknown'}
149
- {supporter.role && <span style={{ color: '#999' }}> ({supporter.role})</span>}
150
- </div>
151
- ))}
152
- {supporters.length > 2 && (
153
- <div style={{ fontSize: 10, color: '#999', fontStyle: 'italic' }}>
154
- +{supporters.length - 2} more
155
- </div>
156
- )}
157
- </div>
158
- )}
159
-
160
- {opponents.length > 0 && (
161
- <div style={{ flex: 1 }}>
162
- <div style={{
163
- fontSize: 10,
164
- color: '#D85A30',
165
- fontWeight: 500,
166
- marginBottom: 4,
167
- display: 'flex',
168
- alignItems: 'center',
169
- gap: 4
170
- }}>
171
- <Users size={10} />
172
- Opponents ({opponents.length})
173
- </div>
174
- {opponents.slice(0, 2).map((opponent, i) => (
175
- <div key={i} style={{ fontSize: 11, color: '#666', marginBottom: 2 }}>
176
- • {typeof opponent === 'string' ? opponent : opponent.name || 'Unknown'}
177
- {opponent.role && <span style={{ color: '#999' }}> ({opponent.role})</span>}
178
- </div>
179
- ))}
180
- {opponents.length > 2 && (
181
- <div style={{ fontSize: 10, color: '#999', fontStyle: 'italic' }}>
182
- +{opponents.length - 2} more
183
- </div>
184
- )}
185
- </div>
186
- )}
187
- </div>
188
- )}
189
-
190
- {/* Tradeoffs & Evidence */}
191
- <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
192
- {tradeoffs_discussed.length > 0 && (
193
- <span style={{
194
- fontSize: 10,
195
- padding: '2px 8px',
196
- borderRadius: 10,
197
- background: '#fff4e6',
198
- color: '#BA7517',
199
- display: 'flex',
200
- alignItems: 'center',
201
- gap: 4
202
- }}>
203
- {tradeoffs_discussed.length} tradeoff{tradeoffs_discussed.length > 1 ? 's' : ''}
204
- </span>
205
- )}
206
- {evidence_cited.length > 0 && (
207
- <span style={{
208
- fontSize: 10,
209
- padding: '2px 8px',
210
- borderRadius: 10,
211
- background: '#e6f4f1',
212
- color: '#1D9E75',
213
- display: 'flex',
214
- alignItems: 'center',
215
- gap: 4
216
- }}>
217
- <FileText size={10} />
218
- {evidence_cited.length} source{evidence_cited.length > 1 ? 's' : ''}
219
- </span>
220
- )}
221
- {community_gap?.nonprofit_filling_gap && (
222
- <span style={{
223
- fontSize: 10,
224
- padding: '2px 8px',
225
- borderRadius: 10,
226
- background: '#dcfce7',
227
- color: '#059669',
228
- display: 'flex',
229
- alignItems: 'center',
230
- gap: 4,
231
- fontWeight: 500
232
- }}>
233
- 🤝 Community filling gap
234
- </span>
235
- )}
236
- {ntee_code && (
237
- <span style={{
238
- fontSize: 10,
239
- padding: '2px 8px',
240
- borderRadius: 10,
241
- background: '#f3f4f6',
242
- color: '#6b7280',
243
- display: 'flex',
244
- alignItems: 'center',
245
- gap: 4
246
- }}>
247
- NTEE: {ntee_code}
248
- </span>
249
- )}
250
- </div>
251
- </div>
252
- );
253
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/shared/FilterPanel.jsx DELETED
@@ -1,240 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Search, Filter, X } from 'lucide-react';
3
-
4
- /**
5
- * FilterPanel Component
6
- * Allows filtering by policy domains/keywords and search
7
- */
8
- export default function FilterPanel({
9
- selectedDomains = [],
10
- onDomainToggle,
11
- searchQuery = "",
12
- onSearchChange,
13
- onClear
14
- }) {
15
- const [showFilters, setShowFilters] = useState(false);
16
-
17
- const policyDomains = [
18
- { id: 'health', label: 'Health & Wellness', color: '#1D9E75' },
19
- { id: 'education', label: 'Education & Curriculum', color: '#185FA5' },
20
- { id: 'facilities', label: 'Facilities & Infrastructure', color: '#BA7517' },
21
- { id: 'budget', label: 'Budget & Finance', color: '#D85A30' },
22
- { id: 'personnel', label: 'Personnel & Staffing', color: '#6B4C9A' },
23
- { id: 'safety', label: 'Safety & Security', color: '#E24B4A' },
24
- { id: 'community', label: 'Community & Partnerships', color: '#2C7A7B' },
25
- { id: 'policy', label: 'Policy & Governance', color: '#744210' }
26
- ];
27
-
28
- const keywords = [
29
- 'dental', 'health', 'screening', 'wellness', 'nurse',
30
- 'budget', 'funding', 'capital', 'expenditure',
31
- 'facility', 'building', 'construction', 'renovation',
32
- 'teacher', 'salary', 'contract', 'hiring',
33
- 'curriculum', 'textbook', 'program', 'academic',
34
- 'safety', 'security', 'police', 'emergency',
35
- 'community', 'partnership', 'grant', 'donation'
36
- ];
37
-
38
- const hasActiveFilters = selectedDomains.length > 0 || searchQuery.length > 0;
39
-
40
- return (
41
- <div style={{ marginBottom: 20 }}>
42
- {/* Search Bar */}
43
- <div style={{
44
- display: 'flex',
45
- gap: 8,
46
- marginBottom: 12
47
- }}>
48
- <div style={{
49
- position: 'relative',
50
- flex: 1
51
- }}>
52
- <Search
53
- size={16}
54
- style={{
55
- position: 'absolute',
56
- left: 12,
57
- top: '50%',
58
- transform: 'translateY(-50%)',
59
- color: '#999'
60
- }}
61
- />
62
- <input
63
- type="text"
64
- placeholder="Search decisions, speakers, rationales..."
65
- value={searchQuery}
66
- onChange={(e) => onSearchChange(e.target.value)}
67
- style={{
68
- width: '100%',
69
- padding: '8px 12px 8px 36px',
70
- border: '1px solid #ddd',
71
- borderRadius: 8,
72
- fontSize: 13,
73
- fontFamily: 'inherit'
74
- }}
75
- />
76
- {searchQuery && (
77
- <button
78
- onClick={() => onSearchChange('')}
79
- style={{
80
- position: 'absolute',
81
- right: 8,
82
- top: '50%',
83
- transform: 'translateY(-50%)',
84
- background: 'none',
85
- border: 'none',
86
- cursor: 'pointer',
87
- color: '#999',
88
- padding: 4
89
- }}
90
- >
91
- <X size={14} />
92
- </button>
93
- )}
94
- </div>
95
-
96
- <button
97
- onClick={() => setShowFilters(!showFilters)}
98
- style={{
99
- padding: '8px 16px',
100
- border: '1px solid',
101
- borderColor: showFilters ? '#888' : '#ddd',
102
- borderRadius: 8,
103
- background: showFilters ? '#f5f5f2' : 'white',
104
- cursor: 'pointer',
105
- fontSize: 13,
106
- fontWeight: 500,
107
- display: 'flex',
108
- alignItems: 'center',
109
- gap: 6,
110
- color: showFilters ? '#222' : '#666'
111
- }}
112
- >
113
- <Filter size={14} />
114
- Filters
115
- {selectedDomains.length > 0 && (
116
- <span style={{
117
- background: '#D85A30',
118
- color: 'white',
119
- borderRadius: '50%',
120
- width: 18,
121
- height: 18,
122
- display: 'flex',
123
- alignItems: 'center',
124
- justifyContent: 'center',
125
- fontSize: 10,
126
- fontWeight: 600
127
- }}>
128
- {selectedDomains.length}
129
- </span>
130
- )}
131
- </button>
132
-
133
- {hasActiveFilters && (
134
- <button
135
- onClick={onClear}
136
- style={{
137
- padding: '8px 16px',
138
- border: '1px solid #ddd',
139
- borderRadius: 8,
140
- background: 'white',
141
- cursor: 'pointer',
142
- fontSize: 13,
143
- color: '#D85A30'
144
- }}
145
- >
146
- Clear All
147
- </button>
148
- )}
149
- </div>
150
-
151
- {/* Filter Panel */}
152
- {showFilters && (
153
- <div style={{
154
- background: '#f5f5f2',
155
- borderRadius: 8,
156
- padding: 16,
157
- marginTop: 8
158
- }}>
159
- <div style={{ marginBottom: 12 }}>
160
- <div style={{
161
- fontSize: 12,
162
- fontWeight: 500,
163
- color: '#666',
164
- textTransform: 'uppercase',
165
- letterSpacing: '0.05em',
166
- marginBottom: 10
167
- }}>
168
- Policy Domains
169
- </div>
170
- <div style={{
171
- display: 'flex',
172
- flexWrap: 'wrap',
173
- gap: 8
174
- }}>
175
- {policyDomains.map(domain => {
176
- const isSelected = selectedDomains.includes(domain.id);
177
- return (
178
- <button
179
- key={domain.id}
180
- onClick={() => onDomainToggle(domain.id)}
181
- style={{
182
- padding: '6px 12px',
183
- borderRadius: 16,
184
- border: '1px solid',
185
- borderColor: isSelected ? domain.color : '#ddd',
186
- background: isSelected ? domain.color : 'white',
187
- color: isSelected ? 'white' : '#666',
188
- fontSize: 12,
189
- cursor: 'pointer',
190
- fontWeight: isSelected ? 500 : 400,
191
- transition: 'all 0.2s ease'
192
- }}
193
- >
194
- {domain.label}
195
- </button>
196
- );
197
- })}
198
- </div>
199
- </div>
200
-
201
- <div>
202
- <div style={{
203
- fontSize: 12,
204
- fontWeight: 500,
205
- color: '#666',
206
- textTransform: 'uppercase',
207
- letterSpacing: '0.05em',
208
- marginBottom: 10
209
- }}>
210
- Common Keywords
211
- </div>
212
- <div style={{
213
- display: 'flex',
214
- flexWrap: 'wrap',
215
- gap: 6
216
- }}>
217
- {keywords.map(keyword => (
218
- <button
219
- key={keyword}
220
- onClick={() => onSearchChange(keyword)}
221
- style={{
222
- padding: '4px 10px',
223
- borderRadius: 12,
224
- border: '1px solid #ddd',
225
- background: searchQuery === keyword ? '#f0f0ee' : 'white',
226
- color: '#666',
227
- fontSize: 11,
228
- cursor: 'pointer'
229
- }}
230
- >
231
- {keyword}
232
- </button>
233
- ))}
234
- </div>
235
- </div>
236
- </div>
237
- )}
238
- </div>
239
- );
240
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/shared/InsightBox.jsx DELETED
@@ -1,37 +0,0 @@
1
- import React from 'react';
2
-
3
- /**
4
- * InsightBox Component
5
- * Bottom summary box with "The logic" explanation
6
- */
7
- export default function InsightBox({ title = "The logic", children, type = "default" }) {
8
- const styles = {
9
- default: {
10
- background: '#f5f5f2',
11
- borderLeft: 'none'
12
- },
13
- warning: {
14
- background: '#fff4e6',
15
- borderLeft: '3px solid #BA7517'
16
- },
17
- critical: {
18
- background: '#ffe6e6',
19
- borderLeft: '3px solid #D85A30'
20
- }
21
- };
22
-
23
- const style = styles[type] || styles.default;
24
-
25
- return (
26
- <div style={{
27
- ...style,
28
- borderRadius: 8,
29
- padding: 14,
30
- fontSize: 13,
31
- color: '#555',
32
- marginTop: 16
33
- }}>
34
- <strong style={{ color: '#222' }}>{title}:</strong> {children}
35
- </div>
36
- );
37
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/components/shared/MetricCard.jsx DELETED
@@ -1,36 +0,0 @@
1
- import React from 'react';
2
-
3
- /**
4
- * MetricCard Component
5
- * Display key metrics with optional positive/negative/neutral tone
6
- */
7
- export default function MetricCard({ value, label, tone = "neutral" }) {
8
- const colors = {
9
- positive: "#1D9E75",
10
- negative: "#D85A30",
11
- neutral: "#222"
12
- };
13
-
14
- return (
15
- <div style={{
16
- background: "#f5f5f2",
17
- borderRadius: 8,
18
- padding: 14
19
- }}>
20
- <div style={{
21
- fontSize: 22,
22
- fontWeight: 500,
23
- color: colors[tone]
24
- }}>
25
- {value}
26
- </div>
27
- <div style={{
28
- fontSize: 12,
29
- color: "#888",
30
- marginTop: 3
31
- }}>
32
- {label}
33
- </div>
34
- </div>
35
- );
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/data/dashboardData.js DELETED
@@ -1,321 +0,0 @@
1
- /**
2
- * Dashboard Data Configuration
3
- *
4
- * This file contains all the data for the accountability dashboards.
5
- * UPDATE THIS FILE with real data from your Python analysis output.
6
- *
7
- * Data source: output/tuscaloosa_accountability_dashboards.json
8
- */
9
-
10
- export const metadata = {
11
- title: "P.A.T.H. – Policy Accountability and Tracking Hub",
12
- description: "Track government decisions and discover community solutions",
13
- analysisDate: "2026-04-24",
14
- maxDiscomfortScore: 10
15
- };
16
-
17
- // ================================================================
18
- // DASHBOARD 1: Words vs. Dollars (Rhetoric Gap)
19
- // ================================================================
20
- export const rhetoricGapData = {
21
- // What they SAY
22
- sentimentScore: 92, // % positive sentiment
23
- totalMentions: 50,
24
- sampleQuotes: [
25
- "Student wellness is a top priority for this district",
26
- "We are deeply committed to the health and wellbeing of every child",
27
- "Social-emotional and physical health are foundational to learning"
28
- ],
29
-
30
- // What they FUND
31
- budgetCategory: "Contracted Health Services (Dental/Vision)",
32
- priorYearAmount: 320000,
33
- currentYearAmount: 200000,
34
- budgetDelta: -120000,
35
- budgetDeltaPercent: -37.5,
36
-
37
- // Context
38
- adminCostGrowth: 31, // % increase in admin costs same period
39
-
40
- // Benchmarks - UPDATE WITH REAL DATA
41
- benchmarks: {
42
- thisDistrict: {
43
- perStudent: 41,
44
- label: "This District"
45
- },
46
- republicanAvg: {
47
- perStudent: 74,
48
- label: "Republican Districts Avg"
49
- },
50
- democraticAvg: {
51
- perStudent: 98,
52
- label: "Democratic Districts Avg"
53
- },
54
- nationalAvg: {
55
- perStudent: 112,
56
- label: "National Average"
57
- }
58
- },
59
-
60
- // The Logic
61
- gapType: "Marketing Rationale",
62
- conclusion: "Verbal commitment to student health is high — fiscal priority is declining.",
63
- inference: "Words go up, dollars go down. Health funds are being used as a buffer for rising admin costs."
64
- };
65
-
66
- // ================================================================
67
- // DASHBOARD 2: Delayed 6 Months and Counting (Logic Chain)
68
- // ================================================================
69
- export const logicChainData = {
70
- topic: "West Alabama Community Dental Clinic Partnership",
71
-
72
- // Timeline
73
- firstMentioned: "2025-10-15",
74
- monthsInLimbo: 6,
75
- totalDeferrals: 4,
76
-
77
- // Justification history
78
- justifications: [
79
- {
80
- month: "Month 1 (Oct 2025)",
81
- status: "deferred",
82
- reason: "Waiting for state tax revenue projections",
83
- speaker: "Board Member Johnson"
84
- },
85
- {
86
- month: "Month 2 (Nov 2025)",
87
- status: "work session",
88
- reason: "Moved to subcommittee for further review",
89
- speaker: "Superintendent Smith"
90
- },
91
- {
92
- month: "Month 3 (Dec 2025)",
93
- status: "deferred",
94
- reason: "No quorum reached on agenda item",
95
- speaker: "Board Chair Davis"
96
- },
97
- {
98
- month: "Month 4 (Jan 2026)",
99
- status: "deferred",
100
- reason: "Waiting for legal clarity on liability",
101
- speaker: "Risk Manager Patricia Johnson"
102
- },
103
- {
104
- month: "Month 5-6 (Feb-Apr 2026)",
105
- status: "not scheduled",
106
- reason: "Election cycle approaching — not scheduled for vote",
107
- speaker: "N/A"
108
- }
109
- ],
110
-
111
- // Benchmarks
112
- benchmarks: {
113
- thisDistrict: {
114
- activePrograms: 0,
115
- label: "This District"
116
- },
117
- republicanAvg: {
118
- activePrograms: 14,
119
- label: "Republican States"
120
- },
121
- democraticAvg: {
122
- activePrograms: 21,
123
- label: "Democratic States"
124
- },
125
- nationalAvg: {
126
- activePrograms: 35,
127
- label: "States with Programs"
128
- }
129
- },
130
-
131
- // The Logic
132
- patternType: "Rationale of Attrition",
133
- conclusion: "Administrative 'analysis' is a strategic tool to avoid a final yes or no.",
134
- inference: "The board isn't debating merit — they're waiting for momentum to fade before the election cycle."
135
- };
136
-
137
- // ================================================================
138
- // DASHBOARD 3: What Got Funded Instead (Displacement Matrix)
139
- // ================================================================
140
- export const displacementData = {
141
- topic: "2026 Capital Project Prioritization",
142
-
143
- // The matrix
144
- displacements: [
145
- {
146
- winner: "Athletic Turf Replacement",
147
- winnerAmount: 850000,
148
- loser: "Fluoride Water System Upgrade",
149
- loserAmount: 0,
150
- tradeoffFactor: "Visibility — Turf is a PR win; fluoride is hidden"
151
- },
152
- {
153
- winner: "Admin Building HVAC System",
154
- winnerAmount: 2000000,
155
- loser: "Dental Screening Pilot Program",
156
- loserAmount: 0,
157
- tradeoffFactor: "Asset Maintenance — Building upkeep over health services"
158
- },
159
- {
160
- winner: "Police Vehicle Fleet Upgrade",
161
- winnerAmount: 450000,
162
- loser: "School Nurse Salary Supplements",
163
- loserAmount: 0,
164
- tradeoffFactor: "Public Safety — Police are a 'primary' rationale"
165
- }
166
- ],
167
-
168
- // Benchmarks - per student spending
169
- benchmarks: {
170
- thisDistrict: {
171
- healthCapital: 0,
172
- athleticCapital: 170,
173
- label: "This District"
174
- },
175
- republicanAvg: {
176
- healthCapital: 29,
177
- athleticCapital: 95,
178
- label: "Republican Districts"
179
- },
180
- democraticAvg: {
181
- healthCapital: 48,
182
- athleticCapital: 85,
183
- label: "Democratic Districts"
184
- },
185
- nationalAvg: {
186
- healthCapital: 42,
187
- athleticCapital: 88,
188
- label: "National Average"
189
- }
190
- },
191
-
192
- // The Logic
193
- priorityPattern: "Legacy Rationale",
194
- conclusion: "The Board prioritizes visible assets over invisible health infrastructure.",
195
- inference: "Officials fund ribbon-cuttings. The community trades dental health for political visibility."
196
- };
197
-
198
- // ================================================================
199
- // DASHBOARD 4: One Memo Beat 240 Residents (Influence Radar)
200
- // ================================================================
201
- export const influenceData = {
202
- topic: "School-Based Dental Screening Policy",
203
-
204
- // Influence actors
205
- actors: [
206
- {
207
- actor: "Risk / Legal memo (1 document)",
208
- influence: 92,
209
- type: "blocker",
210
- contactName: "Patricia Johnson, Risk Manager",
211
- documents: 1
212
- },
213
- {
214
- actor: "External consultant report",
215
- influence: 85,
216
- type: "blocker",
217
- contactName: "Education Finance Group LLC",
218
- documents: 1
219
- },
220
- {
221
- actor: "240+ citizen comments in favor",
222
- influence: 4,
223
- type: "public",
224
- contactName: "Public testimony",
225
- documents: 240
226
- }
227
- ],
228
-
229
- // Summary metrics
230
- publicComments: 240,
231
- publicSupportRatio: 98, // % in favor
232
- legalMemos: 1,
233
- consultantReports: 1,
234
-
235
- // Benchmarks - liability context
236
- benchmarks: {
237
- thisDistrict: {
238
- liabilitySuits: "Program Blocked",
239
- label: "This District"
240
- },
241
- republicanAvg: {
242
- liabilitySuits: 0,
243
- label: "Republican States"
244
- },
245
- democraticAvg: {
246
- liabilitySuits: 0,
247
- label: "Democratic States"
248
- },
249
- nationalAvg: {
250
- liabilitySuits: 0,
251
- label: "All States Combined"
252
- }
253
- },
254
-
255
- // The Logic
256
- powerStructure: "Technocratic Rationale",
257
- vetoHolder: "Patricia Johnson, Risk Manager",
258
- conclusion: "Internal risk management holds veto power that outweighs 100% of public input.",
259
- inference: "The district's lawyers and CFO are writing public health policy. One liability memo has 92% influence despite zero successful suits in any state with screening programs."
260
- };
261
-
262
- // ================================================================
263
- // SUMMARY PAGE DATA
264
- // ================================================================
265
- export const summaryData = {
266
- headline: "This isn't a left-vs-right debate. It's a pattern.",
267
- subheadline: "Four ways decision-making in Tuscaloosa diverges from both Republican and Democratic averages",
268
-
269
- findings: [
270
- {
271
- id: 1,
272
- title: "They cut health spending while praising wellness",
273
- metric: "$41/student",
274
- context: "vs. $112 national avg",
275
- discomfort: 9,
276
- summary: "92% positive sentiment about 'wellness' in meetings, but $120k budget cut to dental/vision services"
277
- },
278
- {
279
- id: 2,
280
- title: "Delayed 6 months and counting",
281
- metric: "4 deferrals",
282
- context: "4 different excuses",
283
- discomfort: 10,
284
- summary: "Dental clinic partnership has been 'under review' with shifting justifications since October 2025"
285
- },
286
- {
287
- id: 3,
288
- title: "What got funded instead",
289
- metric: "$850k turf",
290
- context: "vs. $0 dental screening",
291
- discomfort: 9,
292
- summary: "Visible projects (athletic fields, HVAC) prioritized over invisible health infrastructure"
293
- },
294
- {
295
- id: 4,
296
- title: "One memo beat 240 residents",
297
- metric: "92% influence",
298
- context: "from 1 risk manager",
299
- discomfort: 10,
300
- summary: "Patricia Johnson's liability memo had 23x more influence than 240 citizen testimonies"
301
- }
302
- ],
303
-
304
- howToUse: {
305
- title: "How to use this in the room",
306
- strategies: [
307
- {
308
- dont: "Argue the 'need'",
309
- do: "Show the rhetoric gap — they already agree health matters (50 mentions, 92% positive)"
310
- },
311
- {
312
- dont: "Accept 'budget constraints'",
313
- do: "Show the displacement — they funded $850k for turf in the same budget cycle"
314
- },
315
- {
316
- dont: "Let them hide behind 'the board decided'",
317
- do: "Name the veto holder — Patricia Johnson's memo had 92% influence vs. 4% for public input"
318
- }
319
- ]
320
- }
321
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/index.css DELETED
@@ -1,31 +0,0 @@
1
- body {
2
- margin: 0;
3
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
- sans-serif;
6
- -webkit-font-smoothing: antialiased;
7
- -moz-osx-font-smoothing: grayscale;
8
- font-size: 16px;
9
- line-height: 1.6;
10
- }
11
-
12
- html {
13
- overflow-x: hidden;
14
- }
15
-
16
- code {
17
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
18
- monospace;
19
- }
20
-
21
- * {
22
- box-sizing: border-box;
23
- }
24
-
25
- button {
26
- font-family: inherit;
27
- }
28
-
29
- input {
30
- font-family: inherit;
31
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/policy-dashboards/src/index.js DELETED
@@ -1,11 +0,0 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom/client';
3
- import './index.css';
4
- import App from './App';
5
-
6
- const root = ReactDOM.createRoot(document.getElementById('root'));
7
- root.render(
8
- <React.StrictMode>
9
- <App />
10
- </React.StrictMode>
11
- );
 
 
 
 
 
 
 
 
 
 
 
 
frontend/public/communityone_logo.jpg DELETED
Binary file (54 kB)
 
frontend/public/communityone_logo.png DELETED
Binary file (24.2 kB)
 
frontend/public/communityone_logo.svg DELETED
frontend/public/communityone_logo_64.png DELETED
Binary file (3.59 kB)
 
frontend/public/favicon.ico DELETED
Binary file (10.3 kB)
 
frontend/public/privacyfacebook.html DELETED
@@ -1,276 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Privacy Policy - Open Navigator for Engagement</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
16
- line-height: 1.6;
17
- color: #333;
18
- background-color: #f9fafb;
19
- }
20
-
21
- .container {
22
- max-width: 800px;
23
- margin: 0 auto;
24
- padding: 40px 20px;
25
- background-color: white;
26
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
27
- }
28
-
29
- header {
30
- border-bottom: 3px solid #2563eb;
31
- padding-bottom: 20px;
32
- margin-bottom: 30px;
33
- }
34
-
35
- h1 {
36
- color: #1e40af;
37
- font-size: 2.5rem;
38
- margin-bottom: 10px;
39
- }
40
-
41
- .last-updated {
42
- color: #6b7280;
43
- font-size: 0.9rem;
44
- }
45
-
46
- h2 {
47
- color: #1e40af;
48
- font-size: 1.75rem;
49
- margin-top: 30px;
50
- margin-bottom: 15px;
51
- }
52
-
53
- h3 {
54
- color: #374151;
55
- font-size: 1.25rem;
56
- margin-top: 20px;
57
- margin-bottom: 10px;
58
- }
59
-
60
- p {
61
- margin-bottom: 15px;
62
- color: #4b5563;
63
- }
64
-
65
- ul {
66
- margin-left: 20px;
67
- margin-bottom: 15px;
68
- }
69
-
70
- li {
71
- margin-bottom: 8px;
72
- color: #4b5563;
73
- }
74
-
75
- .highlight {
76
- background-color: #dbeafe;
77
- padding: 15px;
78
- border-left: 4px solid #2563eb;
79
- margin: 20px 0;
80
- }
81
-
82
- footer {
83
- margin-top: 50px;
84
- padding-top: 20px;
85
- border-top: 1px solid #e5e7eb;
86
- text-align: center;
87
- color: #6b7280;
88
- font-size: 0.9rem;
89
- }
90
-
91
- a {
92
- color: #2563eb;
93
- text-decoration: none;
94
- }
95
-
96
- a:hover {
97
- text-decoration: underline;
98
- }
99
- </style>
100
- </head>
101
- <body>
102
- <div class="container">
103
- <header>
104
- <h1>🏛️ Privacy Policy</h1>
105
- <p class="last-updated">Last Updated: April 26, 2026</p>
106
- </header>
107
-
108
- <section>
109
- <p><strong>Open Navigator for Engagement</strong> ("we," "our," or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our platform.</p>
110
- </section>
111
-
112
- <section>
113
- <h2>1. Information We Collect</h2>
114
-
115
- <h3>1.1 Information You Provide</h3>
116
- <p>When you create an account or use our services, we may collect:</p>
117
- <ul>
118
- <li><strong>Account Information:</strong> Email address, name, and profile information</li>
119
- <li><strong>OAuth Provider Data:</strong> When you log in via Google, Facebook, GitHub, or HuggingFace, we receive your public profile information and email address</li>
120
- <li><strong>User Preferences:</strong> Settings and preferences you configure within the application</li>
121
- </ul>
122
-
123
- <h3>1.2 Automatically Collected Information</h3>
124
- <ul>
125
- <li><strong>Usage Data:</strong> Pages visited, features used, and interactions with the platform</li>
126
- <li><strong>Device Information:</strong> Browser type, operating system, IP address</li>
127
- <li><strong>Cookies:</strong> We use essential cookies for authentication and session management</li>
128
- </ul>
129
- </section>
130
-
131
- <section>
132
- <h2>2. How We Use Your Information</h2>
133
- <p>We use the collected information to:</p>
134
- <ul>
135
- <li>Provide, maintain, and improve our services</li>
136
- <li>Authenticate your account and maintain security</li>
137
- <li>Personalize your experience on the platform</li>
138
- <li>Send important updates about the service</li>
139
- <li>Analyze usage patterns to improve functionality</li>
140
- <li>Comply with legal obligations</li>
141
- </ul>
142
- </section>
143
-
144
- <section>
145
- <h2>3. Third-Party Authentication</h2>
146
- <div class="highlight">
147
- <p><strong>OAuth Providers:</strong> We support login via Google, Facebook, GitHub, and HuggingFace. When you use these services:</p>
148
- <ul>
149
- <li>We only request access to your email address and basic profile information</li>
150
- <li>We do not store your social media passwords</li>
151
- <li>We do not post to your social media accounts</li>
152
- <li>You can revoke our access at any time through your provider's settings</li>
153
- </ul>
154
- </div>
155
- </section>
156
-
157
- <section>
158
- <h2>4. Data Storage and Security</h2>
159
- <p>We implement appropriate technical and organizational measures to protect your information:</p>
160
- <ul>
161
- <li><strong>Encryption:</strong> Data is encrypted in transit using HTTPS/TLS</li>
162
- <li><strong>Access Controls:</strong> Strict access controls limit who can access your data</li>
163
- <li><strong>Secure Authentication:</strong> JWT tokens with secure secret keys</li>
164
- <li><strong>Regular Updates:</strong> We keep our systems updated with security patches</li>
165
- </ul>
166
- <p>However, no method of transmission over the internet is 100% secure, and we cannot guarantee absolute security.</p>
167
- </section>
168
-
169
- <section>
170
- <h2>5. Data Sharing and Disclosure</h2>
171
- <p>We do not sell your personal information. We may share your information only in the following circumstances:</p>
172
- <ul>
173
- <li><strong>With Your Consent:</strong> When you explicitly authorize us to share information</li>
174
- <li><strong>Service Providers:</strong> With trusted third-party services that help us operate (e.g., hosting providers)</li>
175
- <li><strong>Legal Requirements:</strong> When required by law, court order, or governmental authority</li>
176
- <li><strong>Business Transfers:</strong> In connection with a merger, acquisition, or sale of assets</li>
177
- </ul>
178
- </section>
179
-
180
- <section>
181
- <h2>6. Public Data Sources</h2>
182
- <p>Our platform aggregates publicly available information from:</p>
183
- <ul>
184
- <li>City council meeting minutes and transcripts</li>
185
- <li>Government public records and budgets</li>
186
- <li>Nonprofit organization databases (IRS Form 990 data)</li>
187
- <li>Legislative information from state and local governments</li>
188
- </ul>
189
- <p>This public information is not considered personal data and is used to provide civic engagement insights.</p>
190
- </section>
191
-
192
- <section>
193
- <h2>7. Your Rights and Choices</h2>
194
- <p>You have the following rights regarding your personal information:</p>
195
- <ul>
196
- <li><strong>Access:</strong> Request a copy of the personal information we hold about you</li>
197
- <li><strong>Correction:</strong> Request correction of inaccurate information</li>
198
- <li><strong>Deletion:</strong> Request deletion of your account and personal data</li>
199
- <li><strong>Data Portability:</strong> Request your data in a portable format</li>
200
- <li><strong>Opt-Out:</strong> Unsubscribe from non-essential communications</li>
201
- </ul>
202
- <p>To exercise these rights, contact us at the email address provided below.</p>
203
- </section>
204
-
205
- <section>
206
- <h2>8. Children's Privacy</h2>
207
- <p>Our service is not intended for children under 13 years of age. We do not knowingly collect personal information from children under 13. If you become aware that a child has provided us with personal information, please contact us, and we will delete such information.</p>
208
- </section>
209
-
210
- <section>
211
- <h2>9. Data Retention</h2>
212
- <p>We retain your personal information for as long as necessary to:</p>
213
- <ul>
214
- <li>Provide our services to you</li>
215
- <li>Comply with legal obligations</li>
216
- <li>Resolve disputes and enforce our agreements</li>
217
- </ul>
218
- <p>When you delete your account, we will delete or anonymize your personal information within 30 days, except where required to retain it by law.</p>
219
- </section>
220
-
221
- <section>
222
- <h2>10. International Data Transfers</h2>
223
- <p>Your information may be transferred to and processed in countries other than your country of residence. We ensure appropriate safeguards are in place to protect your information in accordance with this Privacy Policy.</p>
224
- </section>
225
-
226
- <section>
227
- <h2>11. Changes to This Privacy Policy</h2>
228
- <p>We may update this Privacy Policy from time to time. We will notify you of any changes by:</p>
229
- <ul>
230
- <li>Posting the new Privacy Policy on this page</li>
231
- <li>Updating the "Last Updated" date</li>
232
- <li>Sending you an email notification (for material changes)</li>
233
- </ul>
234
- <p>Your continued use of our services after changes constitutes acceptance of the updated policy.</p>
235
- </section>
236
-
237
- <section>
238
- <h2>12. Contact Us</h2>
239
- <p>If you have questions about this Privacy Policy or our privacy practices, please contact us:</p>
240
- <ul>
241
- <li><strong>Email:</strong> privacy@communityone.com</li>
242
- <li><strong>Website:</strong> <a href="https://www.communityone.com">www.communityone.com</a></li>
243
- <li><strong>GitHub:</strong> <a href="https://github.com/getcommunityone/open-navigator-for-engagement">github.com/getcommunityone/open-navigator-for-engagement</a></li>
244
- </ul>
245
- </section>
246
-
247
- <section>
248
- <h2>13. Additional Information for EU/UK Users (GDPR)</h2>
249
- <p>If you are located in the European Union or United Kingdom, you have additional rights under GDPR:</p>
250
- <ul>
251
- <li><strong>Legal Basis:</strong> We process your data based on consent, contract performance, and legitimate interests</li>
252
- <li><strong>Data Protection Officer:</strong> You may contact our DPO at privacy@communityone.com</li>
253
- <li><strong>Supervisory Authority:</strong> You have the right to lodge a complaint with your local data protection authority</li>
254
- <li><strong>Automated Decision-Making:</strong> We do not use automated decision-making or profiling that produces legal effects</li>
255
- </ul>
256
- </section>
257
-
258
- <section>
259
- <h2>14. California Privacy Rights (CCPA)</h2>
260
- <p>If you are a California resident, you have specific rights under the California Consumer Privacy Act (CCPA):</p>
261
- <ul>
262
- <li><strong>Right to Know:</strong> What personal information we collect, use, and share</li>
263
- <li><strong>Right to Delete:</strong> Request deletion of your personal information</li>
264
- <li><strong>Right to Opt-Out:</strong> Opt-out of the sale of personal information (we do not sell your data)</li>
265
- <li><strong>Non-Discrimination:</strong> We will not discriminate against you for exercising your rights</li>
266
- </ul>
267
- </section>
268
-
269
- <footer>
270
- <p>&copy; 2026 Community One. All rights reserved.</p>
271
- <p>Open Navigator for Engagement is an open-source project licensed under the MIT License.</p>
272
- <p><a href="https://www.communityone.com">Return to Home</a> | <a href="https://github.com/getcommunityone/open-navigator-for-engagement">View on GitHub</a></p>
273
- </footer>
274
- </div>
275
- </body>
276
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/App.tsx DELETED
@@ -1,66 +0,0 @@
1
- import { Routes, Route } from 'react-router-dom'
2
- import Layout from './components/Layout'
3
- import Home from './pages/Home'
4
- import HomeModern from './pages/HomeModern'
5
- import Dashboard from './pages/Dashboard'
6
- import Analytics from './pages/Analytics'
7
- import Heatmap from './pages/Heatmap'
8
- import Documents from './pages/Documents'
9
- import Opportunities from './pages/Opportunities'
10
- import Nonprofits from './pages/Nonprofits'
11
- import NonprofitsHF from './pages/NonprofitsHF'
12
- import Settings from './pages/Settings'
13
- import PeopleFinder from './pages/PeopleFinder'
14
- import DebateFinder from './pages/DebateGrader'
15
- import Profile from './pages/Profile'
16
- import Explore from './pages/Explore'
17
- import Events from './pages/Events'
18
- import Services from './pages/Services'
19
- import Developers from './pages/Developers'
20
- import Hackathons from './pages/Hackathons'
21
- import OpenSource from './pages/OpenSource'
22
- import AdvocacyTopics from './pages/AdvocacyTopics'
23
- import FactChecking from './pages/FactChecking'
24
- import UnifiedSearch from './pages/UnifiedSearch'
25
- import JurisdictionsSearch from './pages/JurisdictionsSearch'
26
-
27
- function App() {
28
- return (
29
- <Routes>
30
- {/* Modern home page without Layout (has its own header) */}
31
- <Route path="/" element={<HomeModern />} />
32
-
33
- {/* Classic home page (if needed) */}
34
- <Route path="/classic" element={<Layout />}>
35
- <Route index element={<Home />} />
36
- </Route>
37
-
38
- {/* All other pages with sidebar layout */}
39
- <Route path="/" element={<Layout />}>
40
- <Route path="explore" element={<Explore />} />
41
- <Route path="search" element={<UnifiedSearch />} />
42
- <Route path="jurisdictions" element={<JurisdictionsSearch />} />
43
- <Route path="dashboard" element={<Dashboard />} />
44
- <Route path="analytics" element={<Analytics />} />
45
- <Route path="people" element={<PeopleFinder />} />
46
- <Route path="heatmap" element={<Heatmap />} />
47
- <Route path="documents" element={<Documents />} />
48
- <Route path="opportunities" element={<Opportunities />} />
49
- <Route path="nonprofits" element={<Nonprofits />} />
50
- <Route path="nonprofits-hf" element={<NonprofitsHF />} />
51
- <Route path="debate-grader" element={<DebateFinder />} />
52
- <Route path="profile" element={<Profile />} />
53
- <Route path="settings" element={<Settings />} />
54
- <Route path="events" element={<Events />} />
55
- <Route path="services" element={<Services />} />
56
- <Route path="developers" element={<Developers />} />
57
- <Route path="hackathons" element={<Hackathons />} />
58
- <Route path="opensource" element={<OpenSource />} />
59
- <Route path="advocacy-topics" element={<AdvocacyTopics />} />
60
- <Route path="fact-checking" element={<FactChecking />} />
61
- </Route>
62
- </Routes>
63
- )
64
- }
65
-
66
- export default App
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/AddressLookup.tsx DELETED
@@ -1,671 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react'
2
- import { MapPinIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
3
- import { stateNameToCode } from '../utils/stateMapping'
4
- import { useLocation as useLocationContext } from '../contexts/LocationContext'
5
-
6
- interface LocationData {
7
- address: string
8
- state: string
9
- county: string
10
- city: string
11
- latitude?: number
12
- longitude?: number
13
- }
14
-
15
- interface AddressLookupProps {
16
- onLocationFound: (location: LocationData) => void
17
- initialAddress?: string
18
- compact?: boolean
19
- }
20
-
21
- export default function AddressLookup({ onLocationFound, initialAddress = '', compact = false }: AddressLookupProps) {
22
- const { clearLocation } = useLocationContext()
23
- const [address, setAddress] = useState(initialAddress)
24
- const [isLoading, setIsLoading] = useState(false)
25
- const [error, setError] = useState<string | null>(null)
26
- const [suggestions, setSuggestions] = useState<any[]>([])
27
- const [foundLocation, setFoundLocation] = useState<LocationData | null>(null)
28
- const [showSuggestions, setShowSuggestions] = useState(false)
29
- const [selectedIndex, setSelectedIndex] = useState(-1)
30
- const debounceTimer = useRef<number | null>(null)
31
- const inputRef = useRef<HTMLInputElement>(null)
32
-
33
- // Fetch suggestions as user types
34
- const fetchSuggestions = async (query: string) => {
35
- if (query.trim().length < 3) {
36
- setSuggestions([])
37
- setShowSuggestions(false)
38
- return
39
- }
40
-
41
- try {
42
- const response = await fetch(
43
- `https://nominatim.openstreetmap.org/search?` +
44
- `q=${encodeURIComponent(query)}&` +
45
- `format=json&` +
46
- `addressdetails=1&` +
47
- `countrycodes=us&` +
48
- `limit=5`,
49
- {
50
- headers: {
51
- 'User-Agent': 'CommunityOne-Navigator/1.0'
52
- }
53
- }
54
- )
55
-
56
- if (!response.ok) {
57
- return
58
- }
59
-
60
- const data = await response.json()
61
-
62
- // Deduplicate results using OSM unique IDs
63
- const uniqueResults = data.reduce((acc: any[], current: any) => {
64
- const osmKey = `${current.osm_type}_${current.osm_id}`
65
- const exists = acc.some((item) => {
66
- const itemKey = `${item.osm_type}_${item.osm_id}`
67
- return itemKey === osmKey
68
- })
69
- if (!exists) {
70
- acc.push(current)
71
- }
72
- return acc
73
- }, [])
74
-
75
- setSuggestions(uniqueResults)
76
- setShowSuggestions(uniqueResults.length > 0)
77
- setSelectedIndex(-1)
78
- } catch (err) {
79
- console.error('Autocomplete error:', err)
80
- }
81
- }
82
-
83
- // Handle address input change with debouncing
84
- const handleAddressChange = (value: string) => {
85
- setAddress(value)
86
- setError(null)
87
-
88
- // Clear previous timer
89
- if (debounceTimer.current) {
90
- clearTimeout(debounceTimer.current)
91
- }
92
-
93
- // Set new timer
94
- debounceTimer.current = setTimeout(() => {
95
- fetchSuggestions(value)
96
- }, 300)
97
- }
98
-
99
- // Cleanup timer on unmount
100
- useEffect(() => {
101
- return () => {
102
- if (debounceTimer.current) {
103
- clearTimeout(debounceTimer.current)
104
- }
105
- }
106
- }, [])
107
-
108
- const lookupAddress = async (addressToLookup: string) => {
109
- if (!addressToLookup.trim()) {
110
- setError('Please enter an address')
111
- return
112
- }
113
-
114
- setIsLoading(true)
115
- setError(null)
116
- setSuggestions([])
117
- setShowSuggestions(false)
118
-
119
- try {
120
- // Use Nominatim (OpenStreetMap) geocoding service
121
- const response = await fetch(
122
- `https://nominatim.openstreetmap.org/search?` +
123
- `q=${encodeURIComponent(addressToLookup)}&` +
124
- `format=json&` +
125
- `addressdetails=1&` +
126
- `countrycodes=us&` +
127
- `limit=5`,
128
- {
129
- headers: {
130
- 'User-Agent': 'CommunityOne-Navigator/1.0'
131
- }
132
- }
133
- )
134
-
135
- if (!response.ok) {
136
- throw new Error('Failed to lookup address')
137
- }
138
-
139
- const data = await response.json()
140
-
141
- if (data.length === 0) {
142
- setError('Address not found. Please try a different address or be more specific.')
143
- return
144
- }
145
-
146
- // Deduplicate results using OSM unique IDs
147
- const uniqueResults = data.reduce((acc: any[], current: any) => {
148
- // Use OSM type + ID as unique key (most reliable)
149
- const osmKey = `${current.osm_type}_${current.osm_id}`
150
-
151
- const exists = acc.some((item) => {
152
- const itemKey = `${item.osm_type}_${item.osm_id}`
153
- return itemKey === osmKey
154
- })
155
-
156
- if (!exists) {
157
- acc.push(current)
158
- }
159
- return acc
160
- }, [])
161
-
162
- // If we have multiple unique results, show suggestions
163
- if (uniqueResults.length > 1) {
164
- setSuggestions(uniqueResults)
165
- setShowSuggestions(true)
166
- return
167
- }
168
-
169
- // Single result - process it
170
- processResult(uniqueResults[0])
171
- } catch (err) {
172
- console.error('Address lookup error:', err)
173
- setError('Failed to lookup address. Please try again.')
174
- } finally {
175
- setIsLoading(false)
176
- }
177
- }
178
-
179
- const processResult = (result: any) => {
180
- const addr = result.address
181
-
182
- // Convert state name to 2-letter code
183
- const stateName = addr.state || ''
184
- const stateCode = stateNameToCode(stateName)
185
- console.log(`🗺️ [AddressLookup] State conversion: "${stateName}" → "${stateCode}"`)
186
-
187
- const locationData: LocationData = {
188
- address: result.display_name,
189
- state: stateCode,
190
- county: addr.county || '',
191
- city: addr.city || addr.town || addr.village || addr.municipality || '',
192
- latitude: parseFloat(result.lat),
193
- longitude: parseFloat(result.lon),
194
- }
195
-
196
- // Validate we got the essential data
197
- if (!locationData.state || !locationData.city) {
198
- setError('Could not determine city and state from this address. Please be more specific.')
199
- setSuggestions([])
200
- setShowSuggestions(false)
201
- return
202
- }
203
-
204
- console.log('📍 [AddressLookup] Location found:', locationData)
205
- setSuggestions([])
206
- setShowSuggestions(false)
207
- setFoundLocation(locationData)
208
- onLocationFound(locationData)
209
- }
210
-
211
- const handleSubmit = (e: React.FormEvent) => {
212
- e.preventDefault()
213
-
214
- // If a suggestion is selected, use that
215
- if (selectedIndex >= 0 && suggestions[selectedIndex]) {
216
- processResult(suggestions[selectedIndex])
217
- } else {
218
- lookupAddress(address)
219
- }
220
- }
221
-
222
- const handleSuggestionClick = (suggestion: any) => {
223
- setAddress(suggestion.display_name)
224
- processResult(suggestion)
225
- }
226
-
227
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
228
- if (!showSuggestions || suggestions.length === 0) return
229
-
230
- switch (e.key) {
231
- case 'ArrowDown':
232
- e.preventDefault()
233
- setSelectedIndex(prev =>
234
- prev < suggestions.length - 1 ? prev + 1 : prev
235
- )
236
- break
237
- case 'ArrowUp':
238
- e.preventDefault()
239
- setSelectedIndex(prev => prev > 0 ? prev - 1 : -1)
240
- break
241
- case 'Enter':
242
- if (selectedIndex >= 0) {
243
- e.preventDefault()
244
- processResult(suggestions[selectedIndex])
245
- }
246
- break
247
- case 'Escape':
248
- setShowSuggestions(false)
249
- setSelectedIndex(-1)
250
- break
251
- }
252
- }
253
-
254
- const useMyLocation = () => {
255
- if (!navigator.geolocation) {
256
- setError('Geolocation is not supported by your browser')
257
- return
258
- }
259
-
260
- setIsLoading(true)
261
- setError(null)
262
- setSuggestions([])
263
-
264
- navigator.geolocation.getCurrentPosition(
265
- async (position) => {
266
- const { latitude, longitude } = position.coords
267
-
268
- try {
269
- // Reverse geocode using Nominatim
270
- const response = await fetch(
271
- `https://nominatim.openstreetmap.org/reverse?` +
272
- `lat=${latitude}&` +
273
- `lon=${longitude}&` +
274
- `format=json&` +
275
- `addressdetails=1`,
276
- {
277
- headers: {
278
- 'User-Agent': 'CommunityOne-Navigator/1.0'
279
- }
280
- }
281
- )
282
-
283
- if (!response.ok) {
284
- throw new Error('Failed to reverse geocode location')
285
- }
286
-
287
- const data = await response.json()
288
-
289
- // Update the address input field
290
- setAddress(data.display_name)
291
-
292
- // Process the result
293
- processResult(data)
294
- } catch (err) {
295
- console.error('Reverse geocoding error:', err)
296
- setError('Failed to determine your location. Please enter your address manually.')
297
- } finally {
298
- setIsLoading(false)
299
- }
300
- },
301
- (error) => {
302
- console.error('Geolocation error:', error)
303
- setIsLoading(false)
304
-
305
- switch (error.code) {
306
- case error.PERMISSION_DENIED:
307
- setError('Location access denied. Please enter your address manually or enable location permissions.')
308
- break
309
- case error.POSITION_UNAVAILABLE:
310
- setError('Location information unavailable. Please enter your address manually.')
311
- break
312
- case error.TIMEOUT:
313
- setError('Location request timed out. Please try again or enter your address manually.')
314
- break
315
- default:
316
- setError('An error occurred while getting your location. Please enter your address manually.')
317
- }
318
- },
319
- {
320
- enableHighAccuracy: false, // Use fast network-based location instead of GPS
321
- timeout: 5000, // Reduced timeout since network location is faster
322
- maximumAge: 30000 // Allow 30s cached location for faster response
323
- }
324
- )
325
- }
326
-
327
- if (compact) {
328
- return (
329
- <form onSubmit={handleSubmit} className="w-full">
330
- <div className="relative">
331
- <input
332
- key="address-input-compact"
333
- ref={inputRef}
334
- type="text"
335
- value={address}
336
- onChange={(e) => handleAddressChange(e.target.value)}
337
- onKeyDown={handleKeyDown}
338
- placeholder="Enter your address..."
339
- className="w-full px-4 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900"
340
- disabled={isLoading}
341
- autoComplete="off"
342
- />
343
- <MapPinIcon className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
344
- <button
345
- type="submit"
346
- disabled={isLoading}
347
- className="absolute right-2 top-1.5 px-3 py-1 text-white rounded-md transition-colors text-sm disabled:opacity-50"
348
- style={{ backgroundColor: '#354F52' }}
349
- onMouseEnter={(e) => !isLoading && (e.currentTarget.style.backgroundColor = '#2e4346')}
350
- onMouseLeave={(e) => !isLoading && (e.currentTarget.style.backgroundColor = '#354F52')}
351
- >
352
- {isLoading ? 'Finding...' : 'Find'}
353
- </button>
354
-
355
- {/* Autocomplete suggestions dropdown */}
356
- {showSuggestions && suggestions.length > 0 && (
357
- <div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
358
- {suggestions.map((suggestion, index) => {
359
- const addr = suggestion.address
360
- const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown'
361
- return (
362
- <button
363
- key={`${suggestion.osm_type}_${suggestion.osm_id}`}
364
- type="button"
365
- onClick={() => handleSuggestionClick(suggestion)}
366
- className={`w-full px-4 py-2 text-left hover:bg-gray-100 transition-colors ${
367
- index === selectedIndex ? 'bg-gray-100' : ''
368
- }`}
369
- >
370
- <p className="text-sm font-medium text-gray-900">
371
- {suggestion.display_name}
372
- </p>
373
- <p className="text-xs text-gray-500">
374
- {locationName}, {addr.state}
375
- </p>
376
- </button>
377
- )
378
- })}
379
- </div>
380
- )}
381
- </div>
382
- {error && (
383
- <p className="mt-2 text-sm text-red-600">{error}</p>
384
- )}
385
- </form>
386
- )
387
- }
388
-
389
- return (
390
- <div className="w-full">
391
- <form onSubmit={handleSubmit} className="space-y-4">
392
- <div>
393
- <label htmlFor="address" className="block text-sm font-medium text-gray-700 mb-2">
394
- <span className="flex items-center gap-2">
395
- <MapPinIcon className="h-5 w-5" />
396
- Enter Your Address
397
- </span>
398
- </label>
399
- <div className="relative">
400
- <input
401
- key="address-input"
402
- ref={inputRef}
403
- type="text"
404
- id="address"
405
- name="addresslookup"
406
- value={address}
407
- onChange={(e) => handleAddressChange(e.target.value)}
408
- onKeyDown={handleKeyDown}
409
- placeholder="123 Main St, Los Angeles, CA 90001"
410
- className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-base text-gray-900"
411
- disabled={isLoading}
412
- autoComplete="off"
413
- />
414
-
415
- {/* Autocomplete suggestions dropdown */}
416
- {showSuggestions && suggestions.length > 0 && (
417
- <div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
418
- {suggestions.map((suggestion, index) => {
419
- const addr = suggestion.address
420
- const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown'
421
- return (
422
- <button
423
- key={`${suggestion.osm_type}_${suggestion.osm_id}`}
424
- type="button"
425
- onClick={() => handleSuggestionClick(suggestion)}
426
- className={`w-full px-4 py-3 text-left hover:bg-gray-100 transition-colors border-b border-gray-100 last:border-b-0 ${
427
- index === selectedIndex ? 'bg-gray-100' : ''
428
- }`}
429
- >
430
- <p className="text-sm font-medium text-gray-900">
431
- {suggestion.display_name}
432
- </p>
433
- <p className="text-xs text-gray-500 mt-1">
434
- {locationName}, {addr.state}
435
- </p>
436
- </button>
437
- )
438
- })}
439
- </div>
440
- )}
441
- </div>
442
- <p className="mt-1 text-xs text-gray-500">
443
- We'll find your local organizations based on your address
444
- </p>
445
-
446
- {/* Use My Location Button */}
447
- <div className="mt-3">
448
- <button
449
- type="button"
450
- onClick={useMyLocation}
451
- disabled={isLoading}
452
- className="w-full px-4 py-2 bg-white border-2 border-primary-300 text-primary-700 rounded-lg hover:bg-primary-50 hover:border-primary-500 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
453
- >
454
- <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
455
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
456
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
457
- </svg>
458
- <span>Use My Current Location</span>
459
- </button>
460
- </div>
461
-
462
- {/* Divider */}
463
- <div className="relative my-4">
464
- <div className="absolute inset-0 flex items-center">
465
- <div className="w-full border-t border-gray-300"></div>
466
- </div>
467
- <div className="relative flex justify-center text-xs">
468
- <span className="px-2 bg-white text-gray-500">or enter manually</span>
469
- </div>
470
- </div>
471
- </div>
472
-
473
- <button
474
- type="submit"
475
- disabled={isLoading}
476
- className="w-full px-6 py-3 text-white rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
477
- style={{ backgroundColor: '#354F52' }}
478
- onMouseEnter={(e) => !isLoading && (e.currentTarget.style.backgroundColor = '#2e4346')}
479
- onMouseLeave={(e) => !isLoading && (e.currentTarget.style.backgroundColor = '#354F52')}
480
- >
481
- {isLoading ? (
482
- <>
483
- <div className="animate-spin h-5 w-5 border-2 border-white border-t-transparent rounded-full"></div>
484
- <span>Looking up address...</span>
485
- </>
486
- ) : (
487
- <>
488
- <MagnifyingGlassIcon className="h-5 w-5" />
489
- <span>Find My Community</span>
490
- </>
491
- )}
492
- </button>
493
- </form>
494
-
495
- {/* Error Message */}
496
- {error && (
497
- <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
498
- <p className="text-sm text-red-800">{error}</p>
499
- </div>
500
- )}
501
-
502
- {/* Note: Suggestions now appear as autocomplete dropdown above */}
503
-
504
- {/* Location Results */}
505
- {foundLocation && !compact && (
506
- <div className="mt-6 border-2 border-primary-200 rounded-lg overflow-hidden bg-primary-50">
507
- <div className="bg-primary-600 px-4 py-3 flex items-center justify-between">
508
- <h3 className="text-lg font-semibold text-white flex items-center gap-2">
509
- <MapPinIcon className="h-5 w-5" />
510
- Your Local Community
511
- </h3>
512
- <button
513
- onClick={() => window.location.href = '/'}
514
- className="text-sm text-white hover:text-primary-100 underline font-medium"
515
- >
516
- ← Back to Home
517
- </button>
518
- </div>
519
- <div className="p-6 space-y-4">
520
- <p className="text-sm text-gray-700 mb-4">
521
- Select a jurisdiction level below to explore organizations, meeting minutes, and contacts:
522
- </p>
523
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
524
- {/* City */}
525
- {foundLocation.city && (
526
- <button
527
- onClick={() => {
528
- window.location.href = `/?scope=city`
529
- }}
530
- className="bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-blue-500 transition-all text-left w-full group"
531
- >
532
- <div className="flex items-start gap-3">
533
- <div className="p-2 bg-blue-100 rounded-lg group-hover:bg-blue-200 transition-colors">
534
- <svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
535
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
536
- </svg>
537
- </div>
538
- <div className="flex-1">
539
- <p className="text-xs font-medium text-gray-500 uppercase tracking-wider">City</p>
540
- <p className="text-lg font-semibold text-gray-900 mt-1 group-hover:text-blue-600">{foundLocation.city}</p>
541
- <p className="text-sm text-gray-600 mt-1">City Council</p>
542
- <p className="text-xs text-blue-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
543
- Click to explore →
544
- </p>
545
- </div>
546
- </div>
547
- </button>
548
- )}
549
-
550
- {/* County */}
551
- {foundLocation.county && (
552
- <button
553
- onClick={() => {
554
- window.location.href = `/?scope=county`
555
- }}
556
- className="bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-green-500 transition-all text-left w-full group"
557
- >
558
- <div className="flex items-start gap-3">
559
- <div className="p-2 bg-green-100 rounded-lg group-hover:bg-green-200 transition-colors">
560
- <svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
561
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
562
- </svg>
563
- </div>
564
- <div className="flex-1">
565
- <p className="text-xs font-medium text-gray-500 uppercase tracking-wider">County</p>
566
- <p className="text-lg font-semibold text-gray-900 mt-1 group-hover:text-green-600">{foundLocation.county}</p>
567
- <p className="text-sm text-gray-600 mt-1">County Board</p>
568
- <p className="text-xs text-green-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
569
- Click to explore →
570
- </p>
571
- </div>
572
- </div>
573
- </button>
574
- )}
575
-
576
- {/* State */}
577
- {foundLocation.state && (
578
- <button
579
- onClick={() => {
580
- window.location.href = `/?scope=state`
581
- }}
582
- className="bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-purple-500 transition-all text-left w-full group"
583
- >
584
- <div className="flex items-start gap-3">
585
- <div className="p-2 bg-purple-100 rounded-lg group-hover:bg-purple-200 transition-colors">
586
- <svg className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
587
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
588
- </svg>
589
- </div>
590
- <div className="flex-1">
591
- <p className="text-xs font-medium text-gray-500 uppercase tracking-wider">State</p>
592
- <p className="text-lg font-semibold text-gray-900 mt-1 group-hover:text-purple-600">{foundLocation.state}</p>
593
- <p className="text-sm text-gray-600 mt-1">State Legislature</p>
594
- <p className="text-xs text-purple-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
595
- Click to explore →
596
- </p>
597
- </div>
598
- </div>
599
- </button>
600
- )}
601
-
602
- {/* School District */}
603
- {foundLocation.city && (
604
- <button
605
- onClick={() => {
606
- window.location.href = `/?scope=community`
607
- }}
608
- className="bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-amber-500 transition-all text-left w-full group"
609
- >
610
- <div className="flex items-start gap-3">
611
- <div className="p-2 bg-amber-100 rounded-lg group-hover:bg-amber-200 transition-colors">
612
- <svg className="h-6 w-6 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
613
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
614
- </svg>
615
- </div>
616
- <div className="flex-1">
617
- <p className="text-xs font-medium text-gray-500 uppercase tracking-wider">School District</p>
618
- <p className="text-lg font-semibold text-gray-900 mt-1 group-hover:text-amber-600">{foundLocation.city} Unified</p>
619
- <p className="text-sm text-gray-600 mt-1">School Board</p>
620
- <p className="text-xs text-amber-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
621
- Click to explore →
622
- </p>
623
- </div>
624
- </div>
625
- </button>
626
- )}
627
- </div>
628
-
629
- {/* Action Buttons */}
630
- <div className="pt-4 border-t border-primary-200">
631
- <p className="text-sm text-gray-600 mb-3">Quick access to all local resources:</p>
632
- <div className="flex flex-wrap gap-3">
633
- <button
634
- onClick={() => {
635
- window.location.href = `/documents?state=${foundLocation.state}&city=${foundLocation.city}`
636
- }}
637
- className="flex-1 min-w-[200px] px-4 py-2 bg-white border-2 border-primary-600 text-primary-700 rounded-lg hover:bg-primary-50 transition-colors font-medium"
638
- >
639
- 📄 All Meeting Minutes
640
- </button>
641
- <button
642
- onClick={() => {
643
- window.location.href = `/nonprofits?state=${foundLocation.state}&city=${foundLocation.city}`
644
- }}
645
- className="flex-1 min-w-[200px] px-4 py-2 bg-white border-2 border-primary-600 text-primary-700 rounded-lg hover:bg-primary-50 transition-colors font-medium"
646
- >
647
- 🏢 All Local Organizations
648
- </button>
649
- </div>
650
- </div>
651
-
652
- {/* Start Over */}
653
- <div className="text-center pt-2">
654
- <button
655
- onClick={() => {
656
- setFoundLocation(null)
657
- setAddress('')
658
- setError(null)
659
- clearLocation() // Clear the global location context
660
- }}
661
- className="text-sm text-primary-600 hover:text-primary-700 font-medium underline"
662
- >
663
- Search Different Address
664
- </button>
665
- </div>
666
- </div>
667
- </div>
668
- )}
669
- </div>
670
- )
671
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/FollowButton.tsx DELETED
@@ -1,160 +0,0 @@
1
- import { useState } from 'react'
2
- import { useMutation, useQueryClient } from '@tanstack/react-query'
3
- import axios from 'axios'
4
- import { UserPlusIcon, UserMinusIcon } from '@heroicons/react/24/outline'
5
- import { CheckIcon } from '@heroicons/react/24/solid'
6
-
7
- interface FollowButtonProps {
8
- type: 'user' | 'leader' | 'organization' | 'cause'
9
- id: number
10
- initialFollowing?: boolean
11
- initialCount?: number
12
- showCount?: boolean
13
- compact?: boolean
14
- onFollowChange?: (following: boolean, count: number) => void
15
- }
16
-
17
- export default function FollowButton({
18
- type,
19
- id,
20
- initialFollowing = false,
21
- initialCount = 0,
22
- showCount = true,
23
- compact = false,
24
- onFollowChange
25
- }: FollowButtonProps) {
26
- const [isFollowing, setIsFollowing] = useState(initialFollowing)
27
- const [followerCount, setFollowerCount] = useState(initialCount)
28
- const [isHovered, setIsHovered] = useState(false)
29
- const queryClient = useQueryClient()
30
-
31
- const followMutation = useMutation({
32
- mutationFn: async () => {
33
- const endpoint = `/api/social/follow/${type}/${id}`
34
- const response = await axios.post(endpoint)
35
- return response.data
36
- },
37
- onSuccess: (data) => {
38
- setIsFollowing(true)
39
- setFollowerCount(data.follower_count)
40
- onFollowChange?.(true, data.follower_count)
41
- // Invalidate relevant queries
42
- queryClient.invalidateQueries({ queryKey: ['social', 'stats'] })
43
- queryClient.invalidateQueries({ queryKey: ['following', type] })
44
- }
45
- })
46
-
47
- const unfollowMutation = useMutation({
48
- mutationFn: async () => {
49
- const endpoint = `/api/social/follow/${type}/${id}`
50
- const response = await axios.delete(endpoint)
51
- return response.data
52
- },
53
- onSuccess: (data) => {
54
- setIsFollowing(false)
55
- setFollowerCount(data.follower_count)
56
- onFollowChange?.(false, data.follower_count)
57
- // Invalidate relevant queries
58
- queryClient.invalidateQueries({ queryKey: ['social', 'stats'] })
59
- queryClient.invalidateQueries({ queryKey: ['following', type] })
60
- }
61
- })
62
-
63
- const handleClick = () => {
64
- if (isFollowing) {
65
- unfollowMutation.mutate()
66
- } else {
67
- followMutation.mutate()
68
- }
69
- }
70
-
71
- const isLoading = followMutation.isPending || unfollowMutation.isPending
72
-
73
- // LinkedIn/Facebook style button
74
- if (compact) {
75
- return (
76
- <button
77
- onClick={handleClick}
78
- disabled={isLoading}
79
- onMouseEnter={() => setIsHovered(true)}
80
- onMouseLeave={() => setIsHovered(false)}
81
- className={`
82
- inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-sm font-medium
83
- transition-all duration-200 border
84
- ${isFollowing
85
- ? isHovered
86
- ? 'bg-red-50 border-red-300 text-red-700 hover:bg-red-100'
87
- : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
88
- : 'bg-blue-600 border-blue-600 text-white hover:bg-blue-700'
89
- }
90
- disabled:opacity-50 disabled:cursor-not-allowed
91
- `}
92
- >
93
- {isLoading ? (
94
- <span className="h-4 w-4 border-2 border-t-transparent border-current rounded-full animate-spin" />
95
- ) : isFollowing ? (
96
- <>
97
- {isHovered ? (
98
- <UserMinusIcon className="h-4 w-4" />
99
- ) : (
100
- <CheckIcon className="h-4 w-4" />
101
- )}
102
- <span>{isHovered ? 'Unfollow' : 'Following'}</span>
103
- </>
104
- ) : (
105
- <>
106
- <UserPlusIcon className="h-4 w-4" />
107
- <span>Follow</span>
108
- </>
109
- )}
110
- </button>
111
- )
112
- }
113
-
114
- // Full button with count
115
- return (
116
- <div className="inline-flex items-center gap-3">
117
- <button
118
- onClick={handleClick}
119
- disabled={isLoading}
120
- onMouseEnter={() => setIsHovered(true)}
121
- onMouseLeave={() => setIsHovered(false)}
122
- className={`
123
- inline-flex items-center gap-2 px-5 py-2 rounded-lg text-sm font-semibold
124
- transition-all duration-200 border-2
125
- ${isFollowing
126
- ? isHovered
127
- ? 'bg-red-50 border-red-400 text-red-700 hover:bg-red-100'
128
- : 'bg-white border-gray-300 text-gray-700 hover:border-gray-400'
129
- : 'bg-blue-600 border-blue-600 text-white hover:bg-blue-700'
130
- }
131
- disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md
132
- `}
133
- >
134
- {isLoading ? (
135
- <span className="h-5 w-5 border-2 border-t-transparent border-current rounded-full animate-spin" />
136
- ) : isFollowing ? (
137
- <>
138
- {isHovered ? (
139
- <UserMinusIcon className="h-5 w-5" />
140
- ) : (
141
- <CheckIcon className="h-5 w-5" />
142
- )}
143
- <span>{isHovered ? 'Unfollow' : 'Following'}</span>
144
- </>
145
- ) : (
146
- <>
147
- <UserPlusIcon className="h-5 w-5" />
148
- <span>Follow</span>
149
- </>
150
- )}
151
- </button>
152
-
153
- {showCount && (
154
- <span className="text-sm text-gray-600">
155
- {followerCount.toLocaleString()} {followerCount === 1 ? 'follower' : 'followers'}
156
- </span>
157
- )}
158
- </div>
159
- )
160
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/JurisdictionDiscovery.tsx DELETED
@@ -1,268 +0,0 @@
1
- import { useState } from 'react'
2
- import {
3
- ChevronDownIcon,
4
- ChevronUpIcon,
5
- CheckCircleIcon,
6
- GlobeAltIcon,
7
- VideoCameraIcon,
8
- DocumentTextIcon,
9
- ShareIcon
10
- } from '@heroicons/react/24/outline'
11
-
12
- interface JurisdictionDiscoveryProps {
13
- jurisdiction: {
14
- name: string
15
- state: string
16
- website?: string
17
- youtube_channels?: string[]
18
- facebook?: string
19
- twitter?: string
20
- agenda_portal?: string
21
- meeting_platform?: string
22
- completeness: number
23
- }
24
- }
25
-
26
- export default function JurisdictionDiscovery({ jurisdiction }: JurisdictionDiscoveryProps) {
27
- const [isExpanded, setIsExpanded] = useState(false)
28
-
29
- const hasData = jurisdiction.website || jurisdiction.youtube_channels?.length || jurisdiction.facebook
30
-
31
- return (
32
- <div className="border border-gray-200 rounded-lg bg-white hover:shadow-md transition-shadow">
33
- {/* Header - Always Visible */}
34
- <div className="p-4">
35
- <div className="flex items-center justify-between">
36
- <div className="flex-1">
37
- <div className="flex items-center gap-2">
38
- <CheckCircleIcon className="h-5 w-5 text-green-600" />
39
- <h3 className="font-bold text-gray-900 uppercase">
40
- {jurisdiction.name}, {jurisdiction.state} - DISCOVERY COMPLETE!
41
- </h3>
42
- </div>
43
-
44
- {/* Summary Stats */}
45
- <div className="mt-2 flex flex-wrap gap-3 text-sm text-gray-600">
46
- {jurisdiction.website && (
47
- <span className="flex items-center gap-1">
48
- <GlobeAltIcon className="h-4 w-4" />
49
- Website
50
- </span>
51
- )}
52
- {jurisdiction.youtube_channels && jurisdiction.youtube_channels.length > 0 && (
53
- <span className="flex items-center gap-1">
54
- <VideoCameraIcon className="h-4 w-4" />
55
- {jurisdiction.youtube_channels.length} YouTube Channel{jurisdiction.youtube_channels.length > 1 ? 's' : ''}
56
- </span>
57
- )}
58
- {jurisdiction.agenda_portal && (
59
- <span className="flex items-center gap-1">
60
- <DocumentTextIcon className="h-4 w-4" />
61
- Agenda Portal
62
- </span>
63
- )}
64
- {(jurisdiction.facebook || jurisdiction.twitter) && (
65
- <span className="flex items-center gap-1">
66
- <ShareIcon className="h-4 w-4" />
67
- Social Media
68
- </span>
69
- )}
70
- </div>
71
-
72
- {/* Completeness Bar */}
73
- {hasData && (
74
- <div className="mt-3">
75
- <div className="flex items-center gap-2">
76
- <div className="flex-1 bg-gray-200 rounded-full h-2">
77
- <div
78
- className="bg-green-600 h-2 rounded-full transition-all"
79
- style={{ width: `${jurisdiction.completeness}%` }}
80
- />
81
- </div>
82
- <span className="text-sm font-medium text-gray-700">
83
- {jurisdiction.completeness}%
84
- </span>
85
- </div>
86
- <p className="text-xs text-gray-500 mt-1">
87
- Completeness: ~{Math.round(jurisdiction.completeness)}% - {
88
- jurisdiction.completeness >= 75 ? 'Good' :
89
- jurisdiction.completeness >= 50 ? 'Fair' :
90
- 'Limited'
91
- } digital infrastructure!
92
- </p>
93
- </div>
94
- )}
95
- </div>
96
-
97
- {/* Expand/Collapse Button */}
98
- {hasData && (
99
- <button
100
- onClick={() => setIsExpanded(!isExpanded)}
101
- className="ml-4 p-2 hover:bg-gray-100 rounded-lg transition-colors"
102
- >
103
- {isExpanded ? (
104
- <ChevronUpIcon className="h-5 w-5 text-gray-600" />
105
- ) : (
106
- <ChevronDownIcon className="h-5 w-5 text-gray-600" />
107
- )}
108
- </button>
109
- )}
110
- </div>
111
- </div>
112
-
113
- {/* Expandable Details */}
114
- {isExpanded && hasData && (
115
- <div className="border-t border-gray-200 p-4 bg-gray-50">
116
- <h4 className="text-lg font-bold text-gray-900 mb-4">
117
- 🎯 {jurisdiction.name.toUpperCase()}, {jurisdiction.state} FINDINGS
118
- </h4>
119
-
120
- <div className="space-y-4">
121
- {/* Website */}
122
- {jurisdiction.website && (
123
- <div>
124
- <h5 className="font-semibold text-gray-700 mb-2">🌐 Official Website:</h5>
125
- <a
126
- href={jurisdiction.website}
127
- target="_blank"
128
- rel="noopener noreferrer"
129
- className="text-blue-600 hover:underline flex items-center gap-2"
130
- >
131
- ✅ {jurisdiction.website}
132
- </a>
133
- </div>
134
- )}
135
-
136
- {/* Agenda Portal */}
137
- {jurisdiction.agenda_portal && (
138
- <div>
139
- <h5 className="font-semibold text-gray-700 mb-2">📄 Meeting/Agenda Portal:</h5>
140
- <a
141
- href={jurisdiction.agenda_portal}
142
- target="_blank"
143
- rel="noopener noreferrer"
144
- className="text-blue-600 hover:underline flex items-center gap-2"
145
- >
146
- ✅ {jurisdiction.agenda_portal}
147
- </a>
148
- </div>
149
- )}
150
-
151
- {/* YouTube Channels */}
152
- {jurisdiction.youtube_channels && jurisdiction.youtube_channels.length > 0 && (
153
- <div>
154
- <h5 className="font-semibold text-gray-700 mb-2">📺 YouTube Channels:</h5>
155
- {jurisdiction.youtube_channels.map((channel, idx) => (
156
- <div key={idx} className="ml-4 text-blue-600 hover:underline">
157
- ✅ @{channel}
158
- </div>
159
- ))}
160
- </div>
161
- )}
162
-
163
- {/* Social Media */}
164
- {(jurisdiction.facebook || jurisdiction.twitter) && (
165
- <div>
166
- <h5 className="font-semibold text-gray-700 mb-2">📱 Social Media:</h5>
167
- <div className="ml-4 space-y-1">
168
- {jurisdiction.facebook && (
169
- <div className="text-blue-600">
170
- ✅ Facebook: {jurisdiction.facebook}
171
- </div>
172
- )}
173
- {jurisdiction.twitter && (
174
- <div className="text-blue-600">
175
- ✅ Twitter: {jurisdiction.twitter}
176
- </div>
177
- )}
178
- </div>
179
- </div>
180
- )}
181
-
182
- {/* Meeting Platform */}
183
- {jurisdiction.meeting_platform && (
184
- <div>
185
- <h5 className="font-semibold text-gray-700 mb-2">🏛️ Meeting Platform:</h5>
186
- <div className="ml-4">
187
- {jurisdiction.meeting_platform}
188
- </div>
189
- </div>
190
- )}
191
-
192
- {/* Summary Table */}
193
- <div className="mt-6 border-t border-gray-300 pt-4">
194
- <h5 className="font-semibold text-gray-700 mb-3">📊 {jurisdiction.name.toUpperCase()} SUMMARY</h5>
195
- <table className="min-w-full divide-y divide-gray-200 text-sm">
196
- <thead>
197
- <tr className="bg-gray-100">
198
- <th className="px-3 py-2 text-left font-semibold">Category</th>
199
- <th className="px-3 py-2 text-left font-semibold">Found</th>
200
- <th className="px-3 py-2 text-left font-semibold">Details</th>
201
- </tr>
202
- </thead>
203
- <tbody className="divide-y divide-gray-200">
204
- <tr>
205
- <td className="px-3 py-2">Website</td>
206
- <td className="px-3 py-2">{jurisdiction.website ? '✅' : '❌'}</td>
207
- <td className="px-3 py-2 text-gray-600">
208
- {jurisdiction.website ? new URL(jurisdiction.website).hostname : 'Not found'}
209
- </td>
210
- </tr>
211
- <tr>
212
- <td className="px-3 py-2">YouTube</td>
213
- <td className="px-3 py-2">{jurisdiction.youtube_channels?.length ? '✅' : '❌'}</td>
214
- <td className="px-3 py-2 text-gray-600">
215
- {jurisdiction.youtube_channels?.length || 0} channel{jurisdiction.youtube_channels?.length !== 1 ? 's' : ''}
216
- </td>
217
- </tr>
218
- <tr>
219
- <td className="px-3 py-2">Agendas</td>
220
- <td className="px-3 py-2">{jurisdiction.agenda_portal ? '✅' : '❌'}</td>
221
- <td className="px-3 py-2 text-gray-600">
222
- {jurisdiction.agenda_portal ? 'Portal found' : 'Not available'}
223
- </td>
224
- </tr>
225
- <tr>
226
- <td className="px-3 py-2">Social</td>
227
- <td className="px-3 py-2">{jurisdiction.facebook || jurisdiction.twitter ? '✅' : '❌'}</td>
228
- <td className="px-3 py-2 text-gray-600">
229
- {[jurisdiction.facebook && 'Facebook', jurisdiction.twitter && 'Twitter'].filter(Boolean).join(', ') || 'None'}
230
- </td>
231
- </tr>
232
- <tr>
233
- <td className="px-3 py-2">Platform</td>
234
- <td className="px-3 py-2">{jurisdiction.meeting_platform ? '✅' : '❌'}</td>
235
- <td className="px-3 py-2 text-gray-600">
236
- {jurisdiction.meeting_platform || 'Unknown'}
237
- </td>
238
- </tr>
239
- </tbody>
240
- </table>
241
- </div>
242
-
243
- {/* Key Takeaway */}
244
- <div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
245
- <h5 className="font-semibold text-blue-900 mb-2">💡 KEY TAKEAWAY</h5>
246
- <p className="text-sm text-blue-800">
247
- The automation successfully discovered:
248
- </p>
249
- <ul className="mt-2 space-y-1 text-sm text-blue-800">
250
- {jurisdiction.website && <li>✅ Official website (automatic)</li>}
251
- {(jurisdiction.youtube_channels?.length ?? 0) > 0 && <li>✅ YouTube channels (automatic)</li>}
252
- {jurisdiction.agenda_portal && <li>✅ Agenda portal (found via link scanning)</li>}
253
- {(jurisdiction.facebook || jurisdiction.twitter) && <li>✅ Social media (automatic)</li>}
254
- </ul>
255
- </div>
256
- </div>
257
- </div>
258
- )}
259
-
260
- {/* No Data Message */}
261
- {!hasData && (
262
- <div className="border-t border-gray-200 p-4 bg-gray-50 text-center text-gray-500">
263
- <p>No discovery data available yet. Run discovery pipeline to populate.</p>
264
- </div>
265
- )}
266
- </div>
267
- )
268
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/Layout.tsx DELETED
@@ -1,498 +0,0 @@
1
- import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom'
2
- import { useState, Fragment } from 'react'
3
- import { Menu, Transition } from '@headlessui/react'
4
- import {
5
- HomeIcon,
6
- MapIcon,
7
- DocumentTextIcon,
8
- BellAlertIcon,
9
- BuildingLibraryIcon,
10
- Cog6ToothIcon,
11
- ChartBarIcon,
12
- MagnifyingGlassIcon,
13
- BookOpenIcon,
14
- UserGroupIcon,
15
- AcademicCapIcon,
16
- Bars3Icon,
17
- XMarkIcon,
18
- UserCircleIcon,
19
- ArrowRightOnRectangleIcon,
20
- ChevronDownIcon,
21
- MapPinIcon,
22
- HeartIcon,
23
- CodeBracketIcon,
24
- } from '@heroicons/react/24/outline'
25
- import { useAuth } from '../contexts/AuthContext'
26
- import { useLocation as useLocationContext } from '../contexts/LocationContext'
27
-
28
- const navigation = [
29
- { name: 'Home', href: '/', icon: HomeIcon },
30
- { name: 'Explore Data', href: '/explore', icon: MagnifyingGlassIcon },
31
- { name: 'Search', href: '/search', icon: MagnifyingGlassIcon },
32
- { name: 'Jurisdictions', href: '/jurisdictions', icon: MapPinIcon },
33
- {
34
- section: 'Families & Individuals',
35
- items: [
36
- { name: 'Community Events', href: '/events', icon: BookOpenIcon },
37
- { name: 'Services & Resources', href: '/services', icon: HeartIcon },
38
- ]
39
- },
40
- {
41
- section: 'Policy & Government',
42
- items: [
43
- { name: 'Policy Decisions', href: '/documents', icon: DocumentTextIcon },
44
- { name: 'Budget Analysis', href: '/analytics', icon: ChartBarIcon },
45
- { name: 'Elected Officials', href: '/people', icon: UserGroupIcon },
46
- { name: 'Demographics', href: '/heatmap', icon: MapIcon },
47
- ]
48
- },
49
- {
50
- section: 'Community & Advocacy',
51
- items: [
52
- { name: 'Nonprofits', href: '/nonprofits', icon: BuildingLibraryIcon },
53
- { name: 'Advocacy Topics', href: '/advocacy-topics', icon: BellAlertIcon },
54
- { name: 'Fact-Checking', href: '/fact-checking', icon: AcademicCapIcon },
55
- ]
56
- },
57
- {
58
- section: 'Developers',
59
- items: [
60
- { name: 'Open Source', href: '/opensource', icon: CodeBracketIcon },
61
- { name: 'Hackathons', href: '/hackathons', icon: AcademicCapIcon },
62
- ]
63
- },
64
- { name: 'Settings', href: '/settings', icon: Cog6ToothIcon },
65
- ]
66
-
67
- export default function Layout() {
68
- const location = useLocation()
69
- const navigate = useNavigate()
70
- const [searchQuery, setSearchQuery] = useState('')
71
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
72
- const [showLoginMenu, setShowLoginMenu] = useState(false)
73
- const { user, isAuthenticated, login, logout, isLoading } = useAuth()
74
- const { location: userLocation, hasLocation } = useLocationContext()
75
-
76
- // Environment-aware URLs
77
- const docsUrl = import.meta.env.PROD ? '/docs/intro' : 'http://localhost:3000/docs/intro'
78
- const apiDocsUrl = import.meta.env.PROD ? '/api/docs' : 'http://localhost:8000/docs'
79
-
80
- const handleSearch = (e: React.FormEvent) => {
81
- e.preventDefault()
82
- if (searchQuery.trim()) {
83
- navigate(`/search?q=${encodeURIComponent(searchQuery)}`)
84
- }
85
- }
86
-
87
- return (
88
- <div className="min-h-screen" style={{ backgroundColor: '#F1F5F9' }}>
89
- {/* Top Header Bar */}
90
- <div className="fixed top-0 left-0 right-0 bg-white border-b border-gray-200 z-50">
91
- <div className="flex items-center justify-between px-4 md:px-6 py-3">
92
- <div className="flex items-center gap-3">
93
- {/* Mobile menu button */}
94
- <button
95
- onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
96
- className="md:hidden p-2 rounded-lg hover:bg-gray-100 text-gray-700"
97
- aria-label="Toggle menu"
98
- >
99
- {mobileMenuOpen ? (
100
- <XMarkIcon className="h-6 w-6" />
101
- ) : (
102
- <Bars3Icon className="h-6 w-6" />
103
- )}
104
- </button>
105
-
106
- <Link to="/" className="flex items-center gap-2 md:gap-3">
107
- <img
108
- src="/communityone_logo.svg"
109
- alt="CommunityOne Logo"
110
- className="h-10 md:h-12"
111
- />
112
- <h1 className="text-lg md:text-2xl font-bold" style={{ color: '#354F52' }}>
113
- Open Navigator
114
- </h1>
115
- </Link>
116
- </div>
117
-
118
- {/* Global Search - Hidden on home page and mobile */}
119
- {location.pathname !== '/' && (
120
- <form onSubmit={handleSearch} className="hidden md:flex flex-1 max-w-2xl mx-8">
121
- <div className="relative w-full">
122
- <input
123
- type="text"
124
- placeholder="Search people, meetings, organizations, causes..."
125
- value={searchQuery}
126
- onChange={(e) => setSearchQuery(e.target.value)}
127
- className="w-full px-4 py-2 pl-10 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
128
- />
129
- <MagnifyingGlassIcon className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
130
- </div>
131
- </form>
132
- )}
133
-
134
- {/* Header Actions */}
135
- <div className="flex items-center gap-2 md:gap-4">
136
- {/* Location Banner - Compact */}
137
- {hasLocation && userLocation && (
138
- <div className="hidden lg:flex items-center gap-2 px-3 py-1.5 bg-primary-50 border border-primary-200 rounded-lg">
139
- <MapPinIcon className="h-4 w-4 text-primary-600 flex-shrink-0" />
140
- <div className="text-xs">
141
- <div className="font-semibold text-gray-900">
142
- {userLocation.city}, {userLocation.state}
143
- </div>
144
- {userLocation.county && (
145
- <div className="text-gray-700">{userLocation.county}</div>
146
- )}
147
- </div>
148
- <button
149
- onClick={() => navigate('/?tab=community')}
150
- className="text-xs text-primary-600 hover:text-primary-700 font-medium underline ml-2 flex-shrink-0"
151
- >
152
- Change
153
- </button>
154
- </div>
155
- )}
156
- {/* Authentication */}
157
- {isLoading ? (
158
- <div className="px-3 py-2">
159
- <div className="animate-spin h-8 w-8 border-3 border-gray-300 border-t-primary-600 rounded-full"></div>
160
- </div>
161
- ) : isAuthenticated && user ? (
162
- <Menu as="div" className="relative">
163
- <Menu.Button className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors">
164
- {user.avatar_url ? (
165
- <img
166
- src={user.avatar_url}
167
- alt={user.full_name || user.email}
168
- className="h-9 w-9 rounded-full border-2 border-primary-500 shadow-sm"
169
- onError={(e) => {
170
- // If image fails to load, hide it and show fallback
171
- e.currentTarget.style.display = 'none';
172
- const fallback = e.currentTarget.nextElementSibling as HTMLElement | null;
173
- if (fallback) fallback.style.display = 'flex';
174
- }}
175
- />
176
- ) : null}
177
- <div
178
- className="h-9 w-9 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-sm shadow-sm"
179
- style={{ display: user.avatar_url ? 'none' : 'flex' }}
180
- >
181
- {(user.full_name || user.username || user.email).charAt(0).toUpperCase()}
182
- </div>
183
- <span className="hidden md:inline text-sm font-medium text-gray-700">
184
- {user.full_name || user.username || user.email.split('@')[0]}
185
- </span>
186
- <ChevronDownIcon className="hidden md:block h-4 w-4 text-gray-600" />
187
- </Menu.Button>
188
-
189
- <Transition
190
- as={Fragment}
191
- enter="transition ease-out duration-100"
192
- enterFrom="transform opacity-0 scale-95"
193
- enterTo="transform opacity-100 scale-100"
194
- leave="transition ease-in duration-75"
195
- leaveFrom="transform opacity-100 scale-100"
196
- leaveTo="transform opacity-0 scale-95"
197
- >
198
- <Menu.Items className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 focus:outline-none z-50">
199
- <div className="px-4 py-3 border-b border-gray-200">
200
- <div className="flex items-center gap-3 mb-2">
201
- {user.avatar_url ? (
202
- <img
203
- src={user.avatar_url}
204
- alt={user.full_name || user.email}
205
- className="h-12 w-12 rounded-full border-2 border-primary-500"
206
- onError={(e) => {
207
- // If image fails to load, hide it and show fallback
208
- e.currentTarget.style.display = 'none';
209
- const fallback = e.currentTarget.nextElementSibling as HTMLElement | null;
210
- if (fallback) fallback.style.display = 'flex';
211
- }}
212
- />
213
- ) : null}
214
- <div
215
- className="h-12 w-12 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-lg"
216
- style={{ display: user.avatar_url ? 'none' : 'flex' }}
217
- >
218
- {(user.full_name || user.username || user.email).charAt(0).toUpperCase()}
219
- </div>
220
- <div>
221
- <p className="text-sm font-semibold text-gray-900">
222
- {user.full_name || user.username || user.email.split('@')[0]}
223
- </p>
224
- <p className="text-xs text-gray-500 truncate">
225
- {user.email}
226
- </p>
227
- </div>
228
- </div>
229
- {user.oauth_provider && (
230
- <div className="flex items-center gap-1 text-xs text-gray-400">
231
- <span>Signed in via</span>
232
- <span className="font-medium capitalize">{user.oauth_provider}</span>
233
- </div>
234
- )}
235
- </div>
236
- <div className="py-1">
237
- <Menu.Item>
238
- {({ active }) => (
239
- <button
240
- onClick={() => navigate('/profile')}
241
- className={`${
242
- active ? 'bg-gray-50' : ''
243
- } flex items-center gap-3 w-full px-4 py-2.5 text-sm text-gray-700 hover:text-gray-900`}
244
- >
245
- <UserCircleIcon className="h-5 w-5" />
246
- <span>My Profile</span>
247
- </button>
248
- )}
249
- </Menu.Item>
250
- <Menu.Item>
251
- {({ active }) => (
252
- <button
253
- onClick={() => navigate('/settings')}
254
- className={`${
255
- active ? 'bg-gray-50' : ''
256
- } flex items-center gap-3 w-full px-4 py-2.5 text-sm text-gray-700 hover:text-gray-900`}
257
- >
258
- <Cog6ToothIcon className="h-5 w-5" />
259
- <span>Settings</span>
260
- </button>
261
- )}
262
- </Menu.Item>
263
- <Menu.Item>
264
- {({ active }) => (
265
- <button
266
- onClick={logout}
267
- className={`${
268
- active ? 'bg-red-50' : ''
269
- } flex items-center gap-3 w-full px-4 py-2.5 text-sm text-red-600 hover:text-red-700 border-t border-gray-100 mt-1`}
270
- >
271
- <ArrowRightOnRectangleIcon className="h-5 w-5" />
272
- <span className="font-medium">Sign out</span>
273
- </button>
274
- )}
275
- </Menu.Item>
276
- </div>
277
- </Menu.Items>
278
- </Transition>
279
- </Menu>
280
- ) : (
281
- <div className="relative">
282
- <button
283
- onClick={() => setShowLoginMenu(!showLoginMenu)}
284
- className="px-3 md:px-4 py-2 text-white rounded-lg transition-colors text-sm md:text-base font-medium flex items-center gap-2"
285
- style={{ backgroundColor: '#354F52' }}
286
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#2e4346'}
287
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#354F52'}
288
- >
289
- <UserCircleIcon className="h-5 w-5" />
290
- <span className="hidden md:inline">Register</span>
291
- <ChevronDownIcon className="h-4 w-4" />
292
- </button>
293
-
294
- {showLoginMenu && (
295
- <div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
296
- <div className="px-4 py-2 border-b border-gray-200">
297
- <p className="text-sm font-medium text-gray-900">Sign in with:</p>
298
- </div>
299
- <button
300
- onClick={() => { login('google'); setShowLoginMenu(false); }}
301
- className="flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors"
302
- >
303
- <div className="w-6 h-6 flex items-center justify-center">
304
- <svg viewBox="0 0 24 24" className="w-5 h-5">
305
- <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
306
- <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
307
- <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
308
- <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
309
- </svg>
310
- </div>
311
- <span className="text-sm font-medium text-gray-700">Google</span>
312
- </button>
313
- <button
314
- onClick={() => { login('facebook'); setShowLoginMenu(false); }}
315
- className="flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors"
316
- >
317
- <div className="w-6 h-6 flex items-center justify-center">
318
- <svg viewBox="0 0 24 24" className="w-5 h-5" fill="#1877F2">
319
- <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
320
- </svg>
321
- </div>
322
- <span className="text-sm font-medium text-gray-700">Facebook</span>
323
- </button>
324
- <button
325
- onClick={() => { login('github'); setShowLoginMenu(false); }}
326
- className="flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors"
327
- >
328
- <div className="w-6 h-6 flex items-center justify-center">
329
- <svg viewBox="0 0 24 24" className="w-5 h-5" fill="#181717">
330
- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
331
- </svg>
332
- </div>
333
- <span className="text-sm font-medium text-gray-700">GitHub</span>
334
- </button>
335
- <div className="border-t border-gray-100 my-1"></div>
336
- <button
337
- onClick={() => { login('huggingface'); setShowLoginMenu(false); }}
338
- className="flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors"
339
- >
340
- <div className="w-6 h-6 flex items-center justify-center">
341
- <span className="text-2xl">🤗</span>
342
- </div>
343
- <span className="text-sm font-medium text-gray-700">HuggingFace</span>
344
- </button>
345
- </div>
346
- )}
347
- </div>
348
- )}
349
-
350
- <a
351
- href={docsUrl}
352
- target="_blank"
353
- rel="noopener noreferrer"
354
- className="flex items-center gap-1 md:gap-2 px-2 md:px-4 py-2 text-gray-700 hover:text-primary-600 transition-colors"
355
- >
356
- <BookOpenIcon className="h-5 w-5" />
357
- <span className="hidden md:inline font-medium">Docs</span>
358
- </a>
359
- <a
360
- href={apiDocsUrl}
361
- target="_blank"
362
- rel="noopener noreferrer"
363
- className="px-2 md:px-4 py-2 text-white rounded-lg transition-colors text-sm md:text-base"
364
- style={{ backgroundColor: '#354F52' }}
365
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#2e4346'}
366
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#354F52'}
367
- >
368
- API
369
- </a>
370
- </div>
371
- </div>
372
- </div>
373
-
374
- {/* Sidebar */}
375
- <div className={`
376
- fixed top-16 inset-y-0 left-0 w-64 bg-white border-r border-gray-200 z-40
377
- transform transition-transform duration-200 ease-in-out
378
- ${mobileMenuOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
379
- `}>
380
- <nav className="mt-6 px-4 overflow-y-auto h-[calc(100vh-10rem)]">
381
- {navigation.map((item, index) => {
382
- // Handle section headers with nested items
383
- if ('section' in item && item.section && item.items) {
384
- return (
385
- <div key={index} className="mb-6">
386
- <div className="px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
387
- {item.section}
388
- </div>
389
- {item.items.map((subItem) => {
390
- const isActive = location.pathname === subItem.href
391
- const isExternal = 'external' in subItem && subItem.external
392
-
393
- const linkClasses = `
394
- flex items-center gap-3 px-4 py-3 mb-1 rounded-lg transition-colors
395
- ${
396
- isActive
397
- ? 'bg-primary-50 text-primary-700 font-medium'
398
- : 'text-gray-700 hover:bg-gray-100'
399
- }
400
- `
401
-
402
- if (isExternal) {
403
- return (
404
- <a
405
- key={subItem.name}
406
- href={subItem.href}
407
- target="_blank"
408
- rel="noopener noreferrer"
409
- className={linkClasses}
410
- >
411
- <subItem.icon className="h-5 w-5" />
412
- <span className="text-sm">{subItem.name}</span>
413
- </a>
414
- )
415
- }
416
-
417
- return (
418
- <Link
419
- key={subItem.name}
420
- to={subItem.href}
421
- onClick={() => setMobileMenuOpen(false)}
422
- className={linkClasses}
423
- >
424
- <subItem.icon className="h-5 w-5" />
425
- <span className="text-sm">{subItem.name}</span>
426
- </Link>
427
- )
428
- })}
429
- </div>
430
- )
431
- }
432
-
433
- // Handle regular navigation items
434
- if ('href' in item && item.href) {
435
- const isActive = location.pathname === item.href
436
- return (
437
- <Link
438
- key={item.name}
439
- to={item.href}
440
- onClick={() => setMobileMenuOpen(false)}
441
- className={`
442
- flex items-center gap-3 px-4 py-3 mb-2 rounded-lg transition-colors
443
- ${
444
- isActive
445
- ? 'bg-primary-50 text-primary-700 font-medium'
446
- : 'text-gray-700 hover:bg-gray-100'
447
- }
448
- `}
449
- >
450
- <item.icon className="h-6 w-6" />
451
- <span>{item.name}</span>
452
- </Link>
453
- )
454
- }
455
-
456
- return null
457
- })}
458
- </nav>
459
-
460
- {/* Sidebar Footer */}
461
- <div className="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200 bg-white">
462
- <div className="text-sm text-gray-600">
463
- <div className="font-medium mb-1">Open Data Sources</div>
464
- <div className="text-xs">
465
- • <Link to="/jurisdictions" className="hover:text-primary-600 hover:underline">925 Jurisdictions</Link><br />
466
- • <Link to="/search?types=organizations" className="hover:text-primary-600 hover:underline">43,726 Nonprofits</Link><br />
467
- • 6,913 Meeting Pages<br />
468
- • <Link to="/search?types=contacts" className="hover:text-primary-600 hover:underline">362 Officials</Link>
469
- </div>
470
- <div className="mt-3 pt-3 border-t border-gray-100">
471
- <Link
472
- to="/#contact"
473
- className="text-xs text-primary-600 hover:text-primary-700 hover:underline font-medium"
474
- >
475
- 📍 Request Jurisdiction Coverage
476
- </Link>
477
- </div>
478
- </div>
479
- </div>
480
- </div>
481
-
482
- {/* Mobile menu overlay */}
483
- {mobileMenuOpen && (
484
- <div
485
- className="fixed inset-0 bg-black bg-opacity-50 z-30 md:hidden"
486
- onClick={() => setMobileMenuOpen(false)}
487
- />
488
- )}
489
-
490
- {/* Main content */}
491
- <div className="md:pl-64 pt-16">
492
- <main>
493
- <Outlet />
494
- </main>
495
- </div>
496
- </div>
497
- )
498
- }