Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
5285258
feat(tools): align the tool pages with the daily.dev design language
claude Aug 22, 2026
bcd7bb5
feat(tools): show real product logos instead of initials
claude Aug 22, 2026
a27dff9
feat(tools): design pass on the tools directory and tool pages
tsahimatsliah Aug 23, 2026
52bb654
feat(tools): trending posts as a horizontal card rail
tsahimatsliah Aug 23, 2026
05bdbeb
fix(tools): full-width cards in the trending rail
tsahimatsliah Aug 23, 2026
8251a90
fix(tools): cap the trending rail at three columns
tsahimatsliah Aug 23, 2026
6505d8c
feat(tools): API-backed directory search and full category sections
rebelchris Aug 27, 2026
7bab38a
Merge remote-tracking branch 'origin/main' into claude/docker-design-…
rebelchris Aug 27, 2026
ba76b57
Merge remote-tracking branch 'origin/claude/docker-design-review-pi3e…
rebelchris Aug 27, 2026
4d7108a
Merge branch 'main' into claude/docker-design-review-pi3eyd
rebelchris Aug 27, 2026
9bd8ed9
fix(tools): address design-review PR feedback
rebelchris Aug 27, 2026
46774de
fix(tools): align navbar horizontal padding with the tools pages
rebelchris Aug 27, 2026
6f07963
Merge remote-tracking branch 'origin/claude/docker-design-review-pi3e…
rebelchris Aug 27, 2026
301cfbe
feat(tools): catch-all Other section for uncategorized tools
rebelchris Aug 27, 2026
480b8bb
Merge remote-tracking branch 'origin/main' into claude/tools-pages-de…
rebelchris Aug 27, 2026
448ef3d
fix(tools): address second design-pass review round
rebelchris Aug 27, 2026
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
2 changes: 2 additions & 0 deletions packages/shared/src/components/charm/CharmEmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Link from '../utilities/Link';
interface CharmEmptyStateActionBase {
label: string;
icon?: ButtonProps<'button'>['icon'];
loading?: boolean;
}

/** Exactly one of `href` (renders a link) or `onClick` (renders a button). */
Expand Down Expand Up @@ -102,6 +103,7 @@ export function CharmEmptyState({
variant={ButtonVariant.Primary}
size={ButtonSize.Medium}
icon={action.icon}
loading={action.loading}
onClick={action.onClick}
className="mt-5"
>
Expand Down
15 changes: 12 additions & 3 deletions packages/shared/src/components/tools/ToolLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ interface ToolLogoProps {
url?: string | null;
/** Pixel size to request from the icon service. */
size?: number;
/** Styles the tile: size, radius, background, border, padding. */
/** Styles the tile: size, radius, border. */
className?: string;
/**
* Plate styles (background, inset) applied only while a real logo renders.
* The initial fallback stays on the ambient surface so its token color
* keeps contrast in both themes.
*/
plateClassName?: string;
/** Replaces the initial when there is no logo to show. */
fallback?: ReactNode;
}
Expand All @@ -26,12 +32,14 @@ export const ToolLogo = ({
url,
size,
className,
plateClassName,
fallback,
}: ToolLogoProps): ReactElement => {
const [hasFailed, setHasFailed] = useState(false);
const src = faviconUrl || (url ? getSiteIconUrl({ url, size }) : null);
const showsLogo = !!src && !hasFailed;

if (fallback && (!src || hasFailed)) {
if (fallback && !showsLogo) {
return <>{fallback}</>;
}

Expand All @@ -40,9 +48,10 @@ export const ToolLogo = ({
className={classNames(
'grid flex-none place-items-center overflow-hidden',
className,
showsLogo && plateClassName,
)}
>
{!src || hasFailed ? (
{!showsLogo ? (
<span aria-hidden className="font-bold text-text-tertiary">
{title.charAt(0).toUpperCase()}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,11 @@ function UserStackItemBody({
url={sponsoredCreative ? null : tool.url}
className={classNames(
'size-6 typo-footnote',
sponsoredCreative ? 'rounded-full bg-white p-0.5' : 'rounded',
sponsoredCreative ? 'rounded-full' : 'rounded',
)}
plateClassName={

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (blast radius): good call migrating this consumer. One behaviour change to confirm: previously a sponsored creative got rounded-full bg-white p-0.5 unconditionally, including when ToolLogo fell through to the initial; now the plate only renders alongside a real logo, so a sponsored item with a missing or failed image loses its white circle on the profile stack. Almost certainly the intended outcome, but it is a live sponsored surface — worth an explicit yes.

Reviewed by AI.

sponsoredCreative ? 'bg-white p-0.5' : undefined
}
/>
{!!title && (
<div className="flex min-w-0 flex-1 flex-col">
Expand Down
22 changes: 17 additions & 5 deletions packages/shared/src/graphql/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,19 @@ export interface DirectoryTool {
stackCount: number;
}

const TOP_TOOLS_QUERY = gql`
query TopTools($first: Int, $category: String, $trending: Boolean) {
topTools(first: $first, category: $category, trending: $trending) {
export const TOP_TOOLS_QUERY = gql`
query TopTools(
$first: Int
$category: String
$trending: Boolean
$query: String
) {
topTools(
first: $first
category: $category
trending: $trending
query: $query
) {
id
title
slug
Expand All @@ -414,14 +424,16 @@ export const getTopTools = async ({
first = 6,
category,
trending,
query,
}: {
first?: number;
category?: string;
trending?: boolean;
query?: string;
} = {}): Promise<DirectoryTool[]> => {
const result = await gqlClient.request<{ topTools: DirectoryTool[] }>(
TOP_TOOLS_QUERY,
{ first, category, trending },
{ first, category, trending, query },
);
return result.topTools;
};
Expand All @@ -431,7 +443,7 @@ export interface ToolCategoryStat {
toolCount: number;
}

const TOOL_CATEGORIES_QUERY = gql`
export const TOOL_CATEGORIES_QUERY = gql`
query ToolCategories {
toolCategories {
category
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/graphql/user/userStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ToolTopSquad {
name: string;
handle: string;
image: string;
description: string | null;
membersCount: number;
}

Expand Down Expand Up @@ -151,6 +152,7 @@ const TOP_SQUADS_FOR_TOOL_QUERY = gql`
name
handle
image
description
membersCount
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/lib/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export enum LogEvent {
Impression = 'impression',
ManageTags = 'click manage tags',
SearchTags = 'search tags',
SearchTools = 'search tools',
ClickFeedTagChip = 'click feed tag chip',
ClickOnboardingBack = 'click onboarding back',
ClickOnboardingNext = 'click onboarding next',
Expand Down
57 changes: 54 additions & 3 deletions packages/webapp/__tests__/ToolPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import type { RenderResult } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext';
import type { LoggedUser } from '@dailydotdev/shared/src/lib/user';
import loggedUser from '@dailydotdev/shared/__tests__/fixture/loggedUser';
import SettingsContext from '@dailydotdev/shared/src/contexts/SettingsContext';
import { settingsContext } from '@dailydotdev/shared/__tests__/helpers/boot';
import { SourceType } from '@dailydotdev/shared/src/graphql/sources';
import { apiUrl } from '@dailydotdev/shared/src/lib/config';
import { formatDataTileValue } from '@dailydotdev/shared/src/lib/numberFormat';
import type { ToolPageProps } from '../pages/tools/[slug]';
import ToolPage from '../pages/tools/[slug]';

Expand Down Expand Up @@ -57,6 +60,7 @@ const defaultProps: ToolPageProps = {
name: 'Platform crew',
handle: 'platform',
image: 'https://daily.dev/squad.png',
description: null,
membersCount: 42,
},
],
Expand Down Expand Up @@ -124,12 +128,16 @@ const defaultProps: ToolPageProps = {
facts: [],
};

const renderComponent = (props: ToolPageProps = defaultProps): RenderResult =>
const renderComponent = (
props: ToolPageProps = defaultProps,
user?: LoggedUser,
): RenderResult =>
render(
<QueryClientProvider client={new QueryClient()}>
<AuthContext.Provider
value={{
isLoggedIn: false,
user,
isLoggedIn: !!user,
shouldShowLogin: false,
showLogin: jest.fn(),
logout: jest.fn(),
Expand Down Expand Up @@ -164,7 +172,7 @@ it('should render the stat tiles from adoption and vote data', async () => {
renderComponent();

expect(await screen.findByText('In stacks')).toBeInTheDocument();
expect(screen.getByText('1,200')).toBeInTheDocument();
expect(screen.getByText(formatDataTileValue(1200))).toBeInTheDocument();
expect(screen.getByText('Dev sentiment')).toBeInTheDocument();
expect(screen.getByText('80%')).toBeInTheDocument();
expect(screen.getByText('Top 3%')).toBeInTheDocument();
Expand Down Expand Up @@ -201,6 +209,7 @@ it('should skip sections without data', async () => {
const headings = await screen.findAllByRole('heading', { level: 2 });

expect(headings.map((heading) => heading.textContent)).toEqual([
'Adoption on daily.dev',
'Discussion',
]);
});
Expand All @@ -218,3 +227,45 @@ it('should show the real logo for tools without one in the dataset', async () =>
'https://daily.dev/docker.png',
);
});

it('should show the discussion empty state when nobody commented yet', async () => {
renderComponent();

expect(await screen.findByText('No comments yet')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Start the discussion' }),
).toBeInTheDocument();
});

it('should hold the discussion behind the verification check when logged in', async () => {
renderComponent(defaultProps, loggedUser);

await screen.findByRole('heading', { level: 1, name: 'Docker' });

// The companies query never resolves here, so the placeholder wins.
expect(screen.queryByText('No comments yet')).not.toBeInTheDocument();
});

it('should render squads with the directory card details', async () => {
const props: ToolPageProps = {
...defaultProps,
topSquads: [
{
id: 's1',
name: 'Platform crew',
handle: 'platform',
image: 'https://daily.dev/squad.png',
description: 'Where the platform folks hang out.',
membersCount: 42,
},
],
};
renderComponent(props);

expect(await screen.findByText('Platform crew')).toBeInTheDocument();
expect(
screen.getByText('Where the platform folks hang out.'),
).toBeInTheDocument();
expect(screen.getByText(/@platform/)).toBeInTheDocument();
expect(screen.getByText('42 members')).toBeInTheDocument();
});
Loading
Loading