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

216
settings/settings.js Normal file
View File

@@ -0,0 +1,216 @@
import server from "/@server/server.js"
import env from "/_/code/env.js"
import "./SettingsRolesSection.js"
import "./SettingsIntegrationsSection.js"
import "/_/code/components/LoadingCircle.js"
css(`
settings- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
settings- input::placeholder {
color: var(--headertext);
opacity: 0.35;
}
`)
class Settings extends Shadow {
roles = []
allApps = []
roleApps = {}
stripeDetails = null
loaded = false
// ── URL-derived routing ───────────────────────────────────────────
get basePath() {
return window.location.pathname
.replace(/\/roles(\/[^/]*)?$/, '')
.replace(/\/integrations$/, '')
.replace(/\/$/, '') || '/'
}
get section() {
const path = window.location.pathname
if (/\/roles\/[^/]+/.test(path)) return 'role-detail'
if (/\/roles$/.test(path)) return 'roles'
if (/\/integrations$/.test(path)) return 'integrations'
return 'home'
}
get activeRoleId() {
const match = window.location.pathname.match(/\/roles\/([^/]+)$/)
return match ? match[1] : null
}
// ── Render ────────────────────────────────────────────────────────
render() {
const loading = !this.loaded || global.appRefreshing
VStack(() => {
if (this.section === 'roles' || this.section === 'role-detail') {
SettingsRolesSection(
this.roles,
this.allApps,
this.section,
this.activeRoleId,
this.roleApps,
this.basePath,
{
onDeleteRole: (roleId) => this.deleteRole(roleId),
onCreateRole: (name) => this.createRole(name),
onToggleApp: (roleId, appId, add) => this.toggleRoleApp(roleId, appId, add),
},
loading
)
} else if (this.section === 'integrations') {
SettingsIntegrationsSection(
this.stripeDetails,
this.basePath,
{ onConnectStripe: () => this.handleConnectStripe() },
loading
)
} else {
if (loading) LoadingCircle()
else this.renderHome()
}
})
.height(100, pct)
.width(100, pct)
.boxSizing("border-box")
.overflow("hidden")
.onNavigate(() => {
this.rerender()
})
.onAppear(async () => {
if (this.loaded) return
await this.loadData()
})
}
// ── Home ──────────────────────────────────────────────────────────
renderHome() {
VStack(() => {
VStack(() => {}).height(1, px).background("var(--divider)").flexShrink(0)
VStack(() => {
this.menuItem("🎭", "Roles & Apps", `${this.roles.length} roles`, "roles")
VStack(() => {})
.height(1, px)
.background("var(--divider)")
.marginLeft(3.5, em)
this.menuItem("💳", "Stripe", this.stripeDetails?.email ? "Connected" : "Not connected", "integrations")
})
.marginTop(0.5, em)
.paddingHorizontal(1, em)
.flexShrink(0)
})
.height(100, pct)
.overflowY("auto")
}
menuItem(icon, label, subtitle, section) {
HStack(() => {
VStack(() => {
p(icon).margin(0).fontSize(1.1, em).lineHeight("1").color("var(--headertext)")
})
.width(2.2, em).height(2.2, em).borderRadius(0.45, em)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.justifyContent("center").alignItems("center").flexShrink(0)
VStack(() => {
p(label).margin(0).fontSize(0.92, em).fontWeight("600").color("var(--headertext)")
p(subtitle).margin(0).marginTop(0.15, em).fontSize(0.72, em).color("var(--headertext)").opacity(0.42)
})
.flex(1).gap(0)
p("").margin(0).fontSize(1.1, em).color("var(--headertext)").opacity(0.3)
})
.gap(0.75, em).alignItems("center")
.paddingVertical(0.85, em)
.cursor("pointer")
.onTap(() => window.navigateTo(`${this.basePath}/${section}`))
}
// ── Server actions ────────────────────────────────────────────────
async loadData() {
const [roles, apps, stripe] = await Promise.all([
server.getRoles(global.currentNetwork.id),
server.getAllApps(),
server.getStripeProfile(global.currentNetwork.id)
])
this.roles = Array.isArray(roles) ? roles : []
this.allApps = Array.isArray(apps) ? apps : []
this.stripeDetails = stripe
await Promise.all(this.roles.map(async role => {
const roleApps = await server.getRoleApps(role.id)
this.roleApps[role.id] = new Set(Array.isArray(roleApps) ? roleApps.map(a => a.id) : [])
}))
this.loaded = true
this.rerender()
}
async createRole(name) {
const result = await server.createRole(name, global.currentNetwork.id)
if (!result?.error && result?.role) {
this.roles.push(result.role)
this.roleApps[result.role.id] = new Set()
this.rerender()
}
}
async deleteRole(roleId) {
await server.deleteRole(roleId, global.currentNetwork.id)
this.roles = this.roles.filter(r => r.id !== roleId)
delete this.roleApps[roleId]
window.navigateTo(`${this.basePath}/roles`)
}
async toggleRoleApp(roleId, appId, add) {
if (add) {
await server.addRoleApp(roleId, appId)
if (!this.roleApps[roleId]) this.roleApps[roleId] = new Set()
this.roleApps[roleId].add(appId)
} else {
await server.removeRoleApp(roleId, appId)
this.roleApps[roleId]?.delete(appId)
}
if (roleId == global.currentNetwork.role?.id) {
const appName = this.allApps.find(a => a.id === appId)?.name
if (appName) {
if (add) {
global.currentNetwork.apps.push(appName)
} else {
global.currentNetwork.apps = global.currentNetwork.apps.filter(a => a !== appName)
}
document.querySelector("appmenu-")?.rerender()
}
}
this.rerender()
}
handleConnectStripe() {
const state = btoa(JSON.stringify({ returnTo: window.location.href, networkId: global.currentNetwork.id }))
const params = new URLSearchParams({
response_type: "code",
client_id: env.client_id,
scope: "read_write",
redirect_uri: `${env.baseURL}/stripe/onboardingcomplete`,
state,
})
window.location.href = `https://connect.stripe.com/oauth/authorize?${params}`
}
}
register(Settings)