add invite to crew action on user profiles
officers can invite users without a crew via context menu
This commit is contained in:
parent
d2c16d908d
commit
013c1b5eb2
2 changed files with 130 additions and 5 deletions
|
|
@ -1,19 +1,56 @@
|
|||
<script lang="ts">
|
||||
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
||||
import { DropdownMenu } from 'bits-ui'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import DropdownItem from '$lib/components/ui/dropdown/DropdownItem.svelte'
|
||||
import InviteUserModal from '$lib/components/crew/InviteUserModal.svelte'
|
||||
import type { CrewRole } from '$lib/types/api/crew'
|
||||
|
||||
interface Props {
|
||||
username: string
|
||||
userId?: string
|
||||
avatarPicture?: string
|
||||
title?: string
|
||||
activeTab: 'teams' | 'favorites' | 'collection'
|
||||
isOwner?: boolean
|
||||
/** Current user's crew role (null if not in a crew) */
|
||||
viewerCrewRole?: CrewRole | null
|
||||
/** Current user's crew ID */
|
||||
viewerCrewId?: string | null
|
||||
/** Whether the target user is in a crew */
|
||||
targetUserHasCrew?: boolean
|
||||
}
|
||||
|
||||
let { username, avatarPicture = '', title, activeTab, isOwner = false }: Props = $props()
|
||||
let {
|
||||
username,
|
||||
userId,
|
||||
avatarPicture = '',
|
||||
title,
|
||||
activeTab,
|
||||
isOwner = false,
|
||||
viewerCrewRole = null,
|
||||
viewerCrewId = null,
|
||||
targetUserHasCrew = false
|
||||
}: Props = $props()
|
||||
|
||||
const avatarSrc = $derived(getAvatarSrc(avatarPicture))
|
||||
const avatarSrcSet = $derived(getAvatarSrcSet(avatarPicture))
|
||||
const displayTitle = $derived(title || username)
|
||||
|
||||
// Can invite if: viewer is captain/vice_captain AND target user is not in a crew AND not viewing own profile
|
||||
const canInvite = $derived(
|
||||
!isOwner &&
|
||||
viewerCrewRole !== null &&
|
||||
(viewerCrewRole === 'captain' || viewerCrewRole === 'vice_captain') &&
|
||||
!targetUserHasCrew &&
|
||||
userId
|
||||
)
|
||||
|
||||
// Show menu if there are any actions available
|
||||
const showMenu = $derived(canInvite)
|
||||
|
||||
// Invite modal state
|
||||
let inviteModalOpen = $state(false)
|
||||
</script>
|
||||
|
||||
<header class="header">
|
||||
|
|
@ -57,8 +94,40 @@
|
|||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{#if showMenu}
|
||||
<div class="header-actions">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class="menu-trigger">
|
||||
<Icon name="ellipsis" size={16} />
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="dropdown-content" sideOffset={5} align="end">
|
||||
{#if canInvite}
|
||||
<DropdownItem>
|
||||
<button onclick={() => (inviteModalOpen = true)}>
|
||||
<Icon name="user-plus" size={14} />
|
||||
<span>Invite to Crew</span>
|
||||
</button>
|
||||
</DropdownItem>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if canInvite && userId && viewerCrewId}
|
||||
<InviteUserModal
|
||||
bind:open={inviteModalOpen}
|
||||
{userId}
|
||||
{username}
|
||||
crewId={viewerCrewId}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/colors' as *;
|
||||
|
|
@ -113,4 +182,51 @@
|
|||
color: var(--accent-color, #3366ff);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.menu-trigger) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--button-contained-bg-hover, $grey-90);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dropdown-content) {
|
||||
background-color: var(--menu-bg);
|
||||
border-radius: 8px;
|
||||
padding: $unit-half;
|
||||
min-width: 160px;
|
||||
box-shadow:
|
||||
0 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||
0 10px 20px -15px rgba(22, 23, 24, 0.2);
|
||||
z-index: 50;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit-half;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
import ExploreGrid from '$lib/components/explore/ExploreGrid.svelte'
|
||||
import ProfileHeader from '$lib/components/profile/ProfileHeader.svelte'
|
||||
import { userQueries } from '$lib/api/queries/user.queries'
|
||||
import { userQueries, type FavoritesPageResult } from '$lib/api/queries/user.queries'
|
||||
import { crewStore } from '$lib/stores/crew.store.svelte'
|
||||
import { IsInViewport } from 'runed'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
|
|
@ -13,9 +14,13 @@
|
|||
const isOwner = $derived(data.isOwner || false)
|
||||
const activeTab = $derived<'teams' | 'favorites'>(tab === 'favorites' ? 'favorites' : 'teams')
|
||||
|
||||
// Crew info for invite functionality
|
||||
const viewerCrewRole = $derived(crewStore.membership?.role ?? null)
|
||||
const viewerCrewId = $derived(crewStore.crew?.id ?? null)
|
||||
|
||||
// Note: Type assertion needed because favorites and parties queries have different
|
||||
// result structures (items vs results) but we handle both in the items $derived
|
||||
const partiesQuery = createInfiniteQuery(() => {
|
||||
const getQueryOptions = () => {
|
||||
const isFavorites = tab === 'favorites' && isOwner
|
||||
|
||||
if (isFavorites) {
|
||||
|
|
@ -57,8 +62,9 @@
|
|||
}
|
||||
: undefined,
|
||||
initialDataUpdatedAt: 0
|
||||
} as unknown as ReturnType<typeof userQueries.favorites>
|
||||
})
|
||||
}
|
||||
}
|
||||
const partiesQuery = createInfiniteQuery(getQueryOptions as () => ReturnType<typeof userQueries.favorites>)
|
||||
|
||||
const items = $derived(() => {
|
||||
if (!partiesQuery.data?.pages) return data.items || []
|
||||
|
|
@ -93,9 +99,12 @@
|
|||
<section class="profile">
|
||||
<ProfileHeader
|
||||
username={data.user.username}
|
||||
userId={data.user?.id}
|
||||
avatarPicture={data.user?.avatar?.picture}
|
||||
{activeTab}
|
||||
{isOwner}
|
||||
{viewerCrewRole}
|
||||
{viewerCrewId}
|
||||
/>
|
||||
|
||||
{#if partiesQuery.isLoading}
|
||||
|
|
|
|||
Loading…
Reference in a new issue