Global WatchGlobal Watch Docs
Development

Coding Conventions

Coding Conventions

This guide covers the coding standards and best practices for contributing to Global Watch. Following these conventions ensures consistency across the codebase and makes code reviews more efficient.

TypeScript Conventions

Strict Type Safety

Global Watch enforces strict TypeScript. Never use any without explicit justification.

// ❌ WRONG - Using any
const data: any = getData();

// ✅ CORRECT - Using proper types
import type { Database } from '~/lib/database.types';
type ProjectRow = Database['public']['Tables']['projects']['Row'];
const data: ProjectRow = getData();

// ✅ CORRECT - Using unknown with type guards
function process(data: unknown) {
  if (typeof data === 'object' && data !== null) {
    // Type-safe processing
  }
}

Type Imports

Always use import type for type-only imports:

// ✅ CORRECT
import type { Project } from '~/types';
import { createProject } from '~/lib/projects';

// ❌ WRONG
import { Project, createProject } from '~/lib/projects';

Explicit Return Types

Always specify return types for exported functions:

// ✅ CORRECT
export function calculateArea(geometry: Geometry): number {
  // ...
}

// ❌ WRONG - Missing return type
export function calculateArea(geometry: Geometry) {
  // ...
}

Component Organization

File Structure

Organize components following this pattern:

feature/
├── page.tsx              # Page component
├── layout.tsx            # Layout wrapper
├── loading.tsx           # Loading state
├── error.tsx             # Error boundary
├── _components/          # Route-specific components
│   ├── feature-header.tsx
│   └── feature-list.tsx
└── _lib/
    ├── server/           # Server-only code
    │   ├── feature-page.loader.ts
    │   └── feature-server-actions.ts
    └── schemas/          # Zod validation schemas
        └── feature.schema.ts

Server vs Client Components

Use Server Components by default. Only add 'use client' when necessary:

// Server Component (default) - for data fetching
async function ProjectList() {
  const projects = await getProjects();
  return <ProjectListClient projects={projects} />;
}

// Client Component - for interactivity
'use client';

function ProjectListClient({ projects }: { projects: Project[] }) {
  const [filter, setFilter] = useState('');
  // Interactive logic
}

Component Naming

  • PascalCase for component names
  • Descriptive names that indicate purpose
  • Suffix with context when needed (e.g., ProjectListClient)
// ✅ CORRECT
export function ProjectCard({ project }: ProjectCardProps) {}
export function TeamMemberList({ members }: TeamMemberListProps) {}

// ❌ WRONG
export function Card({ data }: any) {}
export function List({ items }: any) {}

File Naming Conventions

General Rules

TypeConventionExample
Componentskebab-case.tsxproject-card.tsx
Utilitieskebab-case.tsdate-utils.ts
Typeskebab-case.tsproject.types.ts
Tests*.test.ts(x)project-card.test.tsx
Server Actions*-server-actions.tsproject-server-actions.ts
Loaders*-page.loader.tsproject-page.loader.ts
Schemas*.schema.tsproject.schema.ts

Directory Naming

  • Use _components/ for route-specific components (underscore prefix)
  • Use _lib/ for route-specific utilities
  • Use kebab-case for all directories

Import Organization

Organize imports in this order:

// 1. React/Next.js imports
import { use, useState } from 'react';
import { notFound } from 'next/navigation';

// 2. External packages
import { z } from 'zod';
import { useQuery } from '@tanstack/react-query';

// 3. Internal packages (@kit/*, @fw/*)
import { Button } from '@kit/ui/button';
import { EntitySettings } from '@fw/entity-settings';

// 4. Local imports (relative paths)
import { ProjectCard } from './_components/project-card';
import type { Project } from './types';

Error Handling

Result Type Pattern

Use the Result type for explicit error handling:

import { Result } from '~/core/shared/result';

// ✅ CORRECT - Return Result type
async function findProject(id: string): Promise<Result<Project, ProjectNotFoundError>> {
  const { data, error } = await supabase
    .from('projects')
    .select('*')
    .eq('id', id)
    .maybeSingle();

  if (error || !data) {
    return Result.err(new ProjectNotFoundError(id));
  }

  return Result.ok(Project.fromPersistence(data));
}

// ✅ CORRECT - Handle Result
const result = await findProject(projectId);

if (!result.ok) {
  console.error(result.error.message);
  return;
}

// TypeScript knows result.value exists
console.log(result.value.name);

Custom Error Types

Create specific error types for different failure modes:

export class ProjectError extends Error {
  constructor(message: string, public readonly code: string) {
    super(message);
    this.name = 'ProjectError';
  }
}

export class ProjectNotFoundError extends ProjectError {
  constructor(projectId: string) {
    super(`Project not found: ${projectId}`, 'PROJECT_NOT_FOUND');
  }
}

Internationalization (i18n)

Always Use Translations

Never hardcode user-facing text. Always use translation keys:

// ❌ WRONG - Hardcoded text
<Button>Save</Button>
<p>No images available</p>

// ✅ CORRECT - Using translations
<Button>{t('common.save')}</Button>
<p>{t('map.toolbar.noImagesAvailable')}</p>

Translation Files

Add translations to all supported languages:

  • apps/web/public/locales/en/ - English
  • apps/web/public/locales/pt-BR/ - Portuguese
  • apps/web/public/locales/ar/ - Arabic

Testing Requirements

Test Coverage

  • 70% minimum overall coverage
  • 90%+ coverage for critical components
  • Write both unit tests and property-based tests

Test File Location

Co-locate tests with source files:

components/
├── project-card.tsx
└── project-card.test.tsx

Test Naming

Use descriptive test names:

// ✅ CORRECT
describe('ProjectCard', () => {
  it('should display project name and area', () => {});
  it('should show archived badge when project is archived', () => {});
  it('should call onEdit when edit button is clicked', () => {});
});

// ❌ WRONG
describe('ProjectCard', () => {
  it('works', () => {});
  it('test 1', () => {});
});

Git Conventions

Commit Message Format

Use conventional commits:

type(scope): Brief description

[Optional body with more details]

[Optional footer]

Types:

TypeDescription
featNew feature
fixBug fix
styleStyling changes
refactorCode refactoring
docsDocumentation
choreMaintenance tasks
testAdding tests

Examples:

feat(projects): add project archiving functionality
fix(auth): resolve session timeout issue
docs(api): update webhook documentation
refactor(billing): extract billing strategy pattern

Branch Naming

Use descriptive branch names:

# Feature branches
feature/project-archiving
feature/team-invitations

# Bug fixes
fix/session-timeout
fix/billing-calculation

# Documentation
docs/api-reference

Performance Guidelines

Bundle Size

  • Import directly from source, avoid barrel files
  • Use dynamic imports for heavy components
// ✅ CORRECT - Direct import
import PanelLeftIcon from 'lucide-react/dist/esm/icons/panel-left';

// ❌ WRONG - Barrel import
import { PanelLeftIcon } from 'lucide-react';

Re-renders

  • Use functional setState for stable callbacks
  • Memoize context values
// ✅ CORRECT - Stable callback
const toggle = useCallback(() => {
  setOpen((prev) => !prev);
}, []);

// ❌ WRONG - Creates new function on state change
const toggle = useCallback(() => {
  setOpen(!open);
}, [open]);

Lazy State Initialization

// ✅ CORRECT - Function called once
const [state, setState] = useState(() => expensiveComputation());

// ❌ WRONG - Computed on every render
const [state, setState] = useState(expensiveComputation());

Code Review Checklist

Before submitting a PR, verify:

  • TypeScript has no errors (pnpm typecheck)
  • Lint passes (pnpm lint:fix)
  • Code is formatted (pnpm format:fix)
  • Tests pass (pnpm --filter web test:run)
  • No any types without justification
  • All text uses i18n translations
  • Components follow naming conventions
  • Imports are properly organized
  • Error handling uses Result type
  • Documentation is updated if needed

Next Steps

On this page