Train5D Frontend Architecture
Last Updated: December 3, 2025
Version: 2.0
Table of Contents
- Overview
- Type Organization
- Style Organization
- Import Conventions
- Component Structure
- Best Practices
- 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 Safety: Leverage TypeScript’s type system with centralized, reusable type definitions
- Style Consistency: Use CSS modules for reusable styles; inline styles only for dynamic values
- Code Reusability: Share types, components, and utilities across the application
- Performance: Minimize bundle size through consolidation and tree-shaking
- Maintainability: Clear organization patterns that scale with the codebase
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
- Avoid
any- Use specific types orunknownif type is truly unknown - Use union types for restricted values:
type Status = 'loading' | 'success' | 'error' - Extend base types rather than duplicating:
interface MuscleGroup extends BaseEntity - Use optional chaining for potentially undefined
values:
exercise.category?.name - 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
- Memoize expensive computations:
const filteredExercises = useMemo(() =>
exercises.filter(e => e.category === selectedCategory),
[exercises, selectedCategory]
);- Use CSS modules to reduce inline style object creation
- Lazy load heavy components:
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>
});Maintainability
- Extract complex logic into utility functions
- Keep components focused - single responsibility
- Document complex types with JSDoc comments
- Use meaningful names - avoid abbreviations
- Consistent naming patterns:
- Components: PascalCase (
ExerciseCard) - Functions: camelCase (
getDifficulty) - CSS classes: kebab-case or camelCase (
card-title,cardTitle) - Constants: UPPER_SNAKE_CASE (
MAX_ITEMS)
- Components: PascalCase (
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:
- 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;
}- 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:
- Add to centralized types:
// app/types/index.ts
export interface Exercise extends BaseEntity {
name: string;
description: string;
category?: string;
// ...other fields
}- Update components:
// ComponentA.tsx
import { Exercise } from '@/types';
// ComponentB.tsx
import { Exercise } from '@/types';- 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.