Skip to content

Code Standards — Frontend & Workflow

ProjectPTX Channel Manager (ptx-cm)
Last Updated2026-02-20
See alsocode-standards.md (principles & naming)

1. Frontend (Next.js/React) Standards

Component File Structure

typescript
// ✓ Good: Typed props, separated concerns, named export
'use client';

import { useForm } from 'react-hook-form';

interface PropertyFormProps {
  onSubmit: (data: CreatePropertyDto) => Promise<void>;
  loading?: boolean;
}

export function PropertyForm({ onSubmit, loading }: PropertyFormProps) {
  const { register, handleSubmit, formState: { errors } } = useForm<CreatePropertyDto>();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      <button type="submit" disabled={loading}>Create</button>
    </form>
  );
}

// ✗ Bad: All logic inlined, no types, default export only
export default function Page() {
  const [properties, setProperties] = useState([]);
  const [loading, setLoading] = useState(false);
  const [formData, setFormData] = useState({});
  // ... 300 lines of inline logic and JSX
}

Data Fetching: SWR via useApiGet

typescript
// ✓ Good: SWR hook with dedup + loading state
import { useApiGet } from '@/lib/use-api';

export function PropertyList() {
  const { data: properties, isLoading } = useApiGet<Property[]>('properties', {
    dedupingInterval: 30_000,
  });

  if (isLoading) return <LoadingSkeleton />;
  if (!properties?.length) return <EmptyState />;

  return <DataTable data={properties} columns={columns} />;
}

// ✗ Bad: Inline fetch in useEffect, no dedup
export function PropertyList() {
  const [properties, setProperties] = useState([]);

  useEffect(() => {
    fetch('/api/v1/properties')
      .then(r => r.json())
      .then(setProperties); // ❌ No loading state, no dedup, no error handling
  }, []);
}

API Client Pattern

typescript
// ✓ Good: Centralized apiFetch with 401 auto-retry
import { apiFetch } from '@/lib/api-client';

export const propertyApi = {
  list: (params?: ListPropertiesDto) => apiFetch<PaginatedResponse<Property>>('properties', { params }),
  create: (dto: CreatePropertyDto) => apiFetch<Property>('properties', { method: 'POST', body: dto }),
  get: (id: string) => apiFetch<Property>(`properties/${id}`),
};

// ✗ Bad: Inline fetch calls duplicated across components
export function PropertyList() {
  useEffect(() => {
    fetch('/api/v1/properties') // ❌ Repeated in multiple places, no error handling
      .then(r => r.json())
      .then(setProperties);
  }, []);
}

Context Provider Usage

typescript
// ✓ Good: Use typed context hooks
import { useAuth } from '@/lib/auth-context';
import { useCountry } from '@/lib/country-context';

export function BookingList() {
  const { user } = useAuth();
  const { country } = useCountry();

  const { data } = useApiGet<Booking[]>(`bookings?country=${country}`);
  // ...
}

// ✗ Bad: Reading context directly via useContext
import { AuthContext } from '@/lib/auth-context';

export function BookingList() {
  const ctx = useContext(AuthContext); // ❌ No null check, not typed
  const user = ctx?.user;
}

Permission Checks

typescript
// ✓ Good: hasPermission helper from types package
import { hasPermission, Permission } from '@ptx-cm/types';

export function PropertyActions({ property }: { property: Property }) {
  const { user } = useAuth();
  const canEdit = hasPermission(user.permissions, 'PROPERTIES', Permission.EDIT);
  const canDelete = hasPermission(user.permissions, 'PROPERTIES', Permission.DELETE);

  return (
    <>
      {canEdit && <button onClick={onEdit}>Edit</button>}
      {canDelete && <button onClick={onDelete}>Delete</button>}
    </>
  );
}

// ✗ Bad: Hardcoded role checks
export function PropertyActions() {
  const { user } = useAuth();
  if (user.roleName === 'manager') { // ❌ Brittle, bypasses permission bitmask
    return <button>Edit</button>;
  }
}

2. Git Commit Standards

Conventional Commits

<type>(<scope>): <subject>

<body>

<footer>

Types:

TypeUse for
featNew feature
fixBug fix
docsDocumentation only
styleFormatting, no logic change
refactorCode restructure, no feature change
testAdd or update tests
choreBuild, CI, dependency updates

Examples:

bash
# ✓ Good
git commit -m "feat(auth): add refresh token revocation on logout"
git commit -m "fix(properties): prevent cross-country property access"
git commit -m "docs: update API_SPEC with country scoping details"
git commit -m "refactor(sync-engine): extract availability calc into separate service"

# ✗ Bad
git commit -m "fix stuff"
git commit -m "Update code"
git commit -m "wip"

Pre-Push Checklist

bash
pnpm lint        # No errors
pnpm test        # All tests pass
pnpm build       # No compile errors

Never commit:

  • .env files or API keys
  • node_modules/, dist/, .next/
  • Generated Prisma client files

3. Performance Guidelines

API Response Size

typescript
// ✓ Good: Select only fields needed for list view
const bookings = await this.prisma.booking.findMany({
  select: {
    id: true,
    otaBookingId: true,
    guestName: true,
    checkIn: true,
    checkOut: true,
    status: true,
    // Exclude: rawData, syncHistory (large fields for detail view only)
  },
});

// ✗ Bad: Return all fields including large nested data
const bookings = await this.prisma.booking.findMany();
// Response includes rawData (full OTA response JSON), syncHistory arrays, etc.

Always Paginate

typescript
// ✓ Good: Default 25, max 100
@Get()
async findAll(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('limit', new DefaultValuePipe(25), ParseIntPipe) limit: number,
) {
  // Cap at 100 to prevent abuse
  const safeLimitimit = Math.min(limit, 100);
  return this.service.findAll(page, safeLimit);
}

Frontend: Avoid Unnecessary Re-renders

typescript
// ✓ Good: Stable callback references
const updateUser = useCallback((updated: Partial<User>) => {
  setUser(prev => prev ? { ...prev, ...updated } : null);
}, []);

// ✓ Good: Memoize derived data
const overdueAlerts = useMemo(
  () => alerts?.filter(a => !a.resolvedAt) ?? [],
  [alerts],
);

// ✗ Bad: Inline functions recreated each render
<button onClick={() => updateUser({ locale: 'vi' })}>
  Switch to Vietnamese
</button>
// ❌ New function reference every render causes child re-render

PTX Channel Manager — Internal Documentation