466 lines
19 KiB
JavaScript
466 lines
19 KiB
JavaScript
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)
|