refac: groups ui

This commit is contained in:
Timothy Jaeryang Baek
2026-02-21 16:01:48 -06:00
parent 96ca47ac9f
commit 914c7ba876
2 changed files with 172 additions and 166 deletions
+151 -152
View File
@@ -12,18 +12,15 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import Search from '$lib/components/icons/Search.svelte';
import User from '$lib/components/icons/User.svelte';
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
import EditGroupModal from './Groups/EditGroupModal.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import GroupItem from './Groups/GroupItem.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import { Select } from 'bits-ui';
import { createNewGroup, getGroups } from '$lib/apis/groups';
import {
getUserDefaultPermissions,
@@ -38,17 +35,12 @@
let groups = [];
let query = '';
let orderBy = '';
let direction = 'asc';
let sortBy = 'members';
const setSortKey = (key) => {
if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
orderBy = key;
direction = 'asc';
}
};
const sortItems = [
{ value: 'name', label: $i18n.t('Name') },
{ value: 'members', label: $i18n.t('Members') }
];
$: filteredGroups = groups
.filter((group) => {
@@ -61,12 +53,10 @@
}
})
.sort((a, b) => {
if (orderBy === 'name') {
const cmp = a.name.localeCompare(b.name);
return direction === 'asc' ? cmp : -cmp;
} else if (orderBy === 'members') {
const cmp = (a.member_count ?? 0) - (b.member_count ?? 0);
return direction === 'asc' ? cmp : -cmp;
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
} else if (sortBy === 'members') {
return (b.member_count ?? 0) - (a.member_count ?? 0);
}
return 0;
});
@@ -129,144 +119,153 @@
onSubmit={addGroupHandler}
/>
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Groups')}
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{groups.length}
</div>
</div>
<div class="flex gap-1">
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<Search />
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search')}
/>
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
<div class="flex justify-between items-center">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Groups')}
</div>
<div>
<Tooltip content={$i18n.t('Create Group')}>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{groups.length}
</div>
</div>
<div class="flex w-full justify-end gap-1.5">
<button
class="px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
on:click={() => {
showAddGroupModal = !showAddGroupModal;
}}
>
<Plus className="size-3" strokeWidth="2.5" />
<div class="hidden md:block md:ml-1 text-xs">{$i18n.t('New Group')}</div>
</button>
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30"
>
<div class="flex items-center w-full space-x-2 py-0.5 px-3.5">
<div class="flex flex-1">
<div class="self-center ml-1 mr-3">
<Search className="size-3.5" />
</div>
<input
class="w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Groups')}
/>
{#if query}
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
<button
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
on:click={() => {
showAddGroupModal = !showAddGroupModal;
query = '';
}}
>
<Plus className="size-3.5" />
<XMark className="size-3" strokeWidth="2" />
</button>
</Tooltip>
</div>
{/if}
</div>
<Select.Root
selected={sortItems.find((item) => item.value === sortBy)}
items={sortItems}
onSelectedChange={(selectedItem) => {
sortBy = selectedItem.value;
}}
>
<Select.Trigger
class="relative flex items-center gap-0.5 px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-850 rounded-xl shrink-0"
aria-label={$i18n.t('Sort by')}
>
<Select.Value
class="inline-flex h-input px-0.5 outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
placeholder={$i18n.t('Sort by')}
/>
<ChevronDown className="size-3.5" strokeWidth="2.5" />
</Select.Trigger>
<Select.Content
class="rounded-2xl min-w-[170px] p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sameWidth={false}
align="end"
>
{#each sortItems as item}
<Select.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
value={item.value}
label={item.label}
>
{item.label}
{#if sortBy === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{#if filteredGroups.length !== 0}
<div class="my-2 px-3 grid grid-cols-1 gap-1">
{#each filteredGroups as group}
<GroupItem {group} {setGroups} {defaultPermissions} />
{/each}
</div>
{:else}
<div class="w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class="text-3xl mb-3">👥</div>
<div class="text-lg font-medium mb-1">{$i18n.t('No groups found')}</div>
<div class="text-gray-500 text-center text-xs">
{$i18n.t('Use groups to organize your users and assign permissions.')}
</div>
</div>
</div>
{/if}
</div>
<hr class="mb-2 border-gray-100/30 dark:border-gray-850/30" />
<EditGroupModal
bind:show={showDefaultPermissionsModal}
tabs={['permissions']}
bind:permissions={defaultPermissions}
custom={false}
onSubmit={updateDefaultPermissionsHandler}
/>
<button
class="flex items-center justify-between rounded-lg w-full transition pt-1"
on:click={() => {
showDefaultPermissionsModal = true;
}}
>
<div class="flex items-center gap-2.5">
<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
<UsersSolid className="size-4" />
</div>
<div class="text-left">
<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
<div class="flex text-xs mt-0.5">
{$i18n.t('applies to all users with the "user" role')}
</div>
</div>
</div>
</div>
<div>
{#if filteredGroups.length === 0}
<div class="flex flex-col items-center justify-center h-40">
<div class=" text-xl font-medium">
{$i18n.t('Organize your users')}
</div>
<div class="mt-1 text-sm dark:text-gray-300">
{$i18n.t('Use groups to group your users and assign permissions.')}
</div>
<div class="mt-3">
<button
class=" px-4 py-1.5 text-sm rounded-full bg-black hover:bg-gray-800 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition font-medium flex items-center space-x-1"
aria-label={$i18n.t('Create Group')}
on:click={() => {
showAddGroupModal = true;
}}
>
{$i18n.t('Create Group')}
</button>
</div>
</div>
{:else}
<div>
<div class=" flex items-center gap-3 justify-between text-xs uppercase px-1 font-medium">
<button
class="w-full basis-3/5 flex items-center gap-1 hover:text-gray-900 dark:hover:text-gray-100 transition cursor-pointer"
on:click={() => setSortKey('name')}
>
{$i18n.t('Group')}
{#if orderBy === 'name'}
{#if direction === 'asc'}
<ChevronUp className="size-3" strokeWidth="2.5" />
{:else}
<ChevronDown className="size-3" strokeWidth="2.5" />
{/if}
{/if}
</button>
<button
class="w-full basis-2/5 flex items-center gap-1 justify-end hover:text-gray-900 dark:hover:text-gray-100 transition cursor-pointer"
on:click={() => setSortKey('members')}
>
{$i18n.t('Users')}
{#if orderBy === 'members'}
{#if direction === 'asc'}
<ChevronUp className="size-3" strokeWidth="2.5" />
{:else}
<ChevronDown className="size-3" strokeWidth="2.5" />
{/if}
{/if}
</button>
</div>
<hr class="mt-1.5 border-gray-100/30 dark:border-gray-850/30" />
{#each filteredGroups as group}
<div class="my-2">
<GroupItem {group} {setGroups} {defaultPermissions} />
</div>
{/each}
</div>
{/if}
<hr class="mb-2 border-gray-100/30 dark:border-gray-850/30" />
<EditGroupModal
bind:show={showDefaultPermissionsModal}
tabs={['permissions']}
bind:permissions={defaultPermissions}
custom={false}
onSubmit={updateDefaultPermissionsHandler}
/>
<button
class="flex items-center justify-between rounded-lg w-full transition pt-1"
on:click={() => {
showDefaultPermissionsModal = true;
}}
>
<div class="flex items-center gap-2.5">
<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
<UsersSolid className="size-4" />
</div>
<div class="text-left">
<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
<div class="flex text-xs mt-0.5">
{$i18n.t('applies to all users with the "user" role')}
</div>
</div>
</div>
<div>
<ChevronRight strokeWidth="2.5" />
</div>
</button>
</div>
<div>
<ChevronRight strokeWidth="2.5" />
</div>
</button>
{/if}
@@ -8,7 +8,6 @@
import { deleteGroupById, updateGroupById } from '$lib/apis/groups';
import Pencil from '$lib/components/icons/Pencil.svelte';
import User from '$lib/components/icons/User.svelte';
import EditGroupModal from './EditGroupModal.svelte';
export let group = {
@@ -63,26 +62,34 @@
/>
<button
class="flex items-center gap-3 justify-between px-1 text-xs w-full transition"
class="flex space-x-4 cursor-pointer text-left w-full px-3.5 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
on:click={() => {
showEdit = true;
}}
>
<div class="flex items-center gap-1.5 w-full font-medium flex-1">
<div class="line-clamp-1">
{group.name}
</div>
</div>
<div class="w-full">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
<div class="text-sm font-medium line-clamp-1">{group.name}</div>
</div>
<div class="flex items-center gap-1.5 w-fit font-medium text-right justify-end">
{group?.member_count}
<div class="flex items-center gap-2 mt-0.5 line-clamp-1">
<div class="text-xs text-gray-500 shrink-0">
{$i18n.t('{{COUNT}} members', { COUNT: group?.member_count ?? 0 })}
</div>
<div>
<User className="size-3.5" />
</div>
{#if group?.description}
<div class="text-xs text-gray-500 line-clamp-1">
{group.description}
</div>
{/if}
</div>
</div>
<div class=" rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition">
<Pencil className="size-3.5" />
<div class="flex self-center ml-2">
<Pencil className="size-3.5" />
</div>
</div>
</div>
</button>