From 907b4503ddcbeccbbdadeab4405cb145c23d8908 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 17 Dec 2025 18:29:52 -0800 Subject: [PATCH] show phantom claims in notifications modal - renamed Invitations modal to Notifications - shows pending phantom assignments with accept/decline - badge counts both invitations and phantom claims - modal accessible to all users with pending notifications --- src/lib/components/Navigation.svelte | 27 +- .../components/crew/InvitationsModal.svelte | 278 +++++++++++++----- src/routes/(app)/crew/members/+page.svelte | 26 +- 3 files changed, 245 insertions(+), 86 deletions(-) diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index 7a7f172f..62191fb0 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -151,8 +151,16 @@ enabled: isAuth })) - // Derived count of pending invitations + // Query for pending phantom claims (only when authenticated and in a crew) + const pendingPhantomClaimsQuery = createQuery(() => ({ + ...crewQueries.pendingPhantomClaims(), + enabled: isAuth && crewStore.isInCrew + })) + + // Derived counts const pendingInvitationCount = $derived(pendingInvitationsQuery.data?.length ?? 0) + const pendingPhantomClaimCount = $derived(pendingPhantomClaimsQuery.data?.length ?? 0) + const totalNotificationCount = $derived(pendingInvitationCount + pendingPhantomClaimCount) // Handle logout async function handleLogout() { @@ -267,9 +275,9 @@ height="24" /> {/if} - {#if pendingInvitationCount > 0} + {#if totalNotificationCount > 0} - + {/if} @@ -309,13 +317,11 @@ {/if} - {#if isAuth && !crewStore.isInCrew} + {#if isAuth && totalNotificationCount > 0} {/if} @@ -385,12 +391,13 @@ /> {/if} - + {#if isAuth} {/if} diff --git a/src/lib/components/crew/InvitationsModal.svelte b/src/lib/components/crew/InvitationsModal.svelte index 3cad1573..0f1c41dc 100644 --- a/src/lib/components/crew/InvitationsModal.svelte +++ b/src/lib/components/crew/InvitationsModal.svelte @@ -2,31 +2,46 @@ - + {#if isLoading}
-

Loading invitations...

+

Loading notifications...

- {:else if invitations.length === 0} + {:else if !hasNotifications}
- -

No pending invitations

-

You'll see crew invitations here when you receive them.

+ +

No pending notifications

+

You'll see crew invitations and phantom assignments here.

{:else} -
- {#each invitations as invitation} - {@const expired = isExpired(invitation.expiresAt)} - {@const crew = invitation.crew} - {@const invitedBy = invitation.invitedBy} - {@const isProcessing = processingId === invitation.id} + + {#if hasPhantomClaims} +
+

Phantom Assignments

+

+ Accept to inherit the phantom's GW scores and join date +

+
+ {#each phantomClaims as phantom} + {@const crew = phantom.crew} + {@const isProcessing = processingId === phantom.id} - {#if crew} -
-
-
-
- {crew.name} - {#if crew.gamertag} - [{crew.gamertag}] - {/if} + {#if crew} +
+
+
+
+ {phantom.name} +
+ + From crew {crew.name}{crew.gamertag ? ` [${crew.gamertag}]` : ''} + + {#if phantom.joinedAt} + + Joined {formatDate(phantom.joinedAt)} + + {/if} +
- {#if invitedBy} - - Invited by {invitedBy.username} - - {/if} -
- {#if expired} -
Expired
- {:else} -
- Expires {formatDate(invitation.expiresAt)} +
+ +
- {/if} -
- - {#if !expired} -
- -
{/if} -
- {/if} - {/each} -
+ {/each} +
+
+ {/if} + + + {#if hasInvitations} +
+

Crew Invitations

+
+ {#each invitations as invitation} + {@const expired = isExpired(invitation.expiresAt)} + {@const crew = invitation.crew} + {@const invitedBy = invitation.invitedBy} + {@const isProcessing = processingId === invitation.id} + + {#if crew} +
+
+
+
+ {crew.name} + {#if crew.gamertag} + [{crew.gamertag}] + {/if} +
+ {#if invitedBy} + + Invited by {invitedBy.username} + + {/if} +
+ + {#if expired} +
Expired
+ {:else} +
+ Expires {formatDate(invitation.expiresAt)} +
+ {/if} +
+ + {#if !expired} +
+ + +
+ {/if} +
+ {/if} + {/each} +
+
+ {/if} {/if}
@@ -190,13 +298,34 @@ } } - .invitations-list { + .section { + &:not(:first-child) { + margin-top: spacing.$unit-3x; + padding-top: spacing.$unit-3x; + border-top: 1px solid var(--border-color); + } + } + + .section-title { + margin: 0 0 spacing.$unit-half 0; + font-size: typography.$font-regular; + font-weight: typography.$medium; + color: var(--text-primary); + } + + .section-description { + margin: 0 0 spacing.$unit-2x 0; + font-size: typography.$font-small; + color: var(--text-secondary); + } + + .notifications-list { display: flex; flex-direction: column; gap: spacing.$unit; } - .invitation-card { + .notification-card { background: var(--surface-secondary, #f9fafb); border: 1px solid var(--border-color); border-radius: 8px; @@ -207,7 +336,7 @@ } } - .invitation-content { + .notification-content { display: flex; justify-content: space-between; align-items: flex-start; @@ -215,19 +344,19 @@ margin-bottom: spacing.$unit; } - .crew-info { + .notification-info { display: flex; flex-direction: column; gap: spacing.$unit-quarter; } - .crew-name-row { + .notification-title-row { display: flex; align-items: baseline; gap: spacing.$unit-half; } - .crew-name { + .notification-title { font-weight: typography.$medium; color: var(--text-primary); } @@ -237,11 +366,16 @@ font-size: typography.$font-small; } - .invited-by { + .notification-subtitle { font-size: typography.$font-small; color: var(--text-secondary); } + .notification-meta { + font-size: typography.$font-tiny; + color: var(--text-tertiary); + } + .expires-info { font-size: typography.$font-tiny; color: var(--text-secondary); @@ -257,7 +391,7 @@ font-weight: typography.$medium; } - .invitation-actions { + .notification-actions { display: flex; justify-content: flex-end; gap: spacing.$unit; diff --git a/src/routes/(app)/crew/members/+page.svelte b/src/routes/(app)/crew/members/+page.svelte index 9bb62215..b3f48b39 100644 --- a/src/routes/(app)/crew/members/+page.svelte +++ b/src/routes/(app)/crew/members/+page.svelte @@ -8,7 +8,8 @@ import { useRemoveMember, useUpdateMembership, - useDeletePhantom + useDeletePhantom, + useDeclinePhantomClaim } from '$lib/api/mutations/crew.mutations' import { crewAdapter } from '$lib/api/adapters/crew.adapter' import { crewStore } from '$lib/stores/crew.store.svelte' @@ -83,6 +84,7 @@ const removeMemberMutation = useRemoveMember() const updateMembershipMutation = useUpdateMembership() const deletePhantomMutation = useDeletePhantom() + const declinePhantomClaimMutation = useDeclinePhantomClaim() // Filter options - Pending only shown to officers const filterOptions = $derived.by(() => { @@ -266,12 +268,26 @@ assignPhantomDialogOpen = true } - // Confirm claim + // Confirm claim (opens modal) function openConfirmClaimDialog(phantom: PhantomPlayer) { phantomToClaim = phantom confirmClaimDialogOpen = true } + // Decline claim (direct action, no confirmation needed) + async function handleDeclineClaim(phantom: PhantomPlayer) { + if (!crewStore.crew) return + + try { + await declinePhantomClaimMutation.mutateAsync({ + crewId: crewStore.crew.id, + phantomId: phantom.id + }) + } catch (error) { + console.error('Failed to decline phantom claim:', error) + } + } + // Format date function formatDate(dateString: string): string { return new Date(dateString).toLocaleDateString(undefined, { @@ -417,7 +433,8 @@ onEdit={() => openEditPhantomDialog(phantom)} onDelete={() => openDeletePhantomDialog(phantom)} onAssign={() => openAssignPhantomDialog(phantom)} - onConfirmClaim={() => openConfirmClaimDialog(phantom)} + onAccept={() => openConfirmClaimDialog(phantom)} + onDecline={() => handleDeclineClaim(phantom)} /> {/each} @@ -438,7 +455,8 @@ onEdit={() => openEditPhantomDialog(phantom)} onDelete={() => openDeletePhantomDialog(phantom)} onAssign={() => openAssignPhantomDialog(phantom)} - onConfirmClaim={() => openConfirmClaimDialog(phantom)} + onAccept={() => openConfirmClaimDialog(phantom)} + onDecline={() => handleDeclineClaim(phantom)} /> {/each}