jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
Raw
History Blame Contribute Delete
18.7 kB
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>
);
}