306 lines
11 KiB
JavaScript
306 lines
11 KiB
JavaScript
import donationServer from "/donations/@server/donations.js"
|
|
import "/_/code/components/LoadingCircle.js"
|
|
|
|
css(`
|
|
donations- {
|
|
font-family: 'Arial';
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
donations-::-webkit-scrollbar { display: none; }
|
|
donations- input::placeholder {
|
|
color: var(--headertext);
|
|
opacity: 0.35;
|
|
}
|
|
`)
|
|
|
|
class Donations extends Shadow {
|
|
donations = []
|
|
subscriptions = []
|
|
typeFilter = "all"
|
|
searchText = ""
|
|
searchOpen = false
|
|
|
|
get allTransactions() {
|
|
const d = this.donations.map(don => ({
|
|
id: `d-${don.id}`,
|
|
created: don.created,
|
|
name: don.name || "—",
|
|
email: don.email || "—",
|
|
amount: Number(don.amount),
|
|
type: "donation",
|
|
product: "Donation",
|
|
status: "complete"
|
|
}))
|
|
const s = this.subscriptions.map(sub => ({
|
|
id: `s-${sub.id}`,
|
|
created: sub.created,
|
|
name: [sub.first_name, sub.last_name].filter(Boolean).join(" ") || sub.email || "—",
|
|
email: sub.email || "—",
|
|
amount: Number(sub.plan_price),
|
|
type: "subscription",
|
|
product: sub.plan_name || "Subscription",
|
|
status: sub.active ? "active" : "inactive"
|
|
}))
|
|
return [...d, ...s].sort((a, b) => new Date(b.created) - new Date(a.created))
|
|
}
|
|
|
|
get transactions() {
|
|
let all = this.allTransactions
|
|
if (this.typeFilter === "donations") all = all.filter(t => t.type === "donation")
|
|
else if (this.typeFilter === "subscriptions") all = all.filter(t => t.type === "subscription")
|
|
if (this.searchText) {
|
|
const q = this.searchText.toLowerCase()
|
|
all = all.filter(t =>
|
|
t.name.toLowerCase().includes(q) ||
|
|
t.email.toLowerCase().includes(q) ||
|
|
t.product.toLowerCase().includes(q)
|
|
)
|
|
}
|
|
return all
|
|
}
|
|
|
|
get stats() {
|
|
const all = this.allTransactions
|
|
const totalRevenue = all.reduce((s, t) => s + t.amount, 0)
|
|
const now = new Date()
|
|
const monthRevenue = all
|
|
.filter(t => {
|
|
const d = new Date(t.created)
|
|
return d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear()
|
|
})
|
|
.reduce((s, t) => s + t.amount, 0)
|
|
const activeSubs = this.subscriptions.filter(s => s.active).length
|
|
const mrr = this.subscriptions.filter(s => s.active).reduce((s, sub) => s + Number(sub.plan_price), 0)
|
|
const donorCount = new Set(this.donations.filter(d => d.email).map(d => d.email)).size
|
|
return { totalRevenue, monthRevenue, activeSubs, mrr, donorCount }
|
|
}
|
|
|
|
render() {
|
|
VStack(() => {
|
|
this.renderHeader()
|
|
this.renderStats()
|
|
this.renderFilters()
|
|
this.renderList()
|
|
})
|
|
.height(100, pct)
|
|
.width(100, pct)
|
|
.maxWidth(100, vw)
|
|
.boxSizing("border-box")
|
|
.overflow("hidden")
|
|
.onAppear(async () => {
|
|
const data = await donationServer.getMoneyData(global.currentNetwork.id)
|
|
if (
|
|
this.donations.length !== (data.purchases || []).length ||
|
|
this.subscriptions.length !== (data.subscriptions || []).length
|
|
) {
|
|
this.donations = data.purchases || []
|
|
this.subscriptions = data.subscriptions || []
|
|
this.rerender()
|
|
}
|
|
})
|
|
}
|
|
|
|
renderHeader() {
|
|
|
|
if (this.searchOpen) {
|
|
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)
|
|
.flexShrink(0)
|
|
}
|
|
}
|
|
|
|
renderStats() {
|
|
const s = this.stats
|
|
const cards = [
|
|
{ label: "Total", value: this.fmt(s.totalRevenue), icon: "📊" },
|
|
{ label: "This Month", value: this.fmt(s.monthRevenue), icon: "📅" },
|
|
{ label: "MRR", value: this.fmt(s.mrr), icon: "🔄" },
|
|
{ label: "Donors", value: String(s.donorCount), icon: "👥" },
|
|
{ label: "Active Subs", value: String(s.activeSubs), icon: "💰" },
|
|
]
|
|
|
|
HStack(() => {
|
|
cards.forEach(c => {
|
|
VStack(() => {
|
|
p(c.icon).margin(0).fontSize(1, em).lineHeight("1")
|
|
p(c.value)
|
|
.margin(0).marginTop(0.35, em)
|
|
.fontSize(1.05, em).fontWeight("700")
|
|
.color("var(--headertext)").lineHeight("1")
|
|
p(c.label)
|
|
.margin(0).marginTop(0.22, em)
|
|
.fontSize(0.62, em)
|
|
.color("var(--headertext)").opacity(0.42)
|
|
})
|
|
.padding(0.8, em)
|
|
.background("var(--darkaccent)")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(0.75, em)
|
|
.alignItems("center")
|
|
.minWidth(80, px)
|
|
.flexShrink(0)
|
|
})
|
|
})
|
|
.paddingHorizontal(1.1, em)
|
|
.paddingBottom(0.75, em)
|
|
.gap(0.6, em)
|
|
.overflowX("auto")
|
|
.flexShrink(0)
|
|
}
|
|
|
|
renderFilters() {
|
|
HStack(() => {
|
|
this.filterTab("All", "all")
|
|
this.filterTab("Donations", "donations")
|
|
this.filterTab("Subscriptions", "subscriptions")
|
|
})
|
|
.paddingHorizontal(1.1, em)
|
|
.paddingBottom(0.65, em)
|
|
.gap(0.4, em)
|
|
.flexShrink(0)
|
|
}
|
|
|
|
filterTab(label, value) {
|
|
const isActive = this.typeFilter === value
|
|
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"}`)
|
|
.cursor("pointer")
|
|
.onTouch((start) => {
|
|
if (!start) {
|
|
this.typeFilter = value
|
|
this.rerender()
|
|
}
|
|
})
|
|
}
|
|
|
|
renderList() {
|
|
const txs = this.transactions
|
|
|
|
VStack(() => {
|
|
if (this.donations.length === 0 && this.subscriptions.length === 0) {
|
|
LoadingCircle()
|
|
} else if (txs.length === 0) {
|
|
VStack(() => {
|
|
p("No transactions match your filters")
|
|
.margin(0).fontSize(0.9, em)
|
|
.color("var(--headertext)").opacity(0.38)
|
|
.textAlign("center")
|
|
}).flex(1).justifyContent("center").alignItems("center")
|
|
} else {
|
|
txs.forEach(tx => this.renderCard(tx))
|
|
}
|
|
})
|
|
.flex(1)
|
|
.minHeight(0)
|
|
.overflowY("auto")
|
|
.paddingHorizontal(1.1, em)
|
|
.paddingBottom(1.5, em)
|
|
.gap(0.6, em)
|
|
}
|
|
|
|
renderCard(tx) {
|
|
VStack(() => {
|
|
HStack(() => {
|
|
// Name + type badge
|
|
VStack(() => {
|
|
p(tx.name)
|
|
.margin(0)
|
|
.fontSize(0.92, em)
|
|
.fontWeight("600")
|
|
.color("var(--headertext)")
|
|
.overflow("hidden")
|
|
.whiteSpace("nowrap")
|
|
.textOverflow("ellipsis")
|
|
.maxWidth(180, px)
|
|
|
|
p(tx.email)
|
|
.margin(0).marginTop(0.18, em)
|
|
.fontSize(0.72, em)
|
|
.color("var(--headertext)").opacity(0.45)
|
|
.overflow("hidden")
|
|
.whiteSpace("nowrap")
|
|
.textOverflow("ellipsis")
|
|
.maxWidth(180, px)
|
|
})
|
|
.flex(1).minWidth(0).alignItems("flex-start")
|
|
|
|
// Amount + status
|
|
VStack(() => {
|
|
p(this.fmt(tx.amount))
|
|
.margin(0)
|
|
.fontSize(1, em).fontWeight("700")
|
|
.color("var(--headertext)")
|
|
.textAlign("right")
|
|
|
|
p(tx.status === "active" ? "Active" : tx.status === "complete" ? "Complete" : "Inactive")
|
|
.margin(0).marginTop(0.2, em)
|
|
.fontSize(0.68, em).fontWeight("500")
|
|
.color(tx.status === "inactive" ? "var(--headertext)" : "#10b981")
|
|
.opacity(tx.status === "inactive" ? 0.35 : 1)
|
|
.textAlign("right")
|
|
})
|
|
.alignItems("flex-end").flexShrink(0)
|
|
})
|
|
.alignItems("flex-start")
|
|
|
|
HStack(() => {
|
|
// Type badge
|
|
p(tx.type === "donation" ? "Donation" : tx.product)
|
|
.margin(0)
|
|
.fontSize(0.68, em).fontWeight("600")
|
|
.color(tx.type === "donation" ? "var(--quillred)" : "#3b82f6")
|
|
.background(tx.type === "donation" ? "rgba(159,28,41,0.12)" : "rgba(59,130,246,0.12)")
|
|
.paddingHorizontal(0.55, em).paddingVertical(0.2, em)
|
|
.borderRadius(100, px)
|
|
.whiteSpace("nowrap")
|
|
|
|
p(this.formatDate(tx.created))
|
|
.margin(0)
|
|
.fontSize(0.72, em)
|
|
.color("var(--headertext)").opacity(0.38)
|
|
.flex(1).textAlign("right")
|
|
})
|
|
.marginTop(0.65, em)
|
|
.alignItems("center")
|
|
.gap(0.5, em)
|
|
})
|
|
.padding(0.95, em)
|
|
.background("var(--darkaccent)")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(0.75, em)
|
|
}
|
|
|
|
formatDate(time) {
|
|
return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric" }).format(new Date(time))
|
|
}
|
|
|
|
fmt(amount) {
|
|
return "$" + Number(amount).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
}
|
|
}
|
|
|
|
register(Donations)
|