Skip to content
Draft
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 apps/web/app/api/item/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ async function getItem(itemTokenId: bigint, itemId: bigint): Promise<string> {
}))
: [],
supply: craftCount,
balance: 0,
};

return superjson.stringify(item);
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/api/owned-items/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export async function POST(request: Request) {
}))
: [],
supply: craftCount,
balance: Number(nft.balance),
};
} catch (e) {
console.error(e);
Expand Down
60 changes: 51 additions & 9 deletions apps/web/components/inventories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { isOtomAtom } from '@/lib/otoms';
import { paths } from '@/lib/paths';
import type { OtomItem } from '@/lib/types';
import type { OtomItem, OwnedItem } from '@/lib/types';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { usePathname } from 'next/navigation';
import { useDeferredValue, useEffect, useMemo, useState, type FC } from 'react';
import { useInView } from 'react-intersection-observer';
import { InlineLink } from './ui/link';

type GroupedOtomItems = {
representativeItem: OtomItem;
representativeItem: OtomItem | OwnedItem;
count: number;
allItems: OtomItem[];
allItems: (OtomItem | OwnedItem)[];
};

export const OtomsInventory: FC<{ usedCounts: Map<string, number> }> = ({ usedCounts }) => {
Expand All @@ -38,6 +38,8 @@ export const OtomsInventory: FC<{ usedCounts: Map<string, number> }> = ({ usedCo
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } =
useGetOtomItemsForUser();

const { data: itemsData = [] } = useGetItemsForUser();

useEffect(() => {
if ((moleculesInView || otomsInView) && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
Expand All @@ -53,7 +55,7 @@ export const OtomsInventory: FC<{ usedCounts: Map<string, number> }> = ({ usedCo
const molecules = filteredItems.filter((item) => !isOtomAtom(item));
const otoms = filteredItems.filter((item) => isOtomAtom(item));

const groupItems = (items: OtomItem[]): GroupedOtomItems[] => {
const groupOtomItems = (items: OtomItem[]): GroupedOtomItems[] => {
const groups = new Map<string, GroupedOtomItems>();
for (const item of items) {
if (groups.has(item.tokenId)) {
Expand All @@ -72,12 +74,32 @@ export const OtomsInventory: FC<{ usedCounts: Map<string, number> }> = ({ usedCo
a.representativeItem.name.localeCompare(b.representativeItem.name)
);
};
const groupItems = (items: OwnedItem[]): GroupedOtomItems[] => {
const groups = new Map<string, GroupedOtomItems>();
for (const item of items) {
if (groups.has(item.tokenId)) {
const group = groups.get(item.tokenId)!;
group.count++;
group.allItems.push(item);
} else {
groups.set(item.tokenId, {
representativeItem: item,
count: 1 + (item?.balance ?? 0),
allItems: [item],
});
}
}
return Array.from(groups.values()).sort((a, b) =>
a.representativeItem.name.localeCompare(b.representativeItem.name)
);
};

return {
molecules: groupItems(molecules),
otoms: groupItems(otoms),
molecules: groupOtomItems(molecules),
otoms: groupOtomItems(otoms),
items: groupItems(itemsData),
};
}, [data?.pages, deferredSearchTerm]);
}, [data?.pages, deferredSearchTerm, itemsData]);

if (isLoading) {
return <InventorySkeleton />;
Expand All @@ -92,6 +114,7 @@ export const OtomsInventory: FC<{ usedCounts: Map<string, number> }> = ({ usedCo

const moleculesAmount = inventory.molecules.reduce((acc, group) => acc + group.count, 0);
const otomsAmount = inventory.otoms.reduce((acc, group) => acc + group.count, 0);
const itemsAmount = inventory.items.reduce((acc, group) => acc + group.count, 0);
const isCreatePage = pathname === paths.create;

return (
Expand Down Expand Up @@ -123,14 +146,33 @@ export const OtomsInventory: FC<{ usedCounts: Map<string, number> }> = ({ usedCo
disabled={isInventoryEmpty}
/>

{itemsAmount > 0 && (
<div className="flex flex-col gap-2">
<h3 className="text-muted-foreground text-sm">Assembly Items ({itemsAmount})</h3>
<ul className="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] items-start gap-2 rounded sm:flex sm:flex-wrap">
{inventory.items.map((group) => (
<OtomItemCard
key={group.representativeItem.tokenId}
representativeItem={group.representativeItem}
count={group.count}
usedCounts={usedCounts}
/>
))}
</ul>
<div ref={moleculesRef} className="text-muted-foreground text-xs">
{isFetchingNextPage && 'Loading more...'}
</div>
</div>
)}

{moleculesAmount > 0 && (
<div className="flex flex-col gap-2">
<h3 className="text-muted-foreground text-sm">Molecules ({moleculesAmount})</h3>
<ul className="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] items-start gap-2 rounded sm:flex sm:flex-wrap">
{inventory.molecules.map((group) => (
<OtomItemCard
key={group.representativeItem.tokenId}
representativeItem={group.representativeItem}
representativeItem={group.representativeItem as OtomItem}
count={group.count}
usedCounts={usedCounts}
/>
Expand Down Expand Up @@ -158,7 +200,7 @@ export const OtomsInventory: FC<{ usedCounts: Map<string, number> }> = ({ usedCo
{inventory.otoms.map((group) => (
<OtomItemCard
key={group.representativeItem.tokenId}
representativeItem={group.representativeItem}
representativeItem={group.representativeItem as OtomItem}
count={group.count}
usedCounts={usedCounts}
/>
Expand Down
30 changes: 18 additions & 12 deletions apps/web/components/items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ const ItemToCraftCard: FC<ItemToCraftCardProps> = ({ item }) => {
};

type OtomItemCardProps = {
representativeItem: OtomItem;
representativeItem: OtomItem | OwnedItem;
count: number;
usedCounts: Map<string, number>;
};
Expand Down Expand Up @@ -491,7 +491,7 @@ export const OtomItemCard: FC<OtomItemCardProps> = ({ representativeItem, count,

// Variable component with exact match criteria
if (isExactMatchCriteria(b.criteria)) {
return checkCriteria(representativeItem, b.criteria);
return checkCriteria(representativeItem as OtomItem, b.criteria);
}

return false;
Expand All @@ -504,7 +504,7 @@ export const OtomItemCard: FC<OtomItemCardProps> = ({ representativeItem, count,
hoveredState.component.itemIdOrOtomTokenId.toString() === representativeItem.tokenId) ||
(hoveredState.component.componentType === 'variable_otom' &&
isExactMatchCriteria(hoveredState.component.criteria) &&
checkCriteria(representativeItem, hoveredState.component.criteria)));
checkCriteria(representativeItem as OtomItem, hoveredState.component.criteria)));

function handleMouseEnter() {
if (isRequiredInBlueprint && !areAllItemsUsed) {
Expand All @@ -517,8 +517,10 @@ export const OtomItemCard: FC<OtomItemCardProps> = ({ representativeItem, count,
}

const mass =
representativeItem.giving_atoms.reduce((acc, atom) => acc + atom.mass, 0) +
representativeItem.receiving_atoms.reduce((acc, atom) => acc + atom.mass, 0);
'giving_atoms' in representativeItem
? representativeItem.giving_atoms.reduce((acc, atom) => acc + atom.mass, 0) +
representativeItem.receiving_atoms.reduce((acc, atom) => acc + atom.mass, 0)
: 0;

const isMolecule = !isOtomAtom(representativeItem);

Expand Down Expand Up @@ -597,11 +599,15 @@ export const OtomItemCard: FC<OtomItemCardProps> = ({ representativeItem, count,

<TooltipContent className="max-w-[300px]">
<p className="text-base font-semibold">{representativeItem.name}</p>
<p className="mb-1">{isMolecule ? 'Molecule (M)' : 'Otom'}</p>
<p>Hardness: {representativeItem.hardness.toFixed(3)}</p>
<p>Toughness: {representativeItem.toughness.toFixed(3)}</p>
<p>Ductility: {representativeItem.ductility.toFixed(3)}</p>
<p>Mass: {mass}</p>
{'giving_atoms' in representativeItem && (
<>
<p className="mb-1">{isMolecule ? 'Molecule (M)' : 'Otom'}</p>
<p>Hardness: {representativeItem.hardness.toFixed(3)}</p>
<p>Toughness: {representativeItem.toughness.toFixed(3)}</p>
<p>Ductility: {representativeItem.ductility.toFixed(3)}</p>
<p>Mass: {mass}</p>
</>
)}
<div className="flex items-center gap-2">
<p>Token ID: {abbreviateHash(representativeItem.tokenId, 3, 4)}</p>
<Button
Expand All @@ -623,7 +629,7 @@ export const OtomItemCard: FC<OtomItemCardProps> = ({ representativeItem, count,
);
};

export const ElementName: FC<{ otom: OtomItem | Molecule; className?: string }> = ({
export const ElementName: FC<{ otom: OtomItem | Molecule | OwnedItem; className?: string }> = ({
otom,
className,
}) => {
Expand All @@ -632,7 +638,7 @@ export const ElementName: FC<{ otom: OtomItem | Molecule; className?: string }>
return (
<div className={cn('relative p-0.5 select-none', className)}>
<span className="flex items-center">
{isAtom && (
{isAtom && 'giving_atoms' in otom && (
<span className="mr-0.5 inline-block text-right text-xs">
<sup className="block leading-1.5 font-semibold">
{otom.giving_atoms[0].nucleus.nucleons}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BlueprintComponent, OtomItem } from '@/lib/types';
import { BlueprintComponent, OtomItem, OwnedItem } from '@/lib/types';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

export const hoveredOtomItemAtom = atom<{
item?: OtomItem | null;
item?: OtomItem | OwnedItem | null;
component?: BlueprintComponent;
} | null>(null);

Expand Down
7 changes: 6 additions & 1 deletion apps/web/lib/otoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Atom,
Molecule,
OtomItem,
OwnedItem,
SolidityCompatibleAtom,
SolidityCompatibleMolecule,
} from '@/lib/types';
Expand Down Expand Up @@ -61,6 +62,10 @@ function solidityAtomToAtom(a: SolidityCompatibleAtom): Atom {
};
}

export function isOtomAtom(otom: OtomItem | Molecule): boolean {
export function isOtomAtom(otom: OtomItem | Molecule | OwnedItem): boolean {
if (!('giving_atoms' in otom) || !('receiving_atoms' in otom)) {
return false;
}

return (otom.giving_atoms?.length ?? 0) + (otom.receiving_atoms?.length ?? 0) === 1;
}
13 changes: 10 additions & 3 deletions apps/web/lib/property-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BlueprintComponent, OtomItem } from './types';
import { BlueprintComponent, OtomItem, OwnedItem } from './types';

// Maps the PropertyType enum values from IOtomItemsCore.sol
// to the corresponding property path within the OtomItem/Atom types (lib/types.ts)
Expand Down Expand Up @@ -81,7 +81,10 @@ export function getNestedValue(obj: Record<string, unknown>, path: PropertyPath)
}
}

export function checkCriteria(item: OtomItem, criteria: BlueprintComponent['criteria']): boolean {
export function checkCriteria(
item: OtomItem | OwnedItem,
criteria: BlueprintComponent['criteria']
): boolean {
// Basic validation of inputs
if (!item || !criteria || criteria.length === 0) {
console.log('checkCriteria: Invalid item or criteria input.');
Expand All @@ -95,7 +98,11 @@ export function checkCriteria(item: OtomItem, criteria: BlueprintComponent['crit
return mapping?.path?.[0] === 'giving_atoms';
});

if (needsAtom && (!item.giving_atoms || item.giving_atoms.length === 0)) {
if (
needsAtom &&
'giving_atoms' in item &&
(!item.giving_atoms || item.giving_atoms.length === 0)
) {
console.warn('Criteria requires atom properties, but item has no giving_atoms.');
return false;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ export type OwnedItem = Item & {
tokenId: string;
tier: number | null;
usagesRemaining: number | null;
balance: number;
};

export type Trait = { name: string; value: string | number; link?: string };

export type OtomItem = Molecule & {
universeHash: Hex;
tokenId: string;
balance: number;
};

// Otom-related types
Expand Down
6 changes: 4 additions & 2 deletions apps/web/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ export function abbreviateHash(
} else if (typeof hash === 'string') {
hashString = hash.startsWith('0x') ? hash.slice(2) : hash;
} else {
throw new Error('Invalid hash format. Expected string or Buffer.');
console.error('Invalid hash format. Expected string or Buffer.');
return '';
}

if (hashString.length < prefixLength + suffixLength) {
throw new Error('Hash is too short to abbreviate.');
// Hash is too short to abbreviate
return hashString;
}

const prefix = hashString.slice(0, prefixLength);
Expand Down