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

465
website/website.js Normal file
View File

@@ -0,0 +1,465 @@
import server from "/@server/server.js"
css(`
website- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
website-::-webkit-scrollbar { display: none; }
website- .stats-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
website- input::placeholder { color: var(--headertext); opacity: 0.35; }
`)
class Website extends Shadow {
contact_form = []
join_form = []
activeTab = "overview" // "overview" | "contact" | "join"
searchText = ""
searchOpen = false
selectedTx = null
// ── helpers ────────────────────────────────────────────────────────────
formatTimeCustom(time) {
if (!time) return "—"
const match = time.match(/(\d{2})\.(\d{2})\.(\d{4})-(\d{1,2}):(\d{2}):\d+(am|pm)/i)
if (!match) return time
const [, month, day, year, hour, minute, ampm] = match
return `${month}/${day}/${year.slice(2)} ${hour}:${minute}${ampm.toLowerCase()}`
}
parseTime(str) {
const match = str.match(/(\d+)\.(\d+)\.(\d+)-(\d+):(\d+):(\d+)(\d{3})(am|pm)/i);
const [, month, day, year, hours, minutes, seconds, ms, ampm] = match;
let hrs = parseInt(hours);
if (ampm.toLowerCase() === 'pm' && hrs !== 12) hrs += 12;
if (ampm.toLowerCase() === 'am' && hrs === 12) hrs = 0;
return new Date(year, month - 1, day, hrs, minutes, seconds, ms);
}
// ── computed ───────────────────────────────────────────────────────────
get stats() {
const now = new Date()
const thisMonth = (entries) => entries.filter(e => {
const d = this.parseTime(e.time)
return d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear()
})
const counties = new Set([
...this.contact_form.map(e => e.county).filter(Boolean),
...this.join_form.map(e => e.county).filter(Boolean),
])
return {
totalContact: this.contact_form.length,
totalJoin: this.join_form.length,
thisMonth: thisMonth(this.contact_form).length + thisMonth(this.join_form).length,
counties: counties.size,
}
}
get currentList() {
const source = this.activeTab === "contact" ? this.contact_form : this.join_form
let list = source.map((e, i) => ({ ...e, _id: i }))
.sort((a, b) => this.parseTime(b.time) - this.parseTime(a.time))
if (this.searchText) {
const q = this.searchText.toLowerCase()
list = list.filter(e =>
[e.fname, e.lname, e.email, e.phone, e.county, e.message]
.some(v => v?.toLowerCase().includes(q))
)
}
return list
}
// ── render ─────────────────────────────────────────────────────────────
render() {
ZStack(() => {
VStack(() => {
this.renderHeader()
this.renderTabs()
if (this.activeTab === "overview") {
this.renderOverview()
} else {
this.renderList()
}
})
.height(100, pct).width(100, pct)
.boxSizing("border-box").overflow("hidden")
if (this.selectedTx) {
this.renderDetail(this.selectedTx)
}
})
.display("block")
.height(100, pct).width(100, pct).overflow("hidden")
.maxWidth(100, vw)
.onAppear(() => this.getData())
}
renderHeader() {
if (this.searchOpen && this.activeTab !== "overview") {
HStack(() => {
input(this.searchText, "100%")
.paddingHorizontal(0.75, em).paddingVertical(0.5, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(0.65, em).fontSize(0.9, em)
.color("var(--headertext)").outline("none")
.attr({ autofocus: "true" })
.onInput(v => { this.searchText = v; this.rerender() })
})
.paddingHorizontal(1.1, em).paddingBottom(0.65, em)
.width(100, pct).boxSizing("border-box").flexShrink(0)
}
}
renderTabs() {
HStack(() => {
[["overview","Overview"],["contact","Contact"],["join","Join"]].forEach(([key, label]) => {
const isActive = this.activeTab === key
p(label)
.margin(0).fontSize(0.8, em)
.fontWeight(isActive ? "600" : "400")
.color("var(--headertext)").opacity(isActive ? 1 : 0.5)
.paddingHorizontal(0.9, em).paddingVertical(0.45, em)
.borderRadius(100, px)
.background(isActive ? "var(--darkaccent)" : "transparent")
.border(`1px solid ${isActive ? "var(--divider)" : "transparent"}`)
.onTouch((start) => {
if (!start) {
this.activeTab = key
this.searchText = ""
this.searchOpen = false
this.selectedTx = null
this.rerender()
}
})
})
})
.paddingHorizontal(1.1, em).paddingBottom(0.65, em)
.gap(0.4, em).width(100, pct).boxSizing("border-box").flexShrink(0)
}
// ── overview ───────────────────────────────────────────────────────────
renderOverview() {
VStack(() => {
this.renderStats()
// Divider
div().height(1, px).background("var(--divider)")
.marginHorizontal(1.1, em).marginBottom(0.85, em).flexShrink(0)
// Recent Contact
this.renderRecentSection("Recent Contact", this.contact_form, "contact")
// Divider
div().height(1, px).background("var(--divider)")
.marginHorizontal(1.1, em).marginVertical(0.85, em).flexShrink(0)
// Recent Join
this.renderRecentSection("Recent Join Requests", this.join_form, "join")
})
.flex(1).minHeight(0).overflowY("auto")
.paddingBottom(1.5, em)
}
renderStats() {
const s = this.stats
const cards = [
{ label: "Contact", value: String(s.totalContact), icon: "✉️" },
{ label: "Join", value: String(s.totalJoin), icon: "🙋" },
{ label: "This Month", value: String(s.thisMonth), icon: "📅" },
{ label: "Counties", value: String(s.counties), icon: "📍" },
{ label: "Page Visits", value: "—", icon: "👁️" },
{ label: "Avg. Time", value: "—", icon: "⏱️" },
]
HStack(() => {
div().width(1.1, em).flexShrink(0)
cards.forEach(c => {
VStack(() => {
p(c.icon).margin(0).fontSize(1.1, em).lineHeight("1")
p(c.value)
.margin(0).marginTop(0.38, em)
.fontSize(1.0, em).fontWeight("700")
.color("var(--headertext)").lineHeight("1")
.opacity(c.value === "—" ? 0.22 : 1)
.whiteSpace("nowrap")
p(c.label)
.margin(0).marginTop(0.25, em)
.fontSize(0.62, em).color("var(--headertext)").opacity(0.42)
.whiteSpace("nowrap")
})
.padding(0.85, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(0.75, em).alignItems("center").flexShrink(0)
})
div().width(1.1, em).flexShrink(0)
})
.attr({ class: "stats-scroll" })
.paddingBottom(0.85, em).gap(0.6, em)
.width(100, pct).boxSizing("border-box").flexShrink(0)
}
renderRecentSection(title, entries, tab) {
const recent = [...entries]
.sort((a, b) => this.parseTime(b.time) - this.parseTime(a.time))
.slice(0, 4)
VStack(() => {
HStack(() => {
p(title)
.margin(0).fontSize(0.72, em).fontWeight("700")
.letterSpacing("0.04em").color("var(--headertext)").opacity(0.4)
.flex(1)
p("See all →")
.margin(0).fontSize(0.72, em).color("var(--headertext)").opacity(0.38)
.onTouch((start) => {
if (!start) {
this.activeTab = tab
this.searchText = ""
this.searchOpen = false
this.rerender()
}
})
})
.alignItems("center").marginBottom(0.5, em)
if (recent.length === 0) {
p("No submissions yet")
.margin(0).fontSize(0.82, em)
.color("var(--headertext)").opacity(0.28).fontStyle("italic")
} else {
recent.forEach((e, i) => {
HStack(() => {
VStack(() => {
p(`${e.fname || ""} ${e.lname || ""}`.trim() || e.email || "—")
.margin(0).fontSize(0.88, em).fontWeight("600")
.color("var(--headertext)")
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
p(e.county || e.email || "—")
.margin(0).marginTop(0.12, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.42)
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
}).flex(1).minWidth(0)
p(this.formatTimeCustom(e.time))
.margin(0).fontSize(0.68, em)
.color("var(--headertext)").opacity(0.32)
.whiteSpace("nowrap").flexShrink(0)
})
.paddingVertical(0.65, em)
.borderBottom(i < recent.length - 1 ? "1px solid var(--divider)" : "none")
.alignItems("center").gap(0.5, em)
.onTouch((start, event) => {
if(start) {
this._touchStartY = event.touches[0].clientY;
this._touchStartX = event.touches[0].clientX;
} else {
const dy = Math.abs(event.changedTouches[0].clientY - this._touchStartY);
const dx = Math.abs(event.changedTouches[0].clientX - this._touchStartX);
if (dy > 10 || dx > 10) return; // was a scroll, ignore
this.selectedTx = { ...e, _tab: tab };
this.rerender();
}
})
})
}
})
.paddingHorizontal(1.1, em)
.flexShrink(0)
}
// ── list ───────────────────────────────────────────────────────────────
renderList() {
const rows = this.currentList
const isContact = this.activeTab === "contact"
VStack(() => {
if (rows.length === 0) {
VStack(() => {
p(this.searchText ? "No results match your search" : "No submissions yet")
.margin(0).fontSize(0.9, em)
.color("var(--headertext)").opacity(0.35).textAlign("center")
}).flex(1).justifyContent("center").alignItems("center")
} else {
rows.forEach(row => this.renderCard(row, isContact))
}
})
.flex(1).minHeight(0).overflowY("auto")
.paddingHorizontal(1.1, em).paddingBottom(1.5, em)
.gap(0.55, em).width(100, pct).boxSizing("border-box")
}
renderCard(row, isContact) {
HStack(() => {
VStack(() => {
p(`${row.fname || ""} ${row.lname || ""}`.trim() || row.email || "—")
.margin(0).fontSize(0.9, em).fontWeight("600")
.color("var(--headertext)")
.whiteSpace("nowrap").overflow("hidden").textOverflow("ellipsis")
p(row.county || "—")
.margin(0).marginTop(0.28, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.45)
.whiteSpace("nowrap")
})
.flex(1).minWidth(0).alignItems("flex-start")
VStack(() => {
p(this.formatTimeCustom(row.time))
.margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.35)
.whiteSpace("nowrap").textAlign("right")
if (isContact && row.message) {
p("Msg →")
.margin(0).marginTop(0.28, em)
.fontSize(0.68, em).fontWeight("600")
.color("var(--quillred)")
}
})
.alignItems("flex-end").flexShrink(0)
})
.padding(0.9, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(0.75, em).alignItems("center")
.width(100, pct).boxSizing("border-box")
.onTouch((start) => {
if (!start) {
this.selectedTx = { ...row, _tab: this.activeTab }
this.rerender()
}
})
}
// ── detail sheet ────────────────────────────────────────────────────────
renderDetail(entry) {
const name = `${entry.fname || ""} ${entry.lname || ""}`.trim() || entry.email || "Submission"
VStack(() => {
// Handle bar
HStack(() => {
div().width(2.5, em).height(4, px).borderRadius(100, px).background("var(--divider)")
})
.justifyContent("center").paddingTop(0.75, em).paddingBottom(0.5, em).flexShrink(0)
// Header
HStack(() => {
VStack(() => {
p(name)
.margin(0).fontSize(1.1, em).fontWeight("700")
.color("var(--headertext)")
p(entry._tab === "contact" ? "Contact Inquiry" : "Join Request")
.margin(0).marginTop(0.18, em)
.fontSize(0.72, em).fontWeight("600")
.color(entry._tab === "contact" ? "var(--quillred)" : "#3b82f6")
})
.flex(1).minWidth(0)
div("✕")
.fontSize(1, em).padding(0.4, em).borderRadius(50, pct)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.color("var(--headertext)").opacity(0.6).flexShrink(0)
.onTouch((start) => {
if (!start) { this.selectedTx = null; this.rerender() }
})
})
.paddingHorizontal(1.3, em).paddingBottom(1, em)
.alignItems("center").gap(0.75, em).flexShrink(0)
div().height(1, px).background("var(--divider)").marginHorizontal(1.3, em).flexShrink(0)
// Detail rows
VStack(() => {
const fields = [
["Date", this.formatTimeCustom(entry.time)],
["First", entry.fname],
["Last", entry.lname],
["Email", entry.email],
["Phone", this.formatPhone(entry.phone)],
["County", entry.county],
]
fields.forEach(([label, value]) => this.detailRow(label, value))
if (entry.message) {
VStack(() => {
p("Message")
.margin(0).fontSize(0.72, em).fontWeight("700")
.letterSpacing("0.04em").color("var(--headertext)").opacity(0.38)
p(entry.message)
.margin(0).marginTop(0.4, em).fontSize(0.9, em)
.color("var(--headertext)").opacity(0.82)
.lineHeight("1.6").whiteSpace("pre-wrap")
})
.paddingVertical(0.85, em)
}
})
.paddingHorizontal(1.3, em).paddingTop(0.5, em)
.flex(1).overflowY("auto")
})
.position("absolute")
.bottom(0, px).left(0, px).right(0, px)
.background("var(--main)")
.borderTop("1px solid var(--divider)")
.borderRadius("1.2em 1.2em 0 0")
.boxShadow("0 -4px 24px rgba(0,0,0,0.12)")
.zIndex(10).width(100, pct).boxSizing("border-box")
.maxHeight(88, vh).overflowY("auto")
}
detailRow(label, value) {
HStack(() => {
p(label)
.margin(0).fontSize(0.8, em)
.color("var(--headertext)").opacity(0.42)
.width(72, px).flexShrink(0)
p(value || "—")
.margin(0).fontSize(0.88, em).fontWeight("500")
.color("var(--headertext)").opacity(value ? 0.88 : 0.28)
.fontStyle(value ? "normal" : "italic")
.flex(1).minWidth(0)
.overflow("hidden").textOverflow("ellipsis").whiteSpace("nowrap")
})
.paddingVertical(0.82, em)
.borderBottom("1px solid var(--divider)")
.alignItems("center")
}
formatPhone(phone) {
if (!phone) return null
const d = phone.replace(/\D/g, "")
if (d.length === 10) return `(${d.slice(0,3)}) ${d.slice(3,6)}-${d.slice(6)}`
return phone
}
async getData() {
const data = await server.getSiteInfo(global.currentNetwork.abbreviation)
if (
data.contact_form.length !== this.contact_form.length ||
data.join_form.length !== this.join_form.length
) {
this.contact_form = data.contact_form || []
this.join_form = data.join_form || []
this.rerender()
}
}
}
register(Website)