Skip to content

Latest commit

 

History

History
456 lines (361 loc) · 23.8 KB

File metadata and controls

456 lines (361 loc) · 23.8 KB

AGENTS.md

This file provides guidance to AI coding agents when working with code in this repository.

Development Philosophy

We're a startup. We move fast, iterate quickly, and embrace change. When implementing features:

  • Favor pragmatic solutions over perfect architecture
  • Code will likely change - don't over-engineer
  • A/B experiments are common (GrowthBook integration)
  • Test coverage isn't a goal - write tests that validate functionality, not hit metrics

Code Style

Control flow:

  • Use early returns instead of if-else blocks for cleaner, flatter code
  • Handle the errors or checks first and return early then proceed with happy path at the end of code block

Invariant handling:

  • Do not silently ignore impossible states (for example, no-op rollback fallbacks in mutation/cache flows)
  • Fail fast with a clear thrown error message when an internal invariant is violated

Drag-and-drop UI:

  • Do not render owner-visible empty drag containers or empty categories unless the product requirement explicitly asks for visible empty drop targets
  • Any drag overlay or tooltip that reads async query data must defensively handle undefined/empty arrays without crashing

Project Architecture

This is a pnpm monorepo containing the daily.dev application suite:

Package Purpose
packages/webapp Next.js web application (main daily.dev site)
packages/extension Browser extension (Chrome/Opera) built with Webpack
packages/shared Shared React components, hooks, utilities, and design system
packages/storybook Component documentation and development environment
packages/eslint-config Shared ESLint configuration
packages/eslint-rules Custom ESLint rules including color consistency enforcement
packages/prettier-config Shared Prettier configuration

Technology Stack

  • Node.js v24.14 (see package.json volta and packageManager properties, also .nvmrc)
  • pnpm 9.14.4 for package management (see package.json packageManager property)
  • TypeScript across all packages
  • React 18.3.1 with Next.js 15 for webapp (Pages Router, NOT App Router/Server Components)
  • TanStack Query v5 for server state and data fetching
  • GraphQL with graphql-request for API communication
  • Tailwind CSS with custom design system
  • Jest for testing
  • GrowthBook for feature flags and A/B experiments

Commonly Used Imports

Components

// Buttons (variants: Primary, Secondary, Tertiary, Float, Subtle, Option, Quiz)
import { Button, ButtonVariant, ButtonSize } from '@dailydotdev/shared/src/components/buttons/Button';
import { ClickableText } from '@dailydotdev/shared/src/components/buttons/ClickableText';

// Typography
import { Typography, TypographyType, TypographyColor } from '@dailydotdev/shared/src/components/typography/Typography';

// Form Fields
import { TextField } from '@dailydotdev/shared/src/components/fields/TextField';
import { Switch } from '@dailydotdev/shared/src/components/fields/Switch';
import { Checkbox } from '@dailydotdev/shared/src/components/fields/Checkbox';
import { Radio } from '@dailydotdev/shared/src/components/fields/Radio';

// Layout & Utilities
import { FlexCol, FlexRow } from '@dailydotdev/shared/src/components/utilities';
import Link from '@dailydotdev/shared/src/components/utilities/Link';

// Icons (500+ available)
import { PlusIcon, ShareIcon, UpvoteIcon } from '@dailydotdev/shared/src/components/icons';
import { IconSize } from '@dailydotdev/shared/src/components/Icon';

// Modals
import { LazyModal } from '@dailydotdev/shared/src/components/modals/common/types';

// Feedback
import { Loader } from '@dailydotdev/shared/src/components/Loader';
import Toast from '@dailydotdev/shared/src/components/notifications/Toast';
import { Tooltip } from '@dailydotdev/shared/src/components/tooltip/Tooltip';

Hooks

// Most commonly used
import { useViewSize, ViewSize } from '@dailydotdev/shared/src/hooks';
import { useLazyModal } from '@dailydotdev/shared/src/hooks/useLazyModal';
import { useToastNotification } from '@dailydotdev/shared/src/hooks/useToastNotification';
import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature';
import { useFeedLayout } from '@dailydotdev/shared/src/hooks/useFeedLayout';
import { usePrompt } from '@dailydotdev/shared/src/hooks/usePrompt';

// Actions & State
import { useActions, usePlusSubscription } from '@dailydotdev/shared/src/hooks';
import useFeedSettings from '@dailydotdev/shared/src/hooks/useFeedSettings';

Contexts

import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext';
import { useNotificationContext } from '@dailydotdev/shared/src/contexts/NotificationsContext';

GraphQL Types

import type { Post, PostType } from '@dailydotdev/shared/src/graphql/posts';
import type { Source, SourceType } from '@dailydotdev/shared/src/graphql/sources';
import { gqlClient } from '@dailydotdev/shared/src/graphql/common';
import { ActionType } from '@dailydotdev/shared/src/graphql/actions';

Utilities

import type { LoggedUser } from '@dailydotdev/shared/src/lib/user';
import { LogEvent, Origin } from '@dailydotdev/shared/src/lib/log';
import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth';
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
import classed from '@dailydotdev/shared/src/lib/classed';

// String utilities
import { stripHtmlTags, capitalize, formatKeyword } from '@dailydotdev/shared/src/lib/strings';

Forms (react-hook-form + Zod)

import { useForm, FormProvider } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

// Controlled components (use within FormProvider)
import ControlledTextField from '@dailydotdev/shared/src/components/fields/ControlledTextField';
import ControlledTextarea from '@dailydotdev/shared/src/components/fields/ControlledTextarea';
import ControlledSwitch from '@dailydotdev/shared/src/components/fields/ControlledSwitch';

IMPORTANT - Zod Type Inference:

  • ALWAYS use z.infer to derive TypeScript types from Zod schemas
  • NEVER manually define types that duplicate Zod schema structure
// ❌ WRONG: Manual type definition that duplicates schema
const userSchema = z.object({
  name: z.string(),
  age: z.number(),
});

interface User {
  name: string;
  age: number;
}

// ✅ RIGHT: Infer type from schema
const userSchema = z.object({
  name: z.string(),
  age: z.number(),
});

export type User = z.infer<typeof userSchema>;

This ensures type safety, reduces duplication, and keeps types automatically in sync with schemas.

Quick Commands

# Setup
nvm use                   # Use correct Node version from .nvmrc
npm i -g [email protected]
pnpm install

# Development
pnpm --filter webapp dev          # Run webapp (HTTPS)
pnpm --filter webapp dev:notls    # Run webapp (HTTP)
pnpm --filter extension dev:chrome # Run Chrome extension
pnpm --filter storybook dev       # Run Storybook

# Testing & Linting
pnpm --filter <package> test      # Run tests
pnpm --filter <package> lint      # Run linter
pnpm --filter <package> lint:fix  # Fix lint issues

# Building for production
pnpm --filter webapp build        # Build webapp
pnpm --filter extension build:chrome # Build Chrome extension

IMPORTANT: When running Jest manually with pnpm exec jest or a direct file path, set NODE_ENV=test so React and Testing Library do not run under a production build.

IMPORTANT: Do NOT run build commands while the dev server is running - it will break hot reload. Only run builds at the end to verify your work compiles successfully. During development, rely on the dev server's hot reload and TypeScript/ESLint checks instead.

IMPORTANT: For changed .ts/.tsx files, run node ./scripts/typecheck-strict-changed.js or the package's strict tsc command before finishing. Do not add context-specific props to shared primitives when the behavior can be scoped in the parent list/container.

IMPORTANT: When changing SEO, gating, or noindex logic, preserve existing undefined/nullable behavior unless the requirement explicitly changes it, and verify field names against the typed GraphQL model instead of ticket prose.

Where Should I Put This Code?

Is it used by both webapp AND extension?
├── Yes → packages/shared/
│   ├── Is it a React component? → src/components/
│   ├── Is it a custom hook? → src/hooks/
│   ├── Is it a GraphQL query/mutation? → src/graphql/
│   ├── Is it a complex feature with multiple files? → src/features/
│   ├── Is it a React context? → src/contexts/
│   └── Is it a utility function? → src/lib/
├── No, webapp only → packages/webapp/
│   ├── Is it a page? → pages/
│   ├── Is it a layout? → components/layouts/
│   └── Is it webapp-specific logic? → components/ or hooks/
└── No, extension only → packages/extension/src/
    ├── Is it for new tab? → newtab/
    ├── Is it for companion widget? → companion/
    └── Is it background logic? → background/

State Management Guide

Use Case Solution
Server data (API responses) TanStack Query
Global app state (user, settings) React Context
Local/UI state useState
Form state react-hook-form + Zod validation

Note: TanStack Query v5 uses isPending for mutations (not isLoading).

Design System Quick Reference

  • Colors: Food-themed palette (burger, cheese, avocado, bacon, etc.)
  • Use semantic tokens: text-primary, bg-surface-primary, not raw colors
  • Typography: Use typo-* classes (typo-title1, typo-body, typo-callout)
  • Responsive: mobileL, mobileXL, tablet, laptop, laptopL, desktop
  • ESLint enforces no-custom-color rule - use design system tokens
  • For dismissible banners/cards, default to the shared CloseButton icon pattern used elsewhere; do not introduce a separate full-width Dismiss button unless the request explicitly calls for text dismiss UI.

Testing Approach

We write tests to validate functionality, not to achieve coverage metrics:

  • Focus on user interactions with React Testing Library
  • Mock API responses with nock
  • Test files live next to source: Component.spec.tsx
  • Run tests: pnpm --filter <package> test
  • For hover/tooltip changes on navigation, verify the real interactive hover target is wrapped by the tooltip component. Do not treat a native title fallback as a substitute for the requested tooltip behavior unless the user explicitly asks for that fallback.

Feature Flags & Experiments

GrowthBook is integrated for A/B testing. Define features in packages/shared/src/lib/featureManagement.ts:

export const featureMyFlag = new Feature('my_flag', false);

Use useConditionalFeature with shouldEvaluate to gate evaluation — only evaluate the flag when the component would otherwise render (e.g., user is authenticated and Plus). This avoids unnecessary GrowthBook evaluations:

import { useConditionalFeature } from '@dailydotdev/shared/src/hooks';
import { featureMyFlag } from '../../lib/featureManagement';

const shouldEvaluate = isAuthReady && isPlus;
const { value: isEnabled } = useConditionalFeature({
  feature: featureMyFlag,
  shouldEvaluate,
});
const showComponent = shouldEvaluate && isEnabled;

When removing a feature flag, do not assume the gated UI should become always-on. Match the product request explicitly: either delete the gated behavior entirely or keep it permanently, and remove dead code/tests for the discarded path.

Key Configuration Files

  • pnpm-workspace.yaml - Monorepo workspace packages
  • packages/webapp/next.config.ts - Next.js configuration
  • packages/shared/tailwind.config.ts - Base Tailwind configuration
  • packages/extension/webpack.config.js - Extension build configuration

Package-Specific Guides

Each package has its own AGENTS.md with detailed guidance:

  • packages/shared/AGENTS.md - Shared components, hooks, design system
  • packages/webapp/AGENTS.md - Next.js webapp specifics
  • packages/extension/AGENTS.md - Browser extension development
  • packages/storybook/AGENTS.md - Component documentation
  • packages/playwright/AGENTS.md - E2E testing with Playwright

Adding Content to Existing Pages

When adding new content (videos, images, text blocks) to existing pages:

  • Study the page structure first - Identify existing sections and their purpose
  • Integrate into existing components rather than creating new wrapper components
  • Avoid duplicating section headers - If a page has "How it works", don't add "See how it works"
  • Extend existing components - Add content to the relevant existing component instead of creating parallel components

Example: Adding a video to the jobs page

  • ❌ Wrong: Create new OpportunityVideo component with its own "See how it works" title
  • ✅ Right: Add the video embed inside the existing OpportunityHowItWorks component

Development Notes

  • Extension uses webextension-polyfill for cross-browser compatibility
  • SVG imports are converted to React components via @svgr/webpack
  • Tailwind utilities preferred over CSS-in-JS
  • GraphQL schema changes require manual TypeScript type updates

No Barrel/Index Exports

NEVER create index.ts files that re-export from other files. Barrel exports cause dependency cycles and hurt build performance.

// ❌ NEVER do this - no index.ts barrel files
// hooks/index.ts
export * from './useAuth';
export * from './useUser';

// ❌ NEVER import from barrel
import { useAuth } from './hooks';

// ✅ ALWAYS import directly from the file
import { useAuth } from './hooks/useAuth';
import { useUser } from './hooks/useUser';

When you see an existing barrel file, delete it and update all imports to use direct paths.

Avoiding Code Duplication

NEVER copy-paste utility functions into multiple files. If a helper is needed in more than one place, add it to a shared utility file and import it. Do not define the same function locally in each file that needs it.

Before implementing new functionality, always check if similar code already exists:

  1. Search for existing utilities - Use Grep/Glob to find similar patterns:

    # Search for similar function names
    grep -r "functionName" packages/shared/src/lib
    
    # Search for similar logic patterns
    grep -r "specific_pattern" packages/
  2. Check shared libraries first:

    • packages/shared/src/lib/func.ts - General utility functions
    • packages/shared/src/lib/strings.ts - String manipulation, text utilities
    • packages/shared/src/lib/links.ts - URL and link utilities
    • packages/shared/src/lib/[domain].ts - Domain-specific utilities
  3. Extract reusable functions:

    • If you write similar logic in multiple places, extract it to a helper
    • If the logic is used only in one package → package-specific file
    • If the logic could be used across packages → packages/shared/src/lib/
    • Don't extract single-use code into separate functions - keep logic inline where it's used
    • Only extract functions when the same logic is needed in multiple places
  4. Real-world example (from PostSEOSchema refactor):

    • Wrong: Duplicate author schema logic in 3 places
    • Right: Create getPersonSchema() helper, reuse everywhere
    • Wrong: Local stripHtml() in webapp-only file
    • Right: Move to shared/src/lib/strings.ts as stripHtmlTags()
  5. Before submitting a PR:

    • Search for similar patterns in the codebase
    • Refactor duplications into reusable functions
    • Place utilities in appropriate shared locations
    • Run lint to ensure code quality: pnpm --filter <package> lint

Writing Readable Hooks

When a hook's callback becomes hard to follow, break it into small helper functions:

  1. Extract repeated selectors to constants
  2. Create single-purpose helpers - each function does one thing
  3. Name helpers by what they do - expandSectionIfCollapsed, focusFirstInput
  4. Keep the main callback simple - it should read like a high-level description

Example (from useMissingFieldNavigation refactor):

// ❌ Wrong: 90-line callback with nested logic
const handleClick = useCallback((key: string) => {
  const el = document.querySelector(`[data-key="${key}"]`);
  if (el) {
    const section = el.closest('[id^="section-"]');
    if (section) {
      const btn = document.querySelector(`button[aria-controls="${section.id}"]`);
      if (btn?.getAttribute('aria-expanded') === 'false') {
        btn.click();
      }
    }
    setTimeout(() => {
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      // ... 50 more lines of highlighting/focusing logic
    }, 100);
  }
  // ... another 30 lines for fallback
}, []);

// ✅ Right: Small helpers + simple callback
const expandSectionIfCollapsed = (element: HTMLElement): void => { /* ... */ };
const scrollHighlightAndFocus = (element: HTMLElement): void => { /* ... */ };
const navigateToField = (fieldElement: HTMLElement): void => { /* ... */ };
const navigateToSection = (checkKey: string): void => { /* ... */ };

const handleClick = useCallback((key: string) => {
  const fieldElement = document.querySelector(`[data-key="${key}"]`);
  if (fieldElement) {
    navigateToField(fieldElement);
    return;
  }
  navigateToSection(key);
}, []);

Pull Requests

Keep PR descriptions concise and to the point. Reviewers should not be exhausted by lengthy explanations.

Use conventional commit messages for all commits, for example fix: ..., feat: ..., or chore: ....

Before opening a PR, run git diff --name-only origin/main...HEAD and confirm every changed file belongs to the current task. If unrelated files appear (for example from reverted or merged commits), clean the branch history first.

Code Review Guidelines

When reviewing code (or writing code that will be reviewed):

  • Always set explicit type on <button> elements in forms - Use type="button" for non-submit actions (close/back/cancel). Never rely on the browser default inside forms.
  • Delete dead code - Remove unused components, functions, exports, and files. Don't leave code "for later"
  • Avoid confusing naming - Don't create multiple components with the same name in different locations (e.g., two AboutMe components)
  • Remove unused exports - If a function/constant is only used internally, don't export it
  • Clean up duplicates - If the same interface/type is defined in multiple places, consolidate to one location and import
  • Activity list modals should be metadata-first - For lists like reposts/upvotes/history in modals, prefer compact rows that emphasize source/author and engagement. Avoid large content images that dominate the layout unless image content is the primary purpose.
  • Reuse feed/list card primitives first - Before adding modal-specific list item components, check existing card building blocks (FeedItemContainer, PostCardHeader, list card primitives) and compose with them.
  • Do not hide accessible data using presentation heuristics - In UI lists, avoid masking content based on flags like source.public; rely on backend access controls and render the data returned by the query.
  • Keep scope tight in design iterations - When adjusting UI, avoid unrelated behavioral/SEO changes in the same commit unless explicitly requested.
  • Confirm target surface before implementing UI fixes - If a bug report names a specific component or screen, update only that target unless expansion is explicitly requested.
  • Keep action spacing consistent in control headers - When adding icon/action buttons near search fields or other controls, match existing horizontal gaps on both sides to avoid controls touching each other.
  • Place feed promos in content flow unless explicitly sticky - If a promo belongs between feed navigation and the feed list, render it in the feed/content layout so it pushes content down. Do not attach it to sticky nav with absolute positioning unless the requirement explicitly asks for overlay behavior.
  • Use searchChildren prop for content above feed results on search pages - In MainFeedLayout, page children render AFTER the <Feed> component. To place banners/promos above feed results, pass them via the searchChildren prop (through layoutProps on the page component). Do NOT render them as page children — they will appear below all posts. Prefer reusing existing props over introducing new ones.
  • Preserve spacing intent across breakpoints - When fixing missing padding/margins, do not re-disable them at larger breakpoints with classes like laptop:mx-0 unless the requirement explicitly asks for desktop edge-to-edge layout. Verify mobile and desktop behavior before shipping.
  • Fix spacing consistently across sibling sections - On page layout bugs, audit all adjacent sections (headers, modules, horizontal feeds, list feeds) and keep spacing rules consistent unless explicitly specified otherwise.
  • Protect generated HTML from markdown regex passes - In markdown conversion utilities, never run formatting regexes across already-generated HTML tags/attributes (for example, image src URLs with _); add regression tests for URL edge cases.
  • Prefer component-level token swaps for one-off contrast fixes - For isolated UI readability issues, use existing semantic color utilities in the impacted components first; avoid changing global tokens in base.css unless explicitly requested.
  • Gate infinite scroll with separate canFetchMore and fetchNextPage props - When adding infinite scroll to dropdowns or lists, never derive canFetchMore from the existence of a callback (e.g. !!onScrollEnd). Instead, pass three separate props: fetchNextPage (the function), canFetchMore (boolean from hasNextPage), and isFetchingNextPage (boolean). The scroll hook should use canFetchMore && !isFetchingNextPage to gate fetches. Follow the InfiniteScrolling component pattern in packages/shared/src/components/containers/InfiniteScrolling.tsx.
  • Handle pre-selected values in paginated dropdowns - When a dropdown uses infinite scroll and a value was previously selected (e.g. from a saved integration), that value may not be in the first page of results. Insert an artificial placeholder entry (e.g. { id, name: "Channel ${id}" }) at the front of the list so the selection is visible immediately, and deduplicate by id once the real item is fetched during scrolling. Do not auto-fetch pages in a loop to find the selected item. Never use findIndex(...) || 0 — it silently falls back to the first item when the value isn't found; use ?? -1 so the dropdown shows the placeholder instead.
  • Portaled drawers must stop click propagation - The BaseDrawer overlay stops click propagation so that portaled child drawers (e.g. ListDrawer inside a Dropdown inside a modal-as-drawer) don't accidentally close the parent modal via useOutsideClick. When adding new portaled overlays, always stopPropagation on the overlay and handle outside-click dismissal locally.
  • Update text-asserting tests when copy changes - If you rename visible labels in menus/buttons/toasts, update the impacted RTL/Jest selectors in the same PR (e.g. findByText('...')) to keep behavior tests aligned with intentional UX copy updates.

Node.js Version Upgrade Checklist

When upgrading Node.js version, update these files:

  • .nvmrc
  • Dockerfile
  • .github/workflows/e2e-tests.yml
  • .circleci/config.yml (multiple occurrences)
  • packages/playwright/package.json (engines field)
  • This file (CLAUDE.md - Technology Stack section)

After updating, run pnpm install to check if lock file needs updating and commit any changes.