diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/ProfileViewsWidget.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/ProfileViewsWidget.tsx new file mode 100644 index 0000000000..e7f466f7de --- /dev/null +++ b/packages/shared/src/features/profile/components/ProfileWidgets/ProfileViewsWidget.tsx @@ -0,0 +1,170 @@ +import type { ReactElement } from 'react'; +import React, { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { + startOfWeek, + startOfMonth, + isAfter, + parseISO, + isEqual, +} from 'date-fns'; +import { ActivityContainer } from '../../../../components/profile/ActivitySection'; +import { + Typography, + TypographyColor, + TypographyTag, + TypographyType, +} from '../../../../components/typography/Typography'; +import { gqlClient } from '../../../../graphql/common'; +import type { + UserProfileAnalytics, + UserProfileAnalyticsHistory, +} from '../../../../graphql/users'; +import { + USER_PROFILE_ANALYTICS_HISTORY_QUERY, + USER_PROFILE_ANALYTICS_QUERY, +} from '../../../../graphql/users'; +import { generateQueryKey, RequestKey } from '../../../../lib/query'; +import { largeNumberFormat } from '../../../../lib'; +import { SummaryCard } from './BadgesAndAwardsComponents'; +import { ElementPlaceholder } from '../../../../components/ElementPlaceholder'; + +interface ProfileViewsWidgetProps { + userId: string; +} + +interface HistoryQueryResult { + userProfileAnalyticsHistory: { + edges: Array<{ + node: UserProfileAnalyticsHistory; + }>; + }; +} + +interface AnalyticsQueryResult { + userProfileAnalytics: UserProfileAnalytics | null; +} + +const ProfileViewsWidgetSkeleton = (): ReactElement => { + return ( + + + Profile Activity + +
+
+ + + + + + + + +
+ + + + +
+
+ ); +}; + +export const ProfileViewsWidget = ({ + userId, +}: ProfileViewsWidgetProps): ReactElement => { + const { data: historyData, isPending: isHistoryPending } = + useQuery({ + queryKey: generateQueryKey(RequestKey.ProfileAnalyticsHistory, { + id: userId, + }), + queryFn: () => + gqlClient.request(USER_PROFILE_ANALYTICS_HISTORY_QUERY, { + userId, + first: 31, + }), + enabled: !!userId, + refetchOnWindowFocus: false, + }); + + const { data: analyticsData, isPending: isAnalyticsPending } = + useQuery({ + queryKey: generateQueryKey(RequestKey.ProfileAnalytics, { id: userId }), + queryFn: () => + gqlClient.request(USER_PROFILE_ANALYTICS_QUERY, { + userId, + }), + enabled: !!userId, + refetchOnWindowFocus: false, + }); + + const { thisWeek, thisMonth } = useMemo(() => { + if (!historyData?.userProfileAnalyticsHistory?.edges) { + return { thisWeek: 0, thisMonth: 0 }; + } + + const now = new Date(); + const weekStart = startOfWeek(now, { weekStartsOn: 1 }); + const monthStart = startOfMonth(now); + + let weekTotal = 0; + let monthTotal = 0; + + historyData.userProfileAnalyticsHistory.edges.forEach(({ node }) => { + const entryDate = parseISO(node.date); + + if (isAfter(entryDate, monthStart) || isEqual(entryDate, monthStart)) { + monthTotal += node.uniqueVisitors; + } + + if (isAfter(entryDate, weekStart) || isEqual(entryDate, weekStart)) { + weekTotal += node.uniqueVisitors; + } + }); + + return { thisWeek: weekTotal, thisMonth: monthTotal }; + }, [historyData]); + + const total = analyticsData?.userProfileAnalytics?.uniqueVisitors ?? 0; + + if (isHistoryPending || isAnalyticsPending) { + return ; + } + + return ( + + + Profile Activity + +
+
+ + +
+ +
+
+ ); +}; diff --git a/packages/shared/src/features/profile/components/ProfileWidgets/ProfileWidgets.tsx b/packages/shared/src/features/profile/components/ProfileWidgets/ProfileWidgets.tsx index 3918a89ceb..8d9ba27522 100644 --- a/packages/shared/src/features/profile/components/ProfileWidgets/ProfileWidgets.tsx +++ b/packages/shared/src/features/profile/components/ProfileWidgets/ProfileWidgets.tsx @@ -10,9 +10,11 @@ import type { ProfileReadingData, ProfileV2 } from '../../../../graphql/users'; import { USER_READING_HISTORY_QUERY } from '../../../../graphql/users'; import { generateQueryKey, RequestKey } from '../../../../lib/query'; import { gqlClient } from '../../../../graphql/common'; +import { canViewUserProfileAnalytics } from '../../../../lib/user'; import { ReadingOverview } from './ReadingOverview'; import { ProfileCompletion } from './ProfileCompletion'; import { Share } from './Share'; +import { ProfileViewsWidget } from './ProfileViewsWidget'; import { useProfileCompletionIndicator } from '../../../../hooks/profile/useProfileCompletionIndicator'; const BadgesAndAwards = dynamic(() => @@ -68,6 +70,10 @@ export function ProfileWidgets({ {isSameUser && ( )} + {canViewUserProfileAnalytics({ + user: loggedUser, + profileUserId: user.id, + }) && } => { const res = await gqlClient.request(USER_STREAK_QUERY); return res.userStreak; }; +export const USER_PROFILE_ANALYTICS_QUERY = gql` + query UserProfileAnalytics($userId: ID!) { + userProfileAnalytics(userId: $userId) { + id + uniqueVisitors + updatedAt + } + } +`; + +export const USER_PROFILE_ANALYTICS_HISTORY_QUERY = gql` + query UserProfileAnalyticsHistory($userId: ID!, $first: Int) { + userProfileAnalyticsHistory(userId: $userId, first: $first) { + edges { + node { + id + date + uniqueVisitors + } + } + } + } +`; + export interface UserStreakRecoverData { canRecover: boolean; cost: number; diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index 4e093f6bf7..e174311f04 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -212,6 +212,8 @@ export enum RequestKey { NotificationSettings = 'notification_settings', PostAnalytics = 'post_analytics', PostAnalyticsHistory = 'post_analytics_history', + ProfileAnalytics = 'profile_analytics', + ProfileAnalyticsHistory = 'profile_analytics_history', CheckLocation = 'check_location', GenerateBrief = 'generate_brief', Opportunity = 'opportunity', diff --git a/packages/shared/src/lib/user.ts b/packages/shared/src/lib/user.ts index 8ffd6c6173..f19655abd1 100644 --- a/packages/shared/src/lib/user.ts +++ b/packages/shared/src/lib/user.ts @@ -306,6 +306,20 @@ export const canViewPostAnalytics = ({ return !!user?.id && user.id === post?.author?.id; }; +export const canViewUserProfileAnalytics = ({ + user, + profileUserId, +}: { + user?: Pick; + profileUserId?: string; +}): boolean => { + if (user?.isTeamMember) { + return true; + } + + return !!user?.id && user.id === profileUserId; +}; + export const userProfileQueryOptions = ({ id }) => { return { queryKey: generateQueryKey(