diff --git a/__tests__/components/brain/notifications/NotificationWaveCreated.test.tsx b/__tests__/components/brain/notifications/NotificationWaveCreated.test.tsx index 9403e74e9d..1f38db6903 100644 --- a/__tests__/components/brain/notifications/NotificationWaveCreated.test.tsx +++ b/__tests__/components/brain/notifications/NotificationWaveCreated.test.tsx @@ -6,7 +6,7 @@ jest.mock('@tanstack/react-query', () => ({ useQuery: (...args:any[]) => queryMo jest.mock('next/link', () => ({ __esModule: true, default: (p:any) => {p.children} })); jest.mock('@/components/waves/header/WaveHeaderFollow', () => ({ __esModule: true, default: () =>
, WaveFollowBtnSize:{} })); jest.mock('@/components/brain/notifications/NotificationsFollowBtn', () => ({ __esModule: true, default: () =>
})); -jest.mock('@/helpers/image.helpers', () => ({ getScaledImageUri: () => 'scaled.jpg', ImageScale:{} })); +jest.mock('@/helpers/image.helpers', () => ({ getScaledImageUri: () => '/scaled.jpg', ImageScale:{} })); jest.mock('@/helpers/Helpers', () => ({ getTimeAgoShort: () => '1m' })); const notification = { @@ -35,5 +35,5 @@ it('renders wave data and links', () => { expect(screen.getByTestId('wave-follow')).toBeInTheDocument(); expect(screen.getByTestId('follow-btn')).toBeInTheDocument(); const img = screen.getByRole('img'); - expect(img).toHaveAttribute('src', 'scaled.jpg'); + expect(img.getAttribute('src')).toContain('scaled.jpg'); }); diff --git a/__tests__/components/brain/notifications/drop-quoted/NotificationDropQuoted.test.tsx b/__tests__/components/brain/notifications/drop-quoted/NotificationDropQuoted.test.tsx index 942cb6f4ea..1874e54b85 100644 --- a/__tests__/components/brain/notifications/drop-quoted/NotificationDropQuoted.test.tsx +++ b/__tests__/components/brain/notifications/drop-quoted/NotificationDropQuoted.test.tsx @@ -9,6 +9,21 @@ jest.mock('next/navigation', () => ({ usePathname: jest.fn(), })); +jest.mock('@/hooks/useDeviceInfo', () => ({ + __esModule: true, + default: () => ({ isApp: false }), +})); + +jest.mock('@/components/brain/notifications/subcomponents/NotificationHeader', () => ({ + __esModule: true, + default: ({ children }: any) =>
{children}
, +})); + +jest.mock('@/components/brain/notifications/NotificationsFollowBtn', () => ({ + __esModule: true, + default: () =>
, +})); + jest.mock('@/components/waves/drops/Drop', () => ({ __esModule: true, default: (props: any) => ( diff --git a/__tests__/components/brain/notifications/drop-replied/NotificationDropReplied.test.tsx b/__tests__/components/brain/notifications/drop-replied/NotificationDropReplied.test.tsx index 5007e128e8..c2dbe0ddcb 100644 --- a/__tests__/components/brain/notifications/drop-replied/NotificationDropReplied.test.tsx +++ b/__tests__/components/brain/notifications/drop-replied/NotificationDropReplied.test.tsx @@ -10,6 +10,11 @@ jest.mock('next/navigation', () => ({ usePathname: jest.fn(), })); +jest.mock('@/hooks/useDeviceInfo', () => ({ + __esModule: true, + default: () => ({ isApp: false }), +})); + jest.mock('@/components/waves/drops/Drop', () => ({ __esModule: true, DropLocation: { MY_STREAM: 'MY_STREAM' }, diff --git a/__tests__/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.test.tsx b/__tests__/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.test.tsx index 9cee6f7346..3b975c730e 100644 --- a/__tests__/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.test.tsx +++ b/__tests__/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.test.tsx @@ -17,6 +17,10 @@ jest.mock('next/navigation', () => ({ useSearchParams: jest.fn(), usePathname: jest.fn(), })); +jest.mock('@/hooks/useDeviceInfo', () => ({ + __esModule: true, + default: () => ({ isApp: false }), +})); import NotificationIdentityMentioned from '@/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned'; diff --git a/components/brain/notifications/drop-quoted/NotificationDropQuoted.tsx b/components/brain/notifications/drop-quoted/NotificationDropQuoted.tsx index 7a95b8c42c..5b981f42b5 100644 --- a/components/brain/notifications/drop-quoted/NotificationDropQuoted.tsx +++ b/components/brain/notifications/drop-quoted/NotificationDropQuoted.tsx @@ -1,5 +1,8 @@ "use client"; +import { getTimeAgoShort } from "@/helpers/Helpers"; +import { UserFollowBtnSize } from "@/components/user/utils/UserFollowBtn"; +import NotificationsFollowBtn from "../NotificationsFollowBtn"; import { DropSize, ExtendedDrop } from "@/helpers/waves/drop.helpers"; import { INotificationDropQuoted } from "@/types/feed.types"; import { ActiveDropState } from "@/types/dropInteractionTypes"; @@ -7,10 +10,9 @@ import Drop, { DropInteractionParams, DropLocation, } from "@/components/waves/drops/Drop"; -import { useRouter } from "next/navigation"; import { ApiDrop } from "@/generated/models/ApiDrop"; -import useDeviceInfo from "@/hooks/useDeviceInfo"; -import { getWaveRoute } from "@/helpers/navigation.helpers"; +import NotificationHeader from "../subcomponents/NotificationHeader"; +import { useWaveNavigation } from "../utils/navigationUtils"; export default function NotificationDropQuoted({ notification, @@ -25,18 +27,7 @@ export default function NotificationDropQuoted({ readonly onQuote: (param: DropInteractionParams) => void; readonly onDropContentClick?: (drop: ExtendedDrop) => void; }) { - const router = useRouter(); - const { isApp } = useDeviceInfo(); - - const navigateToWave = ( - waveId: string, - serialNo: number, - isDirectMessage: boolean - ) => { - router.push( - getWaveRoute({ waveId, serialNo, isDirectMessage, isApp }) - ); - }; + const { navigateToWave } = useWaveNavigation(); const onReplyClick = (serialNo: number) => { const baseWave = notification.related_drops[0].wave as any; @@ -53,25 +44,47 @@ export default function NotificationDropQuoted({ }; return ( - +
+ + } + > + + quoted you + + + + • + + {getTimeAgoShort(notification.created_at)} + + + + +
); } diff --git a/components/brain/notifications/drop-reacted/NotificationDropReacted.tsx b/components/brain/notifications/drop-reacted/NotificationDropReacted.tsx index b0cc3b1350..5fb605cfbb 100644 --- a/components/brain/notifications/drop-reacted/NotificationDropReacted.tsx +++ b/components/brain/notifications/drop-reacted/NotificationDropReacted.tsx @@ -1,14 +1,11 @@ "use client"; -import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers"; import { getTimeAgoShort, numberWithCommas } from "@/helpers/Helpers"; -import Link from "next/link"; import Drop, { DropInteractionParams, DropLocation, } from "@/components/waves/drops/Drop"; import { ActiveDropState } from "@/types/dropInteractionTypes"; -import { useRouter } from "next/navigation"; import { ApiDrop } from "@/generated/models/ApiDrop"; import { DropSize, ExtendedDrop } from "@/helpers/waves/drop.helpers"; import NotificationsFollowBtn from "../NotificationsFollowBtn"; @@ -18,9 +15,8 @@ import type { INotificationDropVoted, INotificationDropReacted, } from "@/types/feed.types"; -import UserProfileTooltipWrapper from "@/components/utils/tooltip/UserProfileTooltipWrapper"; -import useDeviceInfo from "@/hooks/useDeviceInfo"; -import { getWaveRoute } from "@/helpers/navigation.helpers"; +import NotificationHeader from "../subcomponents/NotificationHeader"; +import { useWaveNavigation } from "../utils/navigationUtils"; export const getNotificationVoteColor = (vote: number) => { if (vote > 0) return "tw-text-green"; @@ -45,9 +41,8 @@ export default function NotificationDropReacted({ onQuote, onDropContentClick, }: Props) { - const router = useRouter(); + const { navigateToWave } = useWaveNavigation(); const { findCustomEmoji, findNativeEmoji } = useEmoji(); - const { isApp } = useDeviceInfo(); // Determine if this notification is a "vote" or a "reaction" const isVoted = @@ -66,20 +61,13 @@ export default function NotificationDropReacted({ actionElement = ( <> - - - {notification.related_identity.handle} - - rated + )} tw-font-medium tw-text-sm`}> {voteValue > 0 && "+"} {numberWithCommas(voteValue)} @@ -125,13 +113,6 @@ export default function NotificationDropReacted({ actionElement = ( <> - - - {notification.related_identity.handle} - - reacted @@ -152,81 +133,50 @@ export default function NotificationDropReacted({ const baseIsDm = baseWave?.chat?.scope?.group?.is_direct_message ?? false; const onReplyClick = (serialNo: number) => { - router.push( - getWaveRoute({ - waveId: baseWave.id, - serialNo, - isDirectMessage: baseIsDm, - isApp, - }) - ); + navigateToWave(baseWave.id, serialNo, baseIsDm); }; + const onQuoteClick = (quote: ApiDrop) => { const quoteWave = quote.wave as any; const isDirectMessage = quoteWave?.chat?.scope?.group?.is_direct_message ?? baseIsDm; - - router.push( - getWaveRoute({ - waveId: quote.wave.id, - serialNo: quote.serial_no, - isDirectMessage, - isApp, - }) - ); + navigateToWave(quote.wave.id, quote.serial_no, isDirectMessage); }; return ( -
-
-
-
-
- {notification.related_identity.pfp ? ( - {notification.related_identity.handle - ) : ( -
- )} -
- - {actionElement} - -
- +
+ -
- - -
+ } + > + {actionElement} + + +
); } diff --git a/components/brain/notifications/drop-replied/NotificationDropReplied.tsx b/components/brain/notifications/drop-replied/NotificationDropReplied.tsx index e71ebd99de..1e23242752 100644 --- a/components/brain/notifications/drop-replied/NotificationDropReplied.tsx +++ b/components/brain/notifications/drop-replied/NotificationDropReplied.tsx @@ -1,10 +1,5 @@ "use client"; -import Link from "next/link"; -import { - getScaledImageUri, - ImageScale, -} from "@/helpers/image.helpers"; import { INotificationDropReplied } from "@/types/feed.types"; import { getTimeAgoShort } from "@/helpers/Helpers"; import { ActiveDropState } from "@/types/dropInteractionTypes"; @@ -13,13 +8,11 @@ import Drop, { DropLocation, } from "@/components/waves/drops/Drop"; import { DropSize, ExtendedDrop } from "@/helpers/waves/drop.helpers"; -import { useRouter } from "next/navigation"; import { ApiDrop } from "@/generated/models/ApiDrop"; import NotificationsFollowBtn from "../NotificationsFollowBtn"; import { UserFollowBtnSize } from "@/components/user/utils/UserFollowBtn"; -import UserProfileTooltipWrapper from "@/components/utils/tooltip/UserProfileTooltipWrapper"; -import useDeviceInfo from "@/hooks/useDeviceInfo"; -import { getWaveRoute } from "@/helpers/navigation.helpers"; +import NotificationHeader from "../subcomponents/NotificationHeader"; +import { useWaveNavigation } from "../utils/navigationUtils"; export default function NotificationDropReplied({ notification, @@ -34,102 +27,63 @@ export default function NotificationDropReplied({ readonly onQuote: (param: DropInteractionParams) => void; readonly onDropContentClick?: (drop: ExtendedDrop) => void; }) { - const router = useRouter(); - const { isApp } = useDeviceInfo(); + const { navigateToWave } = useWaveNavigation(); const baseWave = notification.related_drops[1].wave as any; const isDirectMessage = baseWave?.chat?.scope?.group?.is_direct_message ?? false; const onReplyClick = (serialNo: number) => { - router.push( - getWaveRoute({ - waveId: notification.related_drops[1].wave.id, - serialNo, - isDirectMessage, - isApp, - }) - ); + navigateToWave(notification.related_drops[1].wave.id, serialNo, isDirectMessage); }; const onQuoteClick = (quote: ApiDrop) => { const quoteWave = quote.wave as any; const quoteIsDm = quoteWave?.chat?.scope?.group?.is_direct_message ?? isDirectMessage; - - router.push( - getWaveRoute({ - waveId: quote.wave.id, - serialNo: quote.serial_no, - isDirectMessage: quoteIsDm, - isApp, - }) - ); + navigateToWave(quote.wave.id, quote.serial_no, quoteIsDm); }; return ( -
-
-
-
-
- {notification.related_drops[1].author.pfp ? ( - # - ) : ( -
- )} -
- - - - {notification.related_drops[1].author.handle} - - {" "} - - replied - {" "} - - - • - {" "} - {getTimeAgoShort(notification.created_at)} - - -
+
+ -
+ } + > + + replied + + + + • + + {getTimeAgoShort(notification.created_at)} + + - -
+
); } diff --git a/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.tsx b/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.tsx index 9cfa2c1e18..4542c01e43 100644 --- a/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.tsx +++ b/components/brain/notifications/identity-mentioned/NotificationIdentityMentioned.tsx @@ -1,10 +1,5 @@ "use client"; -import Link from "next/link"; -import { - getScaledImageUri, - ImageScale, -} from "@/helpers/image.helpers"; import { INotificationIdentityMentioned } from "@/types/feed.types"; import { getTimeAgoShort } from "@/helpers/Helpers"; import { ActiveDropState } from "@/types/dropInteractionTypes"; @@ -13,13 +8,13 @@ import Drop, { DropLocation, } from "@/components/waves/drops/Drop"; import { DropSize, ExtendedDrop } from "@/helpers/waves/drop.helpers"; -import { useRouter } from "next/navigation"; -import { ApiDrop } from "@/generated/models/ApiDrop"; import { UserFollowBtnSize } from "@/components/user/utils/UserFollowBtn"; import NotificationsFollowBtn from "../NotificationsFollowBtn"; -import UserProfileTooltipWrapper from "@/components/utils/tooltip/UserProfileTooltipWrapper"; -import useDeviceInfo from "@/hooks/useDeviceInfo"; -import { getWaveRoute } from "@/helpers/navigation.helpers"; +import NotificationHeader from "../subcomponents/NotificationHeader"; +import { + useWaveNavigation, + getIsDirectMessage, +} from "../utils/navigationUtils"; export default function NotificationIdentityMentioned({ notification, @@ -34,102 +29,58 @@ export default function NotificationIdentityMentioned({ readonly onQuote: (param: DropInteractionParams) => void; readonly onDropContentClick?: (drop: ExtendedDrop) => void; }) { - const router = useRouter(); - const { isApp } = useDeviceInfo(); - const baseWave = notification.related_drops[0].wave as any; - const baseIsDm = baseWave.chat?.scope?.group?.is_direct_message ?? false; - - const navigateToDropInWave = ( - waveId: string, - serialNo: number, - isDirectMessage: boolean - ) => { - router.push( - getWaveRoute({ - waveId, - serialNo, - isDirectMessage, - isApp, - }) - ); - }; + const { navigateToWave, createQuoteClickHandler } = useWaveNavigation(); + const baseWave = notification.related_drops[0].wave; + const baseIsDm = getIsDirectMessage(baseWave); const onReplyClick = (serialNo: number) => { - navigateToDropInWave(baseWave.id, serialNo, baseIsDm); + navigateToWave(baseWave.id, serialNo, baseIsDm); }; - const onQuoteClick = (quote: ApiDrop) => { - const quoteWave = quote.wave as any; - const quoteIsDm = - quoteWave?.chat?.scope?.group?.is_direct_message ?? baseIsDm; - navigateToDropInWave(quote.wave.id, quote.serial_no, quoteIsDm); - }; + const onQuoteClick = createQuoteClickHandler(baseIsDm); return ( -
-
-
-
-
- {notification.related_drops[0].author.pfp ? ( - # - ) : ( -
- )} -
- - - - {notification.related_drops[0].author.handle} - - {" "} - - mentioned you - {" "} - - - • - {" "} - {getTimeAgoShort(notification.created_at)} - - -
+
+ -
+ } + > + + mentioned you + + + + • + + {getTimeAgoShort(notification.created_at)} + + - -
+
); } diff --git a/components/brain/notifications/identity-subscribed/NotificationIdentitySubscribed.tsx b/components/brain/notifications/identity-subscribed/NotificationIdentitySubscribed.tsx index d551eee0b5..800b09e958 100644 --- a/components/brain/notifications/identity-subscribed/NotificationIdentitySubscribed.tsx +++ b/components/brain/notifications/identity-subscribed/NotificationIdentitySubscribed.tsx @@ -1,13 +1,8 @@ -import Link from "next/link"; import { INotificationIdentitySubscribed } from "@/types/feed.types"; -import { - getScaledImageUri, - ImageScale, -} from "@/helpers/image.helpers"; import { getTimeAgoShort } from "@/helpers/Helpers"; import { UserFollowBtnSize } from "@/components/user/utils/UserFollowBtn"; import NotificationsFollowBtn from "../NotificationsFollowBtn"; -import UserProfileTooltipWrapper from "@/components/utils/tooltip/UserProfileTooltipWrapper"; +import NotificationHeader from "../subcomponents/NotificationHeader"; export default function NotificationIdentitySubscribed({ notification, @@ -15,46 +10,26 @@ export default function NotificationIdentitySubscribed({ readonly notification: INotificationIdentitySubscribed; }) { return ( -
-
- {notification.related_identity.pfp ? ( - # + - ) : ( -
- )} -
-
- - - - - {notification.related_identity.handle} - - {" "} - started following you - {" "} - - - • - {" "} - {getTimeAgoShort(notification.created_at)} + } + > + + started following you + + + + • + {getTimeAgoShort(notification.created_at)} - - -
+
); } diff --git a/components/brain/notifications/subcomponents/NotificationHeader.tsx b/components/brain/notifications/subcomponents/NotificationHeader.tsx new file mode 100644 index 0000000000..0ccf9f4c86 --- /dev/null +++ b/components/brain/notifications/subcomponents/NotificationHeader.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; +import Image from "next/image"; +import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers"; +import { parseIpfsUrl } from "@/helpers/Helpers"; +import UserProfileTooltipWrapper from "@/components/utils/tooltip/UserProfileTooltipWrapper"; +import { ApiProfileMin } from "@/generated/models/ApiProfileMin"; + +interface NotificationHeaderProps { + readonly author: ApiProfileMin; + readonly children: React.ReactNode; // Content AFTER the user link (e.g. "replied • 2h") + readonly actions?: React.ReactNode; // Follow button(s) +} + +export default function NotificationHeader({ + author, + children, + actions, +}: NotificationHeaderProps) { + return ( +
+
+ {author.pfp ? ( + {author.handle + ) : ( +
+ )} +
+
+
+ + + {author.handle} + + + {children} +
+ {actions && ( +
+ {actions} +
+ )} +
+
+ ); +} diff --git a/components/brain/notifications/utils/navigationUtils.ts b/components/brain/notifications/utils/navigationUtils.ts new file mode 100644 index 0000000000..f76db12e86 --- /dev/null +++ b/components/brain/notifications/utils/navigationUtils.ts @@ -0,0 +1,38 @@ +import { useRouter } from "next/navigation"; +import { getWaveRoute } from "@/helpers/navigation.helpers"; +import useDeviceInfo from "@/hooks/useDeviceInfo"; +import { ApiDrop } from "@/generated/models/ApiDrop"; + +export function getIsDirectMessage(wave: { id: string }, fallback = false): boolean { + const w = wave as { chat?: { scope?: { group?: { is_direct_message?: boolean } } } }; + return w.chat?.scope?.group?.is_direct_message ?? fallback; +} + +export function useWaveNavigation() { + const router = useRouter(); + const { isApp } = useDeviceInfo(); + + const navigateToWave = ( + waveId: string, + serialNo: number, + isDirectMessage: boolean + ) => { + router.push( + getWaveRoute({ + waveId, + serialNo, + isDirectMessage, + isApp, + }) + ); + }; + + const createQuoteClickHandler = (fallbackIsDm = false) => { + return (quote: ApiDrop) => { + const quoteIsDm = getIsDirectMessage(quote.wave, fallbackIsDm); + navigateToWave(quote.wave.id, quote.serial_no, quoteIsDm); + }; + }; + + return { navigateToWave, createQuoteClickHandler }; +} diff --git a/components/brain/notifications/wave-created/NotificationWaveCreated.tsx b/components/brain/notifications/wave-created/NotificationWaveCreated.tsx index 37333abd5d..32eae3f323 100644 --- a/components/brain/notifications/wave-created/NotificationWaveCreated.tsx +++ b/components/brain/notifications/wave-created/NotificationWaveCreated.tsx @@ -1,9 +1,5 @@ import Link from "next/link"; import { INotificationWaveCreated } from "@/types/feed.types"; -import { - getScaledImageUri, - ImageScale, -} from "@/helpers/image.helpers"; import { getTimeAgoShort } from "@/helpers/Helpers"; import WaveHeaderFollow, { WaveFollowBtnSize, @@ -16,6 +12,7 @@ import NotificationsFollowBtn from "../NotificationsFollowBtn"; import { UserFollowBtnSize } from "@/components/user/utils/UserFollowBtn"; import useDeviceInfo from "@/hooks/useDeviceInfo"; import { getWaveRoute } from "@/helpers/navigation.helpers"; +import NotificationHeader from "../subcomponents/NotificationHeader"; export default function NotificationWaveCreated({ notification, @@ -42,55 +39,40 @@ export default function NotificationWaveCreated({ }); return ( -
-
-
- {notification.related_identity.pfp ? ( - # + + {wave && ( + + )} + - ) : ( -
- )} -
- - - {notification.related_identity.handle} - {" "} - invited you to a wave:{" "} - - {wave?.name} - {" "} - - - • - {" "} - {getTimeAgoShort(notification.created_at)} +
+ } + > + + invited you to a wave: + + + {wave?.name} + + + + • + {getTimeAgoShort(notification.created_at)} -
-
- {wave && ( - - )} - -
+
); } diff --git a/components/header/header-search/HeaderSearchModal.tsx b/components/header/header-search/HeaderSearchModal.tsx index 72a79acfe0..ac672aafad 100644 --- a/components/header/header-search/HeaderSearchModal.tsx +++ b/components/header/header-search/HeaderSearchModal.tsx @@ -930,7 +930,7 @@ export default function HeaderSearchModal({ type="button" onClick={onClose} aria-label="Close search" - className="tw-hidden sm:tw-inline-flex tw-h-9 tw-w-9 tw-items-center tw-justify-center tw-rounded-full tw-border tw-border-iron-700 tw-bg-iron-900 tw-text-iron-300 hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white tw-transition tw-duration-150"> + className="tw-hidden sm:tw-inline-flex tw-h-9 tw-w-9 tw-items-center tw-justify-center tw-rounded-full tw-border tw-border-iron-700 tw-bg-iron-900 tw-text-iron-300 tw-border-solid hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white tw-transition tw-duration-150">
diff --git a/components/user/utils/UserCICAndLevel.tsx b/components/user/utils/UserCICAndLevel.tsx index 185473266f..f9b93d4724 100644 --- a/components/user/utils/UserCICAndLevel.tsx +++ b/components/user/utils/UserCICAndLevel.tsx @@ -1,4 +1,45 @@ -export { default } from "@/components/common/profile/ProfileLevel"; -export { - ProfileLevelSize as UserCICAndLevelSize, -} from "@/components/common/profile/ProfileLevel"; +import { CICType } from "@/entities/IProfile"; +import { CIC_COLOR } from "./raters-table/ProfileRatersTableItem"; + +export enum UserCICAndLevelSize { + SMALL = "SMALL", + MEDIUM = "MEDIUM", + LARGE = "LARGE", + XLARGE = "XLARGE", +} + +export default function UserCICAndLevel({ + level, + cicType, + color, + size = UserCICAndLevelSize.MEDIUM, +}: { + readonly level: number; + readonly cicType: CICType; + readonly color?: string; + readonly size?: UserCICAndLevelSize; +}) { + const mainColor = color ?? "iron-300"; + const LEVEL_SIZE_CLASSES: Record = { + [UserCICAndLevelSize.SMALL]: "tw-h-4 tw-w-4 tw-text-[9px]", + [UserCICAndLevelSize.MEDIUM]: "tw-h-5 tw-w-5 tw-text-[0.625rem]", + [UserCICAndLevelSize.LARGE]: "tw-h-6 tw-w-6 tw-text-[0.75rem]", + [UserCICAndLevelSize.XLARGE]: "tw-h-8 tw-w-8 tw-text-[1rem]", + }; + const CIC_SIZE_CLASSES: Record = { + [UserCICAndLevelSize.SMALL]: "-tw-top-[0.1875rem] tw-h-2 tw-w-2", + [UserCICAndLevelSize.MEDIUM]: "tw-top-0 tw-h-2 tw-w-2", + [UserCICAndLevelSize.LARGE]: "-tw-top-1.5 tw-h-3 tw-w-3", + [UserCICAndLevelSize.XLARGE]: "-tw-top-2 tw-h-4 tw-w-4", + }; + return ( +
+
+ {level} +
+ +
+ ); +} diff --git a/components/user/utils/UserFollowBtn.tsx b/components/user/utils/UserFollowBtn.tsx index d19513171d..e5766f76c9 100644 --- a/components/user/utils/UserFollowBtn.tsx +++ b/components/user/utils/UserFollowBtn.tsx @@ -27,7 +27,7 @@ export enum UserFollowBtnSize { } export const FOLLOW_BTN_BUTTON_CLASSES: Record = { - [UserFollowBtnSize.SMALL]: "tw-gap-x-1 tw-px-2.5 tw-py-1.5 tw-text-xs", + [UserFollowBtnSize.SMALL]: "tw-gap-x-1 tw-px-3 tw-py-2 tw-text-xs", [UserFollowBtnSize.MEDIUM]: "tw-gap-x-2 tw-px-3.5 tw-py-2.5 tw-text-sm", }; diff --git a/components/user/utils/profile/UserProfileTooltip.tsx b/components/user/utils/profile/UserProfileTooltip.tsx index 12582f3c1a..b3e735a8c3 100644 --- a/components/user/utils/profile/UserProfileTooltip.tsx +++ b/components/user/utils/profile/UserProfileTooltip.tsx @@ -1,18 +1,23 @@ import DropPfp from "@/components/drops/create/utils/DropPfp"; +import { cicToType } from "@/helpers/Helpers"; import { useIdentity } from "@/hooks/useIdentity"; import UserFollowBtn, { UserFollowBtnSize, } from "@/components/user/utils/UserFollowBtn"; -import UserCICTypeIcon from "../user-cic-type/UserCICTypeIcon"; -import UserLevel from "../level/UserLevel"; -import { CLASSIFICATIONS, CicStatement } from "@/entities/IProfile"; +import UserCICAndLevel, { UserCICAndLevelSize } from "../UserCICAndLevel"; +import UserProfileTooltipTopRep from "./UserProfileTooltipTopRep"; +import { + ApiProfileRepRatesState, + CLASSIFICATIONS, + CicStatement, +} from "@/entities/IProfile"; import { useQuery } from "@tanstack/react-query"; import { commonApiFetch } from "@/services/api/common-api"; import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper"; import { STATEMENT_GROUP, STATEMENT_TYPE } from "@/helpers/Types"; import { useContext, useEffect, useMemo, useState } from "react"; import { AuthContext } from "@/components/auth/Auth"; -import UserStatsRow from "../stats/UserStatsRow"; +import UserStatsRow, { UserStatsRowSize } from "../stats/UserStatsRow"; import { ApiIncomingIdentitySubscriptionsPage } from "@/generated/models/ApiIncomingIdentitySubscriptionsPage"; export default function UserProfileTooltip({ @@ -25,6 +30,18 @@ export default function UserProfileTooltip({ initialProfile: null, }); + const { data: repRates } = useQuery({ + queryKey: [ + QueryKey.PROFILE_REP_RATINGS, + { handleOrWallet: user.toLowerCase() }, + ], + queryFn: async () => + await commonApiFetch({ + endpoint: `profiles/${user}/rep/ratings/received`, + }), + enabled: !!user && !!profile?.handle, + }); + const { data: statements } = useQuery({ queryKey: [QueryKey.PROFILE_CIC_STATEMENTS, user.toLowerCase()], queryFn: async () => @@ -83,45 +100,40 @@ export default function UserProfileTooltip({ return (
-
-
-
-
- -
-
-
- - {profile?.handle ?? profile?.display} - - {profile && ( -
- -
- )} -
- {description && ( -

{description}

- )} +
+
+
+ +
+
+
+ + {profile?.handle ?? profile?.display} + {profile && ( -
- -
+ )}
+ {description && ( +

{description}

+ )}
- {showFollowButton && profileHandle && ( -
- -
- )}
+ {showFollowButton && profileHandle && ( +
+ +
+ )}
{aboutStatement && ( -

+

{aboutStatement.statement_value}

)} @@ -135,8 +147,10 @@ export default function UserProfileTooltip({ rep={profile?.rep ?? 0} cic={profile?.cic ?? 0} followersCount={followersCount} + size={UserStatsRowSize.SMALL} />
+
); } diff --git a/components/user/utils/profile/UserProfileTooltipTopRep.tsx b/components/user/utils/profile/UserProfileTooltipTopRep.tsx new file mode 100644 index 0000000000..4f81e9deec --- /dev/null +++ b/components/user/utils/profile/UserProfileTooltipTopRep.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { ApiProfileRepRatesState } from "@/entities/IProfile"; +import { formatNumberWithCommas } from "@/helpers/Helpers"; +import { useMemo } from "react"; + +export default function UserProfileTooltipTopRep({ + repRates, +}: { + readonly repRates: ApiProfileRepRatesState | undefined; +}) { + const topRepCategories = useMemo(() => { + if (!repRates?.rating_stats?.length) return []; + return [...repRates.rating_stats] + .sort( + (a, b) => + b.rating - a.rating || b.contributor_count - a.contributor_count + ) + .slice(0, 3); + }, [repRates]); + + if (topRepCategories.length === 0) { + return null; + } + + return ( +
+ + Top Rep + +
+ {topRepCategories.map((rep) => ( +
+ + {rep.category} + + 0 ? "tw-text-green" : "tw-text-red" + }`} + > + {formatNumberWithCommas(rep.rating)} + +
+ ))} +
+
+ ); +} diff --git a/components/user/utils/stats/UserStatsRow.tsx b/components/user/utils/stats/UserStatsRow.tsx index 6fc103ae67..46a1a90ca8 100644 --- a/components/user/utils/stats/UserStatsRow.tsx +++ b/components/user/utils/stats/UserStatsRow.tsx @@ -3,6 +3,25 @@ import Link from "next/link"; import { formatNumberWithCommas, formatStatFloor } from "@/helpers/Helpers"; +export enum UserStatsRowSize { + SMALL = "SMALL", + MEDIUM = "MEDIUM", +} + +const SIZE_CLASSES: Record< + UserStatsRowSize, + { text: string; rate: string } +> = { + [UserStatsRowSize.SMALL]: { + text: "tw-text-sm", + rate: "tw-text-xs", + }, + [UserStatsRowSize.MEDIUM]: { + text: "tw-text-base", + rate: "tw-text-sm", + }, +}; + interface UserStatsRowProps { readonly handle: string; readonly tdh: number; @@ -13,6 +32,7 @@ interface UserStatsRowProps { readonly cic: number; readonly followersCount: number | null; readonly className?: string; + readonly size?: UserStatsRowSize; } export default function UserStatsRow({ @@ -25,10 +45,12 @@ export default function UserStatsRow({ cic, followersCount, className = "", + size = UserStatsRowSize.MEDIUM, }: UserStatsRowProps) { const routeHandle = encodeURIComponent(handle.toLowerCase()); const count = followersCount ?? 0; const followerLabel = count === 1 ? "Follower" : "Followers"; + const classes = SIZE_CLASSES[size]; return (
@@ -37,16 +59,16 @@ export default function UserStatsRow({ href={`/${routeHandle}/collected`} className="tw-no-underline desktop-hover:hover:tw-underline tw-transition tw-duration-300 tw-ease-out" > - + {formatStatFloor(tdh)} {" "} - + TDH {tdh_rate > 0 && ( <> {" "} - + + {formatStatFloor(tdh_rate)} @@ -62,16 +84,16 @@ export default function UserStatsRow({ href={`/${routeHandle}/xtdh`} className="tw-no-underline desktop-hover:hover:tw-underline tw-transition tw-duration-300 tw-ease-out" > - + {formatStatFloor(xtdh)} {" "} - + xTDH {xtdh_rate > 0 && ( <> {" "} - + + {formatStatFloor(xtdh_rate)} @@ -87,10 +109,10 @@ export default function UserStatsRow({ href={`/${routeHandle}/identity`} className="tw-no-underline desktop-hover:hover:tw-underline tw-transition tw-duration-300 tw-ease-out" > - + {formatStatFloor(cic)} {" "} - + NIC @@ -101,10 +123,10 @@ export default function UserStatsRow({ href={`/${routeHandle}/rep`} className="tw-no-underline desktop-hover:hover:tw-underline tw-transition tw-duration-300 tw-ease-out" > - + {formatStatFloor(rep)} {" "} - + Rep @@ -115,10 +137,10 @@ export default function UserStatsRow({ href={`/${routeHandle}/followers`} className="tw-no-underline desktop-hover:hover:tw-underline tw-transition tw-duration-300 tw-ease-out" > - + {formatNumberWithCommas(count)} {" "} - + {followerLabel}