simplify collection page to single code path

- use unified api for all users (no owner/viewer branching)
- remove client-side filtering (api handles it)
- pass userId to AddToCollectionModal
This commit is contained in:
Justin Edmund 2025-12-02 15:31:58 -08:00
parent b8a48771dd
commit 4bbe2ed188
3 changed files with 26 additions and 92 deletions

View file

@ -19,11 +19,12 @@
type SearchResultItem = SearchPageResult['results'][number]
interface Props {
userId: string
open?: boolean
onOpenChange?: (open: boolean) => void
}
let { open = $bindable(false), onOpenChange }: Props = $props()
let { userId, open = $bindable(false), onOpenChange }: Props = $props()
// Search state
let searchQuery = $state('')
@ -45,7 +46,7 @@
let sentinelEl = $state<HTMLElement>()
// Get IDs of characters already in collection
const collectedIdsQuery = createQuery(() => collectionQueries.collectedCharacterIds())
const collectedIdsQuery = createQuery(() => collectionQueries.collectedCharacterIds(userId))
// Build filters for search (using SearchFilters type from search.queries)
const searchFilters = $derived<SearchFilters>({

View file

@ -1,23 +1,12 @@
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
import { collectionAdapter } from '$lib/api/adapters/collection.adapter'
export const load: PageServerLoad = async ({ parent }) => {
const { user, isOwner } = await parent()
try {
// Fetch the user's public character collection
const characters = await collectionAdapter.getPublicCharacters(user.id)
return {
characters,
isOwner
}
} catch (e: any) {
// 403 means collection is private
if (e?.status === 403) {
throw error(403, 'This collection is private')
}
throw error(e?.status || 502, e?.message || 'Failed to load collection')
// User info comes from layout, collection data is fetched client-side via TanStack Query
// The unified API endpoint handles privacy checks server-side
return {
user,
isOwner
}
}

View file

@ -31,83 +31,30 @@
// Sentinel for infinite scroll
let sentinelEl = $state<HTMLElement>()
// Build filters for query
// Build filters for query - all filters are now server-side for everyone
const queryFilters = $derived({
element: elementFilters.length > 0 ? elementFilters : undefined,
rarity: rarityFilters.length > 0 ? rarityFilters : undefined
rarity: rarityFilters.length > 0 ? rarityFilters : undefined,
race: raceFilters.length > 0 ? raceFilters : undefined,
proficiency: proficiencyFilters.length > 0 ? proficiencyFilters : undefined,
gender: genderFilters.length > 0 ? genderFilters : undefined
})
// For owner, use the authenticated collection query with infinite scroll
// For non-owner, use the server-loaded public data
const collectionQuery = createInfiniteQuery(() => ({
...collectionQueries.characters(queryFilters),
enabled: data.isOwner,
initialData: data.isOwner
? undefined
: {
pages: [
{
results: data.characters || [],
page: 1,
totalPages: 1,
total: data.characters?.length || 0,
perPage: data.characters?.length || 20
}
],
pageParams: [1]
}
}))
// Unified query for any user's collection (privacy enforced server-side)
const collectionQuery = createInfiniteQuery(() => {
const userId = data.user.id
const filters = queryFilters
return collectionQueries.characters(userId, filters)
})
// Flatten all characters from pages
const allCharacters = $derived.by((): CollectionCharacter[] => {
if (!data.isOwner) {
return data.characters || []
}
if (!collectionQuery.data?.pages) {
return []
}
return collectionQuery.data.pages.flatMap((page) => page.results ?? [])
})
// Client-side filtering for non-API-supported filters
const filteredCharacters = $derived.by((): CollectionCharacter[] => {
let result = allCharacters
// Apply element filter (client-side for non-owner)
if (elementFilters.length > 0) {
result = result.filter((c) => elementFilters.includes(c.character?.element ?? 0))
}
// Apply rarity filter (client-side for non-owner)
if (rarityFilters.length > 0) {
result = result.filter((c) => rarityFilters.includes(c.character?.rarity ?? 0))
}
// Apply race filter (client-side) - race is nested object
if (raceFilters.length > 0) {
result = result.filter((c) => {
const race1 = c.character?.race?.race1 ?? 0
const race2 = c.character?.race?.race2 ?? 0
return raceFilters.includes(race1) || (race2 && raceFilters.includes(race2))
})
}
// Apply proficiency filter (client-side) - proficiency is an array
if (proficiencyFilters.length > 0) {
result = result.filter((c) => {
const proficiencies = c.character?.proficiency ?? []
return proficiencies.some((p) => proficiencyFilters.includes(p))
})
}
// Apply gender filter (client-side)
if (genderFilters.length > 0) {
result = result.filter((c) => genderFilters.includes(c.character?.gender ?? 0))
}
return result
})
// Infinite scroll
const inViewport = new IsInViewport(() => sentinelEl, {
rootMargin: '200px'
@ -115,7 +62,6 @@
$effect(() => {
if (
data.isOwner &&
inViewport.current &&
collectionQuery.hasNextPage &&
!collectionQuery.isFetchingNextPage &&
@ -125,11 +71,9 @@
}
})
const isLoading = $derived(data.isOwner && collectionQuery.isLoading)
const isEmpty = $derived(!isLoading && filteredCharacters.length === 0)
const showSentinel = $derived(
data.isOwner && collectionQuery.hasNextPage && !collectionQuery.isFetchingNextPage
)
const isLoading = $derived(collectionQuery.isLoading)
const isEmpty = $derived(!isLoading && allCharacters.length === 0)
const showSentinel = $derived(collectionQuery.hasNextPage && !collectionQuery.isFetchingNextPage)
function handleFiltersChange(filters: CollectionFilterState) {
elementFilters = filters.element
@ -223,7 +167,7 @@
</div>
{:else}
<div class="character-grid">
{#each filteredCharacters as character (character.id)}
{#each allCharacters as character (character.id)}
<button
type="button"
class="character-card"
@ -270,9 +214,9 @@
</div>
{/if}
{#if data.isOwner && !collectionQuery.hasNextPage && filteredCharacters.length > 0}
{#if !collectionQuery.hasNextPage && allCharacters.length > 0}
<div class="end-message">
<p>{filteredCharacters.length} character{filteredCharacters.length === 1 ? '' : 's'} in your collection</p>
<p>{allCharacters.length} character{allCharacters.length === 1 ? '' : 's'} in {data.isOwner ? 'your' : 'this'} collection</p>
</div>
{/if}
{/if}
@ -281,7 +225,7 @@
<!-- Add to Collection Modal -->
{#if data.isOwner}
<AddToCollectionModal bind:open={addModalOpen} />
<AddToCollectionModal userId={data.user.id} bind:open={addModalOpen} />
{/if}
<style lang="scss">