Skip to content

Dev Style Guide

Evan Au edited this page Mar 28, 2026 · 1 revision

UAIC Developer Style Guide

Write consistent code :)))))))) google docs version of the guide

Contents

  1. Tooling & Setup
  2. Formatting Rules
  3. TypeScript Conventions
  4. Component Conventions
  5. Import Ordering
  6. Tailwind CSS
  7. Data Fetching & Server Actions
  8. File & Folder Naming
  9. Payload CMS Collections

1. Tooling & Setup

Tool Version / Config
Package manager pnpm
Framework Next.js 15 with Turbopack
Language TypeScript (strict mode)
Formatter Prettier
Linter ESLint (Next.js + TypeScript rules)
CSS Tailwind CSS v4
CMS Payload CMS

Available Scripts

pnpm dev          # Start dev server
pnpm build        # Compiles app for production
pnpm start        # Serves output from build

# Lint a single file
pnpm exec eslint path/to/your/file.tsx

# Format a single file with Prettier
pnpm exec prettier --write path/to/your/file.tsx

# Check formatting (without writing changes)
pnpm exec prettier --check path/to/your/file.tsx

Pre-commit Hooks

Lefthook runs automatically on every commit:

  • Prettier formats staged *.{js,jsx,ts,tsx,css,scss,json,md,yml,yaml} files
  • ESLint lints staged .ts / .tsx files

2. Formatting Rules

All formatting is enforced by Prettier. Don't configure your editor to use different settings.

Rule Value
Quotes Double (")
JSX quotes Double (")
Semicolons Required
Trailing commas All multi-line structures
Arrow function parens (x) => x
Tailwind class order Auto-sorted by prettier-plugin-tailwindcss

3. TypeScript Conventions

Props Interfaces

Define props as an interface directly above the component, named {ComponentName}Props:

interface ProfileCardProps {
  name: string;
  title: string;
  degree: string;
  imageSrc: string;
}

const ProfileCard = ({ name, title, degree, imageSrc }: ProfileCardProps) => {
  // ...
};
  • Use interface (not type) for component props
  • Mark optional fields with ?
  • Destructure props in the function signature, not inside the body

Type Imports

Use import type when importing types only:

import type { CollectionConfig } from "payload";

Return Types on Async Functions

Async server functions should have explicit return types:

export const getBulletins = async (): Promise<Bulletin[]> => { ... };
export const getLatestBulletin = async (): Promise<Bulletin | null> => { ... };

Unused Variables

Prefix unused variables or parameters with _ to suppress the ESLint error:

catch (_error) {
  return [];
}

4. Component Conventions

Arrow Function with Bottom Export

All components follow this pattern:

const MyComponent = () => {
  return <div>...</div>;
};

export default MyComponent;
  • Arrow function assigned to a const
  • export default on the last line — not inline
  • Do not use export default function MyComponent() {}

Client Components

Add "use client" at the very top of the file, before any imports. Only add it when the component truly needs it (event handlers, useState, useEffect, browser APIs). Server components have no directive at all.

"use client";

import React, { useState } from "react";
import Image from "next/image";

const Navbar = () => {
  const [isOpen, setIsOpen] = useState(false);
  // ...
};

export default Navbar;

Async Server Components

Server components that fetch data are async. Fetch data directly in the component body and use early returns for null/empty states:

import { getLatestBulletin } from "@/features/bulletins/data/getBulletins";

const LatestArticle = async () => {
  const latest = await getLatestBulletin();
  if (!latest) return null;
  return <div>...</div>;
};

export default LatestArticle;

Default Exports Only

Components always use default exports. Named exports are only for types, interfaces, or utility functions in non-component files.


5. Import Ordering

There is no auto-sorter for imports. Follow this order manually:

  1. "use client" or "use server" directive (if needed)
  2. Next.js built-ins (next/image, next/link, next/navigation, etc.)
  3. Third-party libraries (icon libraries, etc.)
  4. Local components using @/ alias
  5. Local utilities / data functions using @/ alias
"use client";

import Image from "next/image";
import Link from "next/link";
import { FaInstagram } from "react-icons/fa";
import Navbar from "@/components/Navbar";
import { getBulletins } from "@/features/bulletins/data/getBulletins";

6. Tailwind CSS

Design Tokens

Use the project's custom tokens instead of raw Tailwind values wherever possible:

Token Value Usage
text-darkBlue / bg-darkBlue #145ca9 Primary brand blue
text-babyBlue / bg-babyBlue #5fb4ff Secondary/accent blue
text-lightBlue / bg-lightBlue #ebf7fe Light blue backgrounds
bg-whiteHover #f0f8ff Hover state on white elements
bg-grey-100 / border-grey-100 #d9d9d9 Light grey
bg-grey-200 / border-grey-200 #cbc6c6 Medium grey
text-header 1.3rem Section headings
text-body 1rem Body text
text-title 2rem Page titles
// Good — uses design tokens
<h1 className="text-darkBlue text-header font-bold">Meet the Team</h1>

// Okay for one-off sizes not covered by tokens
<h1 className="text-[41.65px]">...</h1>

Mobile-First Responsive Design

The site is mobile-first. Write base styles for mobile, then layer on larger-screen overrides:

// Base = mobile, sm: = 640px+, md: = 768px+, lg: = 1024px+
<div className="flex flex-col gap-4 p-4 md:flex-row md:gap-8 lg:px-10">

Breakpoint order in class strings: sm:md:lg:xl:2xl:

Class Ordering & Arbitrary Values

Don't manually order Tailwind classes — prettier-plugin-tailwindcss sorts them on format. For one-off values use bracket notation:

className="max-w-[160px] gap-[47px] text-[16px]"

// Spaces inside arbitrary values use underscores:
className="bg-[linear-gradient(to_top,white_25%,transparent_100%)]"

7. Data Fetching & Server Actions

All data fetching goes through Payload CMS via server actions.

File Structure

The intended structure is one domain folder per feature:

src/features/
└── [domain]/
    └── data/
        └── get[Resource].ts   # e.g. getBulletins.ts, getEvents.ts

Server Action Template

New data functions should follow this pattern:

"use server";

import { getPayload } from "payload";
import config from "@payload-config";

export interface MyResource {
  id: string;
  title: string;
  // ... fields
}

export const getMyResources = async (): Promise<MyResource[]> => {
  const payload = await getPayload({ config });

  try {
    const result = await payload.find({
      collection: "my-collection",
      limit: 100,
      sort: ["-createdAt"],
      depth: 1,
    });

    return result.docs.map((doc: any) => ({
      id: doc.id,
      title: doc.title,
    }));
  } catch (error) {
    console.error("Error fetching:", error);
    return [];
  }
};

Note: Not all existing files follow this pattern. getEvents.ts, for example, uses Promise<any[]>, returns result.docs directly without mapping, and has no try/catch. Treat those as legacy — write new functions to this standard.

Key Rules for New Data Functions

  • "use server" at the top
  • Export the interface alongside the function so components can type their props
  • Always wrap Payload calls in try/catch
  • Return a safe fallback on error ([] or null)
  • Map result.docs to your interface explicitly — don't return raw Payload docs

8. File & Folder Naming

What Convention Example
Component files PascalCase ProfileCard.tsx, EventLanding.tsx
Page files always page.tsx src/app/(home)/team/page.tsx
Layout files always layout.tsx src/app/(home)/layout.tsx
Utility/data files camelCase getBulletins.ts, getEvents.ts
Collection files PascalCase Events.ts, Executive.ts
Component folders camelCase components/about/, components/home/
Images (public/) kebab-case danielle-smith.webp, uaic-logo.png

Where to Put New Files

File type Location
Reusable UI components src/components/[section]/
Page-specific layouts src/app/(home)/[route]/page.tsx
Data fetching functions src/features/[domain]/data/
Payload collections src/collections/
Images public/assets/[section]/

9. Payload CMS Collections

Collections live in src/collections/ and are registered in src/payload.config.ts.

Template

import type { CollectionConfig } from "payload";

export const MyCollection: CollectionConfig = {
  slug: "my-collection",
  labels: {
    singular: "My Item",
    plural: "My Items",
  },
  admin: {
    useAsTitle: "title",
  },
  fields: [
    {
      name: "title",
      type: "text",
      label: "Title",
      required: true,
    },
  ],
  access: {
    read: () => true,
  },
};
  • Use import type for Payload types
  • Always set access.read: () => true for public-facing content
  • Always define labels.singular and labels.plural