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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ const ETH_REGISTRAR_CONTROLLER_2 = '0x253553366da8546fc250f225fe3d25d0c782303b';
const BASENAMES_REGISTRAR_CONTROLLER = '0x4ccb0bb02fcaba27e82a56646e81d8c5bc4119a5'; // Basenames RegistrarController
const BASENAMES_EA_REGISTRAR_CONTROLLER = '0xd3e6775ed9b7dc12b205c8e608dc3767b9e5efda'; // Basenames EARegistrarController

// ENS Addresses Set for O(1) lookups
const ENS_ADDRESSES = new Set([
ETH_REGISTRAR_CONTROLLER_1,
ETH_REGISTRAR_CONTROLLER_2,
BASENAMES_REGISTRAR_CONTROLLER,
BASENAMES_EA_REGISTRAR_CONTROLLER,
]);

// Lending and Borrowing
const MOONWELL_WETH_UNWRAPPER = '0x1382cff3cee10d283dcca55a30496187759e4caf'; // Base Moonwell WETH Unwrapper

Expand Down Expand Up @@ -137,12 +145,20 @@ export default function UsernameProfileSectionHeatmap() {
const firstTransactionDate = new Date(Math.min(...timestamps) * 1000);
const lastTransactionDate = new Date(Math.max(...timestamps) * 1000);

const uniqueActiveDaysSet = new Set(
filteredTransactions.map((tx) => new Date(parseInt(tx.timeStamp, 10) * 1000).toDateString()),
);
// Optimize: avoid creating Date objects twice
// Store the first occurrence of each unique day's Date object
const uniqueDatesMap = new Map<string, Date>();
filteredTransactions.forEach((tx) => {
const timestamp = parseInt(tx.timeStamp, 10) * 1000;
const date = new Date(timestamp);
const dateStr = date.toDateString();
if (!uniqueDatesMap.has(dateStr)) {
uniqueDatesMap.set(dateStr, date);
}
});

const sortedDates = Array.from(uniqueActiveDaysSet)
.map((dateStr) => new Date(dateStr))
// Use the cached Date objects directly instead of recreating them
const sortedDates = Array.from(uniqueDatesMap.values())
.sort((a, b) => a.getTime() - b.getTime());

let longestStreakDays = 0;
Expand All @@ -161,7 +177,7 @@ export default function UsernameProfileSectionHeatmap() {
longestStreakDays = Math.max(longestStreakDays, streak);

return {
uniqueActiveDays: uniqueActiveDaysSet.size,
uniqueActiveDays: uniqueDatesMap.size,
longestStreakDays,
currentStreakDays:
sortedDates[sortedDates.length - 1].toDateString() === new Date().toDateString()
Expand Down Expand Up @@ -358,36 +374,44 @@ export default function UsernameProfileSectionHeatmap() {
setCurrentStreak(currentStreakDays);
setActivityPeriod(activity);

setTokenSwapCount(
allTransactions.filter(
(tx) =>
// Optimize: single pass through transactions instead of multiple filters
const counts = allTransactions.reduce(
(acc, tx) => {
// Token swap count
if (
((tx.functionName &&
SWAP_FUNCTION_NAMES.some((fn) => tx.functionName?.includes(fn))) ??
tx.to === UNISWAP_ROUTER) ||
tx.to === AERODROME_ROUTER ||
tx.to === ONEINCH_ROUTER,
).length,
tx.to === ONEINCH_ROUTER
) {
acc.tokenSwap++;
}

// ENS count
if (ENS_ADDRESSES.has(tx.to)) {
acc.ens++;
}

// Bridge count
if (bridges.has(tx.to)) {
acc.bridge++;
}

// Lend count
if (lendBorrowEarn.has(tx.to) || tx.from === MOONWELL_WETH_UNWRAPPER) {
acc.lend++;
}

return acc;
},
{ tokenSwap: 0, ens: 0, bridge: 0, lend: 0 },
);

// ENS count calculation
setEnsCount(
allTransactions.filter((tx) =>
[
ETH_REGISTRAR_CONTROLLER_1,
ETH_REGISTRAR_CONTROLLER_2,
BASENAMES_REGISTRAR_CONTROLLER,
BASENAMES_EA_REGISTRAR_CONTROLLER,
].includes(tx.to),
).length,
);

setBridgeCount(allTransactions.filter((tx) => bridges.has(tx.to)).length);

setLendCount(
allTransactions.filter(
(tx) => lendBorrowEarn.has(tx.to) || tx.from === MOONWELL_WETH_UNWRAPPER,
).length,
);
setTokenSwapCount(counts.tokenSwap);
setEnsCount(counts.ens);
setBridgeCount(counts.bridge);
setLendCount(counts.lend);

setBuildCount(
allEthereumDeployments.length + allBaseDeployments.length + allSepoliaDeployments.length,
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/components/Ecosystem/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from 'classnames';
import { AnimatePresence, motion, cubicBezier } from 'motion/react';
import EcosystemCard from './Card';
import { EcosystemApp } from 'apps/web/src/components/Ecosystem/Content';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { Dispatch, SetStateAction, useCallback, memo } from 'react';
import {
Button,
ButtonSizes,
Expand All @@ -23,7 +23,7 @@ const cardAnimations = {
exit: { opacity: 0 },
};

function AnimatedEcosystemCard({ app }: { app: EcosystemApp }) {
const AnimatedEcosystemCard = memo(({ app }: { app: EcosystemApp }) => {
return (
<motion.div
layout
Expand All @@ -36,7 +36,9 @@ function AnimatedEcosystemCard({ app }: { app: EcosystemApp }) {
<EcosystemCard {...app} />
</motion.div>
);
}
});

AnimatedEcosystemCard.displayName = 'AnimatedEcosystemCard';

export function List({
selectedCategories,
Expand Down
30 changes: 25 additions & 5 deletions apps/web/src/components/Ecosystem/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,35 @@ export function SearchBar({
setSearch: Dispatch<SetStateAction<string>>;
}) {
const [isExpanded, setIsExpanded] = useState(false);
const debounced = useRef<number>();
const [localSearch, setLocalSearch] = useState(search);
const debounced = useRef<ReturnType<typeof setTimeout>>();
const inputRef = useRef<HTMLInputElement>(null);
const mobileInputRef = useRef<HTMLInputElement>(null);

// Sync local search with prop when cleared externally
useEffect(() => {
if (search === '') {
setLocalSearch('');
}
}, [search]);

// Cleanup timeout on unmount to prevent setState on unmounted component
useEffect(() => {
return () => {
clearTimeout(debounced.current);
};
}, []);

const onChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(debounced.current);

const value = e.target.value;
setSearch(value);
setLocalSearch(value); // Update local state immediately for responsive input
// Debounce the parent state update with 300ms delay
debounced.current = setTimeout(() => {
setSearch(value);
}, 300);
},
[setSearch],
);
Expand All @@ -71,6 +90,7 @@ export function SearchBar({
}, []);

const clearInput = useCallback(() => {
setLocalSearch('');
setSearch('');
setIsExpanded(false);
}, [setSearch]);
Expand Down Expand Up @@ -106,7 +126,7 @@ export function SearchBar({
ref={inputRef}
type="text"
id="appsSearchBar"
value={search}
value={localSearch}
onChange={onChange}
className="hidden max-w-[100px] flex-1 font-sans text-base text-black placeholder:text-base-gray-200 focus:outline-none md:block md:max-w-none"
placeholder="Search"
Expand All @@ -118,7 +138,7 @@ export function SearchBar({
<motion.input
ref={mobileInputRef}
type="text"
value={search}
value={localSearch}
onChange={onChange}
onBlur={onBlur}
className="max-w-[100px] flex-1 font-sans text-base text-black placeholder:text-base-gray-200 focus:outline-none md:hidden md:max-w-none"
Expand All @@ -134,7 +154,7 @@ export function SearchBar({
</AnimatePresence>

<AnimatePresence>
{search && (
{localSearch && (
<motion.button
type="button"
onClick={clearInput}
Expand Down