Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| import React, { useState } from 'react'; | |
| import HomePage from './components/HomePage'; | |
| import ImpactDashboard from './components/ImpactDashboard'; | |
| import TopicNavigation from './components/TopicNavigation'; | |
| import Summary from './components/Summary'; | |
| import WordsVsDollars from './components/WordsVsDollars'; | |
| import EndlessStudyLoop from './components/EndlessStudyLoop'; | |
| import WhereMoneyWent from './components/WhereMoneyWent'; | |
| import WhoIsInCharge from './components/WhoIsInCharge'; | |
| import FilterPanel from './components/shared/FilterPanel'; | |
| import DecisionCard from './components/shared/DecisionCard'; | |
| import { metadata } from './data/dashboardData'; | |
| import { Home, Layout, Grid, List, BarChart3 } from 'lucide-react'; | |
| const tabs = [ | |
| { id: 0, label: 'Summary', component: Summary }, | |
| { id: 1, label: 'They cut health spending while praising wellness', component: WordsVsDollars }, | |
| { id: 2, label: 'Delayed 6 months and counting', component: EndlessStudyLoop }, | |
| { id: 3, label: 'What got funded instead', component: WhereMoneyWent }, | |
| { id: 4, label: 'One memo beat 240 residents', component: WhoIsInCharge }, | |
| ]; | |
| export default function App() { | |
| const [active, setActive] = useState(0); | |
| const [viewMode, setViewMode] = useState('home'); // 'home', 'impact', 'dashboards', 'decisions', 'browse' | |
| const [selectedPersona, setSelectedPersona] = useState(null); | |
| const [selectedTopic, setSelectedTopic] = useState(null); | |
| const [selectedDomains, setSelectedDomains] = useState([]); | |
| const [selectedTopics, setSelectedTopics] = useState([]); | |
| const [selectedPatterns, setSelectedPatterns] = useState([]); | |
| const [selectedResources, setSelectedResources] = useState([]); | |
| const [searchQuery, setSearchQuery] = useState(''); | |
| const handlePersonaSelect = (persona, topic) => { | |
| setSelectedPersona(persona); | |
| setSelectedTopic(topic); | |
| setViewMode('impact'); | |
| }; | |
| const handleTopicSelect = (topicId) => { | |
| setSelectedTopics([topicId]); | |
| setViewMode('browse'); | |
| }; | |
| const handleDomainToggle = (domainId) => { | |
| setSelectedDomains(prev => | |
| prev.includes(domainId) | |
| ? prev.filter(d => d !== domainId) | |
| : [...prev, domainId] | |
| ); | |
| }; | |
| const handleTopicToggle = (topicId) => { | |
| setSelectedTopics(prev => | |
| prev.includes(topicId) | |
| ? prev.filter(t => t !== topicId) | |
| : [...prev, topicId] | |
| ); | |
| }; | |
| const handlePatternToggle = (patternId) => { | |
| setSelectedPatterns(prev => | |
| prev.includes(patternId) | |
| ? prev.filter(p => p !== patternId) | |
| : [...prev, patternId] | |
| ); | |
| }; | |
| const handleResourceToggle = (resourceId) => { | |
| setSelectedResources(prev => | |
| prev.includes(resourceId) | |
| ? prev.filter(r => r !== resourceId) | |
| : [...prev, resourceId] | |
| ); | |
| }; | |
| const handleClearFilters = () => { | |
| setSelectedDomains([]); | |
| setSelectedTopics([]); | |
| setSelectedPatterns([]); | |
| setSelectedResources([]); | |
| setSearchQuery(''); | |
| }; | |
| const handleBackToHome = () => { | |
| setViewMode('home'); | |
| setSelectedPersona(null); | |
| setSelectedTopic(null); | |
| }; | |
| // Example decision data - would come from Python export | |
| const exampleDecisions = [ | |
| { | |
| decision_summary: "Approval of $850,000 athletic turf replacement project", | |
| outcome: "Approved", | |
| primary_rationale: "Athletic facilities are essential for student engagement and community pride. The current turf is beyond its useful life and poses safety concerns.", | |
| supporters: [ | |
| { name: "Board Member Johnson", role: "Board Member" }, | |
| { name: "Athletic Director Smith", role: "Staff" } | |
| ], | |
| opponents: [ | |
| { name: "Parent Coalition Representative", role: "Public Comment" } | |
| ], | |
| vote_result: "5-2", | |
| meeting_date: "2026-03-15", | |
| tradeoffs_discussed: ["Capital funding vs. operational needs", "Visibility vs. academic priorities"], | |
| evidence_cited: [{ source: "Facility Assessment Report 2025" }], | |
| policy_domain: "facilities" | |
| }, | |
| { | |
| decision_summary: "Tabled: West Alabama Community Dental Clinic partnership proposal", | |
| outcome: "Deferred", | |
| primary_rationale: "Need additional time to review liability implications and insurance coverage requirements before making a final decision.", | |
| supporters: [ | |
| { name: "Health Department Representative", role: "External Partner" }, | |
| { name: "240+ citizen comments", role: "Public Comment" } | |
| ], | |
| opponents: [], | |
| vote_result: "Motion to table: 6-1", | |
| meeting_date: "2026-04-10", | |
| tradeoffs_discussed: ["Liability concerns vs. student health needs"], | |
| evidence_cited: [ | |
| { source: "Risk Manager Patricia Johnson memo" }, | |
| { source: "Insurance policy documentation" } | |
| ], | |
| policy_domain: "health" | |
| }, | |
| { | |
| decision_summary: "Contracted health services budget reduction of $120,000", | |
| outcome: "Approved as part of budget", | |
| primary_rationale: "Administrative cost increases require reallocation from non-critical service contracts. Health screenings can be covered through existing nurse staff.", | |
| supporters: [ | |
| { name: "CFO Thompson", role: "Staff" }, | |
| { name: "Budget Committee", role: "Committee" } | |
| ], | |
| opponents: [ | |
| { name: "School Nurses Association", role: "Public Comment" } | |
| ], | |
| vote_result: "7-0", | |
| meeting_date: "2026-02-20", | |
| tradeoffs_discussed: ["Cost savings vs. preventive health services"], | |
| evidence_cited: [{ source: "FY2026 Budget Proposal" }], | |
| policy_domain: "budget" | |
| } | |
| ]; | |
| // Filter decisions based on search and domains | |
| const filteredDecisions = exampleDecisions.filter(decision => { | |
| const matchesSearch = searchQuery === '' || | |
| decision.decision_summary.toLowerCase().includes(searchQuery.toLowerCase()); | |
| const matchesDomain = selectedDomains.length === 0 || | |
| selectedDomains.includes(decision.policy_domain); | |
| return matchesSearch && matchesDomain; | |
| }); | |
| const ActiveComponent = tabs[active].component; | |
| return ( | |
| <div style={{ maxWidth: 1200, margin: '0 auto', padding: 24 }}> | |
| {/* Header */} | |
| <div style={{ marginBottom: 24 }}> | |
| <h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 4, color: '#111' }}> | |
| {metadata.title} | |
| </h1> | |
| <p style={{ color: '#666', fontSize: 14, lineHeight: 1.5 }}> | |
| {metadata.description} | |
| </p> | |
| </div> | |
| {/* View Mode Toggle */} | |
| <div style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| alignItems: 'center', | |
| marginBottom: 16 | |
| }}> | |
| <div style={{ display: 'flex', gap: 6 }}> | |
| <button | |
| onClick={() => setViewMode('home')} | |
| style={{ | |
| padding: '8px 16px', | |
| borderRadius: 8, | |
| fontSize: 13, | |
| cursor: 'pointer', | |
| border: '1px solid', | |
| borderColor: viewMode === 'home' ? '#888' : '#ddd', | |
| background: viewMode === 'home' ? '#f5f5f2' : 'white', | |
| fontWeight: viewMode === 'home' ? 500 : 400, | |
| color: viewMode === 'home' ? '#111' : '#666', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 6 | |
| }} | |
| > | |
| <Home size={14} /> | |
| Home | |
| </button> | |
| <button | |
| onClick={() => setViewMode('browse')} | |
| style={{ | |
| padding: '8px 16px', | |
| borderRadius: 8, | |
| fontSize: 13, | |
| cursor: 'pointer', | |
| border: '1px solid', | |
| borderColor: viewMode === 'browse' ? '#888' : '#ddd', | |
| background: viewMode === 'browse' ? '#f5f5f2' : 'white', | |
| fontWeight: viewMode === 'browse' ? 500 : 400, | |
| color: viewMode === 'browse' ? '#111' : '#666', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 6 | |
| }} | |
| > | |
| <Grid size={14} /> | |
| Browse by Topic | |
| </button> | |
| <button | |
| onClick={() => setViewMode('dashboards')} | |
| style={{ | |
| padding: '8px 16px', | |
| borderRadius: 8, | |
| fontSize: 13, | |
| cursor: 'pointer', | |
| border: '1px solid', | |
| borderColor: viewMode === 'dashboards' ? '#888' : '#ddd', | |
| background: viewMode === 'dashboards' ? '#f5f5f2' : 'white', | |
| fontWeight: viewMode === 'dashboards' ? 500 : 400, | |
| color: viewMode === 'dashboards' ? '#111' : '#666', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 6 | |
| }} | |
| > | |
| <BarChart3 size={14} /> | |
| Analysis Dashboards | |
| </button> | |
| <button | |
| onClick={() => setViewMode('decisions')} | |
| style={{ | |
| padding: '8px 16px', | |
| borderRadius: 8, | |
| fontSize: 13, | |
| cursor: 'pointer', | |
| border: '1px solid', | |
| borderColor: viewMode === 'decisions' ? '#888' : '#ddd', | |
| background: viewMode === 'decisions' ? '#f5f5f2' : 'white', | |
| fontWeight: viewMode === 'decisions' ? 500 : 400, | |
| color: viewMode === 'decisions' ? '#111' : '#666', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 6 | |
| }} | |
| > | |
| <List size={14} /> | |
| Alx', | |
| justifyContent: 'space-between', | |
| alignItems: 'center', | |
| marginBottom: 16 | |
| }}> | |
| <div style={{ display: 'flex', gap: 6 }}> | |
| <button | |
| onClick={() => setViewMode('dashboards')} | |
| style={{ | |
| Topic Navigation (show in browse view) */} | |
| {viewMode === 'browse' && ( | |
| <TopicNavigation | |
| selectedTopics={selectedTopics} | |
| selectedPatterns={selectedPatterns} | |
| selectedResources={selectedResources} | |
| onTopicToggle={handleTopicToggle} | |
| onPatternToggle={handlePatternToggle} | |
| onResourceToggle={handleResourceToggle} | |
| onClearAll={handleClearFilters} | |
| /> | |
| )} | |
| {/* padding: '8px 16px', | |
| borderRadius: 8, | |
| fontSize: 13, | |
| cursor: 'pointer', | |
| border: '1px solid', | |
| borderColor: viewMode === 'dashboards' ? '#888' : '#ddd', | |
| background: viewMode === 'dashboards' ? '#f5f5f2' : 'white', | |
| fontWeight: viewMode === 'dashboards' ? 500 : 400, | |
| color: viewMode === 'dashboards' ? '#111' : '#666', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 6 | |
| }} | |
| > | |
| <Layout size={14} /> | |
| Dashboards | |
| </button> | |
| <button | |
| onClick={() => setViewMode('decisions')} | |
| style={{ | |
| padding: '8px 16px', | |
| borderRadius: 8, | |
| fontSize: 13, | |
| cursor: 'pointer', | |
| border: '1px solid', | |
| borderColor: viewMode === 'decisions' ? '#888' : '#ddd', | |
| background: viewMode === 'decisions' ? '#f5f5f2' : 'white', | |
| fontWeight: viewMode === 'decisions' ? 500 : 400, | |
| color: viewMode === 'decisions' ? '#111' : '#666', | |
| display: 'flex', | |
| alignIthome' ? ( | |
| <HomePage | |
| onPersonaSelect={handlePersonaSelect} | |
| onTopicSelect={handleTopicSelect} | |
| /> | |
| ) : viewMode === 'impact' ? ( | |
| <div style={{ | |
| background: 'white', | |
| border: '1px solid #eee', | |
| borderRadius: 12, | |
| padding: '1.5rem', | |
| boxShadow: '0 1px 3px rgba(0,0,0,0.05)' | |
| }}> | |
| <button | |
| onClick={handleBackToHome} | |
| style={{ | |
| fontSize: 13, | |
| color: '#666', | |
| background: 'none', | |
| border: 'none', | |
| cursor: 'pointer', | |
| marginBottom: 16, | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 4 | |
| }} | |
| > | |
| ← Back to Home | |
| </button> | |
| <ImpactDashboard persona={selectedPersona} topic={selectedTopic} /> | |
| </div> | |
| ) : viewMode === 'browse' ? ( | |
| <div style={{ | |
| background: 'white', | |
| border: '1px solid #eee', | |
| borderRadius: 12, | |
| padding: '1.5rem', | |
| boxShadow: '0 1px 3px rgba(0,0,0,0.05)' | |
| }}> | |
| <h2 style={{ | |
| fontSize: 16, | |
| fontWeight: 500, | |
| marginBottom: 16 | |
| }}> | |
| Browse Decisions by Topic | |
| </h2> | |
| {filteredDecisions.length === 0 ? ( | |
| <div style={{ | |
| textAlign: 'center', | |
| padding: '3rem', | |
| color: '#999' | |
| }}> | |
| <p style={{ fontSize: 14, marginBottom: 8 }}> | |
| No decisions match your filters | |
| </p> | |
| <button | |
| onClick={handleClearFilters} | |
| style={{ | |
| fontSize: 13, | |
| color: '#D85A30', | |
| background: 'none', | |
| border: 'none', | |
| cursor: 'pointer', | |
| textDecoration: 'underline' | |
| }} | |
| > | |
| Clear filters | |
| </button> | |
| </div> | |
| ) : ( | |
| <> | |
| <div style={{ fontSize: 13, color: '#888', marginBottom: 16 }}> | |
| {filteredDecisions.length} decision{filteredDecisions.length !== 1 ? 's' : ''} found | |
| </div> | |
| {filteredDecisions.map((decision, i) => ( | |
| <DecisionCard | |
| key={i} | |
| decision={decision} | |
| onClick={() => console.log('View decision:', decision)} | |
| /> | |
| ))} | |
| </> | |
| )} | |
| </div> | |
| ) : viewMode === 'ems: 'center', | |
| gap: 6 | |
| }} | |
| > | |
| <List size={14} /> | |
| Individual Decisions | |
| </button> | |
| </div> | |
| </div> | |
| {/* Filters (show in decisions view) */} | |
| {viewMode === 'decisions' && ( | |
| <FilterPanel | |
| selectedDomains={selectedDomains} | |
| onDomainToggle={handleDomainToggle} | |
| searchQuery={searchQuery} | |
| onSearchChange={setSearchQuery} | |
| onClear={handleClearFilters} | |
| /> | |
| )} | |
| {/* Tab Navigation (show in dashboard view) */} | |
| {viewMode === 'dashboards' && ( | |
| <div style={{ | |
| display: 'flex', | |
| gap: 6, | |
| flexWrap: 'wrap', | |
| marginBottom: 24 | |
| }}> | |
| {tabs.map((t) => ( | |
| <button | |
| key={t.id} | |
| onClick={() => setActive(t.id)} | |
| style={{ | |
| padding: '6px 14px', | |
| borderRadius: 8, | |
| fontSize: 13, | |
| cursor: 'pointer', | |
| border: '1px solid', | |
| borderColor: active === t.id ? '#888' : '#ddd', | |
| background: active === t.id ? '#f5f5f2' : 'white', | |
| fontWeight: active === t.id ? 500 : 400, | |
| color: active === t.id ? '#111' : '#666', | |
| transition: 'all 0.2s ease' | |
| }} | |
| > | |
| {t.label} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| {/* Content */} | |
| {viewMode === 'dashboards' ? ( | |
| <div style={{ | |
| background: 'white', | |
| border: '1px solid #eee', | |
| borderRadius: 12, | |
| padding: '1.5rem', | |
| boxShadow: '0 1px 3px rgba(0,0,0,0.05)' | |
| }}> | |
| <h2 style={{ | |
| fontSize: 14, | |
| textTransform: 'uppercase', | |
| letterSpacing: '0.07em', | |
| color: '#aaa', | |
| marginBottom: 16 | |
| }}> | |
| {tabs[active].label} | |
| </h2> | |
| {active === 0 ? ( | |
| <ActiveComponent onNavigate={handleNavigate} /> | |
| ) : ( | |
| <ActiveComponent /> | |
| )} | |
| </div> | |
| ) : ( | |
| <div style={{ | |
| background: 'white', | |
| border: '1px solid #eee', | |
| borderRadius: 12, | |
| padding: '1.5rem', | |
| boxShadow: '0 1px 3px rgba(0,0,0,0.05)' | |
| }}> | |
| <div style={{ | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| alignItems: 'center', | |
| marginBottom: 16 | |
| }}> | |
| <h2 style={{ | |
| fontSize: 14, | |
| textTransform: 'uppercase', | |
| letterSpacing: '0.07em', | |
| color: '#aaa' | |
| }}> | |
| Individual Decisions | |
| </h2> | |
| <div style={{ fontSize: 13, color: '#888' }}> | |
| {filteredDecisions.length} decision{filteredDecisions.length !== 1 ? 's' : ''} | |
| </div> | |
| </div> | |
| {filteredDecisions.length === 0 ? ( | |
| <div style={{ | |
| textAlign: 'center', | |
| padding: '3rem', | |
| color: '#999' | |
| }}> | |
| <p style={{ fontSize: 14, marginBottom: 8 }}> | |
| No decisions match your filters | |
| </p> | |
| <button | |
| onClick={handleClearFilters} | |
| style={{ | |
| fontSize: 13, | |
| color: '#D85A30', | |
| background: 'none', | |
| border: 'none', | |
| cursor: 'pointer', | |
| textDecoration: 'underline' | |
| }} | |
| > | |
| Clear filters | |
| </button> | |
| </div> | |
| ) : ( | |
| filteredDecisions.map((decision, i) => ( | |
| <DecisionCard | |
| key={i} | |
| decision={decision} | |
| onClick={() => { | |
| // Could open modal or navigate to detail view | |
| console.log('View decision details:', decision); | |
| }} | |
| /> | |
| )) | |
| )} | |
| </div> | |
| )} | |
| {/* Footer */} | |
| <div style={{ | |
| marginTop: 24, | |
| textAlign: 'center', | |
| fontSize: 11, | |
| color: '#999' | |
| }}> | |
| <p> | |
| Generated by <strong>CommunityOne Open Navigator</strong> — Evidence-based advocacy toolkit | |
| </p> | |
| <p style={{ marginTop: 4 }}> | |
| Methodology: Accountability dashboards using public meeting records and budget data | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |