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({
-
+
+ className="tw-w-full tw-relative">
-
+
- [...items].sort((a, d) => { - if (a.rating === d.rating) { - return d.contributor_count - a.contributor_count; - } - return d.rating - a.rating; - }); - - const [reps, setReps] = useState( - sortReps(repRates?.rating_stats ?? []) - ); - - useEffect( - () => setReps(sortReps(repRates?.rating_stats ?? [])), - [repRates?.rating_stats] - ); - - const getCanEditRep = ({ - myProfile, - targetProfile, - }: { - myProfile: ApiIdentity | null; - targetProfile: ApiIdentity; - }) => { - if (!myProfile?.handle) { - return false; - } - if (activeProfileProxy) { - if (profile.handle === activeProfileProxy.created_by.handle) { - return false; - } - return activeProfileProxy.actions.some( - (action) => action.action_type === ApiProfileProxyActionType.AllocateRep - ); - } - if (myProfile.handle === targetProfile.handle) { - return false; - } - return true; - }; - - const [canEditRep, setCanEditRep] = useState( - getCanEditRep({ - myProfile: connectedProfile, - targetProfile: profile, - }) - ); - - useEffect(() => { - setCanEditRep( - getCanEditRep({ - myProfile: connectedProfile, - targetProfile: profile, - }) - ); - }, [connectedProfile, profile]); - - return ( -
-
-
-

- Total Rep -

-
- - - - - - {!!reps.length && ( -
- -
- )} -
-
- ); -} diff --git a/components/user/rep/reps/table/UserPageRepRepsTable.tsx b/components/user/rep/reps/table/UserPageRepRepsTable.tsx deleted file mode 100644 index 83dbcbd761..0000000000 --- a/components/user/rep/reps/table/UserPageRepRepsTable.tsx +++ /dev/null @@ -1,149 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import type { RatingStats } from "@/entities/IProfile"; -import UserPageRepRepsTableBody from "./UserPageRepRepsTableBody"; -import UserPageRepRepsTableHeader from "./UserPageRepRepsTableHeader"; -import { SortDirection } from "@/entities/ISort"; -import { assertUnreachable } from "@/helpers/AllowlistToolHelpers"; -import type { ApiIdentity } from "@/generated/models/ApiIdentity"; -export enum RepsTableSort { - REP = "REP", - RATERS = "RATERS", - MY_RATES = "MY_RATES", -} - -const DEFAULT_INITIAL_COUNT = 10; - -export default function UserPageRepRepsTable({ - reps, - profile, - canEditRep, - initialCount = DEFAULT_INITIAL_COUNT, -}: { - readonly reps: RatingStats[]; - readonly profile: ApiIdentity; - readonly canEditRep: boolean; - readonly initialCount?: number; -}) { - const [showAll, setShowAll] = useState(false); - const [sortType, setSortType] = useState(RepsTableSort.REP); - const [sortDirection, setSortDirection] = useState( - SortDirection.DESC - ); - - const onSortTypeClick = (newSortType: RepsTableSort) => { - if (newSortType === sortType) { - setSortDirection((prev) => - prev === SortDirection.ASC ? SortDirection.DESC : SortDirection.ASC - ); - } else { - setSortType(newSortType); - setSortDirection(SortDirection.DESC); - } - }; - - const compareValuesForSort = ( - a: number, - d: number, - dir: SortDirection, - zeroAtEnd: boolean = false - ) => { - if (!zeroAtEnd) { - return dir === SortDirection.DESC ? d - a : a - d; - } - - if (a === 0 && d === 0) { - return 0; - } else if (a === 0) { - return 1; - } else if (d === 0) { - return -1; - } - return dir === SortDirection.DESC ? d - a : a - d; - }; - - const sortReps = ( - items: RatingStats[], - sort: RepsTableSort, - dir: SortDirection - ): RatingStats[] => { - switch (sort) { - case RepsTableSort.REP: - return items.sort((a, d) => - compareValuesForSort(a.rating, d.rating, dir) - ); - case RepsTableSort.RATERS: - return items.sort((a, d) => - compareValuesForSort(a.contributor_count, d.contributor_count, dir) - ); - case RepsTableSort.MY_RATES: - return items.sort((a, d) => - compareValuesForSort( - a.rater_contribution, - d.rater_contribution, - dir, - true - ) - ); - default: - assertUnreachable(sort); - return items; - } - }; - - const [sortedReps, setSortedReps] = useState( - sortReps(reps, sortType, sortDirection) - ); - - useEffect(() => { - setSortedReps(sortReps(reps, sortType, sortDirection)); - }, [reps, sortType, sortDirection]); - - useEffect(() => { - if (!canEditRep && sortType === RepsTableSort.MY_RATES) { - setSortType(RepsTableSort.REP); - setSortDirection(SortDirection.DESC); - } - }, [canEditRep, sortType]); - - const displayedReps = showAll - ? sortedReps - : sortedReps.slice(0, initialCount); - const hasMore = sortedReps.length > initialCount; - const maxRep = Math.max(...reps.map((r) => Math.abs(r.rating)), 0); - - return ( -
-
- - - - -
-
- {hasMore && ( - - )} -
- ); -} diff --git a/components/user/rep/reps/table/UserPageRepRepsTableBody.tsx b/components/user/rep/reps/table/UserPageRepRepsTableBody.tsx deleted file mode 100644 index 6c1a0d9a80..0000000000 --- a/components/user/rep/reps/table/UserPageRepRepsTableBody.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { RatingStats } from "@/entities/IProfile"; -import UserPageRepRepsTableItem from "./UserPageRepRepsTableItem"; -import type { ApiIdentity } from "@/generated/models/ApiIdentity"; - -export default function UserPageRepRepsTableBody({ - reps, - profile, - canEditRep, - maxRep, -}: { - readonly reps: RatingStats[]; - readonly profile: ApiIdentity; - readonly canEditRep: boolean; - readonly maxRep: number; -}) { - return ( - - {reps.map((rep) => ( - - ))} - - ); -} diff --git a/components/user/rep/reps/table/UserPageRepRepsTableHeader.tsx b/components/user/rep/reps/table/UserPageRepRepsTableHeader.tsx deleted file mode 100644 index 8061e091a0..0000000000 --- a/components/user/rep/reps/table/UserPageRepRepsTableHeader.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import type { SortDirection } from "@/entities/ISort"; -import { useEffect, useState } from "react"; -import { RepsTableSort } from "./UserPageRepRepsTable"; -import UserPageRepRepsTableHeaderSortableCell from "./UserPageRepRepsTableHeaderSortableCell"; - -export default function UserPageRepRepsTableHeader({ - activeType, - sortDirection, - showMyRates, - onSortTypeClick, -}: { - readonly activeType: RepsTableSort; - readonly sortDirection: SortDirection; - readonly showMyRates: boolean; - readonly onSortTypeClick: (newSortType: RepsTableSort) => void; -}) { - const getTypes = (myRates: boolean) => { - if (myRates) { - return [RepsTableSort.REP, RepsTableSort.RATERS, RepsTableSort.MY_RATES]; - } - return [RepsTableSort.REP, RepsTableSort.RATERS]; - }; - - const [types, setTypes] = useState(getTypes(showMyRates)); - useEffect(() => setTypes(getTypes(showMyRates)), [showMyRates]); - - return ( - - - - Category - - {types.map((sortType) => ( - - ))} - - - ); -} diff --git a/components/user/rep/reps/table/UserPageRepRepsTableHeaderSortableCell.tsx b/components/user/rep/reps/table/UserPageRepRepsTableHeaderSortableCell.tsx deleted file mode 100644 index 0fc47ce288..0000000000 --- a/components/user/rep/reps/table/UserPageRepRepsTableHeaderSortableCell.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import CommonTableSortIcon from "@/components/user/utils/icons/CommonTableSortIcon"; -import { SortDirection } from "@/entities/ISort"; -import { useEffect, useState } from "react"; -import { RepsTableSort } from "./UserPageRepRepsTable"; - -export default function UserPageRepRepsTableHeaderSortableCell({ - type, - activeType, - activeDirection, - onSortTypeClick, -}: { - readonly type: RepsTableSort; - readonly activeType: RepsTableSort; - readonly activeDirection: SortDirection; - readonly onSortTypeClick: (newSortType: RepsTableSort) => void; -}) { - const SORT_TYPE_TO_TEXT: Record = { - [RepsTableSort.REP]: "Rep", - [RepsTableSort.RATERS]: "Raters", - [RepsTableSort.MY_RATES]: "My Rates", - }; - - const [isActive, setIsActive] = useState(type === activeType); - useEffect(() => { - setIsActive(type === activeType); - }, [activeType]); - - const [sor, setSor] = useState( - isActive ? activeDirection : SortDirection.DESC - ); - useEffect(() => { - setSor(isActive ? activeDirection : SortDirection.DESC); - }, [activeDirection, isActive]); - - return ( - onSortTypeClick(type)}> - - {SORT_TYPE_TO_TEXT[type]} - - {/* Sort icons intentionally hidden for this iteration. */} - - - - - ); -} diff --git a/components/user/rep/reps/table/UserPageRepRepsTableItem.tsx b/components/user/rep/reps/table/UserPageRepRepsTableItem.tsx deleted file mode 100644 index 0325157b6f..0000000000 --- a/components/user/rep/reps/table/UserPageRepRepsTableItem.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import { useState } from "react"; -import type { RatingStats } from "@/entities/IProfile"; -import UserPageRepModifyModal from "@/components/user/rep/modify-rep/UserPageRepModifyModal"; -import { formatNumberWithCommas } from "@/helpers/Helpers"; -import type { ApiIdentity } from "@/generated/models/ApiIdentity"; - -export default function UserPageRepRepsTableItem({ - rep, - profile, - canEditRep, - maxRep, -}: { - readonly rep: RatingStats; - readonly profile: ApiIdentity; - readonly canEditRep: boolean; - readonly maxRep: number; -}) { - const [isEditRepModalOpen, setIsEditRepModalOpen] = useState(false); - - const onTableClick = () => { - if (canEditRep) { - setIsEditRepModalOpen(true); - } - }; - - const progressPercent = - maxRep > 0 ? (Math.abs(rep.rating) / maxRep) * 100 : 0; - - const cellBase = - "tw-py-2.5 sm:tw-py-3 tw-px-4 tw-bg-gradient-to-r tw-from-[#0f1014]/40 tw-to-[#0A0A0C]/40 tw-border-y tw-border-solid tw-border-white/[0.08] sm:tw-border-white/[0.04] tw-border-x-0 tw-transition-all tw-duration-200 tw-ease-out"; - const hoverClass = canEditRep - ? "group-hover:tw-from-[#12141a]/60 group-hover:tw-to-[#0d0f13]/60 group-hover:tw-border-white/[0.16]" - : ""; - - return ( - <> - - {/* Category + progress bar */} - -
- {rep.category} -
-
-
-
- - - {/* Community (Rep) */} - - - {formatNumberWithCommas(rep.rating)} - - - - {/* People (Raters) */} - - - {formatNumberWithCommas(rep.contributor_count)} - - - - {/* From You (My Rates) */} - {canEditRep && ( - - {rep.rater_contribution ? ( - 0 - ? "tw-text-primary-400/90 group-hover:tw-text-primary-400" - : "tw-text-rose-400" - }`} - > - {formatNumberWithCommas(rep.rater_contribution)} - - ) : ( - - - )} - - )} - - - {canEditRep && isEditRepModalOpen && ( - setIsEditRepModalOpen(false)} - /> - )} - - ); -}