From 82c3f3c4712087176d914075113d904db27152c9 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 13 Dec 2025 18:07:15 -0800 Subject: [PATCH] add scout button and pending invitations to crew members - wire scout button to open ScoutUserModal - collapsible section showing sent invitations for officers --- src/routes/(app)/crew/members/+page.svelte | 198 ++++++++++++++++++++- 1 file changed, 194 insertions(+), 4 deletions(-) diff --git a/src/routes/(app)/crew/members/+page.svelte b/src/routes/(app)/crew/members/+page.svelte index 78cd8b32..4e814fcf 100644 --- a/src/routes/(app)/crew/members/+page.svelte +++ b/src/routes/(app)/crew/members/+page.svelte @@ -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(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 + ) @@ -301,7 +324,14 @@ {/snippet} {#snippet actions()} {#if crewStore.isOfficer} - + {#snippet trigger({ props })} + + {#if invitationsSectionOpen} +
    + {#each invitationsQuery.data as invitation} + {@const expired = isInvitationExpired(invitation.expiresAt)} +
  • +
    + {invitation.user?.username ?? 'Unknown'} + {#if invitation.invitedBy} + + Invited by {invitation.invitedBy.username} + + {/if} +
    +
    + {#if expired} + Expired + {:else} + Expires {formatDate(invitation.expiresAt)} + {/if} +
    +
  • + {/each} +
+ {/if} + + {/if} + {#if membersQuery.isLoading}

Loading...

@@ -602,6 +675,11 @@ {/snippet} + +{#if crewStore.crew?.id} + +{/if} +