diff --git a/components/home/boosted/BoostedDropCardHome.tsx b/components/home/boosted/BoostedDropCardHome.tsx index d5083e20e0..70715f3fa7 100644 --- a/components/home/boosted/BoostedDropCardHome.tsx +++ b/components/home/boosted/BoostedDropCardHome.tsx @@ -221,7 +221,7 @@ const BoostedDropCardHome = memo(
- + {getTimeAgoShort(drop.created_at)}
@@ -293,7 +293,7 @@ const BoostedDropCardHome = memo(
)} -
+
event.stopPropagation()} diff --git a/components/mobile-wrapper-dialog/MobileWrapperDialog.tsx b/components/mobile-wrapper-dialog/MobileWrapperDialog.tsx index d704a1e685..eb6687660f 100644 --- a/components/mobile-wrapper-dialog/MobileWrapperDialog.tsx +++ b/components/mobile-wrapper-dialog/MobileWrapperDialog.tsx @@ -6,6 +6,7 @@ import { Transition, TransitionChild, } from "@headlessui/react"; +import clsx from "clsx"; import { Fragment } from "react"; export default function MobileWrapperDialog({ @@ -49,15 +50,34 @@ export default function MobileWrapperDialog({ return `calc(${viewportHeight} - 10rem)`; }; - const panelClassNames = `mobile-wrapper-dialog tw-pointer-events-auto tw-relative tw-w-screen${ - tabletModal ? "" : " md:tw-max-w-screen-md" - }${isIos ? "" : " tw-transform-gpu tw-will-change-transform"}${ - tabletModal ? ` md:tw-w-full ${maxWidthClass ?? "md:tw-max-w-md"}` : "" - }`; + const panelClassNames = clsx( + "mobile-wrapper-dialog tw-pointer-events-auto tw-relative tw-w-screen", + !tabletModal && "md:tw-max-w-screen-md", + !isIos && "tw-transform-gpu tw-will-change-transform", + tabletModal && ["md:tw-w-full", maxWidthClass ?? "md:tw-max-w-md"] + ); + + const containerClassNames = clsx( + "tw-pointer-events-none tw-fixed tw-inset-x-0 tw-bottom-0 tw-flex tw-max-w-full tw-justify-center tw-pt-10", + tabletModal && "md:tw-inset-0 md:tw-items-center md:tw-pt-0 md:tw-p-6" + ); - const containerClassNames = `tw-pointer-events-none tw-fixed tw-inset-x-0 tw-bottom-0 tw-flex tw-max-w-full tw-justify-center tw-pt-10${ - tabletModal ? " md:tw-inset-0 md:tw-items-center md:tw-pt-0 md:tw-p-6" : "" - }`; + const slideTransition = { + enter: + "tw-duration-250 sm:tw-duration-350 tw-transform tw-transition tw-ease-in-out", + enterFrom: clsx( + "tw-translate-y-full", + tabletModal && "md:tw-translate-y-4 md:tw-opacity-0" + ), + enterTo: clsx("tw-translate-y-0", tabletModal && "md:tw-opacity-100"), + leave: + "tw-duration-250 sm:tw-duration-350 tw-transform tw-transition tw-ease-in-out", + leaveFrom: clsx("tw-translate-y-0", tabletModal && "md:tw-opacity-100"), + leaveTo: clsx( + "tw-translate-y-full", + tabletModal && "md:tw-translate-y-4 md:tw-opacity-0" + ), + }; return ( @@ -92,23 +112,7 @@ export default function MobileWrapperDialog({ onClick={(e) => e.stopPropagation()} >
- +
diff --git a/components/user/identity/header/RateNicCta.tsx b/components/user/identity/header/RateNicCta.tsx new file mode 100644 index 0000000000..1ef4f34339 --- /dev/null +++ b/components/user/identity/header/RateNicCta.tsx @@ -0,0 +1,42 @@ +export const FingerprintIcon = ({ + className, +}: { + readonly className?: string; +}) => ( + +); + +export function RateNicButton({ + onRateClick, +}: { + readonly onRateClick: () => void; +}) { + return ( + + ); +} diff --git a/components/user/identity/header/UserPageIdentityHeader.tsx b/components/user/identity/header/UserPageIdentityHeader.tsx index 6e6448ad48..36dfaaac3d 100644 --- a/components/user/identity/header/UserPageIdentityHeader.tsx +++ b/components/user/identity/header/UserPageIdentityHeader.tsx @@ -5,9 +5,11 @@ import UserPageIdentityHeaderCIC from "./UserPageIdentityHeaderCIC"; export default function UserPageIdentityHeader({ profile, cicOverview, + onRateClick, }: { readonly profile: ApiIdentity; readonly cicOverview: ApiCicOverview | null; + readonly onRateClick?: () => void; }) { return (
@@ -20,7 +22,11 @@ export default function UserPageIdentityHeader({ identity?

- +
); } diff --git a/components/user/identity/header/UserPageIdentityHeaderCIC.tsx b/components/user/identity/header/UserPageIdentityHeaderCIC.tsx index ff8c3821d3..6f3ea76c50 100644 --- a/components/user/identity/header/UserPageIdentityHeaderCIC.tsx +++ b/components/user/identity/header/UserPageIdentityHeaderCIC.tsx @@ -7,15 +7,18 @@ import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import { formatNumberWithCommas } from "@/helpers/Helpers"; import { useMemo } from "react"; import OverlappingAvatars from "@/components/common/OverlappingAvatars"; +import { RateNicButton } from "./RateNicCta"; const TOP_RATERS_COUNT = 5; export default function UserPageIdentityHeaderCIC({ profile, cicOverview, + onRateClick, }: { readonly profile: ApiIdentity; readonly cicOverview: ApiCicOverview | null; + readonly onRateClick?: () => void; }) { const cicRating = cicOverview?.total_cic ?? profile.cic; const raterCount = cicOverview?.contributor_count ?? 0; @@ -44,11 +47,11 @@ export default function UserPageIdentityHeaderCIC({ ); return ( -
+
NIC
-
+
{formatNumberWithCommas(cicRating)} @@ -74,6 +77,26 @@ export default function UserPageIdentityHeaderCIC({
+ {onRateClick && ( +
+ + {cicOverview !== null && + cicOverview.authenticated_user_contribution !== null && + cicOverview.authenticated_user_contribution !== 0 && ( +
+ + Your Rating: + + + {cicOverview.authenticated_user_contribution > 0 && "+"} + {formatNumberWithCommas( + cicOverview.authenticated_user_contribution + )} + +
+ )} +
+ )}
); } diff --git a/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx b/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx index 36a9d265aa..cd69f95d88 100644 --- a/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx +++ b/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx @@ -5,10 +5,7 @@ import { useContext, useEffect, useState } from "react"; import type { ApiProfileRaterCicState } from "@/entities/IProfile"; import { getStringAsNumberOrZero } from "@/helpers/Helpers"; import { AuthContext } from "@/components/auth/Auth"; -import { - commonApiFetch, - commonApiPost, -} from "@/services/api/common-api"; +import { commonApiFetch, commonApiPost } from "@/services/api/common-api"; import { useMutation, useQuery } from "@tanstack/react-query"; import { QueryKey, @@ -39,10 +36,12 @@ export default function UserPageIdentityHeaderCICRate({ profile, isTooltip, onSuccess, + onCancel, }: { readonly profile: ApiIdentity; readonly isTooltip: boolean; readonly onSuccess?: () => void; + readonly onCancel?: () => void; }) { const { address } = useSeizeConnectContext(); const { requestAuth, setToast, connectedProfile, activeProfileProxy } = @@ -192,8 +191,7 @@ export default function UserPageIdentityHeaderCICRate({ const haveChanged = newRating !== originalRating; const isProxy = !!activeProfileProxy; const isValidValue = - isProxy || - (newRating >= minMaxValues.min && newRating <= minMaxValues.max); + isProxy || (newRating >= minMaxValues.min && newRating <= minMaxValues.max); const isSaveDisabled = !haveChanged || !isValidValue; const onSave = async () => { @@ -219,9 +217,10 @@ export default function UserPageIdentityHeaderCICRate({ const rateInput = (
+ className={`tw-relative tw-flex tw-w-full ${ + isTooltip ? "tw-mt-1.5" : "tw-mb-2" + }`} + > - {!isTooltip && ( -
- - - Rate NIC - -
- )} +
-
+ {isTooltip ? ( <>
{rateInput}
-
+
@@ -345,7 +310,8 @@ export default function UserPageIdentityHeaderCICRate({ <> @@ -353,16 +319,28 @@ export default function UserPageIdentityHeaderCICRate({ {adjustmentHelper} - +
+ + {onCancel && ( + + )} +
)} diff --git a/components/user/identity/statements/UserPageIdentityStatements.tsx b/components/user/identity/statements/UserPageIdentityStatements.tsx index d6899670b7..bca38d7c0e 100644 --- a/components/user/identity/statements/UserPageIdentityStatements.tsx +++ b/components/user/identity/statements/UserPageIdentityStatements.tsx @@ -85,13 +85,11 @@ export default function UserPageIdentityStatements({ }, [statements]); return ( -
+
- -
-
+ +
+
@@ -138,8 +136,9 @@ export default function UserPageIdentityStatements({ positionStrategy="fixed" offset={8} opacity={1} - style={TOOLTIP_STYLES}> -
    + style={TOOLTIP_STYLES} + > +
    • All statements are optional.
    • All statements are fully and permanently public.
    • diff --git a/components/user/identity/statements/consolidated-addresses/UserPageIdentityStatementsConsolidatedAddressesItem.tsx b/components/user/identity/statements/consolidated-addresses/UserPageIdentityStatementsConsolidatedAddressesItem.tsx index d4e77b1fe3..b4ba3d1fb8 100644 --- a/components/user/identity/statements/consolidated-addresses/UserPageIdentityStatementsConsolidatedAddressesItem.tsx +++ b/components/user/identity/statements/consolidated-addresses/UserPageIdentityStatementsConsolidatedAddressesItem.tsx @@ -189,7 +189,7 @@ export default function UserPageIdentityStatementsConsolidatedAddressesItem({ return (
    • -
      +
      @@ -291,7 +291,7 @@ export default function UserPageIdentityStatementsConsolidatedAddressesItem({ {isOpen && (
      diff --git a/components/user/rep/MobileIdentityTabContent.tsx b/components/user/rep/MobileIdentityTabContent.tsx new file mode 100644 index 0000000000..21290f43cb --- /dev/null +++ b/components/user/rep/MobileIdentityTabContent.tsx @@ -0,0 +1,88 @@ +import type { ActivityLogParams } from "@/components/profile-activity/ProfileActivityLogs"; +import type { ApiCicOverview } from "@/generated/models/ApiCicOverview"; +import type { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { formatNumberWithCommas } from "@/helpers/Helpers"; +import { RateMatter } from "@/types/enums"; +import { FingerprintIcon } from "../identity/header/RateNicCta"; +import UserPageIdentityStatementsAddButton from "../identity/statements/add/UserPageIdentityStatementsAddButton"; +import UserPageIdentityStatements from "../identity/statements/UserPageIdentityStatements"; +import UserPageRateWrapper from "../utils/rate/UserPageRateWrapper"; +import UserPageCombinedActivityLog from "./UserPageCombinedActivityLog"; + +export default function MobileIdentityTabContent({ + profile, + cicOverview, + initialActivityLogParams, + canEditNic, + canEditStatements, + onRateNic, +}: { + readonly profile: ApiIdentity; + readonly cicOverview: ApiCicOverview | null; + readonly initialActivityLogParams: ActivityLogParams; + readonly canEditNic: boolean; + readonly canEditStatements: boolean; + readonly onRateNic: () => void; +}) { + return ( + <> + {canEditNic && ( +
      + + + +
      + )} + + {/* Identity Statements */} +
      +

      + ID Statements +

      + {canEditStatements && ( + + )} +
      +
      + +
      + +
      + +
      + + ); +} diff --git a/components/user/rep/MobileRepTabContent.tsx b/components/user/rep/MobileRepTabContent.tsx new file mode 100644 index 0000000000..fe548f4cea --- /dev/null +++ b/components/user/rep/MobileRepTabContent.tsx @@ -0,0 +1,183 @@ +import type { ActivityLogParams } from "@/components/profile-activity/ProfileActivityLogs"; +import type { ApiRepCategory } from "@/generated/models/ApiRepCategory"; +import type { ApiRepOverview } from "@/generated/models/ApiRepOverview"; +import type { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { formatNumberWithCommas } from "@/helpers/Helpers"; +import { RateMatter } from "@/types/enums"; +import type { RepDirection } from "./UserPageRep.helpers"; +import RepDirectionToggle from "./RepDirectionToggle"; +import RepCategoryPill from "./RepCategoryPill"; +import UserPageCombinedActivityLog from "./UserPageCombinedActivityLog"; +import UserPageRateWrapper from "../utils/rate/UserPageRateWrapper"; + +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 MobileRepTabContent({ + profile, + overview, + categories, + repDirection, + onRepDirectionChange, + initialActivityLogParams, + loading, + canEditRep, + visibleCount, + onShowMore, + onGrantRep, + onEditCategory, +}: { + readonly profile: ApiIdentity; + readonly overview: ApiRepOverview | null; + readonly categories: ApiRepCategory[]; + readonly repDirection: RepDirection; + readonly onRepDirectionChange: (direction: RepDirection) => void; + readonly initialActivityLogParams: ActivityLogParams; + readonly loading: boolean; + readonly canEditRep: boolean; + readonly visibleCount: number; + readonly onShowMore: () => void; + readonly onGrantRep: () => void; + readonly onEditCategory: (category: string) => void; +}) { + return ( + <> + {canEditRep && repDirection === "received" && ( +
      + + + +
      + )} + + {repDirection === "given" && + overview !== null && + overview.authenticated_user_contribution !== null && + overview.authenticated_user_contribution !== 0 && ( +
      + + Assigned To You: + + + {overview.authenticated_user_contribution > 0 && "+"} + {formatNumberWithCommas( + overview.authenticated_user_contribution + )} + +
      + )} + +
      +
      + Rep Categories +
      + + {/* Received / Given toggle */} +
      + +
      +
      + + {categories.length > 0 && ( +
      +
      + {categories.slice(0, visibleCount).map((cat) => ( + + ))} + {categories.length > visibleCount && ( + + )} +
      +
      + )} + + {categories.length === 0 && ( + + )} + +
      + +
      + + ); +} diff --git a/components/user/rep/MobileTabCards.tsx b/components/user/rep/MobileTabCards.tsx new file mode 100644 index 0000000000..a98b9b442e --- /dev/null +++ b/components/user/rep/MobileTabCards.tsx @@ -0,0 +1,169 @@ +import { useMemo, type ComponentProps } from "react"; +import type { ApiRepOverview } from "@/generated/models/ApiRepOverview"; +import type { ApiCicOverview } from "@/generated/models/ApiCicOverview"; +import type { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { formatNumberWithCommas } from "@/helpers/Helpers"; +import UserCICStatus from "../utils/user-cic-status/UserCICStatus"; +import UserCICTypeIcon from "../utils/user-cic-type/UserCICTypeIcon"; +import OverlappingAvatars from "@/components/common/OverlappingAvatars"; +import { buildRepAvatarItems } from "./buildRepAvatarItems"; +import { getContributorLabel, type RepDirection } from "./UserPageRep.helpers"; + +type MobileTab = "rep" | "identity"; + +export default function MobileTabCards({ + activeTab, + onTabChange, + overview, + cicOverview, + profile, + repDirection, + cicAvatarItems, +}: { + readonly activeTab: MobileTab; + readonly onTabChange: (tab: MobileTab) => void; + readonly overview: ApiRepOverview | null; + readonly cicOverview: ApiCicOverview | null; + readonly profile: ApiIdentity; + readonly repDirection: RepDirection; + readonly cicAvatarItems: ComponentProps["items"]; +}) { + const repAvatarItems = useMemo( + () => buildRepAvatarItems(overview?.contributors.data ?? [], 3), + [overview?.contributors.data] + ); + + return ( +
      + + + +
      + ); +} diff --git a/components/user/rep/RepCategoryPill.tsx b/components/user/rep/RepCategoryPill.tsx index f839428737..78641ed665 100644 --- a/components/user/rep/RepCategoryPill.tsx +++ b/components/user/rep/RepCategoryPill.tsx @@ -20,7 +20,10 @@ export default function RepCategoryPill({ readonly compact?: boolean; readonly direction?: RepDirection; }) { - const paddingClass = compact ? "tw-px-3 tw-py-2" : "tw-px-4 tw-py-2.5"; + const paddingClass = compact ? "tw-px-3 tw-py-2" : "tw-px-4 tw-h-11"; + const layoutClass = compact + ? "tw-inline-flex tw-flex-wrap tw-items-center tw-gap-x-2.5 tw-gap-y-1.5" + : "tw-inline-flex tw-items-center tw-gap-2.5"; const avatarItems = useMemo( () => @@ -46,14 +49,14 @@ export default function RepCategoryPill({ const content = ( <> - + {category.category} {formatNumberWithCommas(category.total_rep)} - · + · {avatarItems.length > 0 && ( - {!!category.authenticated_user_contribution && ( + {category.authenticated_user_contribution !== null && + category.authenticated_user_contribution !== 0 && ( <> · - {direction === "given" ? "To Me:" : "My Rate:"}{" "} + {direction === "given" + ? "Assigned To You:" + : "You Assigned:"}{" "} + {category.authenticated_user_contribution > 0 && "+"} {formatNumberWithCommas(category.authenticated_user_contribution)} @@ -88,7 +95,7 @@ export default function RepCategoryPill({ ); - 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}`; + const baseClasses = `group ${layoutClass} tw-rounded-lg tw-border tw-border-solid tw-border-white/10 tw-bg-[#18191B] tw-transition-all tw-duration-300 tw-ease-out ${paddingClass}`; if (canEdit) { return ( diff --git a/components/user/rep/RepDirectionToggle.tsx b/components/user/rep/RepDirectionToggle.tsx new file mode 100644 index 0000000000..af470505c1 --- /dev/null +++ b/components/user/rep/RepDirectionToggle.tsx @@ -0,0 +1,49 @@ +import { ArrowDownLeftIcon, ArrowUpRightIcon } from "@heroicons/react/24/solid"; +import type { RepDirection } from "./UserPageRep.helpers"; + +export default function RepDirectionToggle({ + repDirection, + onRepDirectionChange, + compact, +}: { + readonly repDirection: RepDirection; + readonly onRepDirectionChange: (direction: RepDirection) => void; + readonly compact?: boolean; +}) { + const iconClass = compact + ? "tw-h-3 tw-w-3 tw-flex-shrink-0" + : "tw-h-3.5 tw-w-3.5 tw-flex-shrink-0"; + const textClass = compact ? "tw-text-xs" : "tw-text-[13px]"; + const activeExtra = compact ? "tw-font-semibold" : ""; + + return ( +
      + + +
      + ); +} diff --git a/components/user/rep/UserPageCombinedActivityLog.tsx b/components/user/rep/UserPageCombinedActivityLog.tsx index bc2f7243f7..ad39922afa 100644 --- a/components/user/rep/UserPageCombinedActivityLog.tsx +++ b/components/user/rep/UserPageCombinedActivityLog.tsx @@ -23,7 +23,7 @@ export default function UserPageCombinedActivityLog({ withFilters={true} withMatterFilter={withMatterFilter} > -

      +

      action.action_type === ApiProfileProxyActionType.AllocateCic + ); + } + if (amIUser({ profile: targetProfile, address })) return false; + return true; +} diff --git a/components/user/rep/UserPageRep.tsx b/components/user/rep/UserPageRep.tsx index 74ae9fe590..79a144f89a 100644 --- a/components/user/rep/UserPageRep.tsx +++ b/components/user/rep/UserPageRep.tsx @@ -7,10 +7,13 @@ import type { ApiRepOverview } from "@/generated/models/ApiRepOverview"; import type { ApiRepCategoriesPage } from "@/generated/models/ApiRepCategoriesPage"; import type { ApiCicOverview } from "@/generated/models/ApiCicOverview"; import { commonApiFetch } from "@/services/api/common-api"; +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 { useParams } from "next/navigation"; -import { useState } from "react"; +import { useContext, useMemo, useState } from "react"; import UserPageIdentityHeader from "../identity/header/UserPageIdentityHeader"; import UserPageIdentityHeaderCICRate from "../identity/header/cic-rate/UserPageIdentityHeaderCICRate"; import UserPageIdentityStatements from "../identity/statements/UserPageIdentityStatements"; @@ -18,6 +21,7 @@ import UserPageRateWrapper from "../utils/rate/UserPageRateWrapper"; import UserPageCombinedActivityLog from "./UserPageCombinedActivityLog"; import type { RepDirection } from "./UserPageRep.helpers"; +import { getCanEditNic } from "./UserPageRep.helpers"; import UserPageRepHeader from "./header/UserPageRepHeader"; import UserPageRepMobile from "./UserPageRepMobile"; @@ -29,9 +33,23 @@ export default function UserPageRep({ readonly initialActivityLogParams: ActivityLogParams; }) { const params = useParams(); - const user = (params?.["user"] as string)?.toLowerCase(); + const user = (params["user"] as string).toLowerCase(); + const { connectedProfile, activeProfileProxy } = useContext(AuthContext); + const { address } = useSeizeConnectContext(); const [repDirection, setRepDirection] = useState("received"); + const [isNicRateOpen, setIsNicRateOpen] = useState(false); + + const canEditNic = useMemo( + () => + getCanEditNic({ + connectedProfile, + targetProfile: profile, + activeProfileProxy, + address, + }), + [connectedProfile, profile, activeProfileProxy, address] + ); // --- Incoming (received) rep --- const { data: repOverview, isFetching: isFetchingOverview } = @@ -166,25 +184,43 @@ export default function UserPageRep({ setIsNicRateOpen(true) } + : {})} /> -
      - - - -

      + + setIsNicRateOpen(false)} + tabletModal + maxWidthClass="md:tw-max-w-md" + > +
      + + setIsNicRateOpen(false)} + /> + +
      + +
      +
      +
      ); } diff --git a/components/user/rep/UserPageRepMobile.tsx b/components/user/rep/UserPageRepMobile.tsx index fc0a57ebac..3c446ebb64 100644 --- a/components/user/rep/UserPageRepMobile.tsx +++ b/components/user/rep/UserPageRepMobile.tsx @@ -8,49 +8,22 @@ 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 { buildRepAvatarItems } from "./buildRepAvatarItems"; import { RateMatter } from "@/types/enums"; import { AnimatePresence, motion } from "framer-motion"; -import { useContext, useEffect, useMemo, useState } from "react"; +import { useContext, useMemo, useState } from "react"; import UserPageIdentityHeaderCICRate from "../identity/header/cic-rate/UserPageIdentityHeaderCICRate"; -import UserPageIdentityStatementsAddButton from "../identity/statements/add/UserPageIdentityStatementsAddButton"; -import UserPageIdentityStatements from "../identity/statements/UserPageIdentityStatements"; import UserPageRateWrapper from "../utils/rate/UserPageRateWrapper"; -import UserCICStatus from "../utils/user-cic-status/UserCICStatus"; -import UserCICTypeIcon from "../utils/user-cic-type/UserCICTypeIcon"; -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 } from "./UserPageRep.helpers"; +import type { RepDirection } from "./UserPageRep.helpers"; +import { getCanEditRep, getCanEditNic } from "./UserPageRep.helpers"; +import MobileTabCards from "./MobileTabCards"; +import MobileRepTabContent from "./MobileRepTabContent"; +import MobileIdentityTabContent from "./MobileIdentityTabContent"; 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, overview, @@ -79,9 +52,11 @@ export default function UserPageRepMobile({ const [visibleCount, setVisibleCount] = useState(5); const [editCategory, setEditCategory] = useState(null); - useEffect(() => { + const [prevCategories, setPrevCategories] = useState(categories); + if (categories !== prevCategories) { + setPrevCategories(categories); setVisibleCount(5); - }, [categories]); + } const canEditRep = useMemo( () => @@ -93,176 +68,44 @@ export default function UserPageRepMobile({ [connectedProfile, profile, activeProfileProxy] ); - const canEditNic = useMemo((): boolean => { - if (!connectedProfile?.handle) return false; - if (activeProfileProxy) { - if (profile.handle === activeProfileProxy.created_by.handle) return false; - return activeProfileProxy.actions.some( - (action) => action.action_type === ApiProfileProxyActionType.AllocateCic - ); - } - if (amIUser({ profile, address })) return false; - return true; - }, [connectedProfile, profile, activeProfileProxy, address]); + const canEditNic = useMemo( + () => + getCanEditNic({ + connectedProfile, + targetProfile: profile, + activeProfileProxy, + address, + }), + [connectedProfile, profile, activeProfileProxy, address] + ); const canEditStatements = !activeProfileProxy && - !!profile?.handle && + !!profile.handle && (profile.wallets ?? []).some( (w) => w.wallet.toLowerCase() === address?.toLowerCase() ); - // 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)} - - ), - })), + buildRepAvatarItems(cicOverview?.contributors.data ?? [], 3, { + omitHref: true, + }), [cicOverview?.contributors.data] ); return (
      -
      - - - {/* NIC Score */} - -
      + - {/* Tab Content */} {activeTab === "rep" ? ( - {/* Received / Given toggle */} -
      - - -
      - - {/* Rep Categories */} - {categories.length > 0 && ( -
      -
      - Rep Categories -
      -
      - {categories.slice(0, visibleCount).map((cat) => ( - - ))} - {categories.length > visibleCount && ( - - )} -
      -
      - )} - - {categories.length === 0 && ( - - )} - - {canEditRep && repDirection === "received" && ( -
      - -
      - - Add rep to this identity - - -
      -
      -
      - )} - -
      - -
      + setVisibleCount((prev) => prev + 10)} + onGrantRep={() => setIsGrantRepOpen(true)} + onEditCategory={setEditCategory} + />
      ) : ( - {/* Rate NIC CTA */} - {canEditNic && ( -
      - -
      - - Verify this identity - - -
      -
      -
      - )} - - {/* Identity Statements */} -
      -

      - ID Statements -

      - {canEditStatements && ( - - )} -
      -
      - -
      - -
      - -
      + setIsNicRateOpen(true)} + />
      )}
      @@ -459,17 +170,9 @@ export default function UserPageRepMobile({ profile={profile} isTooltip={false} onSuccess={() => setIsNicRateOpen(false)} + onCancel={() => setIsNicRateOpen(false)} /> -
      - -
      diff --git a/components/user/rep/buildRepAvatarItems.tsx b/components/user/rep/buildRepAvatarItems.tsx new file mode 100644 index 0000000000..90f7a16f64 --- /dev/null +++ b/components/user/rep/buildRepAvatarItems.tsx @@ -0,0 +1,35 @@ +import { formatNumberWithCommas } from "@/helpers/Helpers"; + +interface ContributorData { + readonly profile: { + readonly handle: string | null; + readonly primary_address: string; + readonly pfp: string | null; + }; + readonly contribution: number; +} + +export function buildRepAvatarItems( + contributors: readonly ContributorData[], + maxCount: number, + options?: { omitHref?: boolean } +) { + return contributors.slice(0, maxCount).map((c) => ({ + key: c.profile.handle ?? c.profile.primary_address, + pfpUrl: c.profile.pfp ?? null, + ...(options?.omitHref + ? {} + : { href: `/${c.profile.handle ?? c.profile.primary_address}` }), + 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)} + + ), + })); +} diff --git a/components/user/rep/header/UserPageRepHeader.tsx b/components/user/rep/header/UserPageRepHeader.tsx index bbb272e0a0..ec9a61ba80 100644 --- a/components/user/rep/header/UserPageRepHeader.tsx +++ b/components/user/rep/header/UserPageRepHeader.tsx @@ -5,9 +5,12 @@ 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 { PlusIcon } from "@heroicons/react/24/solid"; +import { useContext, useMemo, useState } from "react"; +import OverlappingAvatars from "@/components/common/OverlappingAvatars"; import RepCategoryPill from "../RepCategoryPill"; +import RepDirectionToggle from "../RepDirectionToggle"; +import { buildRepAvatarItems } from "../buildRepAvatarItems"; import UserPageRepModifyModal from "../modify-rep/UserPageRepModifyModal"; import GrantRepDialog from "../new-rep/GrantRepDialog"; import { @@ -35,9 +38,11 @@ export default function UserPageRepHeader({ const [visibleCount, setVisibleCount] = useState(5); - useEffect(() => { + const [prevCategories, setPrevCategories] = useState(categories); + if (categories !== prevCategories) { + setPrevCategories(categories); setVisibleCount(5); - }, [categories]); + } const visibleCategories = categories.slice(0, visibleCount); const hasMore = categories.length > visibleCount; @@ -52,6 +57,17 @@ export default function UserPageRepHeader({ [connectedProfile, profile, activeProfileProxy] ); + const TOP_CONTRIBUTORS_COUNT = 5; + + const avatarItems = useMemo( + () => + buildRepAvatarItems( + overview?.contributors.data ?? [], + TOP_CONTRIBUTORS_COUNT + ), + [overview?.contributors.data] + ); + const [editCategory, setEditCategory] = useState(null); const [isGrantRepOpen, setIsGrantRepOpen] = useState(false); @@ -64,7 +80,7 @@ export default function UserPageRepHeader({
      -
      +

      Rep @@ -74,95 +90,80 @@ export default function UserPageRepHeader({ ? "What others recognize this identity for." : "What this identity recognizes others for."}

      +
      + +

      - {overview ? ( -
      -
      - Total Rep -
      -
      - {formatNumberWithCommas(overview.total_rep)} -
      - - {formatNumberWithCommas(overview.contributor_count)}{" "} - {getContributorLabel( - repDirection, - overview.contributor_count - )} - +
      +
      + Total Rep
      - ) : ( -
      -
      - Total Rep -
      -
      - — -
      +
      + {overview + ? formatNumberWithCommas(overview.total_rep) + : "—"}
      - )} -
      - -
      - - + {overview && ( +
      + {avatarItems.length > 0 && ( + + )} + + {formatNumberWithCommas(overview.contributor_count)}{" "} + {getContributorLabel( + repDirection, + overview.contributor_count + )} + +
      + )} +
      {(visibleCategories.length > 0 || (canEditRep && repDirection === "received")) && ( -
      -
      - Rep Categories +
      +
      +
      + Rep Categories +
      + {overview !== null && + overview.authenticated_user_contribution !== null && + overview.authenticated_user_contribution !== 0 && ( +
      + + {repDirection === "given" + ? "Assigned To You:" + : "You Assigned:"} + + + {overview.authenticated_user_contribution > 0 && "+"} + {formatNumberWithCommas( + overview.authenticated_user_contribution + )} + +
      + )}
      -
      +
      {canEditRep && repDirection === "received" && ( )} @@ -191,17 +192,19 @@ export default function UserPageRepHeader({ {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."} -

      +
      +

      + {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") && ( -
      +
      )} diff --git a/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx b/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx index 39e84f095b..49a66d48d0 100644 --- a/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx +++ b/components/user/rep/new-rep/UserPageRepNewRepSearch.tsx @@ -372,7 +372,7 @@ export default function UserPageRepNewRepSearch({ diff --git a/components/user/user-page-header/UserPageHeaderClient.tsx b/components/user/user-page-header/UserPageHeaderClient.tsx index 9475a451f9..45cf83e9df 100644 --- a/components/user/user-page-header/UserPageHeaderClient.tsx +++ b/components/user/user-page-header/UserPageHeaderClient.tsx @@ -184,7 +184,7 @@ export default function UserPageHeaderClient({ />
      -
      +
      diff --git a/components/user/user-page-header/name/UserPageHeaderName.tsx b/components/user/user-page-header/name/UserPageHeaderName.tsx index d38b892227..41d3c7a278 100644 --- a/components/user/user-page-header/name/UserPageHeaderName.tsx +++ b/components/user/user-page-header/name/UserPageHeaderName.tsx @@ -85,17 +85,17 @@ export default function UserPageHeaderName({
      {profile?.classification && ( -
      +
      {CLASSIFICATIONS[profile.classification].title}
      )} {profileEnabledLabel && ( - + )} {profileEnabledLabel && (

      Profile enabled: {profileEnabledLabel}