Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ input UserAddIdentityGrantInput {
input UserCollectionAssetFindManyInput {
collectionId: String!
search: String
searchByOwnerWallet: String
}

input UserCollectionCreateInput {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import { Collection } from './entity/collection.entity'
export class ApiCollectionAssetService {
constructor(private readonly core: ApiCoreService, private readonly cache: ApiCacheService) {}

async findMany({ collectionId, search }: UserCollectionAssetFindManyInput): Promise<CollectionAsset[]> {
async findMany({
collectionId,
search,
searchByOwnerWallet,
}: UserCollectionAssetFindManyInput): Promise<CollectionAsset[]> {
const collection = await this.ensureCollection(collectionId)
if (!collection.token) {
throw new Error(`Token for collection ${collection.slug} not found`)
Expand All @@ -36,15 +40,21 @@ export class ApiCollectionAssetService {
attributes: renameAttributes(asset.content?.metadata?.attributes ?? []),
}))

return assets.filter((assets) => {
if (!search?.length) {
return true
return assets.filter((asset) => {
let matchesSearch = true
let matchesOwner = true

if (search?.length) {
matchesSearch =
asset.name.toLowerCase().includes(search.toLowerCase()) ||
asset.description.toLowerCase().includes(search.toLowerCase())
}

if (searchByOwnerWallet?.length) {
matchesOwner = asset.owner.toLowerCase() === searchByOwnerWallet.toLowerCase()
}

return (
assets.name.toLowerCase().includes(search.toLowerCase()) ||
assets.description.toLowerCase().includes(search.toLowerCase())
)
return matchesSearch && matchesOwner
})
} catch (e) {
console.log('error', e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export class UserCollectionAssetFindManyInput {
collectionId!: string
@Field({ nullable: true })
search?: string
@Field({ nullable: true })
searchByOwnerWallet?: string
}
2 changes: 2 additions & 0 deletions libs/sdk/src/generated/graphql-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,7 @@ export type UserAddIdentityGrantInput = {
export type UserCollectionAssetFindManyInput = {
collectionId: Scalars['String']['input']
search?: InputMaybe<Scalars['String']['input']>
searchByOwnerWallet?: InputMaybe<Scalars['String']['input']>
}

export type UserCollectionCreateInput = {
Expand Down Expand Up @@ -14609,6 +14610,7 @@ export function UserCollectionAssetFindManyInputSchema(): z.ZodObject<Properties
return z.object({
collectionId: z.string(),
search: z.string().nullish(),
searchByOwnerWallet: z.string().nullish(),
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
import { useSdk } from '@pubkey-link/web-core-data-access'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { UserCollectionAssetFindManyInput, UserCollectionFindManyInput } from '@pubkey-link/sdk'
import { useMemo } from 'react'
import { useQueryState, parseAsArrayOf, parseAsString } from 'nuqs'
import { UserCollectionAssetFindManyInput } from '@pubkey-link/sdk'

export function useUserCollectionAssetFindMany(props: { collectionId: string; search?: string }) {
export function useUserCollectionAssetFindMany(props: { collectionId: string }) {
const sdk = useSdk()
const [search, setSearch] = useState<string>(props?.search ?? '')

console.log('search', search)
const [search] = useQueryState('search', parseAsString.withDefault(''))
const [attributeFilters] = useQueryState('filters', parseAsArrayOf(parseAsString).withDefault([]))
const [searchByOwnerWallet] = useQueryState('owner', parseAsString.withDefault(''))

const input: UserCollectionAssetFindManyInput = {
collectionId: props.collectionId,
search,
searchByOwnerWallet,
}
const query = useQuery({
queryKey: ['user', 'collection', 'find-many-asset', input],
queryFn: () => sdk.userCollectionAssetFindMany({ input }).then((res) => res.data.items ?? []),
})

const filteredItems = useMemo(() => {
const allItems = query.data ?? []

if (attributeFilters.length === 0) {
return allItems
}

const filtersByKey = attributeFilters.reduce((acc, filter) => {
const [key, value] = filter.split(':')
if (!acc[key]) acc[key] = []
acc[key].push(value)
return acc
}, {} as Record<string, string[]>)

return allItems.filter((asset) => {
return Object.entries(filtersByKey).every(([key, values]) => {
return asset.attributes?.some((attr) => attr.key === key && values.includes(attr.value ?? ''))
})
})
}, [query.data, attributeFilters])

return {
items: query.data ?? [],
items: filteredItems,
query,
setSearch: (q: string) => {
setSearch(q)
},
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NavLink } from '@mantine/core'
import { ActionIcon, Badge, NavLink } from '@mantine/core'
import { IconX } from '@tabler/icons-react'
import { CollectionAssetAttribute } from '@pubkey-link/sdk'
import React from 'react'
import { useQueryState, parseAsArrayOf, parseAsString } from 'nuqs'

function groupAttributesByKey(attributes: CollectionAssetAttribute[]): {
key: string
Expand All @@ -20,11 +21,71 @@ function groupAttributesByKey(attributes: CollectionAssetAttribute[]): {

export function CollectionUiAttributeTree({ attributes }: { attributes: CollectionAssetAttribute[] }) {
const groups = groupAttributesByKey(attributes)
return groups.map((group) => (
<NavLink key={group.key} label={group.key}>
{group.attributes.map((attr) => (
<NavLink key={`${attr.key}:${attr.value}`} label={`${attr.value} (${attr.count})`} />
))}
</NavLink>
))

const [selectedFilters, setSelectedFilters] = useQueryState('filters', parseAsArrayOf(parseAsString).withDefault([]))

function handleAttributeClick(key: string, value: string) {
const filterKey = `${key}:${value}`
const isSelected = selectedFilters.includes(filterKey)

if (isSelected) {
setSelectedFilters(selectedFilters.filter((filter) => filter !== filterKey))
} else {
setSelectedFilters([...selectedFilters, filterKey])
}
}

return groups.map((group) => {
const selectedCount = selectedFilters.filter((filter) => filter.startsWith(`${group.key}:`)).length

return (
<NavLink
key={group.key}
label={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>{group.key}</span>
{selectedCount > 0 && (
<Badge size="sm" variant="filled" color="blue">
{selectedCount}
</Badge>
)}
</div>
}
>
{group.attributes.map((attr) => {
const filterKey = `${attr.key}:${attr.value}`
const isSelected = selectedFilters.includes(filterKey)

return (
<NavLink
key={filterKey}
label={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<span>
{attr.value} ({attr.count})
</span>
{isSelected && (
<ActionIcon
size="xs"
variant="subtle"
color="gray"
onClick={(e) => {
e.stopPropagation()
handleAttributeClick(attr.key, attr.value ?? '')
}}
>
<IconX size={12} />
</ActionIcon>
)}
</div>
}
active={isSelected}
onClick={() => handleAttributeClick(attr.key, attr.value ?? '')}
style={{ cursor: 'pointer' }}
/>
)
})}
</NavLink>
)
})
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { Box, Flex, Grid } from '@mantine/core'
import { Box, Button, Flex, Grid, SegmentedControl, Stack } from '@mantine/core'
import {
useUserCollectionAssetFindMany,
useUserCollectionFindMany,
useUserCollectionFindOne,
} from '@pubkey-link/web-collection-data-access'
import { CollectionUiAssetGrid, CollectionUiAssetSearch, CollectionUiSelect } from '@pubkey-link/web-collection-ui'
import {
CollectionUiAssetGrid,
CollectionUiAssetSearch,
CollectionUiOwnerSearch,
CollectionUiSelect,
} from '@pubkey-link/web-collection-ui'
import { UiError, UiLoader, UiPage } from '@pubkey-ui/core'
import { IconImageInPicture } from '@tabler/icons-react'
import { useNavigate, useParams } from 'react-router-dom'
import { useState, useMemo } from 'react'
import { useQueryState, parseAsArrayOf, parseAsString } from 'nuqs'
import { CollectionUiAttributeTree } from './collection-ui-attribute-tree'
import { Collection } from '@pubkey-link/sdk'

Expand Down Expand Up @@ -35,9 +42,45 @@ export function UserCollectionDetailLoaded({
communityId: string
}) {
const navigate = useNavigate()
const [cols, setCols] = useState(4)

const [selectedFilters, setSelectedFilters] = useQueryState('filters', parseAsArrayOf(parseAsString).withDefault([]))
const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
const [ownerSearch, setOwnerSearch] = useQueryState('owner', parseAsString.withDefault(''))

const { data: collections } = useUserCollectionFindMany({ communityId })
const { items: assets, setSearch } = useUserCollectionAssetFindMany({ collectionId: collection.id })
const { items: assets } = useUserCollectionAssetFindMany({
collectionId: collection.id,
})

const filteredAttributes = useMemo(() => {
if (!assets || assets.length === 0) return []

const attributeMap = new Map<string, Map<string, number>>()

assets.forEach((asset) => {
asset.attributes?.forEach((attr) => {
if (!attributeMap.has(attr.key)) {
attributeMap.set(attr.key, new Map())
}
const valueMap = attributeMap.get(attr.key)
if (valueMap) {
const currentCount = valueMap.get(attr.value ?? '') || 0
valueMap.set(attr.value ?? '', currentCount + 1)
}
})
})

const result: Array<{ key: string; value: string; count: number }> = []

attributeMap.forEach((valueMap, key) => {
valueMap.forEach((count, value) => {
result.push({ key, value, count })
})
})

return result
}, [assets])

return (
<UiPage title={`Collection ${collection?.name} `} leftAction={<IconImageInPicture />}>
Expand All @@ -58,16 +101,48 @@ export function UserCollectionDetailLoaded({
</Box>

<Box flex={1}>
<CollectionUiAssetSearch setSearch={setSearch} />
<Flex gap="md" align="center">
<Box flex={1}>
<CollectionUiAssetSearch />
</Box>
<CollectionUiOwnerSearch />
<SegmentedControl
value={cols.toString()}
onChange={(value) => setCols(parseInt(value))}
data={[
{ label: '4x', value: '4' },
{ label: '8x', value: '8' },
{ label: '12x', value: '12' },
]}
withItemsBorders={false}
/>
</Flex>
</Box>
</Flex>

<Grid>
<Grid.Col span={3}>
<CollectionUiAttributeTree attributes={collection?.attributes ?? []} />
<Stack gap="md">
{(selectedFilters.length > 0 || search || ownerSearch) && (
<Button
variant="light"
color="gray"
size="sm"
fullWidth
onClick={() => {
setSelectedFilters([])
setSearch('')
setOwnerSearch('')
}}
>
Clear All Filters ({selectedFilters.length + (search ? 1 : 0) + (ownerSearch ? 1 : 0)})
</Button>
)}
<CollectionUiAttributeTree attributes={filteredAttributes} />
</Stack>
</Grid.Col>
<Grid.Col span={9}>
{assets?.length ? <CollectionUiAssetGrid assets={assets} /> : <div>No assets found</div>}
{assets?.length ? <CollectionUiAssetGrid assets={assets} cols={cols} /> : <div>No assets found</div>}
</Grid.Col>
</Grid>
</UiPage>
Expand Down
1 change: 1 addition & 0 deletions libs/web/collection/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './lib/collection-ui-grid-item'
export * from './lib/collection-ui-layout'
export * from './lib/collection-ui-select'
export * from './lib/collection-ui-asset-search'
export * from './lib/collection-ui-owner-search'
Loading