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
29 changes: 23 additions & 6 deletions components/user/rep/MobileRepTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export default function MobileRepTabContent({
canEditRep,
visibleCount,
onShowMore,
hasNextPage,
isFetchingNextPage,
onGrantRep,
onEditCategory,
}: {
Expand All @@ -55,9 +57,25 @@ export default function MobileRepTabContent({
readonly canEditRep: boolean;
readonly visibleCount: number;
readonly onShowMore: () => void;
readonly hasNextPage: boolean;
readonly isFetchingNextPage: boolean;
readonly onGrantRep: () => void;
readonly onEditCategory: (category: string) => void;
}) {
const hiddenLoadedCategoryCount = Math.max(
categories.length - visibleCount,
0
);
const hasMore = hiddenLoadedCategoryCount > 0 || hasNextPage;
let loadMoreLabel = "Load more";
if (hiddenLoadedCategoryCount > 0) {
loadMoreLabel = `+${hiddenLoadedCategoryCount} more`;
} else if (isFetchingNextPage) {
loadMoreLabel = "Loading...";
}
const isLoadMoreDisabled =
isFetchingNextPage && hiddenLoadedCategoryCount === 0;

return (
<>
{canEditRep && repDirection === "received" && (
Expand Down Expand Up @@ -119,9 +137,7 @@ export default function MobileRepTabContent({
</span>
<span className="tw-text-xs tw-font-semibold tw-text-iron-300">
{overview.authenticated_user_contribution > 0 && "+"}
{formatNumberWithCommas(
overview.authenticated_user_contribution
)}
{formatNumberWithCommas(overview.authenticated_user_contribution)}
</span>
</div>
)}
Expand Down Expand Up @@ -154,13 +170,14 @@ export default function MobileRepTabContent({
compact
/>
))}
{categories.length > visibleCount && (
{hasMore && (
<button
type="button"
onClick={onShowMore}
className="tw-inline-flex tw-cursor-pointer tw-items-center tw-gap-2.5 tw-rounded-lg tw-border tw-border-solid tw-border-iron-700/60 tw-bg-iron-900/60 tw-px-4 tw-py-2.5 tw-text-xs tw-font-semibold tw-text-iron-400 tw-transition-colors hover:tw-border-iron-600/60 hover:tw-bg-iron-800/60 hover:tw-text-iron-300"
disabled={isLoadMoreDisabled}
className="tw-inline-flex tw-items-center tw-gap-2.5 tw-rounded-lg tw-border tw-border-solid tw-border-iron-700/60 tw-bg-iron-900/60 tw-px-4 tw-py-2.5 tw-text-xs tw-font-semibold tw-text-iron-400 tw-transition-colors hover:tw-border-iron-600/60 hover:tw-bg-iron-800/60 hover:tw-text-iron-300 disabled:tw-cursor-default disabled:tw-opacity-70"
>
+{categories.length - visibleCount} more
{loadMoreLabel}
</button>
)}
</div>
Expand Down
192 changes: 151 additions & 41 deletions components/user/rep/UserPageRep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { AuthContext } from "@/components/auth/Auth";
import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import MobileWrapperDialog from "@/components/mobile-wrapper-dialog/MobileWrapperDialog";
import { RateMatter } from "@/types/enums";
import { useQuery } from "@tanstack/react-query";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useParams } from "next/navigation";
import { useContext, useMemo, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import UserPageIdentityHeader from "../identity/header/UserPageIdentityHeader";
import UserPageIdentityHeaderCICRate from "../identity/header/cic-rate/UserPageIdentityHeaderCICRate";
import UserPageIdentityStatements from "../identity/statements/UserPageIdentityStatements";
Expand All @@ -25,6 +25,55 @@ import { getCanEditNic } from "./UserPageRep.helpers";
import UserPageRepHeader from "./header/UserPageRepHeader";
import UserPageRepMobile from "./UserPageRepMobile";

const INITIAL_VISIBLE_CATEGORY_COUNT = 5;
const VISIBLE_CATEGORY_LOAD_STEP = 10;
const REP_CATEGORIES_PAGE_SIZE = 25;
const TOP_CONTRIBUTORS_LIMIT = 5;
const FIRST_REP_CATEGORIES_PAGE = 1;

const getDefaultVisibleCounts = (): Record<RepDirection, number> => ({
received: INITIAL_VISIBLE_CATEGORY_COUNT,
given: INITIAL_VISIBLE_CATEGORY_COUNT,
});

const getDefaultFetchStates = (): Record<RepDirection, boolean> => ({
received: false,
given: false,
});

function useRepCategories({
user,
queryDirection,
apiDirection,
enabled,
}: {
readonly user: string;
readonly queryDirection: "incoming" | "outgoing";
readonly apiDirection?: "outgoing";
readonly enabled: boolean;
}) {
return useInfiniteQuery({
queryKey: [
QueryKey.REP_CATEGORIES,
{ handleOrWallet: user, direction: queryDirection },
],
queryFn: async ({ pageParam }: { pageParam: number }) =>
await commonApiFetch<ApiRepCategoriesPage>({
endpoint: `profiles/${user}/rep/categories`,
params: {
...(apiDirection ? { direction: apiDirection } : {}),
page: pageParam.toString(),
page_size: REP_CATEGORIES_PAGE_SIZE.toString(),
top_contributors_limit: TOP_CONTRIBUTORS_LIMIT.toString(),
},
}),
initialPageParam: FIRST_REP_CATEGORIES_PAGE,
getNextPageParam: (lastPage: ApiRepCategoriesPage | undefined) =>
lastPage?.next ? lastPage.page + 1 : undefined,
enabled,
});
}

export default function UserPageRep({
profile,
initialActivityLogParams,
Expand All @@ -39,6 +88,13 @@ export default function UserPageRep({

const [repDirection, setRepDirection] = useState<RepDirection>("received");
const [isNicRateOpen, setIsNicRateOpen] = useState(false);
const [visibleCounts, setVisibleCounts] = useState(getDefaultVisibleCounts);
const visibleCountsRef = useRef(visibleCounts);
const isFetchingNextPageRef = useRef(getDefaultFetchStates());

useEffect(() => {
visibleCountsRef.current = visibleCounts;
}, [visibleCounts]);

const canEditNic = useMemo(
() =>
Expand All @@ -65,23 +121,11 @@ export default function UserPageRep({
enabled: !!user,
});

const { data: repCategories, isFetching: isFetchingCategories } =
useQuery<ApiRepCategoriesPage>({
queryKey: [
QueryKey.REP_CATEGORIES,
{ handleOrWallet: user, direction: "incoming" },
],
queryFn: async () =>
await commonApiFetch<ApiRepCategoriesPage>({
endpoint: `profiles/${user}/rep/categories`,
params: {
page: "1",
page_size: "100",
top_contributors_limit: "5",
},
}),
enabled: !!user,
});
const repCategoriesQuery = useRepCategories({
user,
queryDirection: "incoming",
enabled: !!user,
});

// --- Outgoing (given) rep --- only fetch when active
const { data: repOverviewGiven, isFetching: isFetchingOverviewGiven } =
Expand All @@ -98,24 +142,12 @@ export default function UserPageRep({
enabled: !!user && repDirection === "given",
});

const { data: repCategoriesGiven, isFetching: isFetchingCategoriesGiven } =
useQuery<ApiRepCategoriesPage>({
queryKey: [
QueryKey.REP_CATEGORIES,
{ handleOrWallet: user, direction: "outgoing" },
],
queryFn: async () =>
await commonApiFetch<ApiRepCategoriesPage>({
endpoint: `profiles/${user}/rep/categories`,
params: {
direction: "outgoing",
page: "1",
page_size: "100",
top_contributors_limit: "5",
},
}),
enabled: !!user && repDirection === "given",
});
const repCategoriesGivenQuery = useRepCategories({
user,
queryDirection: "outgoing",
apiDirection: "outgoing",
enabled: !!user && repDirection === "given",
});

// --- CIC overview ---
const { data: cicOverview } = useQuery<ApiCicOverview>({
Expand All @@ -127,19 +159,89 @@ export default function UserPageRep({
enabled: !!user,
});

const repCategories = useMemo(
() => repCategoriesQuery.data?.pages.flatMap((page) => page.data) ?? [],
[repCategoriesQuery.data]
);
const repCategoriesGiven = useMemo(
() =>
repCategoriesGivenQuery.data?.pages.flatMap((page) => page.data) ?? [],
[repCategoriesGivenQuery.data]
);

// Pick active direction's data
const activeOverview =
repDirection === "received"
? (repOverview ?? null)
: (repOverviewGiven ?? null);
const activeCategories =
repDirection === "received" ? repCategories : repCategoriesGiven;
const activeHasNextPage =
repDirection === "received"
? (repCategories?.data ?? [])
: (repCategoriesGiven?.data ?? []);
? Boolean(repCategoriesQuery.hasNextPage)
: Boolean(repCategoriesGivenQuery.hasNextPage);
const activeIsFetchingNextPage =
repDirection === "received"
? repCategoriesQuery.isFetchingNextPage
: repCategoriesGivenQuery.isFetchingNextPage;
const activeVisibleCount = visibleCounts[repDirection];
const activeLoading =
repDirection === "received"
? isFetchingOverview || isFetchingCategories
: isFetchingOverviewGiven || isFetchingCategoriesGiven;
? isFetchingOverview || repCategoriesQuery.isFetching
: isFetchingOverviewGiven || repCategoriesGivenQuery.isFetching;

const handleShowMore = useCallback(() => {
const nextVisibleCounts = {
...visibleCountsRef.current,
[repDirection]:
visibleCountsRef.current[repDirection] + VISIBLE_CATEGORY_LOAD_STEP,
};
visibleCountsRef.current = nextVisibleCounts;
setVisibleCounts(nextVisibleCounts);

const loadedCount =
repDirection === "received"
? repCategories.length
: repCategoriesGiven.length;
const hasNextPage =
repDirection === "received"
? Boolean(repCategoriesQuery.hasNextPage)
: Boolean(repCategoriesGivenQuery.hasNextPage);
const isFetchingNextPage =
repDirection === "received"
? repCategoriesQuery.isFetchingNextPage
: repCategoriesGivenQuery.isFetchingNextPage;
const fetchNextPage =
repDirection === "received"
? repCategoriesQuery.fetchNextPage
: repCategoriesGivenQuery.fetchNextPage;

if (
nextVisibleCounts[repDirection] > loadedCount &&
hasNextPage &&
!isFetchingNextPage &&
!isFetchingNextPageRef.current[repDirection]
) {
isFetchingNextPageRef.current[repDirection] = true;
fetchNextPage()
.catch(() => {
// Errors are surfaced via query state rendered in the UI.
})
.finally(() => {
isFetchingNextPageRef.current[repDirection] = false;
});
}
}, [
repCategories.length,
repCategoriesGiven.length,
repCategoriesQuery.fetchNextPage,
repCategoriesQuery.hasNextPage,
repCategoriesQuery.isFetchingNextPage,
repCategoriesGivenQuery.fetchNextPage,
repCategoriesGivenQuery.hasNextPage,
repCategoriesGivenQuery.isFetchingNextPage,
repDirection,
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<div className="tailwind-scope">
Expand All @@ -153,6 +255,10 @@ export default function UserPageRep({
onRepDirectionChange={setRepDirection}
initialActivityLogParams={initialActivityLogParams}
loading={activeLoading}
visibleCount={activeVisibleCount}
onShowMore={handleShowMore}
hasNextPage={activeHasNextPage}
isFetchingNextPage={activeIsFetchingNextPage}
/>
</div>
<div className="tw-hidden lg:tw-block">
Expand All @@ -165,6 +271,10 @@ export default function UserPageRep({
repDirection={repDirection}
onRepDirectionChange={setRepDirection}
loading={activeLoading}
visibleCount={activeVisibleCount}
onShowMore={handleShowMore}
hasNextPage={activeHasNextPage}
isFetchingNextPage={activeIsFetchingNextPage}
/>
<div className="tw-mt-6 lg:tw-mt-8">
<UserPageCombinedActivityLog
Expand Down
19 changes: 11 additions & 8 deletions components/user/rep/UserPageRepMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export default function UserPageRepMobile({
onRepDirectionChange,
initialActivityLogParams,
loading,
visibleCount,
onShowMore,
hasNextPage,
isFetchingNextPage,
}: {
readonly profile: ApiIdentity;
readonly overview: ApiRepOverview | null;
Expand All @@ -42,22 +46,19 @@ export default function UserPageRepMobile({
readonly onRepDirectionChange: (direction: RepDirection) => void;
readonly initialActivityLogParams: ActivityLogParams;
readonly loading: boolean;
readonly visibleCount: number;
readonly onShowMore: () => void;
readonly hasNextPage: boolean;
readonly isFetchingNextPage: boolean;
}) {
const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
const { address } = useSeizeConnectContext();

const [activeTab, setActiveTab] = useState<MobileTab>("rep");
const [isGrantRepOpen, setIsGrantRepOpen] = useState(false);
const [isNicRateOpen, setIsNicRateOpen] = useState(false);
const [visibleCount, setVisibleCount] = useState(5);
const [editCategory, setEditCategory] = useState<string | null>(null);

const [prevCategories, setPrevCategories] = useState(categories);
if (categories !== prevCategories) {
setPrevCategories(categories);
setVisibleCount(5);
}

const canEditRep = useMemo(
() =>
getCanEditRep({
Expand Down Expand Up @@ -125,7 +126,9 @@ export default function UserPageRepMobile({
loading={loading}
canEditRep={canEditRep}
visibleCount={visibleCount}
onShowMore={() => setVisibleCount((prev) => prev + 10)}
onShowMore={onShowMore}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onGrantRep={() => setIsGrantRepOpen(true)}
onEditCategory={setEditCategory}
/>
Expand Down
3 changes: 2 additions & 1 deletion components/user/rep/UserPageRepWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function UserPageRepWrapper({
readonly initialActivityLogParams: ActivityLogParams;
}) {
const params = useParams();
const user = (params?.["user"] as string)?.toLowerCase();
const user = (params["user"] as string).toLowerCase();

const { profile } = useIdentity({
handleOrWallet: user,
Expand All @@ -24,6 +24,7 @@ export default function UserPageRepWrapper({
return (
<UserPageSetUpProfileWrapper profile={profile ?? initialProfile}>
<UserPageRep
key={user}
profile={profile ?? initialProfile}
initialActivityLogParams={initialActivityLogParams}
/>
Expand Down
Loading
Loading