Platform Architecture - Train5D Documentation

Train5D Platform Documentation

Last Updated: 2026-03-01

Train5D Frontend Architecture

Last Updated: December 3, 2025
Version: 2.0

Table of Contents

  1. Overview
  2. Type Organization
  3. Style Organization
  4. Import Conventions
  5. Component Structure
  6. Best Practices
  7. Migration Guides

Overview

This document outlines the architectural patterns, conventions, and best practices for the Train5D frontend application. Following these guidelines ensures code maintainability, performance optimization, and developer productivity.

Key Principles


Type Organization

1. Domain Types (/app/types/index.ts)

Purpose: Centralized location for core business domain types shared across multiple features or pillars.

What belongs here: - Database entity types (Exercise, MuscleGroup, OrganSystem, Tag, Assessment, User) - API response/request types (ApiResponse, PaginatedResponse, DatabaseResponse) - Shared utility types (ViewMode, Status, FormData, HttpMethod) - Cross-pillar types used in multiple areas

Example:

// app/types/index.ts
export interface Exercise extends BaseEntity {
  name: string;
  description?: string;
  instructions?: string;
  category: string | { id: string; name: string };
  difficulty: string;
  muscle_groups?: string[];
  tags?: string[] | Array<{ id: number; name: string }>;
  equipment?: string[];
  status?: string;
  // ...other fields
}

export interface Tag extends BaseEntity {
  category?: string;
  pillar?: string;
  is_cross_pillar?: boolean;
  color?: string;
  icon?: string;
}

export interface MuscleGroup extends BaseEntity {
  simple_name: string;
  body_region: string;
  plane?: string;
  function?: string;
  origin?: string;
  insertion?: string;
  is_active?: boolean;
}

Usage:

import { Exercise, Tag, MuscleGroup } from '@/types';

2. Component Props (Co-located)

Purpose: Component-specific props interfaces that won’t be reused elsewhere.

What belongs here: - Props interfaces for individual components - Component-specific state types - Local event handler types

Example:

// components/pillars/exercise/ExerciseCard.tsx
import { Exercise } from '@/types';

interface ExerciseCardProps {
  exercise: Exercise;
  onEdit: (id: number) => void;
  onDelete: (id: number) => void;
  viewMode: 'grid' | 'list';
}

const ExerciseCard: React.FC<ExerciseCardProps> = ({ exercise, onEdit, onDelete, viewMode }) => {
  // Component implementation
};

3. Shared Component Types (/app/types/components.ts)

Purpose: Reusable component prop interfaces used across multiple components.

What belongs here: - Modal props interfaces - Form field props - Dropdown/select props - Button/input props - Card/list item props

Example:

// app/types/components.ts
export interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  size?: 'sm' | 'md' | 'lg' | 'xl';
  closeOnOverlayClick?: boolean;
}

export interface FormFieldProps {
  name: string;
  label: string;
  type?: 'text' | 'textarea' | 'select' | 'number';
  required?: boolean;
  placeholder?: string;
  value: string | number;
  onChange: (value: string | number) => void;
}

export interface DropdownProps<T> {
  options: T[];
  value: T;
  onChange: (value: T) => void;
  placeholder?: string;
  disabled?: boolean;
  labelKey?: keyof T;
  valueKey?: keyof T;
}

Usage:

import { ModalProps, FormFieldProps } from '@/types/components';

4. Pillar-Specific Types (Pillar directories)

Purpose: Types specific to individual pillars that won’t be used elsewhere.

Directory Structure:

app/components/pillars/
├── anatomy/
│   └── types.ts          # Anatomy-specific types
├── exercise/
│   └── types.ts          # Exercise-specific types
├── mindset/
│   └── types.ts          # Mindset-specific types
└── nutrition/
    └── types.ts          # Nutrition-specific types

What belongs here: - Pillar-specific form data - Pillar-specific filter types - Pillar-specific view states - Extended versions of domain types for pillar-specific use cases

Example:

// app/components/pillars/exercise/types.ts
import { Exercise } from '@/types';

export interface ExerciseFilters {
  search: string;
  category: string;
  difficulty: string;
  muscleGroup: string;
  equipment: string;
  environment: 'land' | 'pool' | 'both' | 'all';
}

export interface ExerciseFormData {
  name: string;
  description: string;
  instructions: string;
  category_id: number;
  difficulty_level: string;
  sets?: number;
  repetitions?: number;
  selectedMuscleGroups: number[];
  selectedTags: number[];
  selectedEquipment: number[];
}

// Extended exercise type with detail page requirements
export interface ExerciseDetailData extends Exercise {
  category: {
    id: string;
    name: string;
  };
  media?: Array<{
    mediaUrl: string;
    mediaType: string;
    title?: string;
  }>;
  createdBy: {
    id: string;
    name: string;
    email: string;
  };
}

Usage:

import { ExerciseFilters, ExerciseFormData } from './types';
// OR from parent directory:
import { ExerciseFilters } from '@/components/pillars/exercise/types';

Type Organization Decision Tree

Is this type used across multiple pillars?
├─ YES → /app/types/index.ts
└─ NO
   ├─ Is it a reusable component prop interface?
   │  ├─ YES → /app/types/components.ts
   │  └─ NO
   │     ├─ Is it specific to one pillar?
   │     │  ├─ YES → /app/components/pillars/[pillar]/types.ts
   │     │  └─ NO → Co-locate with component
   │     └─ Is it only used in one component?
   │        └─ YES → Co-locate with component

Style Organization

1. CSS Modules (Preferred)

Purpose: Reusable, maintainable styles with CSS scoping.

When to use: - Shared component styles - Repeated style patterns (buttons, cards, modals, forms) - Complex layouts with multiple style variations - Theme-able components

Benefits: - Eliminates duplicate inline style objects (~2-3KB bundle reduction per module) - Better browser caching - Easier theming and maintenance - Auto-generated unique class names (prevents CSS conflicts)

Example Structure:

app/styles/
├── modals.module.css      # Modal components
├── forms.module.css       # Form elements
├── cards.module.css       # Card components
├── buttons.module.css     # Button variations
└── layout.module.css      # Layout utilities

CSS Module Example:

/* app/styles/modals.module.css */
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  border-radius: 8px;
  padding: 1.5rem;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  max-width: 600px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
}

.tagsSection {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 1rem;
}

.compactHeader {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0;
  border-bottom: 1px solid #e5e7eb;
}

.scrollableList {
  max-height: 250px;
  overflow-y: auto;
  border: 1px solid #e5e7eb;
  border-radius: 0.375rem;
  padding: 0.5rem;
}

.tagGrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 0.5rem;
}

Component Usage:

import styles from '@/styles/modals.module.css';

const AddItemModal = () => {
  return (
    <div className={styles.modal}>
      <div className={styles.tagsSection}>
        <div className={styles.compactHeader}>
          <h3>Tags</h3>
        </div>
        <div className={styles.scrollableList}>
          <div className={styles.tagGrid}>
            {/* Tag items */}
          </div>
        </div>
      </div>
    </div>
  );
};

Combining Classes:

// Static combination
<button className={`${styles.button} ${styles.buttonPrimary}`}>

// Conditional combination
<button 
  className={`${styles.button} ${isActive ? styles.buttonActive : styles.buttonInactive}`}
>

// With dynamic inline style for truly dynamic values
<button 
  className={`${styles.colorButton} ${selected ? styles.colorButtonSelected : ''}`}
  style={{ backgroundColor: color }}
>

2. Inline Styles (Limited Use)

When to use: - Truly dynamic values (colors from database, calculated dimensions) - One-off styles that won’t be reused - Style values from props or state

When NOT to use: - Static styles - Repeated patterns - Theme colors or spacing - Responsive styles (use CSS modules with media queries instead)

Good Examples:

// Dynamic background color from database
<button 
  className={styles.colorButton}
  style={{ backgroundColor: item.color }}
/>

// Calculated dimensions
<div style={{ width: `${progress}%` }} />

// Dynamic positioning
<div style={{ left: `${position.x}px`, top: `${position.y}px` }} />

Bad Examples (use CSS modules instead):

// ❌ Static styles - should be in CSS module
<div style={{
  padding: '1rem',
  borderRadius: '0.5rem',
  backgroundColor: '#f3f4f6'
}}>

// ❌ Repeated button styles - create CSS module
<button style={{
  padding: '0.5rem 1rem',
  borderRadius: '0.375rem',
  backgroundColor: '#3b82f6',
  color: 'white',
  cursor: 'pointer'
}}>

3. Global Styles (/app/styles/globals.css)

What belongs here: - CSS reset/normalize - Theme variables (colors, spacing, typography) - Base element styles (body, headings, links) - Utility classes used across the app

Example:

/* globals.css */
:root {
  --color-primary: #3b82f6;
  --color-secondary: #10b981;
  --color-danger: #ef4444;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --border-radius: 0.375rem;
}

body {
  margin: 0;
  font-family: 'Inter', sans-serif;
  color: #1f2937;
}

.utility-text-center {
  text-align: center;
}

.utility-flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

Import Conventions

Path Aliases

Use TypeScript path aliases for cleaner imports:

// ✅ Good - using alias
import { Exercise } from '@/types';
import { Button } from '@/components/ui/Button';
import styles from '@/styles/modals.module.css';

// ❌ Bad - relative paths
import { Exercise } from '../../../types';
import { Button } from '../../components/ui/Button';
import styles from '../../../styles/modals.module.css';

Import Order

Organize imports in the following order for consistency:

// 1. External libraries
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';

// 2. Internal types
import { Exercise, Tag, MuscleGroup } from '@/types';
import { ModalProps } from '@/types/components';

// 3. Components
import DashboardLayout from '@/components/dashboard/DashboardLayout';
import { Button } from '@/components/ui/Button';

// 4. Utilities and helpers
import { capitalizeWords } from '@/lib/text-utils';
import { getDifficultyBadgeClass } from '@/lib/utils';

// 5. Styles
import styles from '@/styles/modals.module.css';

// 6. Local/relative imports
import { ExerciseFilters } from './types';
import ExerciseCard from './ExerciseCard';

Named vs Default Exports

Prefer named exports for better refactoring support and autocomplete:

// ✅ Good - named export
export const ExerciseCard = ({ exercise }: ExerciseCardProps) => {
  // ...
};

// Usage
import { ExerciseCard } from '@/components/ExerciseCard';

// ❌ Acceptable but less preferred - default export
const ExerciseCard = ({ exercise }: ExerciseCardProps) => {
  // ...
};
export default ExerciseCard;

// Usage (can be renamed)
import AnyName from '@/components/ExerciseCard';

Component Structure

File Organization

app/components/
├── ui/                    # Reusable UI primitives
│   ├── Button.tsx
│   ├── Input.tsx
│   ├── Modal.tsx
│   └── Card.tsx
├── dashboard/             # Dashboard-specific components
├── pillars/              # Pillar-specific features
│   ├── anatomy/
│   │   ├── types.ts
│   │   └── components/
│   │       ├── shared/
│   │       │   ├── AddItemModal.tsx
│   │       │   └── EditItemModal.tsx
│   │       ├── muscle-groups/
│   │       │   ├── MuscleGroupCard.tsx
│   │       │   └── MuscleGroupsTab.tsx
│   │       └── organ-systems/
│   ├── exercise/
│   ├── mindset/
│   └── nutrition/
└── shared/               # Cross-cutting shared components

Component Template

// components/pillars/exercise/ExerciseCard.tsx
import React from 'react';
import { useRouter } from 'next/router';
import { Exercise } from '@/types';
import { getDifficultyBadgeClass } from '@/lib/utils';
import styles from '@/styles/cards.module.css';

// Component-specific props
interface ExerciseCardProps {
  exercise: Exercise;
  onEdit?: (id: number) => void;
  onDelete?: (id: number) => void;
  viewMode?: 'grid' | 'list';
}

/**
 * ExerciseCard - Displays an exercise in card format
 * 
 * @param exercise - The exercise data to display
 * @param onEdit - Optional callback when edit button is clicked
 * @param onDelete - Optional callback when delete button is clicked
 * @param viewMode - Display mode (grid or list)
 */
export const ExerciseCard: React.FC<ExerciseCardProps> = ({ 
  exercise, 
  onEdit, 
  onDelete,
  viewMode = 'grid'
}) => {
  const router = useRouter();

  const handleClick = () => {
    router.push(`/learn/exercises/${exercise.id}`);
  };

  return (
    <div className={`${styles.card} ${styles[`card-${viewMode}`]}`} onClick={handleClick}>
      <h3 className={styles.cardTitle}>{exercise.name}</h3>
      <p className={styles.cardDescription}>{exercise.description}</p>
      
      <div className={styles.cardFooter}>
        <span className={getDifficultyBadgeClass(exercise.difficulty)}>
          {exercise.difficulty}
        </span>
        
        {onEdit && (
          <button 
            className={styles.iconButton}
            onClick={(e) => {
              e.stopPropagation();
              onEdit(exercise.id);
            }}
          >
            Edit
          </button>
        )}
      </div>
    </div>
  );
};

Best Practices

Type Safety

  1. Avoid any - Use specific types or unknown if type is truly unknown
  2. Use union types for restricted values: type Status = 'loading' | 'success' | 'error'
  3. Extend base types rather than duplicating: interface MuscleGroup extends BaseEntity
  4. Use optional chaining for potentially undefined values: exercise.category?.name
  5. Type guard functions for runtime type checking:
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isCategoryObject(category: unknown): category is { id: string; name: string } {
  return typeof category === 'object' && category !== null && 'id' in category && 'name' in category;
}

// Usage
const categoryName = typeof exercise.category === 'string' 
  ? exercise.category 
  : exercise.category?.name || '';

Performance

  1. Memoize expensive computations:
const filteredExercises = useMemo(() => 
  exercises.filter(e => e.category === selectedCategory),
  [exercises, selectedCategory]
);
  1. Use CSS modules to reduce inline style object creation
  2. Lazy load heavy components:
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <p>Loading...</p>
});

Maintainability

  1. Extract complex logic into utility functions
  2. Keep components focused - single responsibility
  3. Document complex types with JSDoc comments
  4. Use meaningful names - avoid abbreviations
  5. Consistent naming patterns:
    • Components: PascalCase (ExerciseCard)
    • Functions: camelCase (getDifficulty)
    • CSS classes: kebab-case or camelCase (card-title, cardTitle)
    • Constants: UPPER_SNAKE_CASE (MAX_ITEMS)

Migration Guides

Migrating Inline Styles to CSS Modules

Before:

const MyComponent = () => {
  return (
    <div style={{
      padding: '1rem',
      borderRadius: '0.5rem',
      backgroundColor: '#f3f4f6',
      display: 'flex',
      flexDirection: 'column',
      gap: '0.5rem'
    }}>
      <h3 style={{
        fontSize: '1.125rem',
        fontWeight: 600,
        color: '#1f2937'
      }}>
        Title
      </h3>
    </div>
  );
};

After:

  1. Create CSS module (MyComponent.module.css):
.container {
  padding: 1rem;
  border-radius: 0.5rem;
  background-color: #f3f4f6;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.title {
  font-size: 1.125rem;
  font-weight: 600;
  color: #1f2937;
}
  1. Update component:
import styles from './MyComponent.module.css';

const MyComponent = () => {
  return (
    <div className={styles.container}>
      <h3 className={styles.title}>Title</h3>
    </div>
  );
};

Migrating Local Types to Centralized Types

Before:

// ComponentA.tsx
interface Exercise {
  id: number;
  name: string;
  description: string;
}

// ComponentB.tsx
interface Exercise {
  id: number;
  name: string;
  description: string;
  category: string;
}

After:

  1. Add to centralized types:
// app/types/index.ts
export interface Exercise extends BaseEntity {
  name: string;
  description: string;
  category?: string;
  // ...other fields
}
  1. Update components:
// ComponentA.tsx
import { Exercise } from '@/types';

// ComponentB.tsx
import { Exercise } from '@/types';
  1. If component needs extended version:
// ComponentB.tsx
import { Exercise } from '@/types';

interface ExtendedExercise extends Exercise {
  additionalField: string;
}

Summary

Following these architectural patterns ensures:

Type Safety - Centralized types reduce duplication and errors
Performance - CSS modules and proper bundling reduce bundle size
Maintainability - Clear organization makes code easier to understand and modify
Scalability - Patterns that work well as the codebase grows
Developer Experience - Consistent patterns reduce cognitive load

Questions or additions? Update this document as new patterns emerge or requirements change.