This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

View File

@@ -0,0 +1,414 @@
class PoliticsElections extends Shadow {
constructor(elections, levelFilter, onVote, onThumbsUp) {
super()
this.elections = elections
this.levelFilter = levelFilter
this.onVote = onVote
this.onThumbsUp = onThumbsUp
// which election card is "expanded" for voting
this.expandedId = null
}
get visible() {
let list = this.levelFilter === "all"
? this.elections
: this.elections.filter(e => e.level === this.levelFilter)
return [...list].sort((a, b) => new Date(a.date) - new Date(b.date))
}
get upcoming() { return this.visible.filter(e => new Date(e.date) >= new Date()) }
get past() { return this.visible.filter(e => new Date(e.date) < new Date()) }
render() {
VStack(() => {
// Header
VStack(() => {
h2("Elections")
.margin(0).fontSize(1.1, em).fontWeight("700").color("var(--headertext)")
p(`${this.upcoming.length} upcoming · ${this.past.length} recent`)
.margin(0).marginTop(0.2, em).fontSize(0.78, em)
.color("var(--headertext)").opacity(0.42)
})
.paddingHorizontal(1.75, em).paddingVertical(1.25, em)
.borderBottom("1px solid var(--divider)").flexShrink(0)
VStack(() => {
if (this.upcoming.length > 0) {
this.sectionLabel("UPCOMING")
this.upcoming.forEach(e => this.renderElectionCard(e, false))
}
if (this.past.length > 0) {
this.sectionLabel("RECENT")
this.past.forEach(e => this.renderElectionCard(e, true))
}
if (this.visible.length === 0) {
VStack(() => {
p("🗳")
.margin(0).fontSize(2.2, em).opacity(0.18)
p("No elections found for this level")
.margin(0).marginTop(0.6, em).fontSize(0.88, em)
.color("var(--headertext)").opacity(0.32)
})
.flex(1).justifyContent("center").alignItems("center").paddingTop(4, em)
}
})
.overflowY("auto").flex(1)
.paddingHorizontal(1.75, em).paddingBottom(2, em).paddingTop(0.5, em)
})
.height(100, pct).width(100, pct).overflow("hidden")
}
renderElectionCard(election, isPast) {
const isExpanded = this.expandedId === election.id
const hasVoted = !!election.userVoteId
const daysAway = Math.ceil((new Date(election.date) - new Date()) / 86400000)
VStack(() => {
// ── Card top row ──────────────────────────────────────────
HStack(() => {
// Level badge
VStack(() => {
p(this.levelIcon(election.level))
.margin(0).fontSize(0.8, em).lineHeight("1")
})
.width(2, em).height(2, em).borderRadius(0.38, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
HStack(() => {
p(election.title)
.margin(0).fontSize(0.95, em).fontWeight("700")
.color("var(--headertext)").flex(1).minWidth(0)
// Date chip
HStack(() => {
if (!isPast && daysAway <= 30) {
p(`${daysAway}d away`)
.margin(0).fontSize(0.68, em).fontWeight("700")
.color(daysAway <= 7 ? "#ef4444" : "#f59e0b")
} else {
p(this.formatDate(election.date))
.margin(0).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.4)
}
})
.paddingHorizontal(0.55, em).paddingVertical(0.18, em)
.background(
!isPast && daysAway <= 7 ? "rgba(239,68,68,0.08)" :
!isPast && daysAway <= 30 ? "rgba(245,158,11,0.08)" :
"var(--darkaccent)"
)
.border(`1px solid ${
!isPast && daysAway <= 7 ? "rgba(239,68,68,0.2)" :
!isPast && daysAway <= 30 ? "rgba(245,158,11,0.2)" :
"var(--divider)"
}`)
.borderRadius(100, px).flexShrink(0)
})
.alignItems("center").gap(0.5, em)
HStack(() => {
p(this.capitalize(election.type))
.margin(0).paddingHorizontal(0.45, em).paddingVertical(0.1, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(100, px).fontSize(0.68, em)
.color("var(--headertext)").opacity(0.55)
p(this.capitalize(election.level))
.margin(0).paddingHorizontal(0.45, em).paddingVertical(0.1, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(100, px).fontSize(0.68, em)
.color("var(--headertext)").opacity(0.55)
if (election.description) {
p(election.description)
.margin(0).fontSize(0.75, em)
.color("var(--headertext)").opacity(0.45)
}
})
.gap(0.35, em).alignItems("center").marginTop(0.38, em)
})
.flex(1).minWidth(0).gap(0)
})
.gap(0.75, em).alignItems("flex-start")
// ── Candidates ────────────────────────────────────────────
if (election.candidates.length > 0) {
VStack(() => {
// Candidate vs. row
if (election.candidates.length >= 2) {
this.renderCandidateMatchup(election, isPast)
} else {
// Single candidate / unopposed
this.renderSingleCandidate(election.candidates[0], election, isPast)
}
})
.marginTop(1, em)
}
// ── Footer: thumbs up + weigh in ──────────────────────────
HStack(() => {
// Thumbs up / follow this race
HStack(() => {
button(election.userThumbsUp ? "👍" : "👍")
.padding(0).border("none").background("transparent")
.cursor("pointer").fontSize(0.95, em)
.onClick((done) => {if(!done) return; this.onThumbsUp(election.id)})
p(election.thumbsUp > 0 ? `${election.thumbsUp}` : "Follow")
.margin(0).fontSize(0.75, em)
.color("var(--headertext)")
.opacity(election.userThumbsUp ? 1 : 0.42)
.fontWeight(election.userThumbsUp ? "600" : "400")
})
.gap(0.3, em).alignItems("center")
.paddingHorizontal(0.65, em).paddingVertical(0.32, em)
.background(election.userThumbsUp ? "rgba(59,130,246,0.08)" : "transparent")
.border(`1px solid ${election.userThumbsUp ? "rgba(59,130,246,0.25)" : "var(--divider)"}`)
.borderRadius(100, px).cursor("pointer")
.onClick((done) => {if(!done) return; this.onThumbsUp(election.id)})
HStack(() => {}).flex(1)
if (!isPast && !hasVoted) {
button(isExpanded ? "Close" : "Weigh In →")
.paddingHorizontal(1, em).paddingVertical(0.38, em)
.background(isExpanded ? "transparent" : "var(--quillred)")
.border(isExpanded ? "1px solid var(--divider)" : "none")
.borderRadius(0.45, em)
.color(isExpanded ? "var(--headertext)" : "white")
.fontSize(0.82, em).fontWeight("600").cursor("pointer")
.opacity(isExpanded ? 0.6 : 1)
.onClick((done) => {if(!done) return;
this.expandedId = isExpanded ? null : election.id
this.rerender()
})
} else if (hasVoted) {
HStack(() => {
p("✓ You weighed in")
.margin(0).fontSize(0.75, em).fontWeight("600").color("#22c55e")
})
.paddingHorizontal(0.75, em).paddingVertical(0.32, em)
.background("rgba(34,197,94,0.08)").border("1px solid rgba(34,197,94,0.2)")
.borderRadius(100, px)
}
})
.alignItems("center").marginTop(0.9, em)
// ── Expanded vote panel ───────────────────────────────────
if (isExpanded && !hasVoted) {
VStack(() => {
p("Who do you support?")
.margin(0).marginBottom(0.75, em).fontSize(0.8, em).fontWeight("600")
.color("var(--headertext)").opacity(0.55)
.textAlign("center")
HStack(() => {
election.candidates.forEach((candidate, i) => {
const color = this.partyColor(candidate.party)
if (i > 0) {
p("vs")
.margin(0).paddingHorizontal(0.6, em)
.fontSize(0.72, em).fontWeight("700")
.color("var(--headertext)").opacity(0.3)
.alignSelf("center")
}
button(() => {
VStack(() => {
p(candidate.initials || candidate.name[0])
.margin(0).fontSize(0.9, em).fontWeight("800").color("white").lineHeight("1")
})
.width(2.2, em).height(2.2, em).borderRadius(50, pct)
.background(color).justifyContent("center").alignItems("center")
.marginBottom(0.5, em)
p(candidate.name)
.margin(0).fontSize(0.82, em).fontWeight("700")
.color("var(--headertext)").textAlign("center")
p(`${candidate.party}${candidate.incumbent ? " · Incumbent" : ""}`)
.margin(0).marginTop(0.15, em).fontSize(0.68, em)
.color(color).textAlign("center")
})
.flex(1)
.paddingVertical(0.9, em)
.paddingHorizontal(0.5, em)
.background(`${color}0d`)
.border(`2px solid ${color}33`)
.borderRadius(0.65, em)
.cursor("pointer")
.display("flex").flexDirection("column").alignItems("center")
.onClick((done) => {if(!done) return;
this.expandedId = null
this.onVote(election.id, candidate.id)
})
})
})
.gap(0.5, em).width(100, pct)
})
.marginTop(0.85, em)
.padding(1, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.65, em)
.alignItems("center")
}
})
.padding(1.2, em)
.border("1px solid var(--divider)")
.borderRadius(0.75, em)
.marginBottom(0.85, em)
.boxSizing("border-box")
.opacity(isPast ? 0.72 : 1)
}
renderCandidateMatchup(election, isPast) {
const totalVotes = election.candidates.reduce((s, c) => s + (c.votes || 0), 0)
HStack(() => {
election.candidates.slice(0, 4).forEach((candidate, i) => {
const pct = totalVotes > 0 ? Math.round((candidate.votes / totalVotes) * 100) : null
const color = this.partyColor(candidate.party)
const isUserVote = election.userVoteId === candidate.id
if (i > 0) {
VStack(() => {
p("vs").margin(0).fontSize(0.68, em).fontWeight("700")
.color("var(--headertext)").opacity(0.28)
})
.justifyContent("center").paddingHorizontal(0.5, em).flexShrink(0)
}
VStack(() => {
// Party color bar top
VStack(() => {})
.height(3, px).width(100, pct).background(color).flexShrink(0)
VStack(() => {
// Name + incumbent
HStack(() => {
VStack(() => {
p(candidate.initials || candidate.name.split(" ").map(w => w[0]).join(""))
.margin(0).fontSize(0.72, em).fontWeight("800").color("white").lineHeight("1")
})
.width(1.9, em).height(1.9, em).borderRadius(50, pct)
.background(color).justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
p(candidate.name)
.margin(0).fontSize(0.82, em).fontWeight("700").color("var(--headertext)")
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
HStack(() => {
p(candidate.party)
.margin(0).paddingHorizontal(0.35, em).paddingVertical(0.08, em)
.background(`${color}18`).border(`1px solid ${color}33`)
.borderRadius(100, px).fontSize(0.62, em).fontWeight("700").color(color)
if (candidate.incumbent) {
p("Incumbent")
.margin(0).paddingHorizontal(0.35, em).paddingVertical(0.08, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(100, px).fontSize(0.62, em).color("var(--headertext)").opacity(0.45)
}
})
.gap(0.25, em).alignItems("center").marginTop(0.18, em)
})
.flex(1).minWidth(0)
})
.gap(0.55, em).alignItems("center")
// Support bar
if (pct !== null && totalVotes > 0) {
VStack(() => {
VStack(() => {
HStack(() => {
VStack(() => {})
.height(100, pct).width(pct, pct)
.background(color).borderRadius(100, px)
.transition("width 0.5s ease")
})
.height(100, pct).width(100, pct).alignItems("center")
})
.width(100, pct).height(6, px).background(`${color}22`).borderRadius(100, px).overflow("hidden")
HStack(() => {
p(`${pct}% community support`)
.margin(0).fontSize(0.68, em).color("var(--headertext)").opacity(0.42)
if (isUserVote) {
p("· Your pick ✓")
.margin(0).fontSize(0.68, em).color(color).fontWeight("600")
}
})
.gap(0.3, em).alignItems("center").marginTop(0.35, em)
})
.marginTop(0.65, em).width(100, pct)
}
})
.padding(0.75, em).flex(1)
})
.flex(1).minWidth(0)
.background("var(--darkaccent)")
.border(`1px solid ${isUserVote ? color + "55" : "var(--divider)"}`)
.borderRadius(0.55, em).overflow("hidden")
.boxSizing("border-box")
})
})
.gap(0).alignItems("stretch").width(100, pct)
}
renderSingleCandidate(candidate, election, isPast) {
const color = this.partyColor(candidate.party)
HStack(() => {
VStack(() => {
p(candidate.initials || candidate.name[0])
.margin(0).fontSize(0.85, em).fontWeight("800").color("white").lineHeight("1")
})
.width(2.4, em).height(2.4, em).borderRadius(50, pct)
.background(color).justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
p(candidate.name)
.margin(0).fontSize(0.9, em).fontWeight("700").color("var(--headertext)")
HStack(() => {
p(candidate.party).margin(0).fontSize(0.7, em).fontWeight("600").color(color)
if (candidate.incumbent) {
p("· Incumbent").margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.42)
}
p("· Unopposed").margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.35)
})
.gap(0.35, em).alignItems("center").marginTop(0.2, em)
})
.flex(1)
})
.gap(0.75, em).alignItems("center")
.padding(0.85, em)
.background("var(--darkaccent)").border("1px solid var(--divider)").borderRadius(0.55, em)
}
sectionLabel(text) {
p(text)
.margin(0).marginTop(0.85, em).marginBottom(0.65, em)
.fontSize(0.62, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
}
levelIcon(level) {
return { federal: "🇺🇸", state: "🏠", local: "📍" }[level] || "🗳"
}
formatDate(raw) {
return new Date(raw).toLocaleDateString([], { month: "long", day: "numeric", year: "numeric" })
}
capitalize(s) {
return s ? s[0].toUpperCase() + s.slice(1) : s
}
partyColor(party) {
return { R: "#ef4444", D: "#3b82f6", I: "#8b5cf6", L: "#f59e0b", G: "#22c55e" }[party] || "#6b7280"
}
}
register(PoliticsElections)

View File

@@ -0,0 +1,214 @@
class PoliticsRepresentatives extends Shadow {
constructor(reps, levelFilter) {
super()
this.reps = reps
this.levelFilter = levelFilter
}
get visible() {
if (this.levelFilter === "all") return this.reps
return this.reps.filter(r => r.level === this.levelFilter)
}
get grouped() {
const order = ["federal", "state", "local"]
const map = {}
order.forEach(l => { map[l] = [] })
this.visible.forEach(r => {
if (map[r.level]) map[r.level].push(r)
})
return order.map(l => ({ level: l, reps: map[l] })).filter(g => g.reps.length)
}
render() {
VStack(() => {
// Header
VStack(() => {
HStack(() => {
VStack(() => {
h2("Your Representatives")
.margin(0).fontSize(1.1, em).fontWeight("700").color("var(--headertext)")
p(`${this.visible.length} officials across your districts`)
.margin(0).marginTop(0.2, em).fontSize(0.78, em)
.color("var(--headertext)").opacity(0.42)
})
// Party breakdown pill
HStack(() => {
const counts = { R: 0, D: 0, I: 0 }
this.visible.forEach(r => { counts[r.party] = (counts[r.party] || 0) + 1 })
if (counts.R) this.partyCount(counts.R, "R")
if (counts.D) this.partyCount(counts.D, "D")
if (counts.I) this.partyCount(counts.I, "I")
})
.gap(0.5, em).alignItems("center")
})
.justifyContent("space-between").alignItems("flex-start")
})
.paddingHorizontal(1.75, em).paddingVertical(1.25, em)
.borderBottom("1px solid var(--divider)").flexShrink(0)
// Groups
VStack(() => {
this.grouped.forEach(group => {
VStack(() => {
// Level header
HStack(() => {
HStack(() => {})
.flex(1).height(1, px).background("var(--divider)")
p(this.levelLabel(group.level))
.margin(0).marginHorizontal(0.9, em)
.fontSize(0.68, em).fontWeight("700").letterSpacing("0.08em")
.color("var(--headertext)").opacity(0.35)
.whiteSpace("nowrap")
HStack(() => {})
.flex(1).height(1, px).background("var(--divider)")
})
.alignItems("center").marginBottom(1, em)
// Cards
HStack(() => {
group.reps.forEach(rep => this.renderCard(rep))
})
.gap(0.85, em).flexWrap("wrap")
})
.paddingTop(1.35, em).paddingBottom(0.5, em)
})
})
.paddingHorizontal(1.75, em)
.overflowY("auto").flex(1)
})
.height(100, pct).width(100, pct).overflow("hidden")
}
renderCard(rep) {
const partyColor = this.partyColor(rep.party)
const partyBg = this.partyBg(rep.party)
VStack(() => {
// Party accent bar
VStack(() => {})
.height(4, px).width(100, pct)
.background(partyColor).flexShrink(0)
VStack(() => {
// Avatar + status
ZStack(() => {
// Avatar circle
VStack(() => {
p(rep.initials)
.margin(0).fontSize(1.15, em).fontWeight("800")
.color("white").lineHeight("1")
})
.width(3.8, em).height(3.8, em).borderRadius(50, pct)
.background(partyColor)
.justifyContent("center").alignItems("center")
.flexShrink(0)
// Party badge bottom-right
VStack(() => {
p(rep.party)
.margin(0).fontSize(0.52, em).fontWeight("800")
.color("white").lineHeight("1")
})
.position("absolute").bottom(0).right(0)
.width(1.3, em).height(1.3, em).borderRadius(50, pct)
.background(partyColor)
.boxSizing("border-box")
.justifyContent("center").alignItems("center")
})
.position("relative")
.width(3.8, em).height(3.8, em).marginBottom(0.85, em)
// Name
p(rep.name)
.margin(0).fontSize(0.92, em).fontWeight("700")
.color("var(--headertext)").lineHeight("1.25").textAlign("center")
// Title
p(rep.title)
.margin(0).marginTop(0.22, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.55)
.textAlign("center").lineHeight("1.35")
// District chip
if (rep.district) {
p(rep.district)
.margin(0).marginTop(0.55, em)
.paddingHorizontal(0.6, em).paddingVertical(0.18, em)
.background(partyBg).border(`1px solid ${partyColor}33`)
.borderRadius(100, px)
.fontSize(0.68, em).fontWeight("600")
.color(partyColor)
}
// Since
if (rep.since) {
p(`Since ${rep.since}`)
.margin(0).marginTop(0.42, em)
.fontSize(0.68, em).color("var(--headertext)").opacity(0.32)
}
// Contact row
HStack(() => {
if (rep.phone) {
this.contactBtn("📞", rep.phone)
}
if (rep.website) {
this.contactBtn("🌐", rep.website)
}
})
.gap(0.4, em).marginTop(0.75, em)
})
.padding(1.1, em).alignItems("center")
})
.width(200, px).minWidth(180, px)
.background("var(--main)")
.border("1px solid var(--divider)")
.borderRadius(0.65, em)
.overflow("hidden")
.boxSizing("border-box")
.flexShrink(0)
}
contactBtn(icon, value) {
button(icon)
.padding(0.35, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.4, em)
.fontSize(0.8, em)
.cursor("pointer")
.attr({ title: value })
}
partyCount(count, party) {
HStack(() => {
VStack(() => {})
.width(0.5, em).height(0.5, em).borderRadius(50, pct)
.background(this.partyColor(party)).flexShrink(0)
p(`${count} ${party}`)
.margin(0).fontSize(0.75, em).fontWeight("600")
.color(this.partyColor(party))
})
.gap(0.3, em).alignItems("center")
.paddingHorizontal(0.6, em).paddingVertical(0.22, em)
.background(this.partyBg(party))
.border(`1px solid ${this.partyColor(party)}33`)
.borderRadius(100, px)
}
levelLabel(level) {
return { federal: "FEDERAL DELEGATION", state: "STATE DELEGATION", local: "LOCAL OFFICIALS" }[level] || level.toUpperCase()
}
partyColor(party) {
return { R: "#ef4444", D: "#3b82f6", I: "#8b5cf6", L: "#f59e0b", G: "#22c55e" }[party] || "#6b7280"
}
partyBg(party) {
return { R: "rgba(239,68,68,0.08)", D: "rgba(59,130,246,0.08)", I: "rgba(139,92,246,0.08)", L: "rgba(245,158,11,0.08)", G: "rgba(34,197,94,0.08)" }[party] || "var(--darkaccent)"
}
}
register(PoliticsRepresentatives)

View File

@@ -0,0 +1,163 @@
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)

View File

@@ -0,0 +1,266 @@
import "./PoliticsSidebar.js"
import "./PoliticsRepresentatives.js"
import "./PoliticsElections.js"
css(`
politics- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
`)
class Politics extends Shadow {
constructor() {
super()
this.view = "representatives"
this.levelFilter = "all"
this.jurisdiction = {
federal: "TX-21",
stateSenate: "SD-25",
stateHouse: "HD-20",
county: "Comal County",
}
this.representatives = [
// ── Federal ───────────────────────────────────────────────
{
id: 1, level: "federal",
name: "John Cornyn", initials: "JC",
title: "U.S. Senator", party: "R",
district: "Texas (Class 2)", since: 2002,
phone: "(202) 224-2934", website: "cornyn.senate.gov",
},
{
id: 2, level: "federal",
name: "Ted Cruz", initials: "TC",
title: "U.S. Senator", party: "R",
district: "Texas (Class 1)", since: 2013,
phone: "(202) 224-5922", website: "cruz.senate.gov",
},
{
id: 3, level: "federal",
name: "Chip Roy", initials: "CR",
title: "U.S. Representative", party: "R",
district: "TX-21", since: 2019,
phone: "(202) 225-4236", website: "roy.house.gov",
},
// ── State ─────────────────────────────────────────────────
{
id: 4, level: "state",
name: "Donna Campbell", initials: "DC",
title: "Texas State Senator", party: "R",
district: "Senate District 25", since: 2013,
phone: "(512) 463-0125", website: "senate.texas.gov/member.php?d=25",
},
{
id: 5, level: "state",
name: "Terry Wilson", initials: "TW",
title: "Texas State Representative", party: "R",
district: "House District 20", since: 2017,
phone: "(512) 463-0309", website: "house.texas.gov/members/member-page/?district=20",
},
{
id: 6, level: "state",
name: "Ken Paxton", initials: "KP",
title: "Texas Attorney General", party: "R",
district: "Statewide", since: 2015,
phone: "(512) 463-2100", website: "texasattorneygeneral.gov",
},
// ── Local ─────────────────────────────────────────────────
{
id: 7, level: "local",
name: "Sherman Krause", initials: "SK",
title: "Comal County Judge", party: "R",
district: "Comal County", since: 2019,
phone: "(830) 221-1100", website: "co.comal.tx.us",
},
{
id: 8, level: "local",
name: "Mark Reynolds", initials: "MR",
title: "County Sheriff", party: "R",
district: "Comal County", since: 2017,
phone: "(830) 620-3400", website: null,
},
{
id: 9, level: "local",
name: "Jane Guerrero", initials: "JG",
title: "New Braunfels Mayor", party: "I",
district: "City of New Braunfels", since: 2021,
phone: "(830) 221-4000", website: "newbraunfels.gov",
},
]
const future = (d) => new Date(Date.now() + d * 86400000)
const past = (d) => new Date(Date.now() - d * 86400000)
this.elections = [
// Upcoming
{
id: 1,
level: "federal",
title: "U.S. Senate — Texas",
type: "general",
date: future(209),
description: "Class 2 seat — John Cornyn defending",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 14,
candidates: [
{ id: 101, name: "John Cornyn", initials: "JC", party: "R", incumbent: true, votes: 312 },
{ id: 102, name: "Colin Allred", initials: "CA", party: "D", incumbent: false, votes: 187 },
]
},
{
id: 2,
level: "federal",
title: "U.S. House — TX-21",
type: "general",
date: future(209),
description: "Chip Roy vs. challenger",
userVoteId: null,
userThumbsUp: true,
thumbsUp: 31,
candidates: [
{ id: 201, name: "Chip Roy", initials: "CR", party: "R", incumbent: true, votes: 278 },
{ id: 202, name: "Marc Segers", initials: "MS", party: "D", incumbent: false, votes: 91 },
]
},
{
id: 3,
level: "state",
title: "Texas House District 20",
type: "primary",
date: future(42),
description: "Republican primary — open seat",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 8,
candidates: [
{ id: 301, name: "Terry Wilson", initials: "TW", party: "R", incumbent: true, votes: 203 },
{ id: 302, name: "Brad Fischer", initials: "BF", party: "R", incumbent: false, votes: 144 },
]
},
{
id: 4,
level: "local",
title: "Comal County Commissioner Pct. 2",
type: "general",
date: future(12),
description: "Precinct 2 seat",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 5,
candidates: [
{ id: 401, name: "Lisa Hartmann", initials: "LH", party: "R", incumbent: false, votes: 89 },
{ id: 402, name: "David Moreno", initials: "DM", party: "D", incumbent: false, votes: 53 },
]
},
{
id: 5,
level: "local",
title: "New Braunfels City Council At-Large",
type: "special",
date: future(6),
description: "Filling vacant at-large seat",
userVoteId: null,
userThumbsUp: false,
thumbsUp: 22,
candidates: [
{ id: 501, name: "Karen Ellis", initials: "KE", party: "I", incumbent: false, votes: 112 },
{ id: 502, name: "Tom Riggs", initials: "TR", party: "I", incumbent: false, votes: 74 },
{ id: 503, name: "Anita Solis", initials: "AS", party: "I", incumbent: false, votes: 41 },
]
},
// Recent/past
{
id: 6,
level: "state",
title: "Texas Senate District 25",
type: "primary",
date: past(48),
description: "Donna Campbell won unopposed",
userVoteId: 601,
userThumbsUp: true,
thumbsUp: 41,
candidates: [
{ id: 601, name: "Donna Campbell", initials: "DC", party: "R", incumbent: true, votes: 487 },
]
},
{
id: 7,
level: "local",
title: "Comal County Sheriff",
type: "general",
date: past(180),
description: "Mark Reynolds re-elected",
userVoteId: 701,
userThumbsUp: false,
thumbsUp: 19,
candidates: [
{ id: 701, name: "Mark Reynolds", initials: "MR", party: "R", incumbent: true, votes: 623 },
{ id: 702, name: "Phil Sanderson", initials: "PS", party: "D", incumbent: false, votes: 214 },
]
},
]
}
handleVote(electionId, candidateId) {
const election = this.elections.find(e => e.id === electionId)
if (!election || election.userVoteId) return
election.userVoteId = candidateId
const candidate = election.candidates.find(c => c.id === candidateId)
if (candidate) candidate.votes++
this.rerender()
}
handleThumbsUp(electionId) {
const election = this.elections.find(e => e.id === electionId)
if (!election) return
election.userThumbsUp = !election.userThumbsUp
election.thumbsUp += election.userThumbsUp ? 1 : -1
this.rerender()
}
render() {
HStack(() => {
// Sidebar
VStack(() => {
PoliticsSidebar(
this.view,
this.levelFilter,
(v) => { this.view = v; this.rerender(); },
(l) => { this.levelFilter = l; this.rerender(); },
this.jurisdiction,
this.elections
)
})
.width(230, px).minWidth(210, px)
.height(100, pct)
.borderRight("1px solid var(--divider)")
.flexShrink(0).overflow("hidden")
// Main
VStack(() => {
if (this.view === "representatives") {
PoliticsRepresentatives(this.representatives, this.levelFilter)
} else {
PoliticsElections(
this.elections,
this.levelFilter,
(electionId, candidateId) => this.handleVote(electionId, candidateId),
(electionId) => this.handleThumbsUp(electionId)
)
}
})
.flex(1).height(100, pct).overflow("hidden")
})
.height(100, pct).width(100, pct).overflow("hidden")
}
}
register(Politics)