A React-based range slider component with advanced features including dual thumbs, vertical/horizontal orientation, fixed values, and editable numeric inputs.
This project demonstrates a modern React application architecture with a focus on component design, state management, and server-side data handling.
The application follows a three-layer architecture pattern for data fetching:
Server Components in Next.js fetch data directly by calling services. They:
- Run on the server during rendering
- Call service layer methods directly
- Handle errors and display appropriate UI (notFound, error boundaries)
- Pass data to client components as props
Located in app/, Server Components use async/await to fetch data before rendering.
Services contain business logic and orchestrate data operations.
Located in services/, each service corresponds to a domain entity (e.g., ExerciseService).
Repositories abstract data access.
- Provide a clean API for data operations
- Handle data fetching/mutations
- Can be easily swapped with real database implementations
Located in repositories/, repositories use a mockable database interface.
The database layer is designed to be mockable for development and testing:
- Uses in-memory data structures for mock implementation
- Can be replaced with actual database clients (PostgreSQL, MongoDB, etc.) without changing upper layers
Located in db/, the mock database simulates a real database.
The Range component is a dual-thumb slider with multiple features.
The primary component that orchestrates all functionality:
Key Responsibilities:
- State management (min/max values, editing state, dragging state)
- Value calculations and conversions (value ↔ percentage)
- Handling drag interactions via
useDraggablehook - Managing numeric input synchronization
- Applying business rules (allowPush, thumbGap, fixedValues)
Key Features:
- Controlled Component: Requires
minValueandmaxValueprops - Orientation: Supports horizontal and vertical layouts
- Fixed Values: Can snap to predefined values instead of using min/max/step
- Allow Push: Thumbs can push each other or stop at boundaries
- Thumb Gap: Visual separation when thumbs are too close
- Numeric Inputs: Optional editable inputs for precise value entry
- Format Labels: Custom formatting for displayed values (currency, percentages, etc.)
Props:
{
min?: number // Minimum value (auto-calculated with fixedValues)
max?: number // Maximum value (auto-calculated with fixedValues)
minValue: number // Current minimum value (controlled)
maxValue: number // Current maximum value (controlled)
onChange?: (min: number, max: number) => void
orientation?: "horizontal" | "vertical"
step?: number // Increment step
disabled?: boolean // Disable all interactions
className?: string
allowPush?: boolean // Allow thumbs to push each other
thumbGap?: number // Visual separation between thumbs
showInputs?: boolean // Show numeric inputs
disabledInputs?: boolean // Disable only inputs (slider still works)
fixedValues?: number[] // Array of allowed values
formatLabel?: (value: number) => string // Format display values
}Renders individual draggable thumb handles.
Responsibilities:
- Positioning based on percentage coordinates
- Visual feedback for drag state
- Touch and mouse event handling
- 2D positioning (supports both X and Y movement)
Props:
{
id: string // Unique identifier
percentageX: number // Horizontal position (0-100)
percentageY: number // Vertical position (0-100)
isDragging: boolean // Active drag state
onMouseDown?: (e: React.MouseEvent) => void
onTouchStart?: (e: React.TouchEvent) => void
}Renders the track and active range visualization.
Responsibilities:
- Displaying inactive track (background)
- Displaying active track (selected range)
- Adapting to horizontal/vertical orientation
Props:
{
minPercentage: number; // Start of active range (0-100)
maxPercentage: number; // End of active range (0-100)
orientation: "horizontal" | "vertical";
}Editable numeric input fields for precise value entry.
Responsibilities:
- Display formatted or raw values based on focus state
- Handle keyboard event (Enter to apply)
- Blur to apply changes
Features:
- Shows formatted value when not focused (e.g., "€250")
- Shows raw value when focused for editing (e.g., "250")
CSS Modules are used for component styling to maintain framework independence:
Range.module.css: Main component stylesRangeBar.module.css: Track and active range stylesThumb.module.css: Thumb handle styles
Framework Agnostic Design:
The Range component deliberately uses CSS Modules instead of utility frameworks (like Tailwind CSS).
Note: While the demo pages (/exercise/[id]) use Tailwind CSS for layout and styling, the core Range component and its subcomponents are completely framework-agnostic and can be used in any React application.
<Range
min={0}
max={100}
minValue={25}
maxValue={75}
onChange={(min, max) => console.log(min, max)}
/><Range
fixedValues={[1.99, 5.99, 10.99, 25.99, 50.99, 70.99]}
minValue={5.99}
maxValue={25.99}
onChange={(min, max) => console.log(min, max)}
formatLabel={(value) => `€${value.toFixed(2)}`}
showInputs
/><Range
min={0}
max={1000}
minValue={250}
maxValue={750}
onChange={(min, max) => console.log(min, max)}
orientation="vertical"
showInputs
step={10}
allowPush={false}
/>pnpm i
pnpm devpnpm test/app
/exercise/[id] # Exercise detail page (Server Component)
/test # Range component testing page
/components
/ui
/Range # Main Range component
/NumericInput # Shared input component
/services # Business logic layer
/repositories # Data access layer
/db # Mock database
/hooks # Custom React hooks
/types # TypeScript type definitions