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
- Feature-Based Architecture - Organize code by business features, not technical layers
- Component Composition - Build complex UIs from simple, reusable components
- Type Safety First - Comprehensive TypeScript coverage with strict typing
- Performance Optimized - Code splitting, lazy loading, and efficient re-renders
- 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.tsxComponent 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;
}Navigation & Routing
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 interfaceThis 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/"Breadcrumb Generation
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
-
Plan the Feature
- Define types in
features/[feature]/types/ - Create API functions in
lib/
- Define types in
-
Build Components Bottom-Up
- Start with shared UI primitives
- Build feature-specific components
- Compose into views
-
Add Routing & Navigation
- Create pages in
app/ - Update navigation components
- Create pages in
-
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.