| ← 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:
Button- All button variants and statesInput- Form inputs with validation statesCard- Content containers with consistent spacingDialog- Modal dialogs and confirmationsSelect- Dropdown selections with searchBadge- Status indicators and tagsAvatar- User profile images and initials
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:
AppLayout- Main application wrapperHeader- Top navigation and user menuSidebar- Main navigation menuBreadcrumbs- Navigation breadcrumb trailPageHeader- Consistent page title structure
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:
ObjectCard- Object display in grid/list viewsObjectDetail- Comprehensive object informationObjectForm- Object creation and editingObjectMenu- Action menu for objectsObjectSearch- Object search and filteringObjectGallery- Image gallery for objects
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:
SpatialDashboard- Main spatial organization interfaceSpatialContainer- Drop zone for objectsDraggableObject- Objects that can be movedSpatialBreadcrumb- Location navigationContainerSelector- Container picker dialog
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>
);
};
Related Documentation
- Development Guidelines - Frontend coding standards
- GraphQL Integration - Apollo Client patterns
- State Management - Application state architecture
- Design System - UI component specifications
- Testing Guidelines - Component testing patterns