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
39 changes: 26 additions & 13 deletions src/components/PaginatedNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,27 @@ type PaginationControlsProps = {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
// Optional key whose change can shrink/grow pages immediately (e.g. active filter)
filterKey?: string;
};

export const PaginatedNavigation = ({
currentPage,
totalPages,
onPageChange,
filterKey,
}: PaginationControlsProps) => {
const { chainId } = useUserStore();

const lastValidTotalPagesRef = useRef(1);
const lastChainIdRef = useRef<number | null>(null);
const chainChangeFrameRef = useRef(0);

const lastFilterKeyRef = useRef<string | undefined>(undefined);
const filterChangeFrameRef = useRef(0);

const chainHasChanged = chainId !== lastChainIdRef.current;
const filterHasChanged = filterKey !== lastFilterKeyRef.current;

if (chainHasChanged) {
lastChainIdRef.current = chainId ?? null;
Expand All @@ -36,66 +43,72 @@ export const PaginatedNavigation = ({
chainChangeFrameRef.current++;
}

if (filterHasChanged) {
lastFilterKeyRef.current = filterKey;
filterChangeFrameRef.current = 0;
} else {
filterChangeFrameRef.current++;
}

let stableTotalPages = lastValidTotalPagesRef.current;

const isRecentChainChange = chainChangeFrameRef.current <= 5;

if (chainHasChanged || isRecentChainChange) {
const isRecentFilterChange = filterChangeFrameRef.current <= 5;

if (
chainHasChanged ||
filterHasChanged ||
isRecentChainChange ||
isRecentFilterChange
) {
stableTotalPages = Math.max(totalPages, 1);
} else if (totalPages > 0 && totalPages >= lastValidTotalPagesRef.current) {
stableTotalPages = totalPages;
}
// Reset page if it no longer exists after filter change
if (filterHasChanged && currentPage > stableTotalPages) {
onPageChange(1);
}

lastValidTotalPagesRef.current = stableTotalPages;

// Don't render pagination if no pages or invalid state
if (!stableTotalPages || stableTotalPages <= 0 || currentPage <= 0) {
return null;
}

const generatePages = () => {
const pages: (number | 'ellipsis')[] = [];

// Mobile-first approach: show fewer pages on small screens
const isMobile = typeof window !== 'undefined' && window.innerWidth < 640;
const maxVisiblePages = isMobile ? 3 : 7;

if (stableTotalPages <= maxVisiblePages) {
// Show all pages if within limit
for (let i = 1; i <= stableTotalPages; i++) {
pages.push(i);
}
} else if (isMobile) {
// Mobile: simplified pagination - only show current and neighbors
if (currentPage === 1) {
// At start: 1 2 ... last
pages.push(1, 2, 'ellipsis', stableTotalPages);
} else if (currentPage === stableTotalPages) {
// At end: 1 ... (last-1) last
pages.push(1, 'ellipsis', stableTotalPages - 1, stableTotalPages);
} else {
// Middle: 1 ... current ... last
pages.push(1, 'ellipsis', currentPage, 'ellipsis', stableTotalPages);
}
} else {
// Desktop: full pagination logic
pages.push(1);

if (currentPage <= 3) {
// Near beginning: 1 2 3 4 ... last
for (let i = 2; i <= 4; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(stableTotalPages);
} else if (currentPage >= stableTotalPages - 2) {
// Near end: 1 ... (last-3) (last-2) (last-1) last
pages.push('ellipsis');
for (let i = stableTotalPages - 3; i <= stableTotalPages; i++) {
pages.push(i);
}
} else {
// In middle: 1 ... (current-1) current (current+1) ... last
pages.push('ellipsis');
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
pages.push(i);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function SelectTrigger({
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4" />
<ChevronDownIcon className="text-foreground size-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
Expand Down
45 changes: 45 additions & 0 deletions src/hooks/useFilterParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useSearch, useNavigate } from '@tanstack/react-router';

/**
* Synchronize a string filter value with the URL search params.
* Ensures the value is part of the allowedValues list; otherwise falls back to defaultValue.
*
* @param paramName Query string key to store the filter value under.
* @param allowedValues List of allowed string values for the filter.
* @param defaultValue Default value if none present or invalid.
* @returns [currentValue, setValue]
*/
export function useFilterParam(
paramName: string,
allowedValues: string[],
defaultValue: string
) {
const search = useSearch({ strict: false });
const navigate = useNavigate();

const rawCandidate =
search && Object.prototype.hasOwnProperty.call(search, paramName)
? (search as Record<string, unknown>)[paramName]
: undefined;

const value =
typeof rawCandidate === 'string' && allowedValues.includes(rawCandidate)
? rawCandidate
: defaultValue;

const setValue = (newValue: string) => {
if (!allowedValues.includes(newValue)) return; // ignore invalid values
if (newValue !== value) {
navigate({
search: (prev) => ({
...prev,
[paramName]: newValue,
}),
replace: true,
resetScroll: false,
});
}
};

return [value, setValue] as const;
}
23 changes: 19 additions & 4 deletions src/modules/apps/AppsPreviewTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
import { execute } from '@/graphql/poco/execute';
import { App_OrderBy, OrderDirection } from '@/graphql/poco/graphql';
import { cn } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
import { LoaderCircle } from 'lucide-react';
Expand All @@ -8,25 +9,39 @@ import { DataTable } from '@/components/DataTable';
import AppIcon from '@/components/icons/AppIcon';
import { Button } from '@/components/ui/button';
import useUserStore from '@/stores/useUser.store';
import { createPlaceholderDataFnForQueryKey } from '@/utils/createPlaceholderDataFnForQueryKey';
import { createPlaceholderDataFn } from '@/utils/createPlaceholderDataFnForQueryKey';
import { getRecentFromTimestamp } from '@/utils/format';
import { ErrorAlert } from '../ErrorAlert';
import { appsQuery } from './appsQuery';
import { columns } from './appsTable/columns';

export function AppsPreviewTable({ className }: { className?: string }) {
const { chainId } = useUserStore();

const queryKey = [chainId, 'apps_preview'];
// Pertinent ordering: usageCount desc + recent usage constraint (last 14 days)
const recentFrom = getRecentFromTimestamp();
const orderBy: App_OrderBy = App_OrderBy.UsageCount;
const orderDirection: OrderDirection = OrderDirection.Desc;
const queryKey = [
chainId,
'apps_preview',
orderBy,
orderDirection,
recentFrom,
];
const apps = useQuery({
queryKey,
queryFn: () =>
execute(appsQuery, chainId, {
length: PREVIEW_TABLE_LENGTH,
skip: 0,
orderBy,
orderDirection,
recentFrom,
}),
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
enabled: !!chainId,
placeholderData: createPlaceholderDataFnForQueryKey(queryKey),
placeholderData: createPlaceholderDataFn(),
});

const formattedData =
Expand All @@ -40,7 +55,7 @@ export function AppsPreviewTable({ className }: { className?: string }) {
<div className="flex items-center justify-between">
<h2 className="flex items-center gap-2 font-sans">
<AppIcon size={20} className="text-foreground" />
Latest apps deployed
Most pertinent apps
{apps.data && apps.isError && (
<span className="text-muted-foreground text-sm font-light">
(outdated)
Expand Down
19 changes: 13 additions & 6 deletions src/modules/apps/appsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ export const appsQuery = graphql(`
$skip: Int = 0
$nextSkip: Int = 20
$nextNextSkip: Int = 40
$orderBy: App_orderBy = timestamp
$orderDirection: OrderDirection = desc
$recentFrom: BigInt = 0
) {
apps(
first: $length
skip: $skip
orderBy: timestamp
orderDirection: desc
where: { lastUsageTimestamp_gte: $recentFrom }
orderBy: $orderBy
orderDirection: $orderDirection
) {
address: id
owner {
Expand All @@ -23,6 +27,7 @@ export const appsQuery = graphql(`
multiaddr
checksum
mrenclave
lastUsageTimestamp
transfers(orderBy: timestamp, orderDirection: desc) {
transaction {
txHash: id
Expand All @@ -34,16 +39,18 @@ export const appsQuery = graphql(`
appsHasNext: apps(
first: 1
skip: $nextSkip
orderBy: timestamp
orderDirection: desc
orderBy: $orderBy
orderDirection: $orderDirection
where: { lastUsageTimestamp_gte: $recentFrom }
) {
address: id
}
appsHasNextNext: apps(
first: 1
skip: $nextNextSkip
orderBy: timestamp
orderDirection: desc
orderBy: $orderBy
orderDirection: $orderDirection
where: { lastUsageTimestamp_gte: $recentFrom }
) {
address: id
}
Expand Down
9 changes: 7 additions & 2 deletions src/modules/search/SearcherBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
};

run();
}, [initialSearch, chainId]);

Check warning on line 128 in src/modules/search/SearcherBar.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has a missing dependency: 'mutateAsync'. Either include it or remove the dependency array

useEffect(() => {
if (errorCount > 0) {
Expand Down Expand Up @@ -174,8 +174,13 @@
/>
</div>

<div className={cn('mt-4 flex justify-center gap-4', isError && 'mt-10')}>
<div className="flex justify-center sm:hidden">
<div
className={cn(
'mt-4 flex justify-center gap-4 sm:hidden',
isError && 'mt-10'
)}
>
<div className="flex justify-center">
<Button variant="outline" onClick={handleSearch} disabled={isPending}>
{isPending ? 'Searching...' : 'Search'}
</Button>
Expand Down
23 changes: 19 additions & 4 deletions src/modules/workerpools/WorkerpoolsPreviewTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PREVIEW_TABLE_LENGTH, PREVIEW_TABLE_REFETCH_INTERVAL } from '@/config';
import { execute } from '@/graphql/poco/execute';
import { Workerpool_OrderBy, OrderDirection } from '@/graphql/poco/graphql';
import { cn } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
import { LoaderCircle } from 'lucide-react';
Expand All @@ -8,25 +9,39 @@ import { DataTable } from '@/components/DataTable';
import WorkerpoolIcon from '@/components/icons/WorkerpoolIcon';
import { Button } from '@/components/ui/button';
import useUserStore from '@/stores/useUser.store';
import { createPlaceholderDataFnForQueryKey } from '@/utils/createPlaceholderDataFnForQueryKey';
import { createPlaceholderDataFn } from '@/utils/createPlaceholderDataFnForQueryKey';
import { getRecentFromTimestamp } from '@/utils/format';
import { ErrorAlert } from '../ErrorAlert';
import { workerpoolsQuery } from './workerpoolsQuery';
import { columns } from './workerpoolsTable/columns';

export function WorkerpoolsPreviewTable({ className }: { className?: string }) {
const { chainId } = useUserStore();

const queryKey = [chainId, 'workerpools_preview'];
// Pertinent ordering: usageCount desc + recent usage constraint (last 14 days)
const recentFrom = getRecentFromTimestamp();
const orderBy: Workerpool_OrderBy = Workerpool_OrderBy.UsageCount;
const orderDirection: OrderDirection = OrderDirection.Desc;
const queryKey = [
chainId,
'workerpools_preview',
orderBy,
orderDirection,
recentFrom,
];
const workerpools = useQuery({
queryKey,
queryFn: () =>
execute(workerpoolsQuery, chainId, {
length: PREVIEW_TABLE_LENGTH,
skip: 0,
orderBy,
orderDirection,
recentFrom,
}),
refetchInterval: PREVIEW_TABLE_REFETCH_INTERVAL,
enabled: !!chainId,
placeholderData: createPlaceholderDataFnForQueryKey(queryKey),
placeholderData: createPlaceholderDataFn(),
});

const formattedData =
Expand All @@ -40,7 +55,7 @@ export function WorkerpoolsPreviewTable({ className }: { className?: string }) {
<div className="flex items-center justify-between">
<h2 className="flex items-center gap-2 font-sans">
<WorkerpoolIcon size={20} className="text-foreground" />
Latest workerpools deployed
Most pertinent workerpools
{workerpools.data && workerpools.isError && (
<span className="text-muted-foreground text-sm font-light">
(outdated)
Expand Down
Loading
Loading