init
This commit is contained in:
581
website/desktop/website.js
Normal file
581
website/desktop/website.js
Normal 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)
|
||||
Reference in New Issue
Block a user