Add roles

This commit is contained in:
Jon Staab
2026-06-22 13:36:57 -07:00
parent fd4e7a9f2d
commit 7ec5a28d1f
42 changed files with 1169 additions and 384 deletions
+1 -1
View File
@@ -248,7 +248,7 @@
</div>
</div>
</PageBar>
<PageContent class="flex flex-col gap-2 p-2">
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 p-4">
<div class="flex flex-col gap-2" bind:this={element}>
{#each PLATFORM_RELAYS as url (url)}
<Button
@@ -0,0 +1,163 @@
<script lang="ts">
import {tick} from "svelte"
import {derived} from "svelte/store"
import {page} from "$app/stores"
import {displayRelayUrl} from "@welshman/util"
import {displayProfileByPubkey, deriveRelay} from "@welshman/app"
import Pen from "@assets/icons/pen.svg?dataurl"
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
import BillList from "@assets/icons/bill-list.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import ContentSearch from "@lib/components/ContentSearch.svelte"
import RelayName from "@app/components/RelayName.svelte"
import SpaceBar from "@app/components/SpaceBar.svelte"
import SpaceDetails from "@app/components/SpaceDetails.svelte"
import SpaceMember from "@app/components/SpaceMember.svelte"
import SpaceInvite from "@app/components/SpaceInvite.svelte"
import SpaceRoles from "@app/components/SpaceRoles.svelte"
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
import {
deriveSpaceRoles,
deriveSpaceMembers,
deriveSpaceMemberRoles,
deriveUserIsSpaceAdmin,
type SpaceRole,
} from "@app/members"
import {decodeRelay} from "@app/relays"
import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay!)
const relay = deriveRelay(url)
const roles = deriveSpaceRoles(url)
const owner = $derived($relay?.pubkey)
const members = deriveSpaceMembers(url)
const memberRoles = deriveSpaceMemberRoles(url)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
// Each member with their resolved roles (sorted by order).
const memberList = derived([members, memberRoles, roles], ([$members, $memberRoles, $roles]) => {
const byId = new Map($roles.map(role => [role.id, role]))
return $members.map(pubkey => ({
pubkey,
roleList: ($memberRoles.get(pubkey) ?? [])
.map(id => byId.get(id))
.filter((role): role is SpaceRole => Boolean(role)),
}))
})
let menuOpen = $state(false)
const inviteMembers = () => {
menuOpen = false
pushModal(SpaceInvite, {url})
}
const manageRoles = () => {
menuOpen = false
pushModal(SpaceRoles, {url})
}
const bannedMembers = () => {
menuOpen = false
pushModal(SpaceMembersBanned, {url})
}
// In-place search: filter member cards by member info, and keep role sections
// whose name matches the term even when their members don't.
let term = $state("")
const matchesTerm = (pubkey: string, t: string) =>
displayProfileByPubkey(pubkey).toLowerCase().includes(t) || pubkey.toLowerCase().includes(t)
// In-place search: match by member info or by the name of any role they hold.
const visibleMembers = $derived.by(() => {
const t = term.trim().toLowerCase()
if (!t) return $memberList
return $memberList.filter(
({pubkey, roleList}) =>
matchesTerm(pubkey, t) || roleList.some(role => role.label.toLowerCase().includes(t)),
)
})
const clearSearch = () => {
term = ""
}
</script>
<PageContent class="flex flex-col gap-4 p-4">
<SpaceDetails {url} />
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex justify-between">
<h3 class="flex items-center gap-2 text-lg font-semibold">
<Icon icon={UsersGroup} />
Members
</h3>
<div class="flex gap-2">
<button class="btn btn-primary btn-sm" onclick={inviteMembers}>
<Icon icon={AddCircle} />
Invite people
</button>
{#if $userIsAdmin}
<div class="relative">
<button
class="btn btn-neutral btn-sm btn-square"
aria-label="More options"
onclick={() => (menuOpen = !menuOpen)}>
<Icon size={4} icon={MenuDots} />
</button>
{#if menuOpen}
<Popover hideOnClick onClose={() => (menuOpen = false)}>
<ul
transition:fly
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
<li>
<Button onclick={manageRoles}>
<Icon icon={UsersGroup} />
Manage Roles
</Button>
</li>
<li>
<Button onclick={bannedMembers}>
<Icon icon={MinusCircle} />
Banned Members
</Button>
</li>
</ul>
</Popover>
{/if}
</div>
{/if}
</div>
</div>
<label class="input input-sm input-bordered flex w-full items-center gap-2">
<Icon size={4} icon={Magnifier} />
<input
bind:value={term}
class="min-w-0 grow"
type="text"
placeholder="Search people or roles..." />
</label>
{#if visibleMembers.length === 0}
<p class="flex flex-col items-center py-20 text-center">No members found.</p>
{:else}
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2 xl:grid-cols-3">
{#each visibleMembers as { pubkey, roleList } (pubkey)}
<SpaceMember {url} {pubkey} roles={roleList} />
{/each}
</div>
{/if}
</div>
</PageContent>
@@ -126,9 +126,9 @@
{/snippet}
</SpaceBar>
<PageContent bind:element class="flex flex-col gap-2 p-2">
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:px-4">
{#each items as { event, dateDisplay, isFirstFutureEvent }, i (event.id)}
<div class={"calendar-event-" + event.id}>
<div class="flex flex-col gap-2 calendar-event-{event.id}">
{#if isFirstFutureEvent}
<div class="flex items-center gap-2 p-2">
<div class="h-px grow bg-primary"></div>
@@ -65,7 +65,7 @@
{/snippet}
</SpaceBar>
<PageContent class="flex flex-col gap-3 p-2">
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#if $event}
<div class="card2 bg-alt col-3 z-feature">
<div class="flex items-start gap-4">
@@ -77,7 +77,7 @@
{/snippet}
</SpaceBar>
<PageContent bind:element class="flex flex-col gap-2 p-2">
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#each items as event (event.id)}
<div in:fly>
<ClassifiedItem {url} event={$state.snapshot(event)} />
@@ -62,7 +62,7 @@
{/snippet}
</SpaceBar>
<PageContent class="flex flex-col gap-2 p-2">
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#if $event}
<div class="flex flex-col gap-3">
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
+1 -1
View File
@@ -77,7 +77,7 @@
{/snippet}
</SpaceBar>
<PageContent bind:element class="flex flex-col gap-2 p-2">
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#each items as event (event.id)}
<div in:fly>
<GoalItem {url} event={$state.snapshot(event)} />
@@ -63,7 +63,7 @@
{/snippet}
</SpaceBar>
<PageContent class="flex flex-col gap-2 p-2">
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#if $event}
<div class="flex flex-col gap-3">
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
+1 -1
View File
@@ -77,7 +77,7 @@
{/snippet}
</SpaceBar>
<PageContent bind:element class="flex flex-col gap-2 p-2">
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#each items as event (event.id)}
<div in:fly>
<PollItem {url} event={$state.snapshot(event)} />
@@ -64,7 +64,7 @@
{/snippet}
</SpaceBar>
<PageContent class="flex flex-col gap-2 p-2">
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#if $event}
<div class="flex flex-col gap-3">
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
@@ -294,7 +294,7 @@
{/snippet}
</SpaceBar>
<PageContent class="flex flex-col gap-2 p-2" bind:element>
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4" bind:element>
{#if $recentActivity.length === 0}
<p class="flex flex-col items-center py-20 text-center">No recent activity found!</p>
{:else}
@@ -83,7 +83,7 @@
{/snippet}
</SpaceBar>
<PageContent bind:element class="flex flex-col gap-4 p-2">
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#each threadFeed.boards as [h, threads] (h || "general")}
<ThreadBoard {url} {h} {threads} />
{/each}
+1 -1
View File
@@ -10,7 +10,7 @@
</script>
<Page>
<PageContent class="flex flex-col items-center gap-2 p-2">
<PageContent class="flex flex-col items-center gap-2 p-2 sm:gap-4 sm:p-4">
<PageHeader>
{#snippet title()}
<div>Choose your Hosting Plan</div>