← Back to Frontend Documentation Main Documentation

Frontend Component Architecture

Created: Tue 29 Jul 2025 07:32:15 CEST
Document Version: 1.0 - Initial component architecture and patterns
Security Classification: Internal Technical Documentation
Target Audience: Frontend Developers, UI/UX Designers, Technical Leads
Author: Paul Wisén

Overview

The Plings frontend follows a modular component architecture built with React 18 and TypeScript, emphasizing reusability, type safety, and consistent design patterns. This document outlines the component hierarchy, design patterns, and architectural decisions that guide frontend development.

Component Hierarchy

1. Application Structure

src/
├── components/           # Reusable UI components
│   ├── ui/              # Base design system components (shadcn/ui)
│   ├── layout/          # Layout and navigation components
│   ├── objects/         # Object-related components
│   ├── spatial/         # Spatial dashboard components
│   ├── auth/            # Authentication components
│   └── common/          # Common utility components
├── pages/               # Page-level components
├── contexts/            # React Context providers
├── hooks/               # Custom React hooks
└── lib/                 # Utilities and configurations

2. Component Categories

Base UI Components (components/ui/)

Built on shadcn/ui, these provide the foundational design system:

// Example: Button component with variants
import { Button } from '@/components/ui/button';

<Button variant="default" size="lg">Primary Action</Button>
<Button variant="outline" size="sm">Secondary Action</Button>
<Button variant="ghost" size="icon">
  <IconTrash />
</Button>

Key Components:

Layout Components (components/layout/)

Handle application structure and navigation:

// Main layout with responsive navigation
export const AppLayout: React.FC<{ children: ReactNode }> = ({ children }) => {
  return (
    <div className="min-h-screen bg-gray-50">
      <Header />
      <div className="flex">
        <Sidebar className="hidden md:flex" />
        <main className="flex-1 p-6">
          {children}
        </main>
      </div>
    </div>
  );
};

Key Components:

Object Components (components/objects/)

Handle object-specific functionality:

// Object card with actions
export const ObjectCard: React.FC<ObjectCardProps> = ({ object, onEdit, onMove }) => {
  return (
    <Card className="hover:shadow-md transition-shadow">
      <CardContent className="p-4">
        <div className="flex items-start justify-between">
          <div className="flex-1">
            <h3 className="font-semibold text-gray-900">{object.name}</h3>
            <p className="text-sm text-gray-600">{object.description}</p>
            <Badge variant="secondary">{object.objectClass.name}</Badge>
          </div>
          <ObjectMenu object={object} onEdit={onEdit} onMove={onMove} />
        </div>
        {object.mainImageUrl && (
          <img 
            src={object.mainImageUrl} 
            alt={object.name}
            className="w-full h-32 object-cover rounded mt-3"
          />
        )}
      </CardContent>
    </Card>
  );
};

Key Components:

Spatial Components (components/spatial/)

Handle spatial relationships and drag-and-drop:

// Drag-and-drop container
export const SpatialContainer: React.FC<SpatialContainerProps> = ({ 
  container, 
  objects, 
  onDrop 
}) => {
  const [{ isOver }, drop] = useDrop({
    accept: 'object',
    drop: (item: { id: string }) => onDrop(item.id, container.id),
    collect: (monitor) => ({
      isOver: monitor.isOver(),
    }),
  });

  return (
    <div 
      ref={drop}
      className={cn(
        "min-h-32 p-4 border-2 border-dashed rounded-lg",
        isOver ? "border-blue-500 bg-blue-50" : "border-gray-300"
      )}
    >
      <h3 className="font-medium mb-2">{container.name}</h3>
      <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
        {objects.map(obj => (
          <DraggableObject key={obj.id} object={obj} />
        ))}
      </div>
    </div>
  );
};

Key Components:

Design Patterns

1. Component Composition Pattern

Components are built using composition rather than complex inheritance:

// Base Modal component
export const Modal: React.FC<ModalProps> = ({ children, ...props }) => (
  <Dialog {...props}>
    <DialogContent>
      {children}
    </DialogContent>
  </Dialog>
);

// Specialized Create Object Modal
export const CreateObjectModal: React.FC = () => (
  <Modal>
    <ModalHeader>
      <ModalTitle>Create New Object</ModalTitle>
    </ModalHeader>
    <ObjectForm onSubmit={handleCreate} />
    <ModalFooter>
      <Button variant="outline" onClick={onCancel}>Cancel</Button>
      <Button type="submit">Create Object</Button>
    </ModalFooter>
  </Modal>
);

2. Render Props Pattern

For complex state management and reusable logic:

// Data fetching with render props
export const ObjectProvider: React.FC<ObjectProviderProps> = ({ 
  children, 
  objectId 
}) => {
  const { data: object, loading, error } = useQuery(GET_OBJECT, {
    variables: { id: objectId }
  });

  return children({ object, loading, error });
};

// Usage
<ObjectProvider objectId={selectedId}>
  {({ object, loading, error }) => (
    <>
      {loading && <LoadingSpinner />}
      {error && <ErrorMessage error={error} />}
      {object && <ObjectDetail object={object} />}
    </>
  )}
</ObjectProvider>

3. Hook-Based State Management

Custom hooks encapsulate component logic:

// Custom hook for object operations
export const useObject = (objectId: string) => {
  const [object, setObject] = useState<ObjectInstance | null>(null);
  const [loading, setLoading] = useState(true);
  
  const { data } = useQuery(GET_OBJECT, {
    variables: { id: objectId },
    onCompleted: (data) => {
      setObject(data.object);
      setLoading(false);
    }
  });

  const [updateObject] = useMutation(UPDATE_OBJECT, {
    onCompleted: (data) => {
      setObject(data.updateObject);
      toast.success('Object updated successfully');
    }
  });

  const handleUpdate = (updates: Partial<ObjectInstance>) => {
    updateObject({
      variables: { id: objectId, input: updates }
    });
  };

  return {
    object,
    loading,
    updateObject: handleUpdate,
    refetch: () => { /* refetch logic */ }
  };
};

State Management Architecture

1. Context Providers

// Authentication Context
export const AuthContext = createContext<AuthContextType | null>(null);

export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<User | null>(null);
  const [session, setSession] = useState<Session | null>(null);

  useEffect(() => {
    // Supabase auth state listener
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setSession(session);
        setUser(session?.user ?? null);
      }
    );

    return () => subscription.unsubscribe();
  }, []);

  const signOut = async () => {
    await supabase.auth.signOut();
    setUser(null);
    setSession(null);
  };

  return (
    <AuthContext.Provider value=>
      {children}
    </AuthContext.Provider>
  );
};

2. Apollo Client Integration

// GraphQL state management
export const useObjects = (organizationId: string) => {
  const { data, loading, error, refetch } = useQuery(GET_MY_OBJECTS, {
    variables: { organizationId },
    fetchPolicy: 'cache-first',
    errorPolicy: 'all'
  });

  const [createObject] = useMutation(CREATE_OBJECT, {
    update: (cache, { data }) => {
      if (data?.createObject.success) {
        // Update Apollo cache
        const existing = cache.readQuery({
          query: GET_MY_OBJECTS,
          variables: { organizationId }
        });
        
        if (existing) {
          cache.writeQuery({
            query: GET_MY_OBJECTS,
            variables: { organizationId },
            data: {
              myObjects: [...existing.myObjects, data.createObject.object]
            }
          });
        }
      }
    }
  });

  return {
    objects: data?.myObjects || [],
    loading,
    error,
    createObject,
    refetch
  };
};

Form Handling Patterns

1. React Hook Form Integration

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Form validation schema
const objectSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  description: z.string().optional(),
  classId: z.string().min(1, 'Object class is required'),
  images: z.array(z.string()).optional()
});

type ObjectFormData = z.infer<typeof objectSchema>;

export const ObjectForm: React.FC<ObjectFormProps> = ({ onSubmit, initialData }) => {
  const form = useForm<ObjectFormData>({
    resolver: zodResolver(objectSchema),
    defaultValues: initialData
  });

  const handleSubmit = (data: ObjectFormData) => {
    onSubmit(data);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Object Name</FormLabel>
              <FormControl>
                <Input placeholder="Enter object name" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="description"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Description</FormLabel>
              <FormControl>
                <Textarea placeholder="Enter description" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? 'Creating...' : 'Create Object'}
        </Button>
      </form>
    </Form>
  );
};

Error Boundary Pattern

// Global error boundary
export class ErrorBoundary extends React.Component<
  { children: ReactNode; fallback?: ComponentType<{ error: Error }> },
  { hasError: boolean; error: Error | null }
> {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Component error:', error, errorInfo);
    // Log to monitoring service
  }

  render() {
    if (this.state.hasError) {
      const FallbackComponent = this.props.fallback || DefaultErrorFallback;
      return <FallbackComponent error={this.state.error!} />;
    }

    return this.props.children;
  }
}

// Usage
<ErrorBoundary fallback={ObjectErrorFallback}>
  <ObjectList />
</ErrorBoundary>

Performance Optimization Patterns

1. Component Memoization

// Memoized object card for performance
export const ObjectCard = React.memo<ObjectCardProps>(({ 
  object, 
  onEdit, 
  onMove 
}) => {
  // Component implementation
}, (prevProps, nextProps) => {
  // Custom comparison
  return (
    prevProps.object.id === nextProps.object.id &&
    prevProps.object.updatedAt === nextProps.object.updatedAt
  );
});

2. Virtual Scrolling for Large Lists

import { FixedSizeList as List } from 'react-window';

export const ObjectVirtualList: React.FC<{ objects: ObjectInstance[] }> = ({ 
  objects 
}) => {
  const Row = ({ index, style }: { index: number; style: CSSProperties }) => (
    <div style={style}>
      <ObjectCard object={objects[index]} />
    </div>
  );

  return (
    <List
      height={600}
      itemCount={objects.length}
      itemSize={120}
      itemData={objects}
    >
      {Row}
    </List>
  );
};

Testing Patterns

1. Component Testing

import { render, screen, fireEvent } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { ObjectCard } from '../ObjectCard';

const mockObject = {
  id: '1',
  name: 'Test Object',
  description: 'Test Description',
  objectClass: { name: 'Test Class' }
};

describe('ObjectCard', () => {
  it('renders object information correctly', () => {
    render(
      <MockedProvider>
        <ObjectCard object={mockObject} />
      </MockedProvider>
    );

    expect(screen.getByText('Test Object')).toBeInTheDocument();
    expect(screen.getByText('Test Description')).toBeInTheDocument();
    expect(screen.getByText('Test Class')).toBeInTheDocument();
  });

  it('calls onEdit when edit button is clicked', () => {
    const onEdit = jest.fn();
    
    render(
      <MockedProvider>
        <ObjectCard object={mockObject} onEdit={onEdit} />
      </MockedProvider>
    );

    fireEvent.click(screen.getByRole('button', { name: /edit/i }));
    expect(onEdit).toHaveBeenCalledWith(mockObject);
  });
});

TypeScript Integration

1. Component Props Types

// Base component props with common patterns
interface BaseComponentProps {
  className?: string;
  children?: ReactNode;
}

// Object-related component props
interface ObjectCardProps extends BaseComponentProps {
  object: ObjectInstance;
  onEdit?: (object: ObjectInstance) => void;
  onMove?: (object: ObjectInstance) => void;
  onDelete?: (objectId: string) => void;
  variant?: 'default' | 'compact' | 'detailed';
}

// Form component props with generic data
interface FormProps<T> extends BaseComponentProps {
  initialData?: Partial<T>;
  onSubmit: (data: T) => void | Promise<void>;
  onCancel?: () => void;
  disabled?: boolean;
}

2. Custom Hook Types

// Hook return types for consistency
interface UseObjectReturn {
  object: ObjectInstance | null;
  loading: boolean;
  error: Error | null;
  updateObject: (updates: Partial<ObjectInstance>) => Promise<void>;
  deleteObject: () => Promise<void>;
  refetch: () => Promise<void>;
}

interface UseObjectsReturn {
  objects: ObjectInstance[];
  loading: boolean;
  error: Error | null;
  createObject: (data: CreateObjectInput) => Promise<ObjectInstance>;
  refetch: () => Promise<void>;
}

Accessibility Guidelines

1. ARIA Labels and Roles

// Accessible object card
export const ObjectCard: React.FC<ObjectCardProps> = ({ object }) => {
  return (
    <Card 
      role="article"
      aria-labelledby={`object-${object.id}-title`}
      className="focus-within:ring-2 focus-within:ring-blue-500"
    >
      <CardContent>
        <h3 
          id={`object-${object.id}-title`}
          className="font-semibold"
        >
          {object.name}
        </h3>
        <p aria-describedby={`object-${object.id}-description`}>
          {object.description}
        </p>
        <Button
          aria-label={`Edit ${object.name}`}
          onClick={() => onEdit(object)}
        >
          <IconEdit aria-hidden="true" />
          Edit
        </Button>
      </CardContent>
    </Card>
  );
};

2. Keyboard Navigation

// Keyboard-accessible spatial dashboard
export const SpatialDashboard: React.FC = () => {
  const handleKeyDown = (event: KeyboardEvent) => {
    switch (event.key) {
      case 'ArrowRight':
        focusNextObject();
        break;
      case 'ArrowLeft':
        focusPreviousObject();
        break;
      case 'Enter':
      case ' ':
        selectCurrentObject();
        break;
    }
  };

  return (
    <div 
      role="application"
      aria-label="Spatial object organization"
      onKeyDown={handleKeyDown}
      tabIndex={0}
    >
      {/* Dashboard content */}
    </div>
  );
};

Mobile Optimization

1. Responsive Components

// Mobile-optimized object list
export const ObjectList: React.FC = () => {
  const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
  const isMobile = useMediaQuery('(max-width: 768px)');

  useEffect(() => {
    if (isMobile) {
      setViewMode('list');
    }
  }, [isMobile]);

  return (
    <div className="space-y-4">
      {!isMobile && (
        <ViewModeToggle value={viewMode} onChange={setViewMode} />
      )}
      <div className={cn(
        viewMode === 'grid' 
          ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
          : 'space-y-2'
      )}>
        {objects.map(object => (
          <ObjectCard 
            key={object.id} 
            object={object} 
            variant={viewMode === 'list' ? 'compact' : 'default'}
          />
        ))}
      </div>
    </div>
  );
};

2. Touch-Friendly Interactions

// Touch-optimized drag and drop
export const TouchDraggableObject: React.FC<{ object: ObjectInstance }> = ({ 
  object 
}) => {
  const [isDragging, setIsDragging] = useState(false);
  
  const handleTouchStart = (event: TouchEvent) => {
    setIsDragging(true);
    // Add visual feedback
  };

  const handleTouchEnd = (event: TouchEvent) => {
    setIsDragging(false);
    // Handle drop logic
  };

  return (
    <div
      className={cn(
        "touch-none select-none p-4 min-h-[44px]", // 44px minimum touch target
        isDragging && "opacity-50 scale-95"
      )}
      onTouchStart={handleTouchStart}
      onTouchEnd={handleTouchEnd}
    >
      <ObjectCard object={object} />
    </div>
  );
};