PEC ACM Website Code Style and Formatting #
The PEC ACM Website codebase mainly uses Biome for code formatting in order to quickly and automatically check for formatting (and sometimes logical) errors in code.
The Project includes pnpm scripts for linting and formatting, in addition to pre-commit hooks with husky. These scripts include:
pnpm run format # Run the formatter on the project and write any changes to the filesystem, if applicable
pnpm run format:check # Run the formatter on the project but don't write any changes to the filesystem
pnpm run lint # Run various checks on the project
pnpm run check # Runs formatter, linter and import sorting on the project
The guiding principles of the code style and code formatting can be summarized as:
- no nonsense
- use your own judgement
- break the rules if it makes sense e.g.: it improves readability substantially
- use the formatting of the file you're in, even if it breaks the rules
- no nonsense
It's worth noting that modern IDEs and text editors can run Biome automatically on save. It may be worth looking into whether this functionality is supported in your editor for a more convenient development workflow.
Table of Contents #
Path Aliases #
The following path aliases are configured to improve import statements and maintain clarity across the codebase:
- $components: Points to the
src/componentsdirectory. - $data: Points to the
src/datadirectory. - $env: Points to the
src/lib/envdirectory. - $emails: Points to the
src/emailsdirectory and contains React Email templates. - $hooks: Points to the
src/hooksdirectory. - $lib: Points to the
src/libdirectory and is the place for creating internal packages. - $assets: Points to the
src/lib/assetsdirectory and contains all the static assets used in the project. - $styles: Points to the
src/stylesdirectory and contains all Sass stylesheets.
Formatting #
- Biome is enabled for formatting with the following settings:
- Indentation Style: Spaces
- Indent Width: 2 spaces
- Attribute Position: Multiline
- Quote Style: Double quotes
- Arrow Parentheses: Add parentheses only as needed.
Naming Conventions #
Files #
- All file names must be in lowercase kebab case (e.g.,
src/components/sidebar/index.tsx). - Directories should follow the same kebab case convention.
Code #
- Type Names and Zod Schemas should use Pascal Case (e.g.,
Sidebar,UserSchema). - Constant Values should be in UPPER_SNAKE_CASE (e.g.,
BACKEND_BASE_URL,MAX_RETRY_COUNT). - Functions and Variables should be in camelCase (e.g.,
handleClick,userData). - Use meaningful names that clearly describe the purpose of the variables, functions, and types.
Import Sorting #
Import Categories #
Imports must be sorted into the following categories:
- Types: All type imports at the top.
- External Packages:
- Imports starting with a symbol come first.
- Followed by named imports.
- Assets: Static files like images, fonts, icons.
- Components: Imported from
$componentspath alias. - Data: Imported from
$datapath alias. - Hooks: Imported from
$hookspath alias. - Internal Packages:
- All internal imports are created inside
src/lib/directory and can be imported using the$libpath alias.
- All internal imports are created inside
- Styles: Sass/SCSS files imported from
$stylespath alias.
Rules:
- Each category must be separated by a single line to enhance readability.
- Within each category, sort imports alphabetically when possible.
Complex Example #
// Types
import type { FC, ReactNode } from 'react';
import type { ApiResponse } from '$lib/types/api';
import type { User, UserRole } from '$lib/types/user';
// External Packages
import { treaty } from '@elysiajs/eden';
import { useCallback, useEffect, useState } from 'react';
import { z } from 'zod';
// Assets
import DEFAULT_AVATAR from '$assets/images/default-avatar.png';
// Components
import Avatar from '$components/avatar';
import Button from '$components/button';
import Modal from '$components/modal';
import Spinner from '$components/spinner';
// Data
import { USER_DEFAULTS } from '$data/constants';
import { ROLE_PERMISSIONS } from '$data/permissions';
// Hooks
import { useAuth } from '$hooks/use-auth';
import { useDebounce } from '$hooks/use-debounce';
import { useLocalStorage } from '$hooks/use-local-storage';
// Internal Packages
import { api } from '$lib/api';
import { UserSchema } from '$lib/schemas/user';
import { formatDate, truncate } from '$lib/utils';
// Styles
import styles from '$styles/components/user-profile.module.scss';
Route/Page Example #
// Types
import type { EventData, EventFilters } from '$lib/types/events';
// External Packages
import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { useMemo, useState } from 'react';
// Assets
import HERO_IMAGE from '$assets/images/events-hero.jpg';
// Components
import EventCard from '$components/event-card';
import FilterBar from '$components/filter-bar';
import Pagination from '$components/pagination';
// Data
import { EVENT_CATEGORIES } from '$data/categories';
// Hooks
import { useFilters } from '$hooks/use-filters';
// Internal Packages
import { eventsQueryOptions } from '$lib/api/events';
import { filterEvents, sortEvents } from '$lib/utils/events';
// Styles
import styles from '$styles/pages/events.module.scss';
export const Route = createFileRoute('/events')({
component: EventsPage,
loader: ({ context }) => context.queryClient.ensureQueryData(eventsQueryOptions()),
});
function EventsPage() {
const { data: events } = useSuspenseQuery(eventsQueryOptions());
// ... component logic
}
Notes on Import Sorting #
- React imports: Place React-specific imports (
useState,useEffect, etc.) in the External Packages section - TanStack imports: Place
@tanstack/react-router,@tanstack/react-query, and@tanstack/startin External Packages - Route exports:
createFileRouteandcreateAPIFileRouteshould be imported early in External Packages - Server functions:
createServerFnbelongs in External Packages - Sass imports:
- Component styles:
$styles/components/component-name.module.scss - Page styles:
$styles/pages/page-name.module.scss - Global styles:
$styles/globals.scssor$styles/variables.scss - Use CSS Modules (
.module.scss) for component-scoped styles
- Component styles:
- Assets: Static files like images, fonts, icons should come early (after External Packages) since they're often used in JSX, and should be named like constants.
- Relative imports: Avoid when possible; use path aliases instead
- Side-effect imports: Place at the very top if absolutely necessary (e.g.,
import './polyfills')
Common Patterns #
// Good - Clear categories with separation
import type { Props } from '$lib/types';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';
import ICON from '$assets/icons/star.svg';
import Button from '$components/button';
import { api } from '$lib/api';
import styles from '$styles/components/card.module.scss';
// Good - Multiple imports from same source on one line
import { useCallback, useEffect, useState } from 'react';
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
// Good - Destructured imports alphabetically
import { api, auth, cache } from '$lib/api';
// Avoid - Mixed categories without separation
import Button from '$components/button';
import styles from '$styles/app.module.scss';
import { useState } from 'react';
import type { Props } from './types';
import icon from '$assets/icon.svg';
React Conventions #
- Prefer functional components with hooks
- Use named exports for components
- Avoid default exports except for route components
Component Props #
- Always define an explicit props interface, even if empty
- Props interfaces should be named
ComponentNameProps - Place the interface directly above the component definition
- Use
interface(nottype) for props to allow future extension
Examples #
// Good - Explicit empty interface
interface HeaderProps {}
export function Header(props: HeaderProps) {
return <header>PEC ACM</header>;
}
// Good - With props
interface ButtonProps {
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
export function Button({ variant = 'primary', onClick }: ButtonProps) {
return <button onClick={onClick}>{variant}</button>;
}
// Avoid - No explicit props type
export function Footer({ children }) {
return <footer>{children}</footer>;
}
Extending HTML Element Props #
When creating wrapper components, extend the appropriate HTML attributes:
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
}
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
}
interface DivProps extends React.HTMLAttributes<HTMLDivElement> {
customProp?: string;
}
TypeScript Guidelines #
- Prefer
interfacefor object shapes that may be extended - Prefer
typefor unions, intersections, and primitives - Avoid
any; useunknownwhen type is truly unknown - Generic type parameters: single capital letter (
T,K,V) or descriptive PascalCase (TUser,TResponse)
Examples #
// Good - Interface for extendable objects
interface User {
id: string;
name: string;
}
interface Admin extends User {
permissions: string[];
}
// Good - Type for unions and primitives
type Status = 'pending' | 'active' | 'inactive';
type ID = string | number;
// Good - Generic with descriptive name
function fetchData<TResponse>(url: string): Promise<TResponse> {
// ...
}
// Avoid - Using 'any'
function process(data: any) {
// ...
}
// Good - Using 'unknown' instead
function process(data: unknown) {
if (typeof data === 'string') {
// ...
}
}