Add roles
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user