diff --git a/__tests__/components/user/rep/UserPageRep.test.tsx b/__tests__/components/user/rep/UserPageRep.test.tsx
index 3e95d3cadd..f7ec122db4 100644
--- a/__tests__/components/user/rep/UserPageRep.test.tsx
+++ b/__tests__/components/user/rep/UserPageRep.test.tsx
@@ -6,11 +6,7 @@ import { render } from "@testing-library/react";
jest.mock("@tanstack/react-query", () => ({ useQuery: jest.fn() }));
let headerProps: any;
-let newRepProps: any;
-let repsProps: any;
-let tableParams: any[] = [];
let activityProps: any;
-let rateWrapperProps: any;
jest.mock(
"@/components/user/rep/header/UserPageRepHeader",
@@ -20,34 +16,21 @@ jest.mock(
}
);
jest.mock(
- "@/components/user/rep/new-rep/UserPageRepNewRep",
+ "@/components/user/rep/UserPageCombinedActivityLog",
() => (props: any) => {
- newRepProps = props;
- return
;
- }
-);
-jest.mock("@/components/user/rep/reps/UserPageRepReps", () => (props: any) => {
- repsProps = props;
- return ;
-});
-jest.mock(
- "@/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapper",
- () => (props: any) => {
- tableParams.push(props.initialParams);
- return ;
+ activityProps = props;
+ return ;
}
);
jest.mock(
- "@/components/user/rep/UserPageRepActivityLog",
+ "@/components/user/rep/UserPageRepMobile",
() => (props: any) => {
- activityProps = props;
- return ;
+ return ;
}
);
jest.mock(
"@/components/user/utils/rate/UserPageRateWrapper",
() => (props: any) => {
- rateWrapperProps = props;
return {props.children}
;
}
);
@@ -56,13 +39,7 @@ describe("UserPageRep", () => {
const queryMock = useQuery as jest.Mock;
beforeEach(() => {
- headerProps =
- newRepProps =
- repsProps =
- activityProps =
- rateWrapperProps =
- undefined;
- tableParams = [];
+ headerProps = activityProps = undefined;
queryMock.mockReturnValue({ data: { score: 1 } });
});
@@ -74,19 +51,12 @@ describe("UserPageRep", () => {
value={{ connectedProfile: { handle: "charlie" } } as any}>
);
expect(headerProps.repRates).toEqual({ score: 1 });
- expect(newRepProps.profile).toBe(profile);
- expect(newRepProps.repRates).toEqual({ score: 1 });
- expect(repsProps.profile).toBe(profile);
- expect(repsProps.repRates).toEqual({ score: 1 });
- expect(tableParams.slice(0, 2)).toEqual([params, params]);
+ expect(headerProps.profile).toBe(profile);
expect(activityProps.initialActivityLogParams).toBe(params);
- expect(rateWrapperProps.profile).toBe(profile);
});
});
diff --git a/__tests__/components/user/rep/header/UserPageRepHeader.test.tsx b/__tests__/components/user/rep/header/UserPageRepHeader.test.tsx
index 9400b97d1c..1d17256f5a 100644
--- a/__tests__/components/user/rep/header/UserPageRepHeader.test.tsx
+++ b/__tests__/components/user/rep/header/UserPageRepHeader.test.tsx
@@ -20,6 +20,6 @@ describe('UserPageRepHeader', () => {
it('renders without repRates', () => {
const { container } = render();
- expect(container).toHaveTextContent('Reputation');
+ expect(container).toHaveTextContent('Rep');
});
});
diff --git a/__tests__/components/user/rep/new-rep/UserPageRepNewRepSearchHeader.test.tsx b/__tests__/components/user/rep/new-rep/UserPageRepNewRepSearchHeader.test.tsx
deleted file mode 100644
index 0a354db338..0000000000
--- a/__tests__/components/user/rep/new-rep/UserPageRepNewRepSearchHeader.test.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { render, screen, waitFor } from '@testing-library/react';
-import React from 'react';
-import UserPageRepNewRepSearchHeader from '@/components/user/rep/new-rep/UserPageRepNewRepSearchHeader';
-import { AuthContext } from '@/components/auth/Auth';
-import { useQuery } from '@tanstack/react-query';
-import { ApiProfileProxyActionType } from '@/generated/models/ApiProfileProxyActionType';
-
-jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() }));
-jest.mock('next/link', () => ({ __esModule: true, default: ({ href, children }: any) => {children} }));
-jest.mock('@/components/utils/CommonInfoBox', () => (p: any) => {p.message}
);
-
-const useQueryMock = useQuery as jest.Mock;
-
-describe('UserPageRepNewRepSearchHeader', () => {
- const profile = { handle: 'bob', query: 'bob' } as any;
- const repRates = { rep_rates_left_for_rater: 5, total_rep_rating_by_rater: 3 } as any;
-
- it('shows available rep when no proxy', () => {
- useQueryMock.mockReturnValue({});
- render(
-
-
-
- );
- expect(screen.getByText('Your available Rep:')).toBeInTheDocument();
- expect(screen.getByText('5')).toBeInTheDocument();
- });
-
- it('shows proxy info and info box when credit is zero', async () => {
- useQueryMock.mockReturnValue({ data: { rep_rates_left_for_rater: 0, total_rep_rating_by_rater: 0 } });
- const proxy = {
- created_by: { handle: 'alice' },
- actions: [{ action_type: ApiProfileProxyActionType.AllocateRep, credit_amount: 1, credit_spent: 1 }],
- } as any;
- render(
-
-
-
- );
- await waitFor(() => expect(screen.getByText('You are acting as proxy for:')).toBeInTheDocument());
- expect(screen.getByRole('link')).toHaveAttribute('href', '/alice');
- expect(screen.getByTestId('infobox')).toHaveTextContent("You don't have any rep left to rate");
- });
-});
diff --git a/__tests__/components/user/rep/reps/UserPageRepReps.test.tsx b/__tests__/components/user/rep/reps/UserPageRepReps.test.tsx
deleted file mode 100644
index ef400d72b5..0000000000
--- a/__tests__/components/user/rep/reps/UserPageRepReps.test.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { AuthContext } from "@/components/auth/Auth";
-import UserPageRepReps from "@/components/user/rep/reps/UserPageRepReps";
-import { ApiProfileProxyActionType } from "@/generated/models/ApiProfileProxyActionType";
-import { render, screen } from "@testing-library/react";
-
-function setMatchMedia(matches: boolean) {
- Object.defineProperty(window, "matchMedia", {
- writable: true,
- value: jest.fn().mockReturnValue({
- matches,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- }),
- });
-}
-
-const sampleReps = [
- { category: "a", rating: 5, contributor_count: 2, rater_contribution: 1 },
- { category: "b", rating: 5, contributor_count: 3, rater_contribution: 0 },
- { category: "c", rating: 10, contributor_count: 1, rater_contribution: 0 },
-];
-
-const profile = { handle: "target" } as any;
-
-function renderComponent(authValue: any) {
- return render(
-
-
-
- );
-}
-
-describe("UserPageRepReps", () => {
- beforeEach(() => {
- setMatchMedia(false);
- });
- it("sorts reps by rating and contributor count", () => {
- renderComponent({ connectedProfile: null, activeProfileProxy: null });
-
- const items = screen.getAllByRole("button");
- // first top rep should be rating 10 (category c)
- expect(items[0]).toHaveTextContent("c");
- });
-
- it("disables editing when viewing own profile", () => {
- renderComponent({
- connectedProfile: { handle: "target" },
- activeProfileProxy: null,
- });
- const button = screen.getAllByRole("button")[0];
- expect(button).toBeDisabled();
- });
-
- it("allows editing when proxy has allocate rep action", () => {
- renderComponent({
- connectedProfile: { handle: "me" },
- activeProfileProxy: {
- created_by: { handle: "other" },
- actions: [{ action_type: ApiProfileProxyActionType.AllocateRep }],
- },
- });
- const button = screen.getAllByRole("button")[0];
- expect(button).toBeEnabled();
- });
-});
diff --git a/__tests__/components/user/rep/reps/table/UserPageRepRepsTable.test.tsx b/__tests__/components/user/rep/reps/table/UserPageRepRepsTable.test.tsx
deleted file mode 100644
index 574cfbae5d..0000000000
--- a/__tests__/components/user/rep/reps/table/UserPageRepRepsTable.test.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import UserPageRepRepsTable, { RepsTableSort } from '@/components/user/rep/reps/table/UserPageRepRepsTable';
-
-let latestReps: any[] = [];
-jest.mock('@/components/user/rep/reps/table/UserPageRepRepsTableBody', () => (props: any) => { latestReps = props.reps; return ; });
-jest.mock('@/components/user/rep/reps/table/UserPageRepRepsTableHeader', () => (props: any) => (
-
-
- |
- |
- |
-
-
-));
-
-const reps = [
- { category: 'a', rating: 1, contributor_count: 2, rater_contribution: 5 },
- { category: 'b', rating: 3, contributor_count: 1, rater_contribution: 0 }
-];
-
-it('sorts by rating descending by default and toggles sort on header click', async () => {
- render();
- expect(latestReps[0].category).toBe('b'); // rating 3 first
- await userEvent.click(screen.getByText('raters'));
- expect(latestReps[0].category).toBe('a'); // contributor_count 2 first
-});
-
-it('reverts to REP sort when MY_RATES clicked without permission', async () => {
- render();
- await userEvent.click(screen.getByText('my'));
- expect(latestReps[0].category).toBe('b'); // should remain REP DESC
-});
diff --git a/components/user/rep/RepCategoryPill.tsx b/components/user/rep/RepCategoryPill.tsx
new file mode 100644
index 0000000000..de70adf8db
--- /dev/null
+++ b/components/user/rep/RepCategoryPill.tsx
@@ -0,0 +1,81 @@
+import type { RatingStats } from "@/entities/IProfile";
+import { formatNumberWithCommas } from "@/helpers/Helpers";
+import type { MouseEvent } from "react";
+import TopRaterAvatars from "./header/TopRaterAvatars";
+
+const stopPropagation = (e: MouseEvent) => e.stopPropagation();
+
+export default function RepCategoryPill({
+ rep,
+ profileHandle,
+ canEdit,
+ onEdit,
+ compact = false,
+}: {
+ readonly rep: RatingStats;
+ readonly profileHandle: string;
+ readonly canEdit: boolean;
+ readonly onEdit: (category: string) => void;
+ readonly compact?: boolean;
+}) {
+ const paddingClass = compact ? "tw-px-3 tw-py-2" : "tw-px-4 tw-py-2.5";
+
+ const content = (
+ <>
+
+
+ {rep.category}
+
+
+ {formatNumberWithCommas(rep.rating)}
+
+
+ ·
+
+
+
+
+
+ {formatNumberWithCommas(rep.contributor_count)}{" "}
+ {rep.contributor_count === 1 ? "rater" : "raters"}
+
+
+ {!!rep.rater_contribution && (
+ <>
+ ·
+
+ My Rate:{" "}
+
+ {formatNumberWithCommas(rep.rater_contribution)}
+
+
+ >
+ )}
+ >
+ );
+
+ const baseClasses = `group tw-inline-flex tw-items-center tw-gap-2.5 tw-rounded-lg tw-border tw-border-solid tw-border-white/10 tw-bg-white/5 tw-backdrop-blur-md tw-transition-all tw-duration-300 tw-ease-out ${paddingClass}`;
+
+ if (canEdit) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/components/user/rep/UserPageRep.tsx b/components/user/rep/UserPageRep.tsx
index 617ccb9eed..a064dde76b 100644
--- a/components/user/rep/UserPageRep.tsx
+++ b/components/user/rep/UserPageRep.tsx
@@ -18,7 +18,6 @@ import UserPageRateWrapper from "../utils/rate/UserPageRateWrapper";
import UserPageCombinedActivityLog from "./UserPageCombinedActivityLog";
import UserPageRepHeader from "./header/UserPageRepHeader";
import UserPageRepMobile from "./UserPageRepMobile";
-import UserPageRepReps from "./reps/UserPageRepReps";
export default function UserPageRep({
profile,
initialActivityLogParams,
@@ -65,27 +64,11 @@ export default function UserPageRep({
{/* Left Column - Rep Content */}
-
-
- {/* Rep raters tables - commented out for now
-
- */}
{/* Right Sidebar - Identity Card */}
@@ -116,18 +99,6 @@ export default function UserPageRep({
- {/* CIC raters tables - commented out for now
-
- */}
);
diff --git a/components/user/rep/UserPageRepMobile.tsx b/components/user/rep/UserPageRepMobile.tsx
index 907b25c5ee..6a3e0f51be 100644
--- a/components/user/rep/UserPageRepMobile.tsx
+++ b/components/user/rep/UserPageRepMobile.tsx
@@ -26,8 +26,9 @@ 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 UserPageRepNewRep from "./new-rep/UserPageRepNewRep";
-import UserPageRepRepsTable from "./reps/table/UserPageRepRepsTable";
+import UserPageRepModifyModal from "./modify-rep/UserPageRepModifyModal";
+import GrantRepDialog from "./new-rep/GrantRepDialog";
+import RepCategoryPill from "./RepCategoryPill";
import UserPageCombinedActivityLog from "./UserPageCombinedActivityLog";
import {
getCanEditRep,
@@ -52,6 +53,8 @@ export default function UserPageRepMobile({
const [activeTab, setActiveTab] = useState("rep");
const [isGrantRepOpen, setIsGrantRepOpen] = useState(false);
const [isNicRateOpen, setIsNicRateOpen] = useState(false);
+ const [visibleCount, setVisibleCount] = useState(5);
+ const [editCategory, setEditCategory] = useState(null);
const { data: nicRatings } = useQuery>({
queryKey: [
@@ -93,6 +96,10 @@ export default function UserPageRepMobile({
return () => mq.removeEventListener("change", handler);
}, []);
+ useEffect(() => {
+ setVisibleCount(5);
+ }, [repRates?.rating_stats]);
+
// --- derived: sorted reps, can-edit flags ---
const reps = useMemo(
() => sortRepsByRatingAndContributors(repRates?.rating_stats ?? []),
@@ -240,7 +247,7 @@ export default function UserPageRepMobile({
-
+
+ {/* Rep Categories */}
+ {reps.length > 0 && (
+
+
+ Rep Categories
+
+
+ {reps.slice(0, visibleCount).map((rep) => (
+
+ ))}
+ {reps.length > visibleCount && (
+
+ )}
+
+
+ )}
+
{canEditRep && (
- Add skill to this identity
+ Add rep to this identity
)}
- {/* Rep Table */}
- {!!reps.length && (
-
-
-
- )}
-
{/* Grant Rep Bottom Sheet */}
- setIsGrantRepOpen(false)}
- tabletModal
- >
-
-
- setIsGrantRepOpen(false)}
- />
-
-
-
-
-
-
+ />
{/* Rate NIC Bottom Sheet */}
+
+ {canEditRep && editCategory && (
+ setEditCategory(null)}
+ />
+ )}
);
}
diff --git a/components/user/rep/header/UserPageRepHeader.tsx b/components/user/rep/header/UserPageRepHeader.tsx
index b2a7997da2..05148b3026 100644
--- a/components/user/rep/header/UserPageRepHeader.tsx
+++ b/components/user/rep/header/UserPageRepHeader.tsx
@@ -1,19 +1,18 @@
"use client";
import { AuthContext } from "@/components/auth/Auth";
-import type { ApiProfileRepRatesState, RatingStats } from "@/entities/IProfile";
+import type { ApiProfileRepRatesState } from "@/entities/IProfile";
import type { ApiIdentity } from "@/generated/models/ApiIdentity";
import { formatNumberWithCommas } from "@/helpers/Helpers";
-import { useContext, useEffect, useState } from "react";
+import { useContext, useEffect, useMemo, useState } from "react";
+import RepCategoryPill from "../RepCategoryPill";
import UserPageRepModifyModal from "../modify-rep/UserPageRepModifyModal";
-import TopRaterAvatars from "./TopRaterAvatars";
+import GrantRepDialog from "../new-rep/GrantRepDialog";
import {
getCanEditRep,
sortRepsByRatingAndContributors,
} from "../UserPageRep.helpers";
-const TOP_REPS_COUNT = 5;
-
export default function UserPageRepHeader({
repRates,
profile,
@@ -23,48 +22,32 @@ export default function UserPageRepHeader({
}) {
const { connectedProfile, activeProfileProxy } = useContext(AuthContext);
- const [topReps, setTopReps] = useState(
- sortRepsByRatingAndContributors(repRates?.rating_stats ?? []).slice(
- 0,
- TOP_REPS_COUNT
- )
+ const allReps = useMemo(
+ () => sortRepsByRatingAndContributors(repRates?.rating_stats ?? []),
+ [repRates?.rating_stats]
);
+ const [visibleCount, setVisibleCount] = useState(5);
+
useEffect(() => {
- setTopReps(
- sortRepsByRatingAndContributors(repRates?.rating_stats ?? []).slice(
- 0,
- TOP_REPS_COUNT
- )
- );
+ setVisibleCount(5);
}, [repRates?.rating_stats]);
- const [canEditRep, setCanEditRep] = useState(
- getCanEditRep({
- myProfile: connectedProfile,
- targetProfile: profile,
- activeProfileProxy,
- })
- );
+ const visibleReps = allReps.slice(0, visibleCount);
+ const hasMore = allReps.length > visibleCount;
- useEffect(() => {
- setCanEditRep(
+ const canEditRep = useMemo(
+ () =>
getCanEditRep({
myProfile: connectedProfile,
targetProfile: profile,
activeProfileProxy,
- })
- );
- }, [connectedProfile, profile, activeProfileProxy]);
+ }),
+ [connectedProfile, profile, activeProfileProxy]
+ );
const [editCategory, setEditCategory] = useState(null);
-
- const openEditCategory = (category: string) => {
- if (!canEditRep) {
- return;
- }
- setEditCategory(category);
- };
+ const [isGrantRepOpen, setIsGrantRepOpen] = useState(false);
return (
<>
@@ -103,56 +86,50 @@ export default function UserPageRepHeader({
- {topReps.length > 0 && (
+ {(visibleReps.length > 0 || canEditRep) && (
- Top Rep
+ Rep Categories
- {topReps.map((rep) => (
-
setIsGrantRepOpen(true)}
+ className="tw-group tw-inline-flex tw-h-11 tw-cursor-pointer tw-items-center tw-justify-center tw-gap-x-1.5 tw-rounded-lg tw-border tw-border-dashed tw-border-white/10 tw-bg-transparent tw-px-4 tw-text-sm tw-font-medium tw-text-iron-400 tw-transition-all tw-duration-300 tw-ease-out hover:tw-border-white/30 hover:tw-bg-white/5 hover:tw-text-white"
>
- {canEditRep ? (
-
- ) : (
-
-
- {rep.category}
-
-
- {formatNumberWithCommas(rep.rating)}
-
-
- )}
-
ยท
-
-
- {formatNumberWithCommas(rep.contributor_count)}{" "}
- {rep.contributor_count === 1 ? "rater" : "raters"}
-
-
+
+
Add new
+
+ )}
+ {visibleReps.map((rep) => (
+
))}
+ {hasMore && (
+
+ )}
)}
@@ -166,6 +143,13 @@ export default function UserPageRepHeader({
onClose={() => setEditCategory(null)}
/>
)}
+
+
setIsGrantRepOpen(false)}
+ />
>
);
}
diff --git a/components/user/rep/new-rep/GrantRepDialog.tsx b/components/user/rep/new-rep/GrantRepDialog.tsx
new file mode 100644
index 0000000000..d983f30aa0
--- /dev/null
+++ b/components/user/rep/new-rep/GrantRepDialog.tsx
@@ -0,0 +1,46 @@
+import type { ApiProfileRepRatesState } from "@/entities/IProfile";
+import type { ApiIdentity } from "@/generated/models/ApiIdentity";
+import { RateMatter } from "@/types/enums";
+import MobileWrapperDialog from "@/components/mobile-wrapper-dialog/MobileWrapperDialog";
+import UserPageRateWrapper from "../../utils/rate/UserPageRateWrapper";
+import UserPageRepNewRep from "./UserPageRepNewRep";
+
+export default function GrantRepDialog({
+ profile,
+ repRates,
+ isOpen,
+ onClose,
+}: {
+ readonly profile: ApiIdentity;
+ readonly repRates: ApiProfileRepRatesState | null;
+ readonly isOpen: boolean;
+ readonly onClose: () => void;
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx b/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
index 2c413b2d92..fd85a17053 100644
--- a/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
+++ b/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
@@ -262,14 +262,9 @@ export default function UserPageRepNewRepSearch({
-
+
-
Your available Rep:
@@ -291,10 +286,10 @@ export default function UserPageRepNewRepSearch({
-