-
+
- {/* Right Sidebar - Identity Card */}
@@ -80,7 +163,10 @@ export default function UserPageRep({
-
);
diff --git a/components/user/rep/UserPageRepMobile.tsx b/components/user/rep/UserPageRepMobile.tsx
index 6a3e0f51be..fc0a57ebac 100644
--- a/components/user/rep/UserPageRepMobile.tsx
+++ b/components/user/rep/UserPageRepMobile.tsx
@@ -3,20 +3,14 @@
import { AuthContext } from "@/components/auth/Auth";
import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import MobileWrapperDialog from "@/components/mobile-wrapper-dialog/MobileWrapperDialog";
-import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import type { ActivityLogParams } from "@/components/profile-activity/ProfileActivityLogs";
-import type {
- ApiProfileRepRatesState,
- RatingWithProfileInfoAndLevel,
-} from "@/entities/IProfile";
-import { SortDirection } from "@/entities/ISort";
+import type { ApiRepOverview } from "@/generated/models/ApiRepOverview";
+import type { ApiRepCategory } from "@/generated/models/ApiRepCategory";
+import type { ApiCicOverview } from "@/generated/models/ApiCicOverview";
import type { ApiIdentity } from "@/generated/models/ApiIdentity";
import { ApiProfileProxyActionType } from "@/generated/models/ApiProfileProxyActionType";
import { amIUser, formatNumberWithCommas } from "@/helpers/Helpers";
-import type { Page } from "@/helpers/Types";
-import { commonApiFetch } from "@/services/api/common-api";
-import { ProfileRatersParamsOrderBy, RateMatter } from "@/types/enums";
-import { useQuery } from "@tanstack/react-query";
+import { RateMatter } from "@/types/enums";
import { AnimatePresence, motion } from "framer-motion";
import { useContext, useEffect, useMemo, useState } from "react";
import UserPageIdentityHeaderCICRate from "../identity/header/cic-rate/UserPageIdentityHeaderCICRate";
@@ -25,30 +19,59 @@ import UserPageIdentityStatements from "../identity/statements/UserPageIdentityS
import UserPageRateWrapper from "../utils/rate/UserPageRateWrapper";
import UserCICStatus from "../utils/user-cic-status/UserCICStatus";
import UserCICTypeIcon from "../utils/user-cic-type/UserCICTypeIcon";
-import TopRaterAvatars from "./header/TopRaterAvatars";
+import OverlappingAvatars from "@/components/common/OverlappingAvatars";
import UserPageRepModifyModal from "./modify-rep/UserPageRepModifyModal";
import GrantRepDialog from "./new-rep/GrantRepDialog";
+import { ArrowDownLeftIcon, ArrowUpRightIcon } from "@heroicons/react/24/solid";
+import { getContributorLabel, type RepDirection } from "./UserPageRep.helpers";
import RepCategoryPill from "./RepCategoryPill";
import UserPageCombinedActivityLog from "./UserPageCombinedActivityLog";
-import {
- getCanEditRep,
- sortRepsByRatingAndContributors,
-} from "./UserPageRep.helpers";
+import { getCanEditRep } from "./UserPageRep.helpers";
type MobileTab = "rep" | "identity";
+function RepEmptyState({
+ loading,
+ repDirection,
+}: {
+ readonly loading: boolean;
+ readonly repDirection: RepDirection;
+}) {
+ if (loading) {
+ return (
+
+ );
+ }
+ return (
+
+ {repDirection === "given" ? "No rep given yet." : "No rep received yet."}
+
+ );
+}
+
export default function UserPageRepMobile({
profile,
- repRates,
+ overview,
+ categories,
+ cicOverview,
+ repDirection,
+ onRepDirectionChange,
initialActivityLogParams,
+ loading,
}: {
readonly profile: ApiIdentity;
- readonly repRates: ApiProfileRepRatesState | null;
+ readonly overview: ApiRepOverview | null;
+ readonly categories: ApiRepCategory[];
+ readonly cicOverview: ApiCicOverview | null;
+ readonly repDirection: RepDirection;
+ readonly onRepDirectionChange: (direction: RepDirection) => void;
readonly initialActivityLogParams: ActivityLogParams;
+ readonly loading: boolean;
}) {
const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
const { address } = useSeizeConnectContext();
- const profileHandle = profile.handle ?? "";
const [activeTab, setActiveTab] = useState
("rep");
const [isGrantRepOpen, setIsGrantRepOpen] = useState(false);
@@ -56,55 +79,9 @@ export default function UserPageRepMobile({
const [visibleCount, setVisibleCount] = useState(5);
const [editCategory, setEditCategory] = useState(null);
- const { data: nicRatings } = useQuery>({
- queryKey: [
- QueryKey.PROFILE_RATERS,
- {
- handleOrWallet: profileHandle,
- matter: RateMatter.NIC,
- page: 1,
- pageSize: 1,
- order: SortDirection.DESC,
- orderBy: ProfileRatersParamsOrderBy.RATING,
- given: false,
- },
- ],
- queryFn: async () =>
- await commonApiFetch>({
- endpoint: `profiles/${profileHandle}/cic/ratings/by-rater`,
- params: {
- page: `${1}`,
- page_size: `${1}`,
- order: SortDirection.DESC.toLowerCase(),
- order_by: ProfileRatersParamsOrderBy.RATING.toLowerCase(),
- given: "false",
- },
- }),
- enabled: !!profileHandle,
- });
-
- // Close modals when viewport reaches lg breakpoint
- useEffect(() => {
- const mq = globalThis.matchMedia("(min-width: 1024px)");
- const handler = (e: MediaQueryListEvent) => {
- if (e.matches) {
- setIsGrantRepOpen(false);
- setIsNicRateOpen(false);
- }
- };
- mq.addEventListener("change", handler);
- return () => mq.removeEventListener("change", handler);
- }, []);
-
useEffect(() => {
setVisibleCount(5);
- }, [repRates?.rating_stats]);
-
- // --- derived: sorted reps, can-edit flags ---
- const reps = useMemo(
- () => sortRepsByRatingAndContributors(repRates?.rating_stats ?? []),
- [repRates?.rating_stats]
- );
+ }, [categories]);
const canEditRep = useMemo(
() =>
@@ -135,13 +112,30 @@ export default function UserPageRepMobile({
(w) => w.wallet.toLowerCase() === address?.toLowerCase()
);
- // --- render ---
+ // Build CIC avatar items from overview contributors
+ const cicAvatarItems = useMemo(
+ () =>
+ (cicOverview?.contributors.data ?? []).slice(0, 3).map((c) => ({
+ key: c.profile.handle ?? c.profile.primary_address,
+ pfpUrl: c.profile.pfp ?? null,
+ ariaLabel: c.profile.handle ?? c.profile.primary_address,
+ fallback: c.profile.handle
+ ? c.profile.handle.charAt(0).toUpperCase()
+ : "?",
+ title: c.profile.handle ?? c.profile.primary_address,
+ tooltipContent: (
+
+ {c.profile.handle ?? c.profile.primary_address} ·{" "}
+ {formatNumberWithCommas(c.contribution)}
+
+ ),
+ })),
+ [cicOverview?.contributors.data]
+ );
return (
-
- {/* Score Cards (tappable navigation) */}
+
- {/* Rep Score */}
- {repRates
- ? formatNumberWithCommas(repRates.total_rep_rating)
- : "\u2014"}
+ {overview ? formatNumberWithCommas(overview.total_rep) : "\u2014"}
- {repRates && (
+ {overview && (
- {formatNumberWithCommas(repRates.number_of_raters)}{" "}
- {repRates.number_of_raters === 1 ? "rater" : "raters"}
+ {formatNumberWithCommas(overview.contributor_count)}{" "}
+ {getContributorLabel(
+ repDirection,
+ overview.contributor_count
+ )}
)}
@@ -236,29 +231,31 @@ export default function UserPageRepMobile({
NIC
- {formatNumberWithCommas(profile.cic)}
+ {formatNumberWithCommas(cicOverview?.total_cic ?? profile.cic)}
-
+
-
+
-
-
-
+ {cicAvatarItems.length > 0 && (
+
+
+
+ )}
- {formatNumberWithCommas(nicRatings?.count ?? 0)}{" "}
- {(nicRatings?.count ?? 0) === 1 ? "rater" : "raters"}
+ {formatNumberWithCommas(cicOverview?.contributor_count ?? 0)}{" "}
+ {(cicOverview?.contributor_count ?? 0) === 1
+ ? "rater"
+ : "raters"}
@@ -275,37 +272,77 @@ export default function UserPageRepMobile({
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: "easeInOut" }}
>
+ {/* Received / Given toggle */}
+
+
+
+
+
{/* Rep Categories */}
- {reps.length > 0 && (
+ {categories.length > 0 && (
-
+
Rep Categories
- {reps.slice(0, visibleCount).map((rep) => (
+ {categories.slice(0, visibleCount).map((cat) => (
))}
- {reps.length > visibleCount && (
+ {categories.length > visibleCount && (
)}
)}
- {canEditRep && (
+ {categories.length === 0 && (
+
+ )}
+
+ {canEditRep && repDirection === "received" && (
- {/* Grant Rep Bottom Sheet */}
setIsGrantRepOpen(false)}
/>
- {/* Rate NIC Bottom Sheet */}
setIsNicRateOpen(false)}
tabletModal
+ maxWidthClass="md:tw-max-w-md"
>
diff --git a/components/user/rep/header/TopRaterAvatars.tsx b/components/user/rep/header/TopRaterAvatars.tsx
deleted file mode 100644
index c1f5d4b822..0000000000
--- a/components/user/rep/header/TopRaterAvatars.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-"use client";
-
-import { useQuery, useQueries } from "@tanstack/react-query";
-import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
-import { commonApiFetch } from "@/services/api/common-api";
-import type { RatingWithProfileInfoAndLevel } from "@/entities/IProfile";
-import type { ApiIdentity } from "@/generated/models/ApiIdentity";
-import type { Page } from "@/helpers/Types";
-import OverlappingAvatars from "@/components/common/OverlappingAvatars";
-import { RateMatter } from "@/types/enums";
-import { formatNumberWithCommas } from "@/helpers/Helpers";
-import type { MouseEvent } from "react";
-
-const STALE_TIME = 5 * 60 * 1000;
-
-export default function TopRaterAvatars({
- handleOrWallet,
- category,
- matter = RateMatter.REP,
- withLinks = true,
- onAvatarClick,
- count = 5,
- size = "sm",
-}: {
- readonly handleOrWallet: string;
- readonly category?: string;
- readonly matter?: RateMatter.REP | RateMatter.NIC;
- readonly withLinks?: boolean;
- readonly onAvatarClick?: (e: MouseEvent) => void;
- readonly count?: number;
- readonly size?: "sm" | "md";
-}) {
- const params: Record = {
- page: "1",
- page_size: `${count}`,
- order: "desc",
- order_by: "rating",
- given: "false",
- };
- if (matter === RateMatter.REP && category) {
- params["category"] = category;
- }
-
- const ratingsEndpoint =
- matter === RateMatter.NIC
- ? `profiles/${handleOrWallet}/cic/ratings/by-rater`
- : `profiles/${handleOrWallet}/rep/ratings/by-rater`;
-
- const { data: ratersPage } = useQuery>({
- queryKey: [
- QueryKey.PROFILE_RATERS,
- {
- handleOrWallet: handleOrWallet.toLowerCase(),
- matter,
- category,
- count,
- },
- ],
- queryFn: async () =>
- await commonApiFetch>({
- endpoint: ratingsEndpoint,
- params,
- }),
- enabled: !!handleOrWallet,
- staleTime: STALE_TIME,
- });
-
- const raterHandles = ratersPage?.data.map((r) => r.handle) ?? [];
-
- const ratingByHandle = new Map(
- ratersPage?.data.map((r) => [r.handle.toLowerCase(), r.rating]) ?? []
- );
-
- const identityQueries = useQueries({
- queries: raterHandles.map((handle) => ({
- queryKey: [QueryKey.PROFILE, handle.toLowerCase()],
- queryFn: async () =>
- await commonApiFetch({
- endpoint: `identities/${handle.toLowerCase()}`,
- }),
- enabled: !!handle,
- staleTime: STALE_TIME,
- })),
- });
-
- const items = identityQueries
- .filter((q) => q.data)
- .map((q) => {
- const identity = q.data!;
- const target = identity.handle ?? identity.primary_wallet;
- const rating = target
- ? ratingByHandle.get(target.toLowerCase())
- : undefined;
- const tooltipContent =
- rating === undefined ? undefined : (
-
- {target} · {formatNumberWithCommas(rating)}
-
- );
- return {
- key: target,
- pfpUrl: identity.pfp ?? null,
- ...(withLinks && target ? { href: `/${target}` } : {}),
- ariaLabel: target,
- fallback: identity.handle
- ? identity.handle.charAt(0).toUpperCase()
- : "?",
- title: target,
- tooltipContent,
- };
- });
-
- if (items.length === 0) {
- return null;
- }
-
- return (
- onAvatarClick?.(e)}
- />
- );
-}
diff --git a/components/user/rep/header/UserPageRepHeader.tsx b/components/user/rep/header/UserPageRepHeader.tsx
index f12becacf8..bbb272e0a0 100644
--- a/components/user/rep/header/UserPageRepHeader.tsx
+++ b/components/user/rep/header/UserPageRepHeader.tsx
@@ -1,40 +1,46 @@
"use client";
import { AuthContext } from "@/components/auth/Auth";
-import type { ApiProfileRepRatesState } from "@/entities/IProfile";
+import type { ApiRepOverview } from "@/generated/models/ApiRepOverview";
+import type { ApiRepCategory } from "@/generated/models/ApiRepCategory";
import type { ApiIdentity } from "@/generated/models/ApiIdentity";
import { formatNumberWithCommas } from "@/helpers/Helpers";
+import { ArrowDownLeftIcon, ArrowUpRightIcon } from "@heroicons/react/24/solid";
import { useContext, useEffect, useMemo, useState } from "react";
import RepCategoryPill from "../RepCategoryPill";
import UserPageRepModifyModal from "../modify-rep/UserPageRepModifyModal";
import GrantRepDialog from "../new-rep/GrantRepDialog";
import {
getCanEditRep,
- sortRepsByRatingAndContributors,
+ getContributorLabel,
+ type RepDirection,
} from "../UserPageRep.helpers";
export default function UserPageRepHeader({
- repRates,
+ overview,
+ categories,
profile,
+ repDirection,
+ onRepDirectionChange,
+ loading,
}: {
- readonly repRates: ApiProfileRepRatesState | null;
+ readonly overview: ApiRepOverview | null;
+ readonly categories: ApiRepCategory[];
readonly profile: ApiIdentity;
+ readonly repDirection: RepDirection;
+ readonly onRepDirectionChange: (direction: RepDirection) => void;
+ readonly loading: boolean;
}) {
const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
- const allReps = useMemo(
- () => sortRepsByRatingAndContributors(repRates?.rating_stats ?? []),
- [repRates?.rating_stats]
- );
-
const [visibleCount, setVisibleCount] = useState(5);
useEffect(() => {
setVisibleCount(5);
- }, [repRates?.rating_stats]);
+ }, [categories]);
- const visibleReps = allReps.slice(0, visibleCount);
- const hasMore = allReps.length > visibleCount;
+ const visibleCategories = categories.slice(0, visibleCount);
+ const hasMore = categories.length > visibleCount;
const canEditRep = useMemo(
() =>
@@ -64,35 +70,83 @@ export default function UserPageRepHeader({
Rep
- What others recognize this identity for.
+ {repDirection === "received"
+ ? "What others recognize this identity for."
+ : "What this identity recognizes others for."}
-
-
- Total Rep
-
-
- {repRates
- ? formatNumberWithCommas(repRates.total_rep_rating)
- : ""}
-
- {repRates && (
+ {overview ? (
+
+
+ Total Rep
+
+
+ {formatNumberWithCommas(overview.total_rep)}
+
- {formatNumberWithCommas(repRates.number_of_raters)}{" "}
- {repRates.number_of_raters === 1 ? "rater" : "raters"}
+ {formatNumberWithCommas(overview.contributor_count)}{" "}
+ {getContributorLabel(
+ repDirection,
+ overview.contributor_count
+ )}
- )}
-
+
+ ) : (
+
+
+ Total Rep
+
+
+ —
+
+
+ )}
- {(visibleReps.length > 0 || canEditRep) && (
+
+
+
+
+
+ {(visibleCategories.length > 0 ||
+ (canEditRep && repDirection === "received")) && (
Rep Categories
- {canEditRep && (
+ {canEditRep && repDirection === "received" && (
)}
- {visibleReps.map((rep) => (
+ {visibleCategories.map((cat) => (
))}
{hasMore && (
@@ -127,12 +181,30 @@ export default function UserPageRepHeader({
onClick={() => setVisibleCount((prev) => prev + 10)}
className="tw-inline-flex tw-h-11 tw-cursor-pointer tw-items-center tw-gap-2 tw-rounded-lg tw-border tw-border-solid tw-border-white/10 tw-bg-white/5 tw-px-4 tw-text-sm tw-font-medium tw-text-iron-400 tw-backdrop-blur-md tw-transition-all tw-duration-300 tw-ease-out hover:tw-border-white/20 hover:tw-bg-white/10 hover:tw-text-white"
>
- +{allReps.length - visibleCount} more
+ +{categories.length - visibleCount} more
)}
)}
+
+ {categories.length === 0 &&
+ !loading &&
+ !(canEditRep && repDirection === "received") && (
+
+ {repDirection === "given"
+ ? "This identity hasn't given any rep yet."
+ : "This identity hasn't received any rep yet."}
+
+ )}
+
+ {categories.length === 0 &&
+ loading &&
+ !(canEditRep && repDirection === "received") && (
+
+ )}