Skip to content

Latest commit

 

History

History
483 lines (375 loc) · 14.6 KB

File metadata and controls

483 lines (375 loc) · 14.6 KB

CONVENTIONS.md — Code Standards and Patterns

Canonical source for all code conventions in this repository. CONTRIBUTING.md, CLAUDE.md, GEMINI.md, and .github/copilot-instructions.md reference this file. Full detail is in docs/development/CODING_STANDARDS.md.

Project: Interact — Employee Engagement & Gamification Platform Last Updated: 2026-03-16


Table of Contents

  1. Naming Conventions
  2. File Organization
  3. Import Ordering
  4. Component Patterns
  5. Error Handling
  6. Testing Conventions
  7. API / Data Fetching
  8. Database / Entity Conventions
  9. Git Conventions
  10. Comment Conventions
  11. Security Rules

1. Naming Conventions

Artifact Convention Example
React component files PascalCase ActivityCard.jsx, UserProfile.jsx
Multi-word component files PascalCase (no separator) UserProfileCard.jsx
Custom hooks use prefix + camelCase useActivityData.js, useMobile.js
Context files PascalCase + Context suffix AuthContext.jsx
Page components PascalCase Dashboard.jsx, ActivityDetail.jsx
Utility/helper files camelCase imageUtils.js, formatDate.js
Constants UPPER_SNAKE_CASE API_BASE_URL, MAX_RETRY_COUNT
Frontend env vars UPPER_SNAKE_CASE + VITE_ prefix VITE_BASE44_APP_ID
Backend env vars UPPER_SNAKE_CASE (no prefix) OPENAI_API_KEY
TypeScript interfaces PascalCase User, ActivityItem
TypeScript types PascalCase ButtonVariant, ThemeMode
Boolean variables is/has/can/should prefix isLoading, hasError, canEdit
Event handlers handle prefix handleSubmit, handleCardClick
Backend Deno functions camelCase awardBadgeForActivity.ts
Test files Same name + .test.js/.test.jsx ActivityCard.test.jsx
CSS class names Tailwind utilities (kebab-case for custom) user-avatar, card-header

2. File Organization

Directory Rules

src/
  pages/           ← Route-level page components (PascalCase.jsx)
  components/
    ui/            ← Radix UI / shadcn primitives only
    common/        ← Shared utility components (ErrorBoundary, PageHeader)
    [feature]/     ← Feature-specific components (activities/, gamification/, etc.)
    docs/          ← Auto-generated (DO NOT EDIT)
  api/             ← API client setup only
  lib/             ← Utilities, contexts, helpers
  hooks/           ← Custom React hooks (use*.js/jsx)
  utils/           ← Pure utility functions (no React)
  test/            ← Test setup, fixtures, shared test utilities
functions/         ← Deno/TypeScript serverless functions (camelCase.ts)
docs/              ← Structured documentation (do not put random files here)

Add a new page

  • Create src/pages/NewPage.jsx using an existing page as a template
  • src/pages.config.js is auto-generated by Base44 — do not edit it manually; the page is auto-registered when you create the file in src/pages/
  • Add the route to src/App.jsx
  • New shared UI primitives → src/components/ui/
  • New feature components → src/components/[feature-name]/
  • New backend logic → functions/[actionName].ts
  • New hooks → src/hooks/use[Name].js
  • New utilities → src/utils/[name].js

Max Nesting

  • 3 levels deep maximum for component directories
  • No index.js barrel files (import directly from component files)

3. Import Ordering

Follow this order within each file (enforced by eslint-plugin-unused-imports):

// 1. React core
import { useState, useEffect, useCallback } from 'react';

// 2. Third-party libraries
import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';

// 3. UI components (Radix / shadcn)
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';

// 4. Feature components
import ActivityCard from '@/components/activities/ActivityCard';

// 5. Utilities and helpers
import { cn } from '@/lib/utils';
import { base44 } from '@/api/base44Client';

// 6. Icons
import { Check, X, Star } from 'lucide-react';

4. Component Patterns

Functional Component Structure

// Order: imports → constants → hooks (ALL at top level) → handlers → JSX
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';

export default function ComponentName({ prop1, prop2 = 'default', className }) {
  // 1. State — all hooks at top level, never conditional
  const [state, setState] = useState(null);
  
  // 2. Effects
  useEffect(() => {
    // side effects
  }, [dependencies]);
  
  // 3. Event handlers
  const handleAction = async () => {
    try {
      // action logic
      toast.success('Success');
    } catch (error) {
      console.error('Action failed:', error);
      toast.error('Something went wrong');
    }
  };
  
  // 4. JSX
  return (
    <div className={cn('p-4', className)}>
      {/* content */}
    </div>
  );
}

React Hooks Rules (CRITICAL)

  • ✅ Call hooks at the top level of the component, before any conditionals or returns
  • Never call hooks inside if, for, while, or nested functions
  • Never call hooks after an early return
  • ✅ Custom hooks must start with use (e.g., useActivityData)
  • Run npm run lint to catch react-hooks/rules-of-hooks violations

Styling

  • Only TailwindCSS utility classes — no inline styles, no separate CSS modules
  • Use cn() from @/lib/utils for conditional class composition:
    import { cn } from '@/lib/utils';
    <div className={cn('base-class', condition && 'conditional-class', className)} />
  • Mobile-first breakpoints: sm:, md:, lg:, xl:
  • Radix UI components from src/components/ui/ for interactive primitives

5. Error Handling

Frontend Pattern

const handleAction = async () => {
  try {
    const result = await someApiCall();
    toast.success('Operation completed');
  } catch (error) {
    // 1. Log detailed error internally
    console.error('Operation failed:', error);
    // 2. Show generic message to user (never expose stack traces or internal paths)
    toast.error('Something went wrong. Please try again.');
  }
};

TanStack Query Error Handling

const mutation = useMutation({
  mutationFn: async (data) => base44.entities.Activity.create(data),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['activities'] });
    toast.success('Activity created!');
  },
  onError: (error) => {
    console.error('Create failed:', error);
    toast.error('Failed to create activity');
  },
});

Error Boundaries

  • <ErrorBoundary> from src/components/common/ErrorBoundary.jsx wraps the root app
  • Wrap heavy page routes with their own <ErrorBoundary> for isolated failures

Backend (Deno Functions)

try {
  // business logic
} catch (error) {
  console.error('Function error:', error);
  // Generic message in response, never expose internals
  return Response.json({ error: 'Internal server error' }, { status: 500 });
}

6. Testing Conventions

File Naming

  • Test files co-located with the file under test: ComponentName.test.jsx
  • Integration/utility tests: src/test/[name].test.js
  • Backend function tests: functions/tests/[name].test.ts

Test Structure (AAA Pattern)

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';

describe('ComponentName', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('renders correctly with required props', () => {
    // Arrange
    const props = { name: 'Test Activity' };
    
    // Act
    render(<ComponentName {...props} />);
    
    // Assert
    expect(screen.getByText('Test Activity')).toBeInTheDocument();
  });

  it('calls handler when button clicked', async () => {
    // ...
  });
});

Query Provider Wrapper

Components using TanStack Query need a QueryClientProvider in tests:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const createWrapper = () => {
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

render(<MyComponent />, { wrapper: createWrapper() });

Mock Strategy

  • Mock external API calls (vi.spyOn(base44.entities.Activity, 'list'))
  • Use vi.fn() for callback props
  • Use createWrapper() utility from src/test/utils.js
  • Do not mock React internal hooks

Coverage Target

  • New utilities and hooks: 80% minimum
  • New components: test all user interactions
  • Global target: 30% by Q2 2026

7. API / Data Fetching

TanStack Query

// Query
const { data, isLoading, error } = useQuery({
  queryKey: ['activities'],               // hierarchical key array
  queryFn: () => base44.entities.Activity.list(),
  staleTime: 5 * 60 * 1000,              // 5 minutes (default)
});

// Mutation
const queryClient = useQueryClient();
const mutation = useMutation({
  mutationFn: (data) => base44.entities.Activity.create(data),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['activities'] }),
});

Query Key Convention

['resource']                    ← all items
['resource', id]                ← single item
['resource', { filter, sort }]  ← filtered collection
['resource', id, 'subresource'] ← nested resource

Backend Function Pattern

import { createClientFromRequest } from 'npm:@base44/sdk@0.8.6';

Deno.serve(async (req) => {
  try {
    const base44 = createClientFromRequest(req);
    const user = await base44.auth.me();
    if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });

    const { param1 } = await req.json();
    if (!param1) return Response.json({ error: 'Missing param1' }, { status: 400 });

    const result = await base44.entities.SomeEntity.create({ ...data });
    return Response.json({ success: true, data: result });
  } catch (error) {
    console.error('Function error:', error);
    return Response.json({ error: 'Internal server error' }, { status: 500 });
  }
});

8. Database / Entity Conventions

Entity Access

// List all
await base44.entities.Activity.list();

// Filtered
await base44.entities.Activity.filter({ status: 'active', created_by: user.email });

// Single
await base44.entities.Activity.get('entity-id');

// Create
await base44.entities.Activity.create({ name, type, created_by: user.email });

// Update
await base44.entities.Activity.update('entity-id', { status: 'completed' });

// Delete
await base44.entities.Activity.delete('entity-id');

Service Role (Admin Operations)

// Bypasses user permissions — use only in trusted backend functions
await base44.asServiceRole.entities.UserPoints.update(id, { total_points: newTotal });

Timestamp Fields

Always use ISO 8601 strings:

created_at: new Date().toISOString()  // "2026-03-16T00:44:06.984Z"

9. Git Conventions

Branch Naming

feat/short-description        ← new feature
fix/short-description         ← bug fix
chore/short-description       ← maintenance, dependencies, tooling
docs/short-description        ← documentation only
refactor/short-description    ← code restructuring
security/short-description    ← security fix
test/short-description        ← tests only

Commit Message Format (Conventional Commits)

<type>(<scope>): <description>

feat(gamification): add streak multiplier for consecutive logins
fix(auth): resolve token refresh race condition
chore(deps): upgrade react-router-dom to fix XSS vulnerability
docs(adr): add ADR-010 for notification service design
test(hooks): add tests for useActivityData hook

Types: feat, fix, chore, docs, refactor, security, test, perf

Merge Strategy

  • Squash merge for feature branches into main
  • No force pushes to main or develop
  • Branch must be up to date with main before merging (CI enforced)

Tag Format

v1.0.0          ← semantic version (MAJOR.MINOR.PATCH)
v1.0.0-beta.1   ← pre-release

10. Comment Conventions

When to Comment

  • Do: complex algorithm logic, non-obvious business rules, temporary workarounds
  • Don't: obvious code, restating what the code does in plain English

TODO Format

// TODO: [Sentry] Implement Sentry integration for production error tracking
// FIXME: [AUTH-123] Token refresh does not handle concurrent requests
// HACK: Temporary workaround until Base44 adds batch operations — remove after migration

Format: // TODO: [context] Description of what needs to be done

JSDoc (for utilities and hooks)

/**
 * Optimizes a Cloudinary image URL with the given transformation options.
 * @param {string} url - Original Cloudinary URL
 * @param {{ width?: number, quality?: number }} options - Transform options
 * @returns {string} Transformed URL
 */
export function optimizeImageUrl(url, { width = 800, quality = 80 } = {}) {

11. Security Rules

  • Never hardcode secrets — all via environment variables
  • Never put secrets in VITE_ variables (they are browser-exposed)
  • Always call base44.auth.me() at the start of every Deno function
  • Always validate inputs in backend functions (Zod or manual)
  • Always use DOMPurify before dangerouslySetInnerHTML:
    import DOMPurify from 'dompurify';
    <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
  • Strip sensitive fields before returning API responses
  • Generic error messages to users; detailed logs to console only
  • Run npm audit before every release — fix all high/critical issues first

Cross-References