Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import classNames from 'classnames';
import type { CommonLeaderboardProps } from './LeaderboardList';
import { LeaderboardList } from './LeaderboardList';
import { LeaderboardListItem } from './LeaderboardListItem';
import type { Company } from '../../../lib/userCompany';
import type { CompanyLeaderboard } from '../../../graphql/leaderboard';
import { ProfileImageSize, ProfilePicture } from '../../ProfilePicture';
import {
runIconPopAnimation,
Expand All @@ -13,10 +13,7 @@ import {
} from './common';
import { TopRankBadge } from './TopRankBadge';

export interface CompanyLeaderboard {
score: number;
company: Company;
}
export type { CompanyLeaderboard };

export function CompanyTopList({
items,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@ import type { ReactElement } from 'react';
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { useAuthContext } from '../../../contexts/AuthContext';
import { gqlClient } from '../../../graphql/common';
import type {
LeaderboardPosition,
LeaderboardType,
} from '../../../graphql/leaderboard';
import type { LeaderboardType } from '../../../graphql/leaderboard';
import {
isLeaderboardPositionSupported,
LEADERBOARD_POSITION_QUERY,
leaderboardPositionQueryOptions,
} from '../../../graphql/leaderboard';
import { generateQueryKey, RequestKey, StaleTime } from '../../../lib/query';
import { largeNumberFormat } from '../../../lib';
import { UserHighlight } from '../../widgets/PostUsersHighlights';

Expand All @@ -32,16 +27,8 @@ export function CurrentUserPositionRow({
isLeaderboardPositionSupported(type);

const { data } = useQuery({
queryKey: generateQueryKey(RequestKey.LeaderboardPosition, user, type),
queryFn: async () => {
const res = await gqlClient.request<{
leaderboardPosition: LeaderboardPosition | null;
}>(LEADERBOARD_POSITION_QUERY, { type });

return res.leaderboardPosition;
},
...leaderboardPositionQueryOptions(user, type),
enabled,
staleTime: StaleTime.Default,
});

if (!enabled || !data) {
Expand Down
13 changes: 5 additions & 8 deletions packages/shared/src/components/cards/Leaderboard/UserTopList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { LeaderboardRowPlaceholder } from './LeaderboardRowPlaceholder';
import { ElementPlaceholder } from '../../ElementPlaceholder';
import { CurrentUserPositionRow } from './CurrentUserPositionRow';
import { UserHighlight } from '../../widgets/PostUsersHighlights';
import type { LoggedUser } from '../../../lib/user';
import type { QuestLevel } from '../../../graphql/quests';
import type { LeaderboardType } from '../../../graphql/leaderboard';
import type {
LeaderboardType,
UserLeaderboard,
} from '../../../graphql/leaderboard';
import {
runIconPopAnimation,
runSparkAnimation,
Expand All @@ -22,11 +23,7 @@ import {
QuestLevelProgressCircle,
} from '../../quest/QuestLevelProgressCircle';

export interface UserLeaderboard {
score: number;
user: LoggedUser;
level?: QuestLevel | null;
}
export type { UserLeaderboard };

export function UserTopList({
items,
Expand Down
57 changes: 57 additions & 0 deletions packages/shared/src/features/leaderboard/LeaderboardActionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ReactElement, ReactNode } from 'react';
import React from 'react';
import classNames from 'classnames';

/**
* One card on the page's first row: what it is in a line, one detail, and
* one button that keeps its size and place on every card. Below laptop the
* button, and whatever sits beside it, fill the card's width.
*/
export const LeaderboardActionCard = ({
title,
detail,
children,
action,
aside,
className,
actionClassName,
}: {
title: ReactNode;
detail?: ReactNode;
children?: ReactNode;
action?: ReactNode;
aside?: ReactNode;
className?: string;
/** Where the action shows, when a card moves it on some widths. */
actionClassName?: string;
}): ReactElement => (
<article
className={classNames(
'flex h-full flex-col gap-3 rounded-16 border p-4',
className ?? 'border-border-subtlest-tertiary',
)}
>
<header className="flex flex-col gap-0.5">
<div className="font-bold typo-callout">{title}</div>
{detail && (
<div className="text-text-tertiary typo-footnote">{detail}</div>
)}
</header>
{children}
{(aside || action) && (
<div className="mt-auto flex flex-wrap items-end justify-end gap-2">
{aside}
{action && (
<span
className={classNames(
'w-full shrink-0 laptop:w-44 [&_button]:w-full',
actionClassName ?? 'flex',
)}
>
{action}
</span>
)}
</div>
)}
</article>
);
113 changes: 113 additions & 0 deletions packages/shared/src/features/leaderboard/LeaderboardBoardTile.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import nock from 'nock';
import { QueryClient } from '@tanstack/react-query';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { TestBootProvider } from '../../../__tests__/helpers/boot';
import { mockGraphQL } from '../../../__tests__/helpers/graphql';
import { mockDesktop } from '../../../__tests__/helpers/media';
import type { UserLeaderboard } from '../../graphql/leaderboard';
import {
HIGHEST_POST_VIEWS_QUERY,
LEADERBOARD_LIST_LIMIT,
LEADERBOARD_TOP_LIMIT,
LeaderboardType,
} from '../../graphql/leaderboard';
import { LogEvent, TargetType } from '../../lib/log';
import { LeaderboardUserBoardTile } from './LeaderboardUserBoardTile';

const logEvent = jest.fn();

const makeItems = (count: number, prefix = 'Dev'): UserLeaderboard[] =>
Array.from({ length: count }, (_, index) => ({
score: 1000 - index,
user: {
id: `${prefix}-${index}`,
username: `${prefix}-${index}`,
name: `${prefix} ${index}`,
image: '',
permalink: `/${prefix}-${index}`,
createdAt: new Date().toISOString(),
bio: '',
reputation: 10,
},
}));

const renderTile = (items: UserLeaderboard[]) =>
render(
<TestBootProvider
client={new QueryClient()}
auth={{ user: undefined }}
log={{ logEvent }}
>
<LeaderboardUserBoardTile
type={LeaderboardType.HighestPostViews}
items={items}
/>
</TestBootProvider>,
);

beforeEach(() => {
nock.cleanAll();
mockDesktop();
logEvent.mockClear();
});

describe('LeaderboardBoardTile', () => {
it('should show the top rows and ask for the full list only once opened', async () => {
const items = makeItems(LEADERBOARD_LIST_LIMIT);
renderTile(items);

expect(screen.getByText('Dev 0')).toBeInTheDocument();
expect(screen.getByText('Dev 3')).toBeInTheDocument();
expect(screen.queryByText('Dev 4')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

mockGraphQL({
request: {
query: HIGHEST_POST_VIEWS_QUERY,
variables: { limit: LEADERBOARD_TOP_LIMIT },
},
result: { data: { highestPostViews: makeItems(20, 'Top') } },
});

fireEvent.click(
screen.getByRole('button', { name: 'Highest post views, open top 50' }),
);

const dialog = await screen.findByRole('dialog');
expect(dialog).toHaveAccessibleName('Highest post views · Top 50');
expect(logEvent).toHaveBeenCalledWith({
event_name: LogEvent.Click,
target_type: TargetType.LeaderboardBoard,
target_id: LeaderboardType.HighestPostViews,
extra: JSON.stringify({ surface: 'popover' }),
});
await screen.findByText('Top 19');
expect(nock.isDone()).toBe(true);

fireEvent.keyDown(document, { key: 'Escape' });
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
);
});

it('should say how many a short board has and not open an empty one', () => {
const { rerender } = renderTile(makeItems(5));

expect(screen.getByText('All 5')).toBeInTheDocument();

rerender(
<TestBootProvider client={new QueryClient()} auth={{ user: undefined }}>
<LeaderboardUserBoardTile
type={LeaderboardType.HighestPostViews}
items={[]}
/>
</TestBootProvider>,
);

expect(
screen.getByText('Nothing here right now. Check back soon.'),
).toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
Loading
Loading