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)