add scout button and pending invitations to crew members
- wire scout button to open ScoutUserModal - collapsible section showing sent invitations for officers
This commit is contained in:
parent
013c1b5eb2
commit
82c3f3c471
1 changed files with 194 additions and 4 deletions
|
|
@ -21,8 +21,9 @@
|
|||
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
||||
import Input from '$lib/components/ui/Input.svelte'
|
||||
import CrewHeader from '$lib/components/crew/CrewHeader.svelte'
|
||||
import ScoutUserModal from '$lib/components/crew/ScoutUserModal.svelte'
|
||||
import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
|
||||
import type { MemberFilter, CrewMembership, PhantomPlayer } from '$lib/types/api/crew'
|
||||
import type { MemberFilter, CrewMembership, PhantomPlayer, CrewInvitation } from '$lib/types/api/crew'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -47,6 +48,12 @@
|
|||
enabled: filter !== 'active' // Only fetch separately if not already viewing active
|
||||
}))
|
||||
|
||||
// Query for pending invitations (officers only)
|
||||
const invitationsQuery = createQuery(() => ({
|
||||
...crewQueries.crewInvitations(crewStore.crew?.id ?? ''),
|
||||
enabled: crewStore.isOfficer && !!crewStore.crew?.id
|
||||
}))
|
||||
|
||||
// Calculate total active roster size (members + phantoms)
|
||||
const activeRosterSize = $derived.by(() => {
|
||||
// Use active filter data if viewing active, otherwise use dedicated query
|
||||
|
|
@ -102,6 +109,12 @@
|
|||
let editingPhantom = $state<PhantomPlayer | null>(null)
|
||||
let editJoinDate = $state('')
|
||||
|
||||
// Dialog state for scout modal
|
||||
let scoutModalOpen = $state(false)
|
||||
|
||||
// Pending invitations section visibility
|
||||
let invitationsSectionOpen = $state(true)
|
||||
|
||||
// Dialog state for phantom creation
|
||||
let phantomDialogOpen = $state(false)
|
||||
let phantomName = $state('')
|
||||
|
|
@ -187,14 +200,14 @@
|
|||
editingMember = member
|
||||
editingPhantom = null
|
||||
// Format date for input
|
||||
editJoinDate = member.joinedAt ? member.joinedAt.split('T')[0] : ''
|
||||
editJoinDate = member.joinedAt ? member.joinedAt.split('T')[0] ?? '' : ''
|
||||
editJoinDateDialogOpen = true
|
||||
}
|
||||
|
||||
function openEditPhantomJoinDateDialog(phantom: PhantomPlayer) {
|
||||
editingPhantom = phantom
|
||||
editingMember = null
|
||||
editJoinDate = phantom.joinedAt ? phantom.joinedAt.split('T')[0] : ''
|
||||
editJoinDate = phantom.joinedAt ? phantom.joinedAt.split('T')[0] ?? '' : ''
|
||||
editJoinDateDialogOpen = true
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +290,16 @@
|
|||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if invitation is expired
|
||||
function isInvitationExpired(expiresAt: string): boolean {
|
||||
return new Date(expiresAt) < new Date()
|
||||
}
|
||||
|
||||
// Get pending (non-expired) invitations count
|
||||
const pendingInvitationsCount = $derived(
|
||||
invitationsQuery.data?.filter((inv) => !isInvitationExpired(inv.expiresAt)).length ?? 0
|
||||
)
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -301,7 +324,14 @@
|
|||
{/snippet}
|
||||
{#snippet actions()}
|
||||
{#if crewStore.isOfficer}
|
||||
<Button variant="secondary" size="small" disabled={isRosterFull}>Scout</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={isRosterFull}
|
||||
onclick={() => (scoutModalOpen = true)}
|
||||
>
|
||||
Scout
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
{#snippet trigger({ props })}
|
||||
<Button variant="secondary" size="small" iconOnly icon="ellipsis" {...props} />
|
||||
|
|
@ -316,6 +346,49 @@
|
|||
{/snippet}
|
||||
</CrewHeader>
|
||||
|
||||
<!-- Pending Invitations Section (officers only) -->
|
||||
{#if crewStore.isOfficer && invitationsQuery.data && invitationsQuery.data.length > 0}
|
||||
<div class="invitations-section">
|
||||
<button
|
||||
class="invitations-header"
|
||||
onclick={() => (invitationsSectionOpen = !invitationsSectionOpen)}
|
||||
>
|
||||
<span class="invitations-title">
|
||||
Pending Invitations
|
||||
{#if pendingInvitationsCount > 0}
|
||||
<span class="invitations-count">{pendingInvitationsCount}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="toggle-icon" class:open={invitationsSectionOpen}>▼</span>
|
||||
</button>
|
||||
|
||||
{#if invitationsSectionOpen}
|
||||
<ul class="invitations-list">
|
||||
{#each invitationsQuery.data as invitation}
|
||||
{@const expired = isInvitationExpired(invitation.expiresAt)}
|
||||
<li class="invitation-item" class:expired>
|
||||
<div class="invitation-info">
|
||||
<span class="invited-user">{invitation.user?.username ?? 'Unknown'}</span>
|
||||
{#if invitation.invitedBy}
|
||||
<span class="invited-by">
|
||||
Invited by {invitation.invitedBy.username}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="invitation-status">
|
||||
{#if expired}
|
||||
<span class="status-badge expired">Expired</span>
|
||||
{:else}
|
||||
<span class="expires-text">Expires {formatDate(invitation.expiresAt)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if membersQuery.isLoading}
|
||||
<div class="loading-state">
|
||||
<p>Loading...</p>
|
||||
|
|
@ -602,6 +675,11 @@
|
|||
{/snippet}
|
||||
</Dialog>
|
||||
|
||||
<!-- Scout User Modal -->
|
||||
{#if crewStore.crew?.id}
|
||||
<ScoutUserModal bind:open={scoutModalOpen} crewId={crewStore.crew.id} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/effects' as effects;
|
||||
|
|
@ -900,4 +978,116 @@
|
|||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// Pending invitations section
|
||||
.invitations-section {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.invitations-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: spacing.$unit-2x spacing.$unit-3x;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.invitations-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit;
|
||||
font-size: typography.$font-small;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.invitations-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
background: colors.$error;
|
||||
color: white;
|
||||
border-radius: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: typography.$medium;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.invitations-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: spacing.$unit;
|
||||
}
|
||||
|
||||
.invitation-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: spacing.$unit spacing.$unit-2x;
|
||||
border-radius: layout.$item-corner;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
&.expired {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.invitation-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.invited-user {
|
||||
font-size: typography.$font-small;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.invited-by {
|
||||
font-size: typography.$font-tiny;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.invitation-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.expires-text {
|
||||
font-size: typography.$font-tiny;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.status-badge.expired {
|
||||
font-size: typography.$font-tiny;
|
||||
color: colors.$error;
|
||||
background: colors.$error--bg--light;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: typography.$medium;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue