Skip to Content
DevelopmentFrontend Architecture

Frontend Architecture

This document explains the frontend architecture of Opndrive, including design patterns, component organization, and development workflows.

Architecture Overview

Opndrive’s frontend is built with Next.js 15 using the App Router, following enterprise-grade patterns inspired by applications like Google Drive, Dropbox, and Notion.

Core Principles

  1. Feature-Based Architecture - Organize code by business features, not technical layers
  2. Component Composition - Build complex UIs from simple, reusable components
  3. Type Safety First - Comprehensive TypeScript coverage with strict typing
  4. Performance Optimized - Code splitting, lazy loading, and efficient re-renders
  5. Accessibility Ready - WCAG compliant components and keyboard navigation

Enterprise Patterns

1. Feature-Based Organization

Instead of organizing by technical layers (components, hooks, utils), we organize by business features:

❌ Technical Layer Organization: src/ ├── components/ ├── hooks/ ├── utils/ └── types/ ✅ Feature-Based Organization: src/ ├── features/ │ ├── dashboard/ │ ├── file-management/ │ └── user-profile/ ├── shared/ └── app/

2. Component Architecture Layers

Component Organization

Dashboard Feature Structure

features/dashboard/components/ ├── layout/ # Layout Infrastructure │ ├── navbar/ # Top navigation bar │ │ ├── dashboard-navbar.tsx │ │ ├── navbar-search.tsx │ │ └── navbar-profile.tsx │ ├── sidebar/ # Side navigation │ │ ├── dashboard-sidebar.tsx │ │ ├── sidebar-nav-item.tsx │ │ └── sidebar-create-button.tsx │ └── breadcrumb/ # Navigation breadcrumbs │ └── enhanced-folder-breadcrumb.tsx # Browse trail, built on shared/ui/breadcrumb ├── ui/ # Business UI Components │ ├── items/ # File/Folder display components │ │ ├── file-item-grid.tsx # Grid view for files │ │ ├── file-item-list.tsx # List view for files │ │ ├── file-thumbnail.tsx # File preview thumbnails │ │ └── folder-item.tsx # Folder display component │ ├── menus/ # Context menus and actions │ │ ├── file-overflow-menu.tsx │ │ ├── overflow-menu.tsx │ │ └── create-menu.tsx │ ├── details/ # Information panels │ │ ├── details-sidebar.tsx │ │ └── view-details.tsx │ └── skeletons/ # Loading states │ ├── dashboard-skeleton.tsx │ ├── file-skeleton.tsx │ └── folder-skeleton.tsx └── views/ # Page-Level Views ├── home/ # Dashboard home view │ ├── drive-hero.tsx # Hero section with search │ ├── suggested-files.tsx # File recommendations │ └── suggested-folders.tsx # Folder recommendations └── search/ # Search interface ├── search-bar.tsx ├── filter-bar.tsx └── search-results.tsx

Component Hierarchy Philosophy

Layout Components (layout/)

  • Handle page structure and navigation
  • Manage global state and user interactions
  • Provide consistent user experience across pages

UI Components (ui/)

  • Encapsulate specific business logic
  • Handle data display and user interactions
  • Reusable within the same feature

View Components (views/)

  • Compose multiple components into full page views
  • Handle page-level state management
  • Connect to data sources and APIs

Technical Implementation

State Management Strategy

Opndrive does not use a data-fetching library like React Query or SWR. State is split across three real mechanisms:

// 1. URL State (Next.js Router) const searchParams = useSearchParams(); // 2. Local State (React hooks) const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()); // 3. Global State (Zustand stores + React Context) // Zustand stores live under features/*/stores (e.g. dashboard, upload) const files = useDashboardStore((s) => s.files); // Context providers handle cross-cutting concerns like auth and theme const { theme, setTheme } = useTheme();

Auth state (context/auth-context.tsx) and drive/folder state (context/data-context.tsx) are the two Context providers most components depend on. See State Management for the full picture.

Component Patterns

1. Compound Components

// Dashboard composition <Dashboard> <Dashboard.Navbar /> <Dashboard.Sidebar /> <Dashboard.Content> <Dashboard.FileGrid files={files} /> </Dashboard.Content> </Dashboard>

2. Render Props & Children Functions

<FileList> {({ files, loading }) => ( loading ? <FileSkeleton /> : <FileItems files={files} /> )} </FileList>

3. Hook-based Logic

// Custom hooks for complex logic function useFileOperations() { const uploadFile = useCallback(...) const deleteFile = useCallback(...) const shareFile = useCallback(...) return { uploadFile, deleteFile, shareFile } }

Type System Architecture

// Base types from S3 SDK import { _Object, CommonPrefix } from '@aws-sdk/client-s3'; // Extended domain types export interface FileItem extends _Object { id: string; name: string; extension: string; size: { value: number; unit: DataUnits }; // ... other properties } // Component prop types export interface FileItemGridProps { file: FileItem; onAction?: (action: string, file: FileItem) => void; className?: string; }

App Router Structure

Opndrive uses Next.js App Router with static routes:

app/ ├── dashboard/ │ ├── page.tsx # Dashboard home │ ├── layout.tsx # Dashboard layout │ └── browse/ │ └── page.tsx # File browsing interface

This handles routes like:

  • /dashboard → Dashboard home page
  • /dashboard/browse → File and folder browsing interface

S3 Integration Pattern

// URL query parameters → S3 Prefix mapping const getS3Prefix = (path?: string) => { return path ? `${path}/` : ''; }; // /dashboard/browse?path=folder/subfolder → "folder/subfolder/"

Callers turn a prefix into crumbs and hand them to the shared <Breadcrumb /> (shared/components/ui/breadcrumb.tsx). A crumb navigates through an href or through a callback, so the same component serves the browse trail (which pushes a route) and the search trail (which only moves the store’s location):

const segments = prefixToPathSegments(prefix); const items: BreadcrumbItem[] = [ { name: 'My Drive', onSelect: () => navigate([]) }, ...segments.map((segment, index) => ({ name: segment, onSelect: () => navigate(segments.slice(0, index + 1)), })), ]; <Breadcrumb items={items} />;

The component owns the two rules that keep a deep path on one line: a name over maxLabelLength (20) is truncated and carries the full name in a tooltip, and a trail over maxVisibleItems (5) collapses its middle into an overflow menu that still reaches every folder it hid. It is deliberately not a scroll container - the menu is absolutely positioned, so an overflow-x: auto ancestor would clip it.

Styling Architecture

Design System Layers

1. Design Tokens (Tailwind CSS variables) 2. Component Primitives (shadcn/ui) 3. Feature Components (business-specific styling) 4. Page Layouts (composition and spacing)

Theme System

// CSS Custom Properties :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; // ... other tokens } [data-theme="dark"] { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; // ... dark mode overrides }

Component Styling Patterns

// 1. Tailwind utility classes <div className="flex items-center gap-2 p-4 rounded-lg border"> // 2. CSS Variables for theming <div className="bg-background text-foreground"> // 3. Conditional styling <div className={cn( "base-styles", variant === "primary" && "primary-styles", className )}>

Performance Optimizations

Code Splitting Strategies

// 1. Route-based splitting (automatic with App Router) app/dashboard/page.tsx → dashboard.chunk.js // 2. Component-based splitting const FileViewer = lazy(() => import('./FileViewer')) // 3. Feature-based splitting const { DashboardComponents } = await import('@/features/dashboard')

Image Optimization

// Next.js Image component with optimization <Image src={thumbnail} alt={fileName} width={200} height={150} placeholder="blur" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw" />

Data Flow Architecture

Unidirectional Data Flow

API/S3 → React Query → Components → User Actions → API/S3 ↑ ↓ └─────────────── Optimistic Updates ─────────────────┘

Error Boundary Strategy

// Feature-level error boundaries <ErrorBoundary fallback={<DashboardError />}> <DashboardFeature /> </ErrorBoundary> // Component-level error handling function FileList() { const { data, error, isLoading } = useFiles() if (error) return <FileListError error={error} /> if (isLoading) return <FileListSkeleton /> return <FileItems files={data} /> }

Testing Strategy

Testing runs on Vitest  plus React Testing Library. There is no Jest and no end-to-end suite (Playwright or otherwise) in this repository today - test coverage is intentionally thin and growing, not a pyramid to model your own tests after yet. See Testing for the current state and how to add to it.

Component Testing Pattern

import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; describe('FileItem', () => { it('displays the file name', () => { render(<FileItem file={{ name: 'document.pdf', size: 1024 }} />); expect(screen.getByText('document.pdf')).toBeInTheDocument(); }); });

Development Workflow

Feature Development Process

  1. Plan the Feature

    • Define types in features/[feature]/types/
    • Create API functions in lib/
  2. Build Components Bottom-Up

    • Start with shared UI primitives
    • Build feature-specific components
    • Compose into views
  3. Add Routing & Navigation

    • Create pages in app/
    • Update navigation components
  4. Test & Polish

    • Add unit tests for logic
    • Integration tests for components
    • E2E tests for critical flows

Code Review Guidelines

  • Architecture: Does it follow feature-based organization?
  • Types: Are all props and functions properly typed?
  • Performance: Are there unnecessary re-renders or large bundles?
  • Accessibility: Is the component keyboard navigable and screen reader friendly?
  • Testing: Are the critical paths covered by tests?

Best Practices

Component Design

// ✅ Good: Focused, single responsibility function FileItem({ file, onAction }: FileItemProps) { return ( <div className="file-item"> <FileIcon type={file.extension} /> <span>{file.name}</span> <FileActions onAction={onAction} /> </div> ) } // ❌ Avoid: Too many responsibilities function FileItemWithEverything({ file, folder, user, settings }) { // Handles files, folders, user data, settings... }

State Management

// ✅ Good: Local state for UI concerns const [isMenuOpen, setIsMenuOpen] = useState(false); // ✅ Good: Server state for data const { data: files } = useQuery(['files', folderId], fetchFiles); // ❌ Avoid: Global state for everything const { files, setFiles, isMenuOpen, setIsMenuOpen } = useGlobalState();

Import Organization

// ✅ Good: Grouped and aliased imports import { useState, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Button } from '@/shared/components/ui/button'; import { FileItem } from '@/features/dashboard/types/file'; import { useFiles } from '@/lib/api-client'; import { cn } from './utils';

This architecture scales from small features to enterprise applications while maintaining code quality and developer experience.

Last updated on