Files
apps/politics/desktop/PoliticsSidebar.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

164 lines
7.7 KiB
JavaScript

class PoliticsSidebar extends Shadow {
constructor(view, levelFilter, onViewChange, onLevelChange, jurisdiction, elections) {
super()
this.view = view
this.levelFilter = levelFilter
this.onViewChange = onViewChange
this.onLevelChange = onLevelChange
this.jurisdiction = jurisdiction
this.elections = elections
}
render() {
VStack(() => {
// ── App title ─────────────────────────────────────────────
VStack(() => {
p("⚖️")
.margin(0).fontSize(1.6, em).lineHeight("1")
h2("Civics")
.margin(0).marginTop(0.35, em).fontSize(1.05, em).fontWeight("700")
.color("var(--headertext)")
p("Your Political Dashboard")
.margin(0).marginTop(0.12, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.38)
})
.paddingHorizontal(1.1, em).paddingTop(1.2, em).paddingBottom(0.9, em)
// ── Divider ───────────────────────────────────────────────
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em)
// ── Main nav ──────────────────────────────────────────────
VStack(() => {
this.sectionLabel("VIEWS")
this.navItem("🏛", "Representatives", "representatives")
this.navItem("🗳", "Elections", "elections")
})
.paddingTop(0.75, em)
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em).marginVertical(0.5, em)
// ── Level filter ──────────────────────────────────────────
VStack(() => {
this.sectionLabel("GOVERNMENT LEVEL")
this.levelItem("all", "🇺🇸 + 🏠 All")
this.levelItem("federal", "🇺🇸 Federal")
this.levelItem("state", "🏠 State")
this.levelItem("local", "📍 Local")
})
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em).marginVertical(0.5, em)
// ── Jurisdiction card ─────────────────────────────────────
VStack(() => {
this.sectionLabel("YOUR DISTRICT")
VStack(() => {
this.jurisdictionRow("Federal", this.jurisdiction.federal)
this.jurisdictionRow("State Senate", this.jurisdiction.stateSenate)
this.jurisdictionRow("State House", this.jurisdiction.stateHouse)
this.jurisdictionRow("County", this.jurisdiction.county)
})
.padding(0.75, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.55, em)
.marginHorizontal(0.75, em)
.gap(0)
})
.paddingTop(0)
VStack(() => {}).flex(1)
// ── Next election countdown ───────────────────────────────
VStack(() => {
const next = this.elections
.filter(e => new Date(e.date) > new Date())
.sort((a, b) => new Date(a.date) - new Date(b.date))[0]
if (next) {
const days = Math.ceil((new Date(next.date) - new Date()) / 86400000)
VStack(() => {
p("NEXT ELECTION")
.margin(0).fontSize(0.6, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
p(next.title)
.margin(0).marginTop(0.35, em).fontSize(0.8, em).fontWeight("600")
.color("var(--headertext)").lineHeight("1.3")
HStack(() => {
p(`${days}`)
.margin(0).fontSize(1.6, em).fontWeight("800")
.color("var(--quillred)").lineHeight("1")
p("days away")
.margin(0).marginLeft(0.35, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.45)
.alignSelf("flex-end").paddingBottom(0.1, em)
})
.alignItems("flex-end").marginTop(0.5, em)
})
.padding(0.85, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.55, em)
.cursor("pointer")
.onClick((done) => {if(!done) return; this.onViewChange("elections")})
}
})
.paddingHorizontal(0.75, em).paddingBottom(1.1, em).flexShrink(0)
})
.height(100, pct).width(100, pct).boxSizing("border-box").overflowY("auto")
}
navItem(icon, label, viewId) {
const isActive = this.view === viewId
HStack(() => {
p(icon).margin(0).fontSize(0.95, em).lineHeight("1").flexShrink(0)
p(label).margin(0).fontSize(0.88, em).fontWeight(isActive ? "600" : "400")
.color("var(--headertext)").opacity(isActive ? 1 : 0.65)
})
.gap(0.62, em).paddingHorizontal(0.85, em).paddingVertical(0.45, em)
.marginHorizontal(0.4, em)
.borderRadius(0.45, em)
.background(isActive ? "var(--app)" : "transparent")
.cursor("pointer").alignItems("center")
.width("calc(100% - 0.8em)").boxSizing("border-box")
.onClick((done) => {if(!done) return; this.onViewChange(viewId)})
}
levelItem(level, label) {
const isActive = this.levelFilter === level
HStack(() => {
p(label).margin(0).fontSize(0.85, em).fontWeight(isActive ? "600" : "400")
.color("var(--headertext)").opacity(isActive ? 1 : 0.58)
})
.paddingHorizontal(0.85, em).paddingVertical(0.38, em)
.marginHorizontal(0.4, em)
.borderRadius(0.45, em)
.background(isActive ? "var(--app)" : "transparent")
.cursor("pointer")
.width("calc(100% - 0.8em)").boxSizing("border-box")
.onClick((done) => {if(!done) return; this.onLevelChange(level)})
}
jurisdictionRow(label, value) {
HStack(() => {
p(label)
.margin(0).fontSize(0.68, em).color("var(--headertext)").opacity(0.4)
.width(5.5, em).flexShrink(0)
p(value)
.margin(0).fontSize(0.72, em).fontWeight("600").color("var(--headertext)")
.flex(1).minWidth(0).overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
})
.paddingVertical(0.32, em)
.borderBottom("1px solid var(--divider)")
.alignItems("flex-start").gap(0.4, em)
}
sectionLabel(text) {
p(text)
.margin(0).marginBottom(0.28, em).paddingHorizontal(1.1, em)
.fontSize(0.62, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
}
}
register(PoliticsSidebar)