diff --git a/src/lib/api/adapters/crew.adapter.ts b/src/lib/api/adapters/crew.adapter.ts index d05515b4..1e9b3c08 100644 --- a/src/lib/api/adapters/crew.adapter.ts +++ b/src/lib/api/adapters/crew.adapter.ts @@ -1,6 +1,7 @@ import { BaseAdapter } from './base.adapter' import { DEFAULT_ADAPTER_CONFIG } from './config' -import type { RequestOptions } from './types' +import type { RequestOptions, PaginatedResponse } from './types' +import type { Party } from '$lib/types/api/party' import type { Crew, CrewMembership, @@ -31,6 +32,23 @@ export class CrewAdapter extends BaseAdapter { return response.crew } + /** + * Get parties shared with the user's crew + */ + async getSharedParties( + page = 1, + perPage = 20, + options?: RequestOptions + ): Promise<{ parties: Party[]; meta: { page: number; totalPages: number; count: number; perPage: number } }> { + return this.request<{ parties: Party[]; meta: { page: number; totalPages: number; count: number; perPage: number } }>( + '/crew/shared_parties', + { + ...options, + params: { page, per_page: perPage } + } + ) + } + /** * Create a new crew (user becomes captain) */ diff --git a/src/lib/api/adapters/party.adapter.ts b/src/lib/api/adapters/party.adapter.ts index 53f26d8a..0e8eb07a 100644 --- a/src/lib/api/adapters/party.adapter.ts +++ b/src/lib/api/adapters/party.adapter.ts @@ -12,6 +12,7 @@ import { BaseAdapter } from './base.adapter' import type { AdapterOptions, PaginatedResponse } from './types' import { DEFAULT_ADAPTER_CONFIG } from './config' import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party' +import type { PartyShare } from '$lib/types/api/partyShare' /** * Parameters for creating a new party @@ -409,6 +410,28 @@ export class PartyAdapter extends BaseAdapter { this.clearCache(`/parties/${shortcode}`) } + /** + * Share a party with the current user's crew + * @param partyId - The party's UUID + */ + async shareWithCrew(partyId: string): Promise { + const response = await this.request<{ share: PartyShare }>(`/parties/${partyId}/shares`, { + method: 'POST' + }) + return response.share + } + + /** + * Remove a party share + * @param partyId - The party's UUID + * @param shareId - The share's UUID to remove + */ + async removeShare(partyId: string, shareId: string): Promise { + await this.request(`/parties/${partyId}/shares/${shareId}`, { + method: 'DELETE' + }) + } + /** * Clears the cache for party-related data */ diff --git a/src/lib/api/mutations/party.mutations.ts b/src/lib/api/mutations/party.mutations.ts index abbeebf3..1ec21c5e 100644 --- a/src/lib/api/mutations/party.mutations.ts +++ b/src/lib/api/mutations/party.mutations.ts @@ -15,6 +15,7 @@ import { } from '$lib/api/adapters/party.adapter' import { partyKeys } from '$lib/api/queries/party.queries' import { userKeys } from '$lib/api/queries/user.queries' +import { crewKeys } from '$lib/api/queries/crew.queries' import type { Party } from '$lib/types/api/party' /** @@ -312,3 +313,69 @@ export function useRegeneratePreview() { } })) } + +/** + * Share party with crew mutation + * + * Shares a party with the current user's crew. + * + * @example + * ```svelte + * + * ``` + */ +export function useSharePartyWithCrew() { + const queryClient = useQueryClient() + + return createMutation(() => ({ + mutationFn: ({ partyId }: { partyId: string; shortcode: string }) => + partyAdapter.shareWithCrew(partyId), + onSuccess: (_share, { shortcode }) => { + // Invalidate the party to refresh its shares + queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) }) + // Invalidate crew's shared parties list + queryClient.invalidateQueries({ queryKey: crewKeys.sharedParties() }) + } + })) +} + +/** + * Remove party share mutation + * + * Removes a share from a party. + * + * @example + * ```svelte + * + * ``` + */ +export function useRemovePartyShare() { + const queryClient = useQueryClient() + + return createMutation(() => ({ + mutationFn: ({ partyId, shareId }: { partyId: string; shareId: string; shortcode: string }) => + partyAdapter.removeShare(partyId, shareId), + onSuccess: (_data, { shortcode }) => { + // Invalidate the party to refresh its shares + queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) }) + // Invalidate crew's shared parties list + queryClient.invalidateQueries({ queryKey: crewKeys.sharedParties() }) + } + })) +} diff --git a/src/lib/api/queries/crew.queries.ts b/src/lib/api/queries/crew.queries.ts index c768226f..778e667d 100644 --- a/src/lib/api/queries/crew.queries.ts +++ b/src/lib/api/queries/crew.queries.ts @@ -7,7 +7,7 @@ * @module api/queries/crew */ -import { queryOptions } from '@tanstack/svelte-query' +import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query' import { crewAdapter } from '$lib/api/adapters/crew.adapter' import type { MemberFilter } from '$lib/types/api/crew' @@ -96,6 +96,30 @@ export const crewQueries = { queryFn: () => crewAdapter.getPendingPhantomClaims(), staleTime: 1000 * 60 * 2, // 2 minutes gcTime: 1000 * 60 * 15 // 15 minutes + }), + + /** + * Parties shared with the crew query options + * Uses infinite query for pagination + */ + sharedParties: () => + infiniteQueryOptions({ + queryKey: ['crew', 'shared_parties'] as const, + queryFn: async ({ pageParam }) => { + const response = await crewAdapter.getSharedParties(pageParam) + return { + parties: response.parties, + page: response.meta.page, + totalPages: response.meta.totalPages, + total: response.meta.count, + perPage: response.meta.perPage + } + }, + initialPageParam: 1, + getNextPageParam: (lastPage) => + lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined, + staleTime: 1000 * 60 * 2, // 2 minutes + gcTime: 1000 * 60 * 15 // 15 minutes }) } @@ -122,6 +146,7 @@ export const crewKeys = { membersAll: () => [...crewKeys.all, 'members'] as const, members: (filter: MemberFilter) => [...crewKeys.all, 'members', filter] as const, crewInvitations: (crewId: string) => [...crewKeys.all, crewId, 'invitations'] as const, + sharedParties: () => [...crewKeys.all, 'shared_parties'] as const, invitations: { all: ['invitations'] as const, pending: () => ['invitations', 'pending'] as const diff --git a/src/lib/components/party/Party.svelte b/src/lib/components/party/Party.svelte index 58851e11..436e1035 100644 --- a/src/lib/components/party/Party.svelte +++ b/src/lib/components/party/Party.svelte @@ -30,7 +30,9 @@ useDeleteParty, useRemixParty, useFavoriteParty, - useUnfavoriteParty + useUnfavoriteParty, + useSharePartyWithCrew, + useRemovePartyShare } from '$lib/api/mutations/party.mutations' // TanStack Query mutations - Job @@ -158,6 +160,8 @@ const remixPartyMutation = useRemixParty() const favoritePartyMutation = useFavoriteParty() const unfavoritePartyMutation = useUnfavoriteParty() + const sharePartyWithCrewMutation = useSharePartyWithCrew() + const removePartyShareMutation = useRemovePartyShare() // TanStack Query mutations - Job const updateJobMutation = useUpdatePartyJob() @@ -443,9 +447,14 @@ function openSettingsPanel() { if (!canEdit()) return + // Check if party is currently shared with a crew + const isSharedWithCrew = party.shares?.some((s) => s.shareableType === 'crew') ?? false + const initialValues: PartyEditValues = { name: party.name ?? '', description: party.description ?? null, + visibility: party.visibility ?? 'public', + sharedWithCrew: isSharedWithCrew, fullAuto: party.fullAuto ?? false, autoGuard: party.autoGuard ?? false, autoSummon: party.autoSummon ?? false, @@ -463,9 +472,11 @@ initialValues, element: userElement, onSave: async (values) => { + // Update party details including visibility await updatePartyDetails({ name: values.name, description: values.description, + visibility: values.visibility, fullAuto: values.fullAuto, autoGuard: values.autoGuard, autoSummon: values.autoSummon, @@ -477,6 +488,34 @@ videoUrl: values.videoUrl, raidId: values.raidId }) + + // Handle crew share toggle + const wasShared = party.shares?.some((s) => s.shareableType === 'crew') ?? false + if (values.sharedWithCrew && !wasShared) { + // Share with crew + try { + await sharePartyWithCrewMutation.mutateAsync({ + partyId: party.id, + shortcode: party.shortcode + }) + } catch (err: any) { + console.error('Failed to share with crew:', err) + } + } else if (!values.sharedWithCrew && wasShared) { + // Remove crew share + const share = party.shares?.find((s) => s.shareableType === 'crew') + if (share) { + try { + await removePartyShareMutation.mutateAsync({ + partyId: party.id, + shareId: share.id, + shortcode: party.shortcode + }) + } catch (err: any) { + console.error('Failed to remove share:', err) + } + } + } } }) } diff --git a/src/lib/components/sidebar/PartyEditSidebar.svelte b/src/lib/components/sidebar/PartyEditSidebar.svelte index 609c465a..02f8d776 100644 --- a/src/lib/components/sidebar/PartyEditSidebar.svelte +++ b/src/lib/components/sidebar/PartyEditSidebar.svelte @@ -14,16 +14,22 @@ import MetricField from '$lib/components/party/edit/MetricField.svelte' import EditRaidPane from '$lib/components/sidebar/EditRaidPane.svelte' import EditDescriptionPane from '$lib/components/sidebar/EditDescriptionPane.svelte' + import Select from '$lib/components/ui/Select.svelte' + import Switch from '$lib/components/ui/switch/Switch.svelte' import { sidebar } from '$lib/stores/sidebar.svelte' import { usePaneStack } from '$lib/stores/paneStack.svelte' - import { untrack } from 'svelte' + import { crewStore } from '$lib/stores/crew.store.svelte' + import { untrack } from 'svelte' import type { Raid } from '$lib/types/api/entities' import type { RaidFull } from '$lib/types/api/raid' + import type { PartyVisibility } from '$lib/types/visibility' import Icon from '$lib/components/Icon.svelte' export interface PartyEditValues { name: string description: string | null + visibility: PartyVisibility + sharedWithCrew: boolean fullAuto: boolean autoGuard: boolean autoSummon: boolean @@ -55,6 +61,8 @@ // Local state - initialized from initialValues let name = $state(initialValues.name) + let visibility = $state(initialValues.visibility) + let sharedWithCrew = $state(initialValues.sharedWithCrew) let fullAuto = $state(initialValues.fullAuto) let autoGuard = $state(initialValues.autoGuard) let autoSummon = $state(initialValues.autoSummon) @@ -68,9 +76,18 @@ let raidId = $state(initialValues.raidId) let description = $state(initialValues.description) + // Visibility options for select + const visibilityOptions: Array<{ value: PartyVisibility; label: string }> = [ + { value: 'public', label: 'Public' }, + { value: 'private', label: 'Private' }, + { value: 'unlisted', label: 'Unlisted' } + ] + // Check if any values have changed const hasChanges = $derived( name !== initialValues.name || + visibility !== initialValues.visibility || + sharedWithCrew !== initialValues.sharedWithCrew || fullAuto !== initialValues.fullAuto || autoGuard !== initialValues.autoGuard || autoSummon !== initialValues.autoSummon || @@ -89,6 +106,8 @@ const values: PartyEditValues = { name, description, + visibility, + sharedWithCrew, fullAuto, autoGuard, autoSummon, @@ -264,6 +283,30 @@ + + + {#snippet children()} +