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 9dbd757961..7a448b37cf 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'; const BadgesAndAwards = dynamic(() => import('./BadgesAndAwards').then((mod) => mod.BadgesAndAwards), @@ -63,6 +65,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 2b84978312..6416c9f355 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( diff --git a/packages/webapp/components/layouts/ProfileLayout/index.tsx b/packages/webapp/components/layouts/ProfileLayout/index.tsx index 301490010c..fc72734834 100644 --- a/packages/webapp/components/layouts/ProfileLayout/index.tsx +++ b/packages/webapp/components/layouts/ProfileLayout/index.tsx @@ -20,8 +20,7 @@ import type { NextSeoProps } from 'next-seo'; import { useProfile } from '@dailydotdev/shared/src/hooks/profile/useProfile'; import CustomAuthBanner from '@dailydotdev/shared/src/components/auth/CustomAuthBanner'; import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; -import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; -import { usePostReferrerContext } from '@dailydotdev/shared/src/contexts/PostReferrerContext'; +import { LogEvent } from '@dailydotdev/shared/src/lib/log'; import { getLayout as getFooterNavBarLayout } from '../FooterNavBarLayout'; import { getLayout as getMainLayout } from '../MainLayout'; import { getTemplatedTitle } from '../utils'; @@ -90,7 +89,6 @@ export default function ProfileLayout({ const { user } = useProfile(initialUser); const [trackedView, setTrackedView] = useState(false); const { logEvent } = useLogContext(); - const { referrerPost } = usePostReferrerContext(); // Auto-collapse sidebar on small screens useProfileSidebarCollapse(); @@ -103,16 +101,9 @@ export default function ProfileLayout({ logEvent({ event_name: LogEvent.ProfileView, target_id: user.id, - ...(!!referrerPost && { - extra: JSON.stringify({ - referrer_target_id: referrerPost.id, - referrer_target_type: TargetType.Post, - author: user?.id && referrerPost.author?.id === user.id ? 1 : 0, - }), - }), }); setTrackedView(true); - }, [user, trackedView, logEvent, referrerPost]); + }, [user, trackedView, logEvent]); if (!isFallback && !user) { return ;