Files
apps/settings/settings.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

217 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)