refactor members page, add pending filter tab
This commit is contained in:
parent
5fb2331958
commit
c875f3cefb
2 changed files with 156 additions and 389 deletions
|
|
@ -10,7 +10,7 @@ export type CrewRole = 'member' | 'vice_captain' | 'captain'
|
||||||
export type InvitationStatus = 'pending' | 'accepted' | 'rejected' | 'expired'
|
export type InvitationStatus = 'pending' | 'accepted' | 'rejected' | 'expired'
|
||||||
|
|
||||||
// Member filter options for GET /crew/members
|
// Member filter options for GET /crew/members
|
||||||
export type MemberFilter = 'active' | 'retired' | 'phantom' | 'all'
|
export type MemberFilter = 'active' | 'retired' | 'phantom' | 'pending' | 'all'
|
||||||
|
|
||||||
// Crew from CrewBlueprint
|
// Crew from CrewBlueprint
|
||||||
export interface Crew {
|
export interface Crew {
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,19 @@
|
||||||
import SettingsRow from '$lib/components/ui/SettingsRow.svelte'
|
import SettingsRow from '$lib/components/ui/SettingsRow.svelte'
|
||||||
import Switch from '$lib/components/ui/switch/Switch.svelte'
|
import Switch from '$lib/components/ui/switch/Switch.svelte'
|
||||||
import CrewHeader from '$lib/components/crew/CrewHeader.svelte'
|
import CrewHeader from '$lib/components/crew/CrewHeader.svelte'
|
||||||
|
import MemberRow from '$lib/components/crew/MemberRow.svelte'
|
||||||
|
import PhantomRow from '$lib/components/crew/PhantomRow.svelte'
|
||||||
import ScoutUserModal from '$lib/components/crew/ScoutUserModal.svelte'
|
import ScoutUserModal from '$lib/components/crew/ScoutUserModal.svelte'
|
||||||
import BulkPhantomModal from '$lib/components/crew/BulkPhantomModal.svelte'
|
import BulkPhantomModal from '$lib/components/crew/BulkPhantomModal.svelte'
|
||||||
|
import AssignPhantomModal from '$lib/components/crew/AssignPhantomModal.svelte'
|
||||||
|
import ConfirmClaimModal from '$lib/components/crew/ConfirmClaimModal.svelte'
|
||||||
import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
|
import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
|
||||||
import type { MemberFilter, CrewMembership, PhantomPlayer, CrewInvitation } from '$lib/types/api/crew'
|
import type {
|
||||||
|
MemberFilter,
|
||||||
|
CrewMembership,
|
||||||
|
PhantomPlayer,
|
||||||
|
CrewInvitation
|
||||||
|
} from '$lib/types/api/crew'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -59,13 +68,9 @@
|
||||||
const activeRosterSize = $derived.by(() => {
|
const activeRosterSize = $derived.by(() => {
|
||||||
// Use active filter data if viewing active, otherwise use dedicated query
|
// Use active filter data if viewing active, otherwise use dedicated query
|
||||||
const activeMembers =
|
const activeMembers =
|
||||||
filter === 'active'
|
filter === 'active' ? (membersQuery.data?.members ?? []) : (activeQuery.data?.members ?? [])
|
||||||
? (membersQuery.data?.members ?? [])
|
|
||||||
: (activeQuery.data?.members ?? [])
|
|
||||||
const activePhantoms =
|
const activePhantoms =
|
||||||
filter === 'active'
|
filter === 'active' ? (membersQuery.data?.phantoms ?? []) : (activeQuery.data?.phantoms ?? [])
|
||||||
? (membersQuery.data?.phantoms ?? [])
|
|
||||||
: (activeQuery.data?.phantoms ?? [])
|
|
||||||
|
|
||||||
const activeMemberCount = activeMembers.filter((m) => !m.retired).length
|
const activeMemberCount = activeMembers.filter((m) => !m.retired).length
|
||||||
const activePhantomCount = activePhantoms.filter((p) => !p.retired).length
|
const activePhantomCount = activePhantoms.filter((p) => !p.retired).length
|
||||||
|
|
@ -79,13 +84,19 @@
|
||||||
const updateMembershipMutation = useUpdateMembership()
|
const updateMembershipMutation = useUpdateMembership()
|
||||||
const deletePhantomMutation = useDeletePhantom()
|
const deletePhantomMutation = useDeletePhantom()
|
||||||
|
|
||||||
// Filter options
|
// Filter options - Pending only shown to officers
|
||||||
const filterOptions: { value: MemberFilter; label: string }[] = [
|
const filterOptions = $derived.by(() => {
|
||||||
{ value: 'all', label: 'All' },
|
const options: { value: MemberFilter; label: string }[] = [
|
||||||
{ value: 'active', label: 'Active' },
|
{ value: 'all', label: 'All' },
|
||||||
{ value: 'phantom', label: 'Phantoms' },
|
{ value: 'active', label: 'Active' },
|
||||||
{ value: 'retired', label: 'Retired' }
|
{ value: 'phantom', label: 'Phantoms' }
|
||||||
]
|
]
|
||||||
|
if (crewStore.isOfficer) {
|
||||||
|
options.push({ value: 'pending', label: 'Pending' })
|
||||||
|
}
|
||||||
|
options.push({ value: 'retired', label: 'Retired' })
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
// Change filter
|
// Change filter
|
||||||
function handleFilterChange(newFilter: MemberFilter) {
|
function handleFilterChange(newFilter: MemberFilter) {
|
||||||
|
|
@ -113,9 +124,6 @@
|
||||||
// Dialog state for scout modal
|
// Dialog state for scout modal
|
||||||
let scoutModalOpen = $state(false)
|
let scoutModalOpen = $state(false)
|
||||||
|
|
||||||
// Pending invitations section visibility
|
|
||||||
let invitationsSectionOpen = $state(true)
|
|
||||||
|
|
||||||
// Dialog state for phantom creation
|
// Dialog state for phantom creation
|
||||||
let bulkPhantomDialogOpen = $state(false)
|
let bulkPhantomDialogOpen = $state(false)
|
||||||
|
|
||||||
|
|
@ -123,28 +131,13 @@
|
||||||
let deletePhantomDialogOpen = $state(false)
|
let deletePhantomDialogOpen = $state(false)
|
||||||
let phantomToDelete = $state<PhantomPlayer | null>(null)
|
let phantomToDelete = $state<PhantomPlayer | null>(null)
|
||||||
|
|
||||||
// Role display helpers
|
// Dialog state for phantom assignment
|
||||||
function getRoleLabel(role: string): string {
|
let assignPhantomDialogOpen = $state(false)
|
||||||
switch (role) {
|
let phantomToAssign = $state<PhantomPlayer | null>(null)
|
||||||
case 'captain':
|
|
||||||
return 'Captain'
|
|
||||||
case 'vice_captain':
|
|
||||||
return 'Vice Captain'
|
|
||||||
default:
|
|
||||||
return 'Member'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoleClass(role: string): string {
|
// Dialog state for confirm claim
|
||||||
switch (role) {
|
let confirmClaimDialogOpen = $state(false)
|
||||||
case 'captain':
|
let phantomToClaim = $state<PhantomPlayer | null>(null)
|
||||||
return 'captain'
|
|
||||||
case 'vice_captain':
|
|
||||||
return 'officer'
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Member actions
|
// Member actions
|
||||||
function openRemoveDialog(member: CrewMembership) {
|
function openRemoveDialog(member: CrewMembership) {
|
||||||
|
|
@ -201,7 +194,7 @@
|
||||||
editingMember = member
|
editingMember = member
|
||||||
editingPhantom = null
|
editingPhantom = null
|
||||||
// Format date for input
|
// Format date for input
|
||||||
editJoinDate = member.joinedAt ? member.joinedAt.split('T')[0] ?? '' : ''
|
editJoinDate = member.joinedAt ? (member.joinedAt.split('T')[0] ?? '') : ''
|
||||||
editRetired = member.retired
|
editRetired = member.retired
|
||||||
editDialogOpen = true
|
editDialogOpen = true
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +202,7 @@
|
||||||
function openEditPhantomDialog(phantom: PhantomPlayer) {
|
function openEditPhantomDialog(phantom: PhantomPlayer) {
|
||||||
editingPhantom = phantom
|
editingPhantom = phantom
|
||||||
editingMember = null
|
editingMember = null
|
||||||
editJoinDate = phantom.joinedAt ? phantom.joinedAt.split('T')[0] ?? '' : ''
|
editJoinDate = phantom.joinedAt ? (phantom.joinedAt.split('T')[0] ?? '') : ''
|
||||||
editRetired = phantom.retired
|
editRetired = phantom.retired
|
||||||
editDialogOpen = true
|
editDialogOpen = true
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +260,18 @@
|
||||||
phantomToDelete = null
|
phantomToDelete = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phantom assignment
|
||||||
|
function openAssignPhantomDialog(phantom: PhantomPlayer) {
|
||||||
|
phantomToAssign = phantom
|
||||||
|
assignPhantomDialogOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm claim
|
||||||
|
function openConfirmClaimDialog(phantom: PhantomPlayer) {
|
||||||
|
phantomToClaim = phantom
|
||||||
|
confirmClaimDialogOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
// Format date
|
// Format date
|
||||||
function formatDate(dateString: string): string {
|
function formatDate(dateString: string): string {
|
||||||
return new Date(dateString).toLocaleDateString(undefined, {
|
return new Date(dateString).toLocaleDateString(undefined, {
|
||||||
|
|
@ -285,6 +290,11 @@
|
||||||
const pendingInvitationsCount = $derived(
|
const pendingInvitationsCount = $derived(
|
||||||
invitationsQuery.data?.filter((inv) => !isInvitationExpired(inv.expiresAt)).length ?? 0
|
invitationsQuery.data?.filter((inv) => !isInvitationExpired(inv.expiresAt)).length ?? 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Get phantoms with pending claims (assigned but not confirmed)
|
||||||
|
const pendingClaimPhantoms = $derived(
|
||||||
|
membersQuery.data?.phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? []
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -293,7 +303,7 @@
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<CrewHeader title="Members">
|
<CrewHeader title="Members" backHref="/crew">
|
||||||
{#snippet belowTitle()}
|
{#snippet belowTitle()}
|
||||||
<div class="filter-tabs">
|
<div class="filter-tabs">
|
||||||
{#each filterOptions as option}
|
{#each filterOptions as option}
|
||||||
|
|
@ -334,50 +344,41 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</CrewHeader>
|
</CrewHeader>
|
||||||
|
|
||||||
<!-- Pending Invitations Section (officers only) -->
|
<!-- Pending Invitations (shown when filter is 'pending') -->
|
||||||
{#if crewStore.isOfficer && invitationsQuery.data && invitationsQuery.data.length > 0}
|
{#if filter === 'pending'}
|
||||||
<div class="invitations-section">
|
{#if invitationsQuery.isLoading}
|
||||||
<button
|
<div class="loading-state">
|
||||||
class="invitations-header"
|
<p>Loading...</p>
|
||||||
onclick={() => (invitationsSectionOpen = !invitationsSectionOpen)}
|
</div>
|
||||||
>
|
{:else if invitationsQuery.data && invitationsQuery.data.length > 0}
|
||||||
<span class="invitations-title">
|
<ul class="member-list">
|
||||||
Pending Invitations
|
{#each invitationsQuery.data as invitation}
|
||||||
{#if pendingInvitationsCount > 0}
|
{@const expired = isInvitationExpired(invitation.expiresAt)}
|
||||||
<span class="invitations-count">{pendingInvitationsCount}</span>
|
<li class="invitation-row" class:expired>
|
||||||
{/if}
|
<div class="invitation-info">
|
||||||
</span>
|
<span class="invited-user">{invitation.user?.username ?? 'Unknown'}</span>
|
||||||
<span class="toggle-icon" class:open={invitationsSectionOpen}>▼</span>
|
{#if invitation.invitedBy}
|
||||||
</button>
|
<span class="invited-by">
|
||||||
|
Invited by {invitation.invitedBy.username}
|
||||||
{#if invitationsSectionOpen}
|
</span>
|
||||||
<ul class="invitations-list">
|
{/if}
|
||||||
{#each invitationsQuery.data as invitation}
|
</div>
|
||||||
{@const expired = isInvitationExpired(invitation.expiresAt)}
|
<div class="invitation-status">
|
||||||
<li class="invitation-item" class:expired>
|
{#if expired}
|
||||||
<div class="invitation-info">
|
<span class="status-badge expired">Expired</span>
|
||||||
<span class="invited-user">{invitation.user?.username ?? 'Unknown'}</span>
|
{:else}
|
||||||
{#if invitation.invitedBy}
|
<span class="expires-text">Expires {formatDate(invitation.expiresAt)}</span>
|
||||||
<span class="invited-by">
|
{/if}
|
||||||
Invited by {invitation.invitedBy.username}
|
</div>
|
||||||
</span>
|
</li>
|
||||||
{/if}
|
{/each}
|
||||||
</div>
|
</ul>
|
||||||
<div class="invitation-status">
|
{:else}
|
||||||
{#if expired}
|
<div class="empty-state">
|
||||||
<span class="status-badge expired">Expired</span>
|
<p>No pending invitations.</p>
|
||||||
{:else}
|
</div>
|
||||||
<span class="expires-text">Expires {formatDate(invitation.expiresAt)}</span>
|
{/if}
|
||||||
{/if}
|
{:else if membersQuery.isLoading}
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if membersQuery.isLoading}
|
|
||||||
<div class="loading-state">
|
<div class="loading-state">
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -390,62 +391,38 @@
|
||||||
{#if membersQuery.data?.members && membersQuery.data.members.length > 0}
|
{#if membersQuery.data?.members && membersQuery.data.members.length > 0}
|
||||||
<ul class="member-list">
|
<ul class="member-list">
|
||||||
{#each membersQuery.data.members as member}
|
{#each membersQuery.data.members as member}
|
||||||
<li class="member-item" class:retired={member.retired}>
|
<MemberRow
|
||||||
<div class="member-info">
|
{member}
|
||||||
<span class="username">{member.user?.username ?? 'Unknown'}</span>
|
onEdit={() => openEditMemberDialog(member)}
|
||||||
<span class="role-badge {getRoleClass(member.role)}">
|
onPromote={() => openPromoteDialog(member)}
|
||||||
{getRoleLabel(member.role)}
|
onDemote={() => openDemoteDialog(member)}
|
||||||
</span>
|
onRemove={() => openRemoveDialog(member)}
|
||||||
{#if member.joinedAt}
|
/>
|
||||||
<span class="joined-date">Joined {formatDate(member.joinedAt)}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if crewStore.isOfficer}
|
|
||||||
<DropdownMenu>
|
|
||||||
{#snippet trigger({ props })}
|
|
||||||
<Button variant="secondary" size="small" iconOnly icon="ellipsis" {...props} />
|
|
||||||
{/snippet}
|
|
||||||
{#snippet menu()}
|
|
||||||
<DropdownMenuBase.Item
|
|
||||||
class="dropdown-menu-item"
|
|
||||||
onclick={() => openEditMemberDialog(member)}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</DropdownMenuBase.Item>
|
|
||||||
{#if crewStore.canActOnMember(member.role) && !member.retired && member.id !== crewStore.membership?.id}
|
|
||||||
{#if member.role === 'member' && crewStore.canPromoteTo('vice_captain')}
|
|
||||||
<DropdownMenuBase.Item
|
|
||||||
class="dropdown-menu-item"
|
|
||||||
onclick={() => openPromoteDialog(member)}
|
|
||||||
>
|
|
||||||
Promote
|
|
||||||
</DropdownMenuBase.Item>
|
|
||||||
{/if}
|
|
||||||
{#if member.role === 'vice_captain' && crewStore.canDemote('vice_captain')}
|
|
||||||
<DropdownMenuBase.Item
|
|
||||||
class="dropdown-menu-item"
|
|
||||||
onclick={() => openDemoteDialog(member)}
|
|
||||||
>
|
|
||||||
Demote
|
|
||||||
</DropdownMenuBase.Item>
|
|
||||||
{/if}
|
|
||||||
<DropdownMenuBase.Item
|
|
||||||
class="dropdown-menu-item danger"
|
|
||||||
onclick={() => openRemoveDialog(member)}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</DropdownMenuBase.Item>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</DropdownMenu>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else if filter !== 'phantom'}
|
{:else if filter !== 'phantom'}
|
||||||
<p class="empty-state">No members found</p>
|
<p class="empty-state">No members found</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Pending Claims Section (officers only) -->
|
||||||
|
{#if crewStore.isOfficer && pendingClaimPhantoms.length > 0 && (filter === 'all' || filter === 'phantom')}
|
||||||
|
<div class="section-divider pending-claims">
|
||||||
|
<span>Pending Claims ({pendingClaimPhantoms.length})</span>
|
||||||
|
</div>
|
||||||
|
<ul class="member-list">
|
||||||
|
{#each pendingClaimPhantoms as phantom}
|
||||||
|
<PhantomRow
|
||||||
|
{phantom}
|
||||||
|
currentUserId={crewStore.membership?.user?.id}
|
||||||
|
onEdit={() => openEditPhantomDialog(phantom)}
|
||||||
|
onDelete={() => openDeletePhantomDialog(phantom)}
|
||||||
|
onAssign={() => openAssignPhantomDialog(phantom)}
|
||||||
|
onConfirmClaim={() => openConfirmClaimDialog(phantom)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Phantom players -->
|
<!-- Phantom players -->
|
||||||
{#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0}
|
{#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0}
|
||||||
{#if filter === 'all' && membersQuery.data.members.length > 0}
|
{#if filter === 'all' && membersQuery.data.members.length > 0}
|
||||||
|
|
@ -455,51 +432,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
<ul class="member-list">
|
<ul class="member-list">
|
||||||
{#each membersQuery.data.phantoms as phantom}
|
{#each membersQuery.data.phantoms as phantom}
|
||||||
<li class="member-item" class:retired={phantom.retired}>
|
<PhantomRow
|
||||||
<div class="member-info">
|
{phantom}
|
||||||
<div class="phantom-details">
|
currentUserId={crewStore.membership?.user?.id}
|
||||||
<span class="username">{phantom.name}</span>
|
onEdit={() => openEditPhantomDialog(phantom)}
|
||||||
{#if phantom.granblueId}
|
onDelete={() => openDeletePhantomDialog(phantom)}
|
||||||
<span class="granblue-id">ID: {phantom.granblueId}</span>
|
onAssign={() => openAssignPhantomDialog(phantom)}
|
||||||
{/if}
|
onConfirmClaim={() => openConfirmClaimDialog(phantom)}
|
||||||
{#if phantom.joinedAt}
|
/>
|
||||||
<span class="joined-date">Joined {formatDate(phantom.joinedAt)}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if phantom.claimConfirmed && phantom.claimedBy}
|
|
||||||
<span class="status-badge claimed">
|
|
||||||
Claimed by {phantom.claimedBy.username}
|
|
||||||
</span>
|
|
||||||
{:else if phantom.claimedBy}
|
|
||||||
<span class="status-badge pending">
|
|
||||||
Pending: {phantom.claimedBy.username}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-badge unclaimed">Unclaimed</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if crewStore.isOfficer}
|
|
||||||
<DropdownMenu>
|
|
||||||
{#snippet trigger({ props })}
|
|
||||||
<Button variant="secondary" size="small" iconOnly icon="ellipsis" {...props} />
|
|
||||||
{/snippet}
|
|
||||||
{#snippet menu()}
|
|
||||||
<DropdownMenuBase.Item
|
|
||||||
class="dropdown-menu-item"
|
|
||||||
onclick={() => openEditPhantomDialog(phantom)}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</DropdownMenuBase.Item>
|
|
||||||
<DropdownMenuBase.Item
|
|
||||||
class="dropdown-menu-item danger"
|
|
||||||
onclick={() => openDeletePhantomDialog(phantom)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</DropdownMenuBase.Item>
|
|
||||||
{/snippet}
|
|
||||||
</DropdownMenu>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else if filter === 'phantom'}
|
{:else if filter === 'phantom'}
|
||||||
|
|
@ -543,7 +483,12 @@
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
onCancel={() => (confirmDialogOpen = false)}
|
onCancel={() => (confirmDialogOpen = false)}
|
||||||
primaryAction={{
|
primaryAction={{
|
||||||
label: confirmAction === 'remove' ? 'Remove' : confirmAction === 'promote' ? 'Promote' : 'Demote',
|
label:
|
||||||
|
confirmAction === 'remove'
|
||||||
|
? 'Remove'
|
||||||
|
: confirmAction === 'promote'
|
||||||
|
? 'Promote'
|
||||||
|
: 'Demote',
|
||||||
onclick: handleConfirmAction,
|
onclick: handleConfirmAction,
|
||||||
destructive: confirmAction === 'remove'
|
destructive: confirmAction === 'remove'
|
||||||
}}
|
}}
|
||||||
|
|
@ -567,10 +512,7 @@
|
||||||
This date is used to determine which events a member was active for when adding
|
This date is used to determine which events a member was active for when adding
|
||||||
historical GW scores.
|
historical GW scores.
|
||||||
</p>
|
</p>
|
||||||
<SettingsRow
|
<SettingsRow title="Retired" subtitle="This player is no longer a part of the crew">
|
||||||
title="Retired"
|
|
||||||
subtitle="This player is no longer a part of the crew"
|
|
||||||
>
|
|
||||||
{#snippet control()}
|
{#snippet control()}
|
||||||
<Switch bind:checked={editRetired} name="retired" />
|
<Switch bind:checked={editRetired} name="retired" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
@ -617,6 +559,16 @@
|
||||||
{#if crewStore.crew?.id}
|
{#if crewStore.crew?.id}
|
||||||
<ScoutUserModal bind:open={scoutModalOpen} crewId={crewStore.crew.id} />
|
<ScoutUserModal bind:open={scoutModalOpen} crewId={crewStore.crew.id} />
|
||||||
<BulkPhantomModal bind:open={bulkPhantomDialogOpen} crewId={crewStore.crew.id} />
|
<BulkPhantomModal bind:open={bulkPhantomDialogOpen} crewId={crewStore.crew.id} />
|
||||||
|
<AssignPhantomModal
|
||||||
|
bind:open={assignPhantomDialogOpen}
|
||||||
|
crewId={crewStore.crew.id}
|
||||||
|
phantom={phantomToAssign}
|
||||||
|
/>
|
||||||
|
<ConfirmClaimModal
|
||||||
|
bind:open={confirmClaimDialogOpen}
|
||||||
|
crewId={crewStore.crew.id}
|
||||||
|
phantom={phantomToClaim}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -627,7 +579,6 @@
|
||||||
@use '$src/themes/layout' as layout;
|
@use '$src/themes/layout' as layout;
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
padding: spacing.$unit-2x 0;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: var(--main-max-width);
|
max-width: var(--main-max-width);
|
||||||
}
|
}
|
||||||
|
|
@ -638,6 +589,10 @@
|
||||||
border-radius: layout.$page-corner;
|
border-radius: layout.$page-corner;
|
||||||
box-shadow: effects.$page-elevation;
|
box-shadow: effects.$page-elevation;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
:global(.header-info) {
|
||||||
|
gap: spacing.$unit-2x;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tabs {
|
.filter-tabs {
|
||||||
|
|
@ -700,86 +655,6 @@
|
||||||
padding: spacing.$unit;
|
padding: spacing.$unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: spacing.$unit spacing.$unit-2x;
|
|
||||||
border-radius: layout.$item-corner;
|
|
||||||
transition: background-color 0.15s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.retired {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: spacing.$unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phantom-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.granblue-id {
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: layout.$item-corner-small;
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
background: rgba(0, 0, 0, 0.04);
|
|
||||||
|
|
||||||
&.captain {
|
|
||||||
background: var(--color-gold-light, #fef3c7);
|
|
||||||
color: var(--color-gold-dark, #92400e);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.officer {
|
|
||||||
background: var(--color-blue-light, #dbeafe);
|
|
||||||
color: var(--color-blue-dark, #1e40af);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: layout.$item-corner-small;
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
|
|
||||||
&.unclaimed {
|
|
||||||
background: rgba(0, 0, 0, 0.04);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.pending {
|
|
||||||
background: var(--color-yellow-light, #fef9c3);
|
|
||||||
color: var(--color-yellow-dark, #854d0e);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.claimed {
|
|
||||||
background: var(--color-green-light, #dcfce7);
|
|
||||||
color: var(--color-green-dark, #166534);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-divider {
|
.section-divider {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -787,6 +662,15 @@
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: rgba(0, 0, 0, 0.02);
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
&.pending-claims {
|
||||||
|
background: var(--color-yellow-light, #fef9c3);
|
||||||
|
border-bottom-color: var(--color-yellow-dark, #854d0e);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--color-yellow-dark, #854d0e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: typography.$font-small;
|
font-size: typography.$font-small;
|
||||||
font-weight: typography.$medium;
|
font-weight: typography.$medium;
|
||||||
|
|
@ -794,35 +678,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: spacing.$unit-half;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border: none;
|
|
||||||
border-radius: layout.$item-corner-small;
|
|
||||||
background: rgba(0, 0, 0, 0.04);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.08);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.danger {
|
|
||||||
color: colors.$error;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(colors.$error, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm dialog styles
|
// Confirm dialog styles
|
||||||
.confirm-message {
|
.confirm-message {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
@ -852,31 +707,6 @@
|
||||||
font-size: typography.$font-small;
|
font-size: typography.$font-small;
|
||||||
font-weight: typography.$medium;
|
font-weight: typography.$medium;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
||||||
.optional {
|
|
||||||
font-weight: typography.$normal;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
padding: spacing.$unit-2x;
|
|
||||||
border: none;
|
|
||||||
border-radius: layout.$input-corner;
|
|
||||||
font-size: typography.$font-regular;
|
|
||||||
font-family: inherit;
|
|
||||||
background: var(--input-bound-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 60px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--input-bound-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -886,11 +716,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.joined-date {
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-input {
|
.date-input {
|
||||||
padding: spacing.$unit spacing.$unit-2x;
|
padding: spacing.$unit spacing.$unit-2x;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -918,75 +743,17 @@
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pending invitations section
|
// Invitation row styles
|
||||||
.invitations-section {
|
.invitation-row {
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.invitations-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
padding: spacing.$unit spacing.$unit spacing.$unit spacing.$unit-2x;
|
||||||
padding: spacing.$unit-2x spacing.$unit-3x;
|
border-radius: layout.$item-corner;
|
||||||
background: rgba(0, 0, 0, 0.02);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.15s;
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: rgba(0, 0, 0, 0.03);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
&.expired {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue