add phantom claiming modals
This commit is contained in:
parent
32eab5bcae
commit
e1d8c92a5b
2 changed files with 371 additions and 0 deletions
219
src/lib/components/crew/AssignPhantomModal.svelte
Normal file
219
src/lib/components/crew/AssignPhantomModal.svelte
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { createQuery } from '@tanstack/svelte-query'
|
||||||
|
import { crewQueries } from '$lib/api/queries/crew.queries'
|
||||||
|
import { useAssignPhantom } from '$lib/api/mutations/crew.mutations'
|
||||||
|
import Dialog from '$lib/components/ui/Dialog.svelte'
|
||||||
|
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
|
||||||
|
import ModalBody from '$lib/components/ui/ModalBody.svelte'
|
||||||
|
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
import type { PhantomPlayer, CrewMembership } from '$lib/types/api/crew'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
crewId: string
|
||||||
|
phantom: PhantomPlayer | null
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open = $bindable(false), crewId, phantom }: Props = $props()
|
||||||
|
|
||||||
|
const assignMutation = useAssignPhantom()
|
||||||
|
|
||||||
|
// Query for active crew members
|
||||||
|
const membersQuery = createQuery(() => ({
|
||||||
|
...crewQueries.members('active'),
|
||||||
|
enabled: open
|
||||||
|
}))
|
||||||
|
|
||||||
|
// State
|
||||||
|
let selectedMemberId = $state<string | null>(null)
|
||||||
|
let assignSuccess = $state(false)
|
||||||
|
let assignError = $state<string | null>(null)
|
||||||
|
|
||||||
|
// Get active members (excluding those who already have scores - per requirement)
|
||||||
|
const availableMembers = $derived(
|
||||||
|
membersQuery.data?.members?.filter((m) => !m.retired) ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assign phantom to selected member
|
||||||
|
async function handleAssign() {
|
||||||
|
if (!phantom || !selectedMemberId) return
|
||||||
|
|
||||||
|
const selectedMember = availableMembers.find((m) => m.id === selectedMemberId)
|
||||||
|
if (!selectedMember?.user?.id) return
|
||||||
|
|
||||||
|
assignError = null
|
||||||
|
try {
|
||||||
|
await assignMutation.mutateAsync({
|
||||||
|
crewId,
|
||||||
|
phantomId: phantom.id,
|
||||||
|
userId: selectedMember.user.id
|
||||||
|
})
|
||||||
|
assignSuccess = true
|
||||||
|
setTimeout(() => {
|
||||||
|
resetState()
|
||||||
|
open = false
|
||||||
|
}, 1500)
|
||||||
|
} catch (err) {
|
||||||
|
assignError = err instanceof Error ? err.message : 'Failed to assign phantom'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
selectedMemberId = null
|
||||||
|
assignSuccess = false
|
||||||
|
assignError = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
resetState()
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state when modal opens
|
||||||
|
$effect(() => {
|
||||||
|
if (open) {
|
||||||
|
resetState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog bind:open>
|
||||||
|
<ModalHeader
|
||||||
|
title="Assign Phantom"
|
||||||
|
description={phantom ? `Assign "${phantom.name}" to a crew member` : 'Select a crew member'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{#if assignSuccess}
|
||||||
|
<div class="success-message">
|
||||||
|
<Icon name="check-circle" size={32} />
|
||||||
|
<p>Phantom assigned successfully!</p>
|
||||||
|
</div>
|
||||||
|
{:else if membersQuery.isLoading}
|
||||||
|
<div class="loading-state">
|
||||||
|
<p>Loading members...</p>
|
||||||
|
</div>
|
||||||
|
{:else if availableMembers.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No crew members available to assign.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="members-list">
|
||||||
|
{#each availableMembers as member}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="member-option"
|
||||||
|
class:selected={selectedMemberId === member.id}
|
||||||
|
onclick={() => (selectedMemberId = member.id)}
|
||||||
|
>
|
||||||
|
<span class="member-username">{member.user?.username ?? 'Unknown'}</span>
|
||||||
|
{#if selectedMemberId === member.id}
|
||||||
|
<Icon name="check" size={16} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if assignError}
|
||||||
|
<div class="assign-error">
|
||||||
|
<p>{assignError}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
{#if !assignSuccess}
|
||||||
|
<ModalFooter
|
||||||
|
onCancel={handleCancel}
|
||||||
|
primaryAction={selectedMemberId
|
||||||
|
? {
|
||||||
|
label: assignMutation.isPending ? 'Assigning...' : 'Assign',
|
||||||
|
onclick: handleAssign,
|
||||||
|
disabled: assignMutation.isPending
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
@use '$src/themes/layout' as layout;
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.empty-state {
|
||||||
|
padding: spacing.$unit-4x;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: spacing.$unit spacing.$unit-2x;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: layout.$item-corner;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s,
|
||||||
|
border-color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: var(--color-blue-light, #dbeafe);
|
||||||
|
border-color: var(--color-blue-dark, #1e40af);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-username {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assign-error {
|
||||||
|
margin-top: spacing.$unit-2x;
|
||||||
|
padding: spacing.$unit;
|
||||||
|
background: colors.$error--bg--light;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: colors.$error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
padding: spacing.$unit-4x;
|
||||||
|
text-align: center;
|
||||||
|
color: colors.$wind-text-20;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
152
src/lib/components/crew/ConfirmClaimModal.svelte
Normal file
152
src/lib/components/crew/ConfirmClaimModal.svelte
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { useConfirmPhantomClaim } from '$lib/api/mutations/crew.mutations'
|
||||||
|
import Dialog from '$lib/components/ui/Dialog.svelte'
|
||||||
|
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
|
||||||
|
import ModalBody from '$lib/components/ui/ModalBody.svelte'
|
||||||
|
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
import type { PhantomPlayer } from '$lib/types/api/crew'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
crewId: string
|
||||||
|
phantom: PhantomPlayer | null
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open = $bindable(false), crewId, phantom }: Props = $props()
|
||||||
|
|
||||||
|
const confirmMutation = useConfirmPhantomClaim()
|
||||||
|
|
||||||
|
// State
|
||||||
|
let confirmSuccess = $state(false)
|
||||||
|
let confirmError = $state<string | null>(null)
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
if (!phantom) return
|
||||||
|
|
||||||
|
confirmError = null
|
||||||
|
try {
|
||||||
|
await confirmMutation.mutateAsync({
|
||||||
|
crewId,
|
||||||
|
phantomId: phantom.id
|
||||||
|
})
|
||||||
|
confirmSuccess = true
|
||||||
|
setTimeout(() => {
|
||||||
|
resetState()
|
||||||
|
open = false
|
||||||
|
}, 1500)
|
||||||
|
} catch (err) {
|
||||||
|
confirmError = err instanceof Error ? err.message : 'Failed to confirm claim'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
confirmSuccess = false
|
||||||
|
confirmError = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
resetState()
|
||||||
|
open = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state when modal opens
|
||||||
|
$effect(() => {
|
||||||
|
if (open) {
|
||||||
|
resetState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog bind:open>
|
||||||
|
<ModalHeader title="Confirm Phantom Claim" />
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{#if confirmSuccess}
|
||||||
|
<div class="success-message">
|
||||||
|
<Icon name="check-circle" size={32} />
|
||||||
|
<p>Claim confirmed! You have inherited the phantom's scores and join date.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="confirm-content">
|
||||||
|
<p class="confirm-message">
|
||||||
|
Are you sure you want to claim <strong>{phantom?.name ?? 'this phantom'}</strong>?
|
||||||
|
</p>
|
||||||
|
<p class="confirm-details">
|
||||||
|
You will inherit this phantom's GW scores and join date. This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if confirmError}
|
||||||
|
<div class="confirm-error">
|
||||||
|
<p>{confirmError}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
{#if !confirmSuccess}
|
||||||
|
<ModalFooter
|
||||||
|
onCancel={handleCancel}
|
||||||
|
primaryAction={{
|
||||||
|
label: confirmMutation.isPending ? 'Confirming...' : 'Confirm Claim',
|
||||||
|
onclick: handleConfirm,
|
||||||
|
disabled: confirmMutation.isPending
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.confirm-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: spacing.$unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-message {
|
||||||
|
margin: 0;
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-details {
|
||||||
|
margin: 0;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-error {
|
||||||
|
padding: spacing.$unit;
|
||||||
|
background: colors.$error--bg--light;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: colors.$error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
padding: spacing.$unit-4x;
|
||||||
|
text-align: center;
|
||||||
|
color: colors.$wind-text-20;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue