Skip to content
Open
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 indexer/src/modules/storage/models/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface Collection {
description?: string;
image?: string;
singleName?: string;
attributeOrder?: string[];
active: boolean;
supply: number;
id: number;
Expand Down
1 change: 1 addition & 0 deletions marketplace/src/app/models/data.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Collection {
discord?: string;
defaultBackground?: string;
mainTrait?: string;
attributeOrder?: string[];
contractAddress?: string;
type?: 'nft' | 'inscription';

Expand Down
36 changes: 33 additions & 3 deletions marketplace/src/app/services/attributes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ export class AttributesService {
private storageSvc: StorageService,
) {}

private sortFilterKeys(
keys: string[],
attributeOrder: string[] | undefined,
): string[] {
const order = attributeOrder || [];
const orderIndex = new Map(order.map((key, index) => [key, index]));

return [...keys].sort((a, b) => {
if (a === 'trait_count') return 1;
if (b === 'trait_count') return -1;

const aIndex = orderIndex.get(a);
const bIndex = orderIndex.get(b);

if (aIndex !== undefined && bIndex !== undefined) return aIndex - bIndex;
if (aIndex !== undefined) return -1;
if (bIndex !== undefined) return 1;

return a.localeCompare(b);
});
}

/**
* Adds attributes to an array of Phunks
* @param slug Collection slug
Expand Down Expand Up @@ -193,7 +215,7 @@ export class AttributesService {
});

// Convert the Map of Sets into a plain object with arrays
const attributeObject: { [key: string]: string[] | number[] } = {};
const unorderedAttributeObject: { [key: string]: string[] | number[] } = {};
attributeMap.forEach((values, key) => {
// Sort values by frequency (most common first)
const sortedValues = Array.from(values).sort((a, b) => {
Expand All @@ -210,12 +232,20 @@ export class AttributesService {
sortedValues.unshift('none');
}

attributeObject[key] = sortedValues;
unorderedAttributeObject[key] = sortedValues;
});

// Add trait count filter options
const sortedTraitCounts = Array.from(traitCounts).sort((a, b) => a - b);
attributeObject['trait_count'] = sortedTraitCounts.map(count => count);
unorderedAttributeObject['trait_count'] = sortedTraitCounts.map(count => count);

const attributeObject: { [key: string]: string[] | number[] } = {};
this.sortFilterKeys(
Object.keys(unorderedAttributeObject),
collection?.attributeOrder,
).forEach((key) => {
attributeObject[key] = unorderedAttributeObject[key];
});

// Generate rarity data from value frequencies
const rarityData: { [key: string]: number } = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
-- Migration: Add attributeOrder to collections tables
-- Description: Stores per-collection trait display order for marketplace UI sorting

ALTER TABLE public.collections
ADD COLUMN "attributeOrder" text[];

COMMENT ON COLUMN public.collections."attributeOrder" IS
'Ordered list of trait keys for collection-specific attribute/filter display.';

ALTER TABLE public.collections_sepolia
ADD COLUMN "attributeOrder" text[];

COMMENT ON COLUMN public.collections_sepolia."attributeOrder" IS
'Ordered list of trait keys for collection-specific attribute/filter display.';

CREATE OR REPLACE FUNCTION "public"."fetch_collections_with_previews"("preview_limit" integer DEFAULT 25, "show_inactive" boolean DEFAULT false)
RETURNS TABLE("ethscription" "json")
LANGUAGE "plpgsql"
AS $$BEGIN
RETURN QUERY
SELECT json_build_object(
'id', c.id,
'slug', c.slug,
'name', c.name,
'description', c.description,
'singleName', c."singleName",
'image', c.image,
'supply', c.supply,
'active', c.active,
'mintEnabled', c."mintEnabled",
'isMinting', c."isMinting",
'hasBackgrounds', c."hasBackgrounds",
'createdAt', c."createdAt",
'posterHashId', c."posterHashId",
'website', c.website,
'twitter', c.twitter,
'discord', c.discord,
'notifications', c.notifications,
'defaultBackground', c."defaultBackground",
'standalone', c.standalone,
'adminAddress', c."adminAddress",
'ignoredTraitFilters', c."ignoredTraitFilters",
'ignoredTraitFiltersForCounts', c."ignoredTraitFiltersForCounts",
'mainTrait', c."mainTrait",
'attributeOrder', c."attributeOrder",
'contractAddress', c."contractAddress",
'type', c."type",
'previews', json_agg(json_build_object(
'hashId', e."hashId",
'tokenId', e."tokenId",
'slug', c.slug,
'sha', e.sha
)) FILTER (WHERE e."hashId" IS NOT NULL)
)
FROM public.collections c
LEFT JOIN LATERAL (
SELECT e."hashId", e."tokenId", e.sha
FROM public.ethscriptions e
WHERE e.slug = c.slug
ORDER BY RANDOM()
LIMIT preview_limit
) e ON true
WHERE
(CASE
WHEN show_inactive = TRUE THEN c.active = FALSE
ELSE c.active = TRUE
END)
GROUP BY c.id, c.slug, c.name, c.image, c.description, c."singleName", c.supply, c.active,
c."mintEnabled", c."isMinting", c."hasBackgrounds", c."createdAt", c."posterHashId",
c.website, c.twitter, c.discord, c.notifications, c."defaultBackground", c.standalone,
c."adminAddress", c."ignoredTraitFilters", c."ignoredTraitFiltersForCounts",
c."mainTrait", c."attributeOrder", c."contractAddress", c."type";
END;$$;

CREATE OR REPLACE FUNCTION "public"."fetch_collections_with_previews_sepolia"("preview_limit" integer DEFAULT 25, "show_inactive" boolean DEFAULT false)
RETURNS TABLE("ethscription" "json")
LANGUAGE "plpgsql"
AS $$BEGIN
RETURN QUERY
SELECT json_build_object(
'id', c.id,
'slug', c.slug,
'name', c.name,
'description', c.description,
'singleName', c."singleName",
'image', c.image,
'supply', c.supply,
'active', c.active,
'mintEnabled', c."mintEnabled",
'isMinting', c."isMinting",
'hasBackgrounds', c."hasBackgrounds",
'createdAt', c."createdAt",
'posterHashId', c."posterHashId",
'website', c.website,
'twitter', c.twitter,
'discord', c.discord,
'notifications', c.notifications,
'defaultBackground', c."defaultBackground",
'standalone', c.standalone,
'adminAddress', c."adminAddress",
'ignoredTraitFilters', c."ignoredTraitFilters",
'ignoredTraitFiltersForCounts', c."ignoredTraitFiltersForCounts",
'mainTrait', c."mainTrait",
'attributeOrder', c."attributeOrder",
'contractAddress', c."contractAddress",
'type', c."type",
'previews', json_agg(json_build_object(
'hashId', e."hashId",
'tokenId', e."tokenId",
'slug', c.slug,
'sha', e.sha
)) FILTER (WHERE e."hashId" IS NOT NULL)
)
FROM public.collections_sepolia c
LEFT JOIN LATERAL (
SELECT e."hashId", e."tokenId", e.sha
FROM public.ethscriptions_sepolia e
WHERE e.slug = c.slug
ORDER BY RANDOM()
LIMIT preview_limit
) e ON true
WHERE
(CASE
WHEN show_inactive = TRUE THEN c.active = FALSE
ELSE c.active = TRUE
END)
GROUP BY c.id, c.slug, c.name, c.image, c.description, c."singleName", c.supply, c.active,
c."mintEnabled", c."isMinting", c."hasBackgrounds", c."createdAt", c."posterHashId",
c.website, c.twitter, c.discord, c.notifications, c."defaultBackground", c.standalone,
c."adminAddress", c."ignoredTraitFilters", c."ignoredTraitFiltersForCounts",
c."mainTrait", c."attributeOrder", c."contractAddress", c."type";
END;$$;