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

581
website/desktop/website.js Normal file
View File

@@ -0,0 +1,581 @@
import server from "/@server/server.js"
css(`
website- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
website- input::placeholder { color: var(--headertext); opacity: 0.35; }
website- textarea { font-family: 'Arial'; }
`)
class Website extends Shadow {
contact_form = []
join_form = []
activeTab = "overview" // "overview" | "contact" | "join"
searchText = ""
sortKey = "time"
sortDir = "desc"
selectedId = null // selected row id for detail drawer
// ── helpers ────────────────────────────────────────────────────────────
formatTimeCustom(time) {
if (!time) return "—"
// input: 01.28.2026-10:26:550910pm
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);
}
fmt(n) {
return Number(n).toLocaleString("en-US")
}
// ── stats ──────────────────────────────────────────────────────────────
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,
monthContact: thisMonth(this.contact_form).length,
monthJoin: thisMonth(this.join_form).length,
counties: counties.size,
}
}
get monthlyData() {
const now = new Date()
const months = []
for (let i = 5; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
months.push({ label: d.toLocaleString("en-US", { month: "short" }), year: d.getFullYear(), month: d.getMonth(), contact: 0, join: 0 })
}
const bucket = (entries, key) => entries.forEach(e => {
const d = this.parseTime(e.time)
const m = months.find(m => m.year === d.getFullYear() && m.month === d.getMonth())
if (m) m[key]++
})
bucket(this.contact_form, "contact")
bucket(this.join_form, "join")
return months
}
// ── sorted/filtered rows ───────────────────────────────────────────────
get rows() {
const source = this.activeTab === "contact" ? this.contact_form : this.join_form
let list = source.map((e, i) => ({ ...e, _id: i }))
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))
)
}
list.sort((a, b) => {
let av, bv
if (this.sortKey === "time") { av = this.parseTime(a.time); bv = this.parseTime(b.time) }
else if (this.sortKey === "first") { av = a.fname || ""; bv = b.fname || "" }
else if (this.sortKey === "last") { av = a.lname || ""; bv = b.lname || "" }
else if (this.sortKey === "email") { av = a.email || ""; bv = b.email || "" }
else if (this.sortKey === "county") { av = a.county || ""; bv = b.county || "" }
else { av = a[this.sortKey] || ""; bv = b[this.sortKey] || "" }
const cmp = typeof av === "string" ? av.localeCompare(bv) : av - bv
return this.sortDir === "asc" ? cmp : -cmp
})
return list
}
get selectedRow() {
const source = this.activeTab === "contact" ? this.contact_form : this.join_form
return this.selectedId !== null ? source[this.selectedId] : null
}
// ── render ─────────────────────────────────────────────────────────────
render() {
VStack(() => {
this.renderToolbar()
HStack(() => {
// main content
VStack(() => {
if (this.activeTab === "overview") {
this.renderOverview()
} else {
this.renderTableView()
}
})
.flex(1).minWidth(0).height(100, pct).overflow("hidden")
// detail drawer (contact tab only)
if (this.activeTab === "contact" && this.selectedRow) {
this.renderDetailDrawer(this.selectedRow)
}
})
.flex(1).minHeight(0).overflow("hidden")
})
.height(100, pct).width(100, pct).overflow("hidden")
.onAppear(() => this.getData())
}
renderToolbar() {
HStack(() => {
// Tab strip
HStack(() => {
[["overview","Overview"], ["contact","Contact Inquiries"], ["join","Join Requests"]]
.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.75, em).paddingVertical(0.35, em)
.borderRadius(0.35, em)
.background(isActive ? "var(--app)" : "transparent")
.cursor("pointer")
.onClick((done) => {
if (!done) return
this.activeTab = key
this.searchText = ""
this.selectedId = null
this.sortKey = "time"
this.sortDir = "desc"
this.rerender()
})
})
})
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(0.5, em).padding(0.2, em).gap(0.15, em)
})
.marginTop(20, px)
.paddingHorizontal(1.25, em)
.paddingTop(1.1, em)
.paddingBottom(0.85, em)
.alignItems("center")
.gap(0.75, em)
.flexShrink(0)
}
// ── overview ───────────────────────────────────────────────────────────
renderOverview() {
VStack(() => {
// Stats row
HStack(() => {
const s = this.stats
this.statCard("Contact Submissions", String(s.totalContact), "✉️")
this.statCard("Join Requests", String(s.totalJoin), "🙋")
this.statCard("This Month", String(s.monthContact + s.monthJoin), "📅")
this.statCard("Counties Reached", String(s.counties), "📍")
this.statCard("Page Visits", "—", "👁️")
this.statCard("Avg. Time on Site", "—", "⏱️")
})
.paddingHorizontal(1.25, em).paddingBottom(1, em)
.gap(0.65, em).alignItems("stretch").flexShrink(0)
VStack(() => {}).height(1, px).background("var(--divider)")
.marginHorizontal(1.25, em).flexShrink(0)
// Chart + recent columns
HStack(() => {
this.renderChart()
VStack(() => {}).width(1, px).background("var(--divider)").flexShrink(0)
// Recent submissions split
HStack(() => {
this.renderRecentColumn("Recent Contact", this.contact_form, "contact")
VStack(() => {}).width(1, px).background("var(--divider)")
this.renderRecentColumn("Recent Join Requests", this.join_form, "join")
})
.flex(1).minWidth(0).height(100, pct)
})
.flex(1).minHeight(0).gap(0).overflow("hidden")
})
.height(100, pct).width(100, pct).overflow("hidden")
.paddingTop(1, em)
}
statCard(label, value, icon) {
VStack(() => {
p(icon).margin(0).fontSize(1.05, em).lineHeight("1")
p(value)
.margin(0).marginTop(0.5, em)
.fontSize(value === "—" ? 1.3 : 1.2, em).fontWeight("700")
.color(value === "—" ? "var(--headertext)" : "var(--headertext)")
.opacity(value === "—" ? 0.25 : 1)
.lineHeight("1")
p(label)
.margin(0).marginTop(0.28, em)
.fontSize(0.68, em).color("var(--headertext)").opacity(0.42)
})
.padding(0.9, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(0.6, em).minWidth(0).flex(1)
}
renderChart() {
const months = this.monthlyData
const max = Math.max(...months.map(m => m.contact + m.join), 1)
VStack(() => {
p("Monthly Submissions")
.margin(0).marginBottom(0.6, em)
.fontSize(0.68, em).fontWeight("700").letterSpacing("0.06em")
.color("var(--headertext)").opacity(0.4)
HStack(() => {
months.forEach(m => {
const total = m.contact + m.join
const barPct = Math.max((total / max) * 100, total > 0 ? 3 : 0)
VStack(() => {
VStack(() => {
if (m.join > 0) VStack(() => {}).flex(m.join).background("#3b82f6")
if (m.contact > 0) VStack(() => {}).flex(m.contact).background("var(--quillred)")
if (total === 0) VStack(() => {}).flex(1).background("var(--divider)")
})
.height(barPct, pct).width(60, pct).borderRadius(3, px)
.overflow("hidden").alignSelf("center")
.minHeight(total === 0 ? 2 : 3, px)
p(m.label)
.margin(0).marginTop(0.4, em).fontSize(0.65, em)
.color("var(--headertext)").opacity(0.4).textAlign("center")
})
.flex(1).height(100, pct).alignItems("stretch")
.justifyContent("flex-end").boxSizing("border-box")
})
})
.flex(1).alignItems("flex-end").gap(0.3, em)
HStack(() => {
HStack(() => {
VStack(() => {}).width(8, px).height(8, px).borderRadius(50, pct).background("var(--quillred)").flexShrink(0)
p("Contact").margin(0).fontSize(0.65, em).color("var(--headertext)").opacity(0.45)
}).gap(0.3, em).alignItems("center")
HStack(() => {
VStack(() => {}).width(8, px).height(8, px).borderRadius(50, pct).background("#3b82f6").flexShrink(0)
p("Join").margin(0).fontSize(0.65, em).color("var(--headertext)").opacity(0.45)
}).gap(0.3, em).alignItems("center")
}).gap(0.85, em).marginTop(0.55, em).flexShrink(0)
})
.padding(1, em).paddingLeft(1.25, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(0.6, em).width(320, px).height(180, px)
.boxSizing("border-box").flexShrink(0).margin(1.25, em)
.marginRight(0)
}
renderRecentColumn(title, entries, tab) {
const recent = [...entries]
.sort((a, b) => this.parseTime(b.time) - this.parseTime(a.time))
.slice(0, 6)
VStack(() => {
HStack(() => {
p(title)
.margin(0).fontSize(0.7, em).fontWeight("700")
.letterSpacing("0.05em").color("var(--headertext)").opacity(0.38)
.flex(1)
if (entries.length > 0) {
p(`View all →`)
.margin(0).fontSize(0.72, em).color("var(--headertext)").opacity(0.4)
.cursor("pointer")
.onClick((done) => {
if (!done) return
this.activeTab = tab
this.searchText = ""
this.selectedId = null
this.rerender()
})
}
})
.alignItems("center").marginBottom(0.55, 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 => {
HStack(() => {
VStack(() => {
p(`${e.fname || ""} ${e.lname || ""}`.trim() || e.email || "—")
.margin(0).fontSize(0.82, 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.7, em).color("var(--headertext)").opacity(0.35)
.whiteSpace("nowrap").flexShrink(0)
})
.paddingVertical(0.55, em)
.borderBottom("1px solid var(--divider)")
.alignItems("center").gap(0.5, em)
})
}
})
.padding(1.25, em).flex(1).minWidth(0).overflow("hidden")
}
// ── table view ─────────────────────────────────────────────────────────
renderTableView() {
const isContact = this.activeTab === "contact"
const COLS = isContact
? [
{ label: "Date", key: "time", width: "150px" },
{ label: "First", key: "first", width: "130px" },
{ label: "Last", key: "last", width: "130px" },
{ label: "Email", key: "email", width: "1fr" },
{ label: "Phone", key: "phone", width: "140px" },
{ label: "County", key: "county", width: "130px" },
{ label: "Message", key: "msg", width: "90px", sortable: false },
]
: [
{ label: "Date", key: "time", width: "150px" },
{ label: "First", key: "first", width: "130px" },
{ label: "Last", key: "last", width: "130px" },
{ label: "Email", key: "email", width: "1fr" },
{ label: "Phone", key: "phone", width: "140px" },
{ label: "County", key: "county", width: "130px" },
]
const gridCols = COLS.map(c => c.width).join(" ")
const rows = this.rows
VStack(() => {
// Search bar
HStack(() => {
p("🔍").margin(0).fontSize(0.82, em).opacity(0.35).flexShrink(0)
input("", "240px")
.attr({ type: "text", placeholder: `Search ${isContact ? "contact inquiries" : "join requests"}`, value: this.searchText })
.border("none").outline("none").background("transparent")
.color("var(--headertext)").fontSize(0.85, em)
.onInput((e) => { this.searchText = e.target.value; this.selectedId = null; this.rerender() })
})
.gap(0.5, em).paddingHorizontal(0.8, em).paddingVertical(0.52, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.borderRadius(0.5, em).alignItems("center")
.marginHorizontal(1.25, em).marginBottom(0.75, em).flexShrink(0)
// Count
p(`${rows.length} result${rows.length !== 1 ? "s" : ""}`)
.margin(0).marginBottom(0.5, em)
.paddingHorizontal(1.25, em)
.fontSize(0.78, em).color("var(--headertext)").opacity(0.38)
.flexShrink(0)
// Header
HStack(() => {
COLS.forEach(col => {
const isSort = this.sortKey === col.key
HStack(() => {
p(col.label + (isSort ? (this.sortDir === "asc" ? " ↑" : " ↓") : ""))
.margin(0).fontSize(0.7, em).fontWeight("700").letterSpacing("0.04em")
.color("var(--headertext)").opacity(isSort ? 0.85 : 0.38)
.userSelect("none").whiteSpace("nowrap")
})
.alignItems("center")
.cursor(col.sortable === false ? "default" : "pointer")
.onClick((done) => {
if (!done || col.sortable === false) return
if (this.sortKey === col.key) this.sortDir = this.sortDir === "asc" ? "desc" : "asc"
else { this.sortKey = col.key; this.sortDir = "asc" }
this.rerender()
})
})
})
.attr({ style: `display: grid; grid-template-columns: ${gridCols};` })
.paddingHorizontal(1.25, em).paddingVertical(0.6, em)
.borderTop("1px solid var(--divider)").borderBottom("1px solid var(--divider)")
.flexShrink(0)
// Rows
VStack(() => {
if (rows.length === 0) {
VStack(() => {
p("No results match your search")
.margin(0).fontSize(0.9, em)
.color("var(--headertext)").opacity(0.35).textAlign("center")
}).flex(1).justifyContent("center").alignItems("center")
} else {
rows.forEach((row, i) => {
const isSelected = this.selectedId === row._id
HStack(() => {
p(this.formatTimeCustom(row.time))
.margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.6)
.whiteSpace("nowrap")
p(row.fname || "—")
.margin(0).fontSize(0.82, em).color("var(--headertext)")
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
p(row.lname || "—")
.margin(0).fontSize(0.82, em).color("var(--headertext)")
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
p(row.email || "—")
.margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.6)
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
.minWidth(0)
p(this.formatPhone(row.phone))
.margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.55)
.whiteSpace("nowrap")
p(row.county || "—")
.margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.55)
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
if (isContact) {
p(row.message ? "View →" : "—")
.margin(0).fontSize(0.78, em)
.color(row.message ? "var(--quillred)" : "var(--headertext)")
.opacity(row.message ? 1 : 0.22)
.fontWeight(row.message ? "600" : "400")
.cursor(row.message ? "pointer" : "default")
}
})
.attr({ style: `display: grid; grid-template-columns: ${gridCols}; align-items: center;` })
.paddingHorizontal(1.25, em).paddingVertical(0.6, em)
.background(isSelected ? "var(--app)" : i % 2 !== 0 ? "var(--darkaccent)" : "transparent")
.borderBottom("1px solid var(--divider)")
.cursor("pointer")
.onClick((done) => {
if (!done) return
this.selectedId = isSelected ? null : row._id
this.rerender()
})
})
}
})
.flex(1).overflowY("auto")
})
.height(100, pct).width(100, pct).overflow("hidden")
.paddingTop(1, em)
}
renderDetailDrawer(row) {
VStack(() => {
// Header
HStack(() => {
p(`${row.fname || ""} ${row.lname || ""}`.trim() || row.email || "Contact")
.margin(0).fontSize(0.92, em).fontWeight("600")
.color("var(--headertext)").flex(1).minWidth(0)
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
button("✕")
.border("none").background("transparent")
.color("var(--headertext)").opacity(0.4)
.fontSize(0.82, em).cursor("pointer").padding(0.25, em)
.borderRadius(0.3, em)
.onClick((done) => { if (!done) return; this.selectedId = null; this.rerender() })
})
.paddingHorizontal(1.25, em).paddingVertical(0.82, em)
.borderBottom("1px solid var(--divider)")
.alignItems("center").flexShrink(0)
// Fields
VStack(() => {
const fields = [
["Date", this.formatTimeCustom(row.time)],
["First", row.fname],
["Last", row.lname],
["Email", row.email],
["Phone", this.formatPhone(row.phone)],
["County", row.county],
]
fields.forEach(([label, value]) => {
VStack(() => {
p(label)
.margin(0).fontSize(0.68, em).fontWeight("700")
.letterSpacing("0.04em").color("var(--headertext)").opacity(0.38)
p(value || "—")
.margin(0).marginTop(0.2, em).fontSize(0.88, em)
.color("var(--headertext)").opacity(value ? 0.85 : 0.28)
.fontStyle(value ? "normal" : "italic")
})
.paddingVertical(0.72, em)
.borderBottom("1px solid var(--divider)")
})
if (row.message) {
VStack(() => {
p("Message")
.margin(0).fontSize(0.68, em).fontWeight("700")
.letterSpacing("0.04em").color("var(--headertext)").opacity(0.38)
p(row.message)
.margin(0).marginTop(0.35, em).fontSize(0.88, em)
.color("var(--headertext)").opacity(0.82)
.lineHeight("1.55")
.whiteSpace("pre-wrap")
})
.paddingVertical(0.72, em)
}
})
.paddingHorizontal(1.25, em)
.flex(1).overflowY("auto")
})
.width(320, px).height(100, pct)
.borderLeft("1px solid var(--divider)")
.flexShrink(0).overflow("hidden")
}
// ── data ───────────────────────────────────────────────────────────────
formatPhone(phone) {
if (!phone) return "—"
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)