Files
BanGUI/Docs/Web-Development.md
Lukas e8214b5856 fix: use backend service name in Vite proxy target
Vite runs inside the frontend container where 'localhost' resolves to
the container itself, not the backend.  Change the /api proxy target
from http://localhost:8000 to http://backend:8000 so the request is
routed to the backend service over the compose network.
2026-03-01 19:21:30 +01:00

21 KiB
Raw Blame History

Frontend Development — Rules & Guidelines

Rules and conventions every frontend developer must follow. Read this before writing your first line of code.


1. Language & Typing

  • TypeScript is mandatory — no plain JavaScript files (.js, .jsx) in the codebase.
  • Use strict mode ("strict": true in tsconfig.json) — the project must compile with zero errors.
  • Never use any. If a type is truly unknown, use unknown and narrow it with type guards.
  • Prefer interfaces for object shapes that may be extended, type aliases for unions, intersections, and utility types.
  • Every function must have explicit parameter types and return types — including React components (React.FC is discouraged; type props and return JSX.Element explicitly).
  • Use T | null or T | undefined instead of Optional patterns — be explicit about nullability.
  • Use as const for constant literals and enums where it improves type narrowness.
  • Run tsc --noEmit in CI — the codebase must pass with zero type errors.
// Good
interface BanEntry {
  ip: string;
  jail: string;
  bannedAt: string;
  expiresAt: string | null;
}

function BanRow({ ban }: { ban: BanEntry }): JSX.Element {
  return <tr><td>{ban.ip}</td><td>{ban.jail}</td></tr>;
}

// Bad — untyped, uses `any`
function BanRow({ ban }: any) {
  return <tr><td>{ban.ip}</td></tr>;
}

2. Reusable Types

  • All shared type definitions live in a dedicated types/ directory.
  • Group types by domain: types/ban.ts, types/jail.ts, types/auth.ts, types/api.ts, etc.
  • Import types using the import type syntax when the import is only used for type checking — this keeps the runtime bundle clean.
  • Component-specific prop types may live in the same file as the component, but any type used by two or more files must move to types/.
  • Never duplicate a type definition — define it once, import everywhere.
  • Export API response shapes alongside their domain types so consumers always know what the server returns.
// types/ban.ts
export interface Ban {
  ip: string;
  jail: string;
  bannedAt: string;
  expiresAt: string | null;
  banCount: number;
  country: string | null;
}

export interface BanListResponse {
  bans: Ban[];
  total: number;
}
// components/BanTable.tsx
import type { Ban } from "../types/ban";

3. Type Safety in API Calls

  • Every API call must have a typed request and typed response.
  • Define response shapes as TypeScript interfaces in types/ and cast the response through them.
  • Use a central API client (e.g., a thin wrapper around fetch or axios) that returns typed data — individual components never call fetch directly.
  • Validate or assert the response structure at the boundary when dealing with untrusted data; for critical flows, consider a runtime validation library (e.g., zod).
  • API endpoint paths are constants defined in a single file (api/endpoints.ts) — never hard-code URLs in components.
// api/client.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? "/api";

async function get<T>(path: string): Promise<T> {
  const response: Response = await fetch(`${BASE_URL}${path}`, {
    credentials: "include",
  });
  if (!response.ok) {
    throw new ApiError(response.status, await response.text());
  }
  return (await response.json()) as T;
}

export const api = { get, post, put, del } as const;
// api/bans.ts
import type { BanListResponse } from "../types/ban";
import { api } from "./client";

export async function fetchBans(hours: number): Promise<BanListResponse> {
  return api.get<BanListResponse>(`/bans?hours=${hours}`);
}

4. Code Organization

Project Structure

frontend/
├── public/
├── src/
│   ├── api/                 # API client, endpoint definitions, per-domain request files
│   ├── assets/              # Static images, fonts, icons
│   ├── components/          # Reusable UI components (buttons, modals, tables, etc.)
│   ├── hooks/               # Custom React hooks
│   ├── layouts/             # Page-level layout wrappers (sidebar, header, etc.)
│   ├── pages/               # Route-level page components (one per route)
│   ├── providers/           # React context providers (auth, theme, etc.)
│   ├── theme/               # Fluent UI custom theme, tokens, and overrides
│   ├── types/               # Shared TypeScript type definitions
│   ├── utils/               # Pure helper functions, constants, formatters
│   ├── App.tsx              # Root component, FluentProvider + router setup
│   ├── main.tsx             # Entry point
│   └── vite-env.d.ts        # Vite type shims
├── .eslintrc.cjs
├── .prettierrc
├── tsconfig.json
├── vite.config.ts           # Dev proxy: /api → http://backend:8000 (service DNS)
└── package.json

Dev proxy target: vite.config.ts proxies all /api requests to http://backend:8000. Use the compose service name (backend), not localhost — inside the container network localhost resolves to the frontend container itself and causes ECONNREFUSED.

Separation of Concerns

  • Pages handle routing and compose layout + components — they contain no business logic.
  • Components are reusable, receive data via props, and emit changes via callbacks — they never call the API directly.
  • Hooks encapsulate stateful logic, side effects, and API calls so components stay declarative.
  • API layer handles all HTTP communication — components and hooks consume typed functions from api/, never raw fetch.
  • Types are purely declarative — no runtime code in types/ files.
  • Utils are pure functions with no side effects and no React dependency.
  • Theme contains exclusively Fluent UI custom token overrides and theme definitions — no component logic.

5. UI Framework — Fluent UI React (v9)

  • Fluent UI React Components v9 (@fluentui/react-components) is the only UI component library allowed — do not add alternative component libraries (Material UI, Chakra, Ant Design, etc.).
  • Install via npm:
npm install @fluentui/react-components @fluentui/react-icons

FluentProvider

  • Wrap the entire application in <FluentProvider> at the root — this supplies the theme and design tokens to all Fluent components.
  • The provider must sit above the router so every page inherits the theme.
// App.tsx
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { BrowserRouter } from "react-router-dom";
import AppRoutes from "./AppRoutes";

function App(): JSX.Element {
  return (
    <FluentProvider theme={webLightTheme}>
      <BrowserRouter>
        <AppRoutes />
      </BrowserRouter>
    </FluentProvider>
  );
}

export default App;

Theming & Design Tokens

  • Use the built-in themes (webLightTheme, webDarkTheme) as the base.
  • Customise design tokens by creating a custom theme in theme/ — never override Fluent styles with raw CSS.
  • Reference tokens via the tokens object from @fluentui/react-components when writing makeStyles rules.
  • If light/dark mode is needed, switch the theme prop on FluentProvider — never duplicate style definitions for each mode.
// theme/customTheme.ts
import { createLightTheme, createDarkTheme } from "@fluentui/react-components";
import type { BrandVariants, Theme } from "@fluentui/react-components";

const brandColors: BrandVariants = {
  10: "#020305",
  // ... define brand colour ramp
  160: "#e8ebf9",
};

export const lightTheme: Theme = createLightTheme(brandColors);
export const darkTheme: Theme = createDarkTheme(brandColors);

Styling with makeStyles (Griffel)

  • All custom styling is done via makeStyles from @fluentui/react-components — Fluent UI uses Griffel (CSS-in-JS with atomic classes) under the hood.
  • Never use inline style props, global CSS, or external CSS frameworks for Fluent components.
  • Co-locate styles in the same file as the component they belong to, defined above the component function.
  • Use mergeClasses when combining multiple style sets conditionally.
  • Reference Fluent design tokens (tokens.colorBrandBackground, tokens.fontSizeBase300, etc.) instead of hard-coded values — this ensures consistency and automatic theme support.
import { makeStyles, tokens, mergeClasses } from "@fluentui/react-components";

const useStyles = makeStyles({
  root: {
    padding: tokens.spacingVerticalM,
    backgroundColor: tokens.colorNeutralBackground1,
  },
  highlighted: {
    backgroundColor: tokens.colorPaletteRedBackground2,
  },
});

function BanCard({ isHighlighted }: BanCardProps): JSX.Element {
  const styles = useStyles();
  return (
    <div className={mergeClasses(styles.root, isHighlighted && styles.highlighted)}>
      {/* ... */}
    </div>
  );
}

Component Usage Rules

  • Always prefer Fluent UI components over plain HTML elements for interactive and presentational UI: <Button>, <Input>, <Table>, <Dialog>, <Card>, <Badge>, <Spinner>, <Toast>, <MessageBar>, etc.
  • Use <DataGrid> for data-heavy tables (ban lists, jail lists) — it provides sorting, selection, and accessibility out of the box.
  • Use Fluent UI <Dialog> for modals and confirmations — never build custom modal overlays.
  • Use @fluentui/react-icons for all icons — do not mix icon libraries.
  • Customise Fluent components only through their public API (props, slots, makeStyles) — never patch internal DOM or override internal class names.

Libraries you must NOT use alongside Fluent UI

  • tailwindcss — use makeStyles and design tokens.
  • styled-components / emotion — Fluent UI uses Griffel; mixing CSS-in-JS runtimes causes conflicts.
  • @mui/*, antd, chakra-ui — one design system only.
  • Global CSS files that target Fluent class names — use makeStyles overrides.

6. Component Rules

  • One component per file. The filename matches the component name: BanTable.tsx exports BanTable.
  • Use function declarations for components — not arrow-function variables.
  • Keep components small and focused — if a component exceeds ~150 lines, split it.
  • Props are defined as an interface named <ComponentName>Props in the same file (or imported from types/ if shared).
  • Destructure props in the function signature.
  • Never mutate props or state directly — always use immutable update patterns.
  • Avoid inline styles — use makeStyles from Fluent UI for all custom styling (see section 5).
  • Supply a key prop whenever rendering lists — never use array indices as keys if the list can reorder.
  • Prefer Fluent UI components (Button, Table, Input, …) over raw HTML elements for any interactive or styled element.
import { Table, TableBody, TableRow, TableCell, Button } from "@fluentui/react-components";
import type { Ban } from "../types/ban";

interface BanTableProps {
  bans: Ban[];
  onUnban: (ip: string) => void;
}

function BanTable({ bans, onUnban }: BanTableProps): JSX.Element {
  return (
    <Table>
      <TableBody>
        {bans.map((ban) => (
          <TableRow key={ban.ip}>
            <TableCell>{ban.ip}</TableCell>
            <TableCell>{ban.jail}</TableCell>
            <TableCell>
              <Button appearance="subtle" onClick={() => onUnban(ban.ip)}>Unban</Button>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

export default BanTable;

7. Hooks & State Management

  • Prefix custom hooks with use — e.g., useBans, useAuth, useJails.
  • Each hook lives in its own file under hooks/.
  • Use useState for local UI state, useReducer for complex state transitions.
  • Use React Context sparingly — only for truly global concerns (auth, theme). Do not use context as a replacement for prop drilling one or two levels.
  • Avoid useEffect for derived data — compute it during render or use useMemo.
  • Always include the correct dependency arrays in useEffect, useMemo, and useCallback. Disable the ESLint exhaustive-deps rule only with a comment explaining why.
  • Clean up side effects (subscriptions, timers, abort controllers) in the useEffect cleanup function.
// hooks/useBans.ts
import { useState, useEffect } from "react";
import type { Ban } from "../types/ban";
import { fetchBans } from "../api/bans";

interface UseBansResult {
  bans: Ban[];
  loading: boolean;
  error: string | null;
}

function useBans(hours: number): UseBansResult {
  const [bans, setBans] = useState<Ban[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    async function load(): Promise<void> {
      setLoading(true);
      try {
        const data = await fetchBans(hours);
        setBans(data.bans);
        setError(null);
      } catch (err) {
        if (!controller.signal.aborted) {
          setError(err instanceof Error ? err.message : "Unknown error");
        }
      } finally {
        setLoading(false);
      }
    }

    load();
    return () => controller.abort();
  }, [hours]);

  return { bans, loading, error };
}

export default useBans;

8. Naming Conventions

Element Convention Example
Components PascalCase BanTable, JailCard
Component files PascalCase .tsx BanTable.tsx
Hooks camelCase with use prefix useBans, useAuth
Hook files camelCase .ts useBans.ts
Type / Interface PascalCase BanEntry, JailListResponse
Type files camelCase .ts ban.ts, jail.ts
Utility functions camelCase formatDate, buildQuery
Constants UPPER_SNAKE_CASE MAX_RETRIES, API_BASE_URL
makeStyles hooks useStyles (file-scoped) const useStyles = makeStyles({…})
makeStyles keys camelCase slot names root, header, highlighted
Directories lowercase kebabcase or camelCase components/, hooks/
Boolean props/variables is/has/should prefix isLoading, hasError

9. Linting & Formatting

  • ESLint with the following plugins is required:
    • @typescript-eslint/eslint-plugin — TypeScript-specific rules.
    • eslint-plugin-react — React best practices.
    • eslint-plugin-react-hooks — enforce rules of hooks.
    • eslint-plugin-import — ordered and valid imports.
  • Prettier handles all formatting — ESLint must not conflict with Prettier (use eslint-config-prettier).
  • Format on save is expected — every developer must enable it in their editor.
  • Run eslint . --max-warnings 0 and prettier --check . in CI — zero warnings, zero formatting diffs.
  • Line length: 100 characters max.
  • Strings: use double quotes (").
  • Semicolons: always.
  • Trailing commas: all (ES5+).
  • Indentation: 2 spaces.
  • No unused variables, no unused imports, no @ts-ignore without an accompanying comment.
  • Import order (enforced by ESLint): React → third-party → aliases → relative, each group separated by a blank line.
// .prettierrc
{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2
}

10. Clean Code Principles

  • Single Responsibility: Every function, hook, and component does one thing well.
  • DRY (Don't Repeat Yourself): Extract repeated JSX into components, repeated logic into hooks or utils. If you copy-paste, refactor.
  • KISS (Keep It Simple, Stupid): Prefer the simplest solution that works. Avoid premature abstraction.
  • Meaningful Names: Variable and function names describe what, not how. Avoid abbreviations (btnbutton, idxindex) except universally understood ones (id, url, ip).
  • Small Functions: If a function exceeds ~30 lines, it likely does too much — split it.
  • No Magic Numbers / Strings: Extract constants with descriptive names.
  • Explicit over Implicit: Favor clarity over cleverness. Code is written once and read many times.
  • No Dead Code: Remove unused functions, commented-out blocks, and unreachable branches before committing.
  • Early Returns: Reduce nesting by returning early from guard clauses.
  • Immutability: Default to const. Use spread / map / filter instead of mutating arrays and objects.
// Bad — magic number, unclear name
if (data.length > 50) { ... }

// Good
const MAX_VISIBLE_BANS = 50;
if (data.length > MAX_VISIBLE_BANS) { ... }

11. Error Handling

  • Wrap API calls in try-catch inside hooks — components should never see raw exceptions.
  • Display user-friendly error messages — never expose stack traces or raw server responses in the UI.
  • Use an error boundary (ErrorBoundary component) at the page level to catch unexpected render errors.
  • Log errors to the console (or a future logging service) with sufficient context for debugging.
  • Always handle the loading, error, and empty states for every data-driven component.

12. Performance

  • Use React.memo only when profiling reveals unnecessary re-renders — do not wrap every component by default.
  • Use useMemo and useCallback for expensive computations and stable callback references passed to child components — not for trivial values.
  • Lazy-load route-level pages with React.lazy + Suspense to reduce initial bundle size.
  • Avoid creating new objects or arrays inside render unless necessary — stable references prevent child re-renders.
  • Keep bundle size in check — review dependencies before adding them and prefer lightweight alternatives.

13. Accessibility

  • Use semantic HTML elements (<button>, <nav>, <table>, <main>, <header>) — not <div> with click handlers.
  • Every interactive element must be keyboard accessible (focusable, operable with Enter/Space/Escape as appropriate).
  • Images and icons require alt text or aria-label.
  • Form inputs must have associated <label> elements.
  • Maintain sufficient color contrast ratios (WCAG AA minimum).
  • Test with a screen reader periodically.

14. Testing

  • Write tests for every new component, hook, and utility function.
  • Use Vitest (or Jest) as the test runner and React Testing Library for component tests.
  • Test behavior, not implementation — query by role, label, or text, never by CSS class or internal state.
  • Mock API calls at the network layer (e.g., msw — Mock Service Worker) — components should not know about mocks.
  • Aim for >80 % line coverage — critical paths (auth flow, ban/unban actions) must be 100 %.
  • Test name pattern: it("should <expected behavior> when <condition>").
  • Wrap components under test in <FluentProvider> so Fluent UI styles and tokens resolve correctly:
import { render, screen } from "@testing-library/react";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import BanTable from "./BanTable";
import type { Ban } from "../types/ban";

const mockBans: Ban[] = [
  { ip: "192.168.1.1", jail: "sshd", bannedAt: "2026-02-28T12:00:00Z", expiresAt: null, banCount: 3, country: "DE" },
];

function renderWithProvider(ui: JSX.Element) {
  return render(<FluentProvider theme={webLightTheme}>{ui}</FluentProvider>);
}

it("should render a row for each ban", () => {
  renderWithProvider(<BanTable bans={mockBans} onUnban={vi.fn()} />);
  expect(screen.getByText("192.168.1.1")).toBeInTheDocument();
});

15. Git & Workflow

  • Branch naming: feature/<short-description>, fix/<short-description>, chore/<short-description>.
  • Commit messages: imperative tense, max 72 chars first line (Add ban table component, Fix date formatting in dashboard).
  • Every merge request must pass: ESLint, Prettier, TypeScript compiler, all tests.
  • Do not merge with failing CI.
  • Keep pull requests small and focused — one feature or fix per PR.
  • Review your own diff before requesting review.

16. Quick Reference — Do / Don't

Do Don't
Type every prop, state, and return value Use any or leave types implicit
Keep shared types in types/ Duplicate interfaces across files
Call API from hooks, not components Scatter fetch calls across the codebase
Use import type for type-only imports Import types as regular imports
One component per file Export multiple components from one file
Destructure props in the signature Access props.x throughout the body
Use Fluent UI components for all interactive UI Build custom buttons, inputs, dialogs from scratch
Style with makeStyles + design tokens Use inline styles, global CSS, or Tailwind
Wrap the app in <FluentProvider> Render Fluent components outside a provider
Use @fluentui/react-icons for icons Mix multiple icon libraries
Use semantic HTML elements Use <div> for everything
Handle loading, error, and empty states Only handle the happy path
Name booleans with is/has/should Name booleans as plain nouns (loading)
Extract constants for magic values Hard-code numbers and strings
Clean up effects (abort, unsub) Let effects leak resources
Format and lint before every commit Push code that doesn't pass CI