Skip to content

Latest commit

 

History

History
513 lines (397 loc) · 20.9 KB

File metadata and controls

513 lines (397 loc) · 20.9 KB

TV Guide Implementation Documentation

Overview

This document describes the implementation of the TV Program Guide component.

Architecture

File Structure

app/
├── components/TVGuide/
│   ├── desktop/
│   │   ├── TVGuide.tsx              Main desktop container
│   │   ├── TimelineWithScrollButtons.tsx Timeline wrapper with scroll buttons
│   │   ├── ProgramTrack.tsx         Channel track with program cards
│   │   └── CurrentTimeIndicator.tsx Red line indicator for current time
│   ├── mobile/
│   │   ├── TVGuideMobile.tsx        Main mobile container
│   │   ├── ProgramListMobile.tsx    Vertical program list
│   │   ├── ChannelRowMobile.tsx     Individual channel row in mobile list
│   │   ├── CurrentTimeIndicatorMobile.tsx
│   │   ├── CenteredTimeIndicator.tsx Shows time at viewport center
│   │   └── CenteredTimeContext.tsx  Context for centered time tracking
│   └── shared/
│       ├── timeline/
│       │   ├── TimelineConfig.ts     Timeline configuration interface
│       │   ├── TimelineContext.tsx  Context + all utility hooks
│       │   └── Timeline.tsx          Unified Timeline component
│       ├── DateNavigation.tsx       Shared date picker + Jump to Now button
│       ├── ProgramDetailDialog.tsx  Shared modal dialog for program details
│       ├── SkeletonLoader.tsx       Shared loading state skeletons
│       ├── ScrollContainerContext.tsx Shared context for scroll container (desktop only)
│       ├── ChannelStack.tsx         Unused component (logos integrated in TVGuide)
│       └── const.ts                 Layout constants
├── hooks/
│   └── useIsMobile.ts               Mobile detection hook (used in route.tsx)
├── lib/
│   ├── types.ts                     TypeScript interfaces
│   ├── testData.ts                  Test data generator with faker
│   └── utils.ts                     Utility functions (cn helper)
└── routes/_index/
    └── route.tsx                    Route with conditional rendering (desktop/mobile)

Implementation Details

0. Component Selection

Route-Level Decision:

  • app/routes/_index/route.tsx uses useIsMobile() hook to conditionally render components
  • Breakpoint: window.innerWidth <= 640px (Tailwind sm breakpoint)
  • Desktop: Renders TVGuide component from desktop/ directory
  • Mobile: Renders TVGuideMobile component from mobile/ directory
  • Both components share identical props interface: channels, programs, startTime, selectedDate, onDateChange
  • Selection happens at route level, not within components (clean separation of concerns)

1. Grid System Architecture

Implementation:

  • Minute-based grid: Grid columns calculated dynamically from time range via useGridColumns() hook (typically 24 * 60 - 1 = 1439 columns for 24 hours)
  • Grid gap: DESKTOP_TIMELINE_GRID_GAP = 7px per minute column (desktop), MOBILE_TIMELINE_GRID_GAP = 3px (mobile)
  • Timeline marks: Displayed at 30-minute intervals using eachMinuteOfInterval with step: 30
  • Program positioning: Calculated using useTimeToGridColumn() hook to determine grid column start/end
  • Rationale: Minute-based grid allows programs to start/end at any minute, providing precise positioning without rounding

2. Desktop Layout Structure

CSS Grid Layout:

gridTemplateRows: `auto auto`
gridTemplateColumns: `${CHANNEL_LOGO_WIDTH}px auto`

Component Hierarchy:

  1. Row 1 (Timeline): Sticky top, spans full width with left margin for channel logos
  2. Row 2 (Content):
    • Column 1: Channel logos (sticky left, z-20)
    • Column 2: Program tracks (z-10, overflow-hidden)

Sticky Positioning:

  • Timeline: sticky top-0 z-30
  • Channel logos: sticky left-0 z-20
  • Scroll buttons: sticky left-2 and sticky right-2 z-30
  • Current time indicator: sticky top-0 bottom-0 z-40

2a. Mobile Layout Structure

Layout Pattern:

  • Vertical split layout (timeline on top, program list below)
  • Two separate scroll containers (independent scrolling)
  • Fixed height container: h-[500px] with overflow-hidden

Component Hierarchy:

  1. Timeline Section (top, horizontal scroll):

    • TimelineMobileWrapper (internal to TVGuideMobile): Wraps unified Timeline with mobile padding and scroll tracking
    • CurrentTimeIndicatorMobile: Red line for current time
    • CenteredTimeIndicator: Shows time at viewport center
  2. Program List Section (bottom, vertical scroll):

    • ProgramListMobile: Vertical list of channels
    • Each channel rendered as ChannelRowMobile with programs

Key Differences from Desktop:

  • No channel logo column (logos in mobile list items)
  • List-based rendering instead of grid tracks
  • Separate scroll containers (timeline vs programs)
  • Centered time indicator for navigation reference
  • Grid gap: MOBILE_TIMELINE_GRID_GAP = 3px (vs 7px desktop)

3. Timeline Architecture

Unified Timeline Component:

  • Single Timeline component in shared/timeline/Timeline.tsx
  • Takes optional config prop, falls back to TimelineContext
  • Mark generation is internal (hardcoded 30-minute step, "HH:mm" format)
  • Calculates grid columns from time range: differenceInMinutes(end, start)
  • Renders timeline marks with proper grid positioning

Timeline Configuration:

  • TimelineConfig interface: { gridGap: number, start: Date, end: Date }
  • Helper function: createTimelineConfig(selectedDate, startTime, gridGap)
  • Desktop: gridGap = 7px, Mobile: gridGap = 3px

Timeline Context & Utilities:

  • TimelineContext exported directly (no Provider component)
  • All utility hooks in TimelineContext.tsx:
    • useTimeline() - Get config from context
    • useTimeToPosition() - Convert time to pixels
    • useTimeToGridColumn() - Convert time to grid column (1-based)
    • usePositionToTime() - Convert pixels to time
    • useGridColumns() - Get total grid columns
    • useTimelineWidth() - Get timeline width in pixels
  • Components use hooks for all time/position conversions

3a. Timeline Component (Desktop)

TimelineWithScrollButtons Wrapper:

  • Wraps Timeline component with scroll button logic
  • Forward/backward scroll buttons (3-hour increments: DESKTOP_SCROLL_INCREMENT = 3 * 60 * 7px = 1260px)
  • Buttons disabled at scroll boundaries (start/end)
  • Buttons positioned absolutely over timeline
  • Uses useScrollPosition hook for boundary detection

Scroll Behavior:

  • Uses scrollBy with behavior: "smooth" for animated scrolling
  • Scroll position tracked via useScrollPosition hook
  • Boundary detection with 1px tolerance
  • Buttons automatically disable/enable based on scroll position

3b. Timeline Component (Mobile)

TimelineMobileWrapper:

  • Wraps Timeline component with mobile-specific behavior
  • Applies 50% padding (left/right) for centered time indicator
  • Handles scroll tracking to update CenteredTimeContext
  • Uses usePositionToTime hook to convert scroll position to time
  • Independent scroll container (separate from program list)

Centered Time Indicator:

  • Shows which time is currently centered in viewport
  • Updates on scroll via CenteredTimeContext
  • Used for time-based navigation reference
  • Position calculated from scroll position and viewport center

4. Program Track & Cards (Desktop)

Grid Layout:

  • Each track uses CSS Grid matching timeline columns
  • Grid columns calculated via useGridColumns() hook
  • Grid gap from config: config.gridGap (7px for desktop)
  • Program cards positioned using gridColumn: ${gridColumnStart} / ${gridColumnEnd}
  • Cards filtered: only programs intersecting timeline range are rendered
  • Uses useTimeToGridColumn() hook for program positioning

Program Card Features:

  • Thumbnail: 160px width, object-cover
  • Content: Time range (HH:mm - HH:mm), title (line-clamp-2), genre tags
  • Hover Expansion: Uses Motion (motion/react) for smooth animations
  • Click Interaction: Opens program detail dialog

Hover Expansion Logic:

  • Expansion only when card is clipped or wider than visible area
  • Calculates optimal position to keep content visible
  • Handles edge cases:
    • Cards wider than viewport: centers with margins
    • Clipping on left: aligns left edge to visible area
    • Clipping on right: expands leftward to fit
    • No clipping: expands to minWidth (420px) if needed
  • Uses useMemo for position calculations based on card/container rects
  • Animation duration: 0.3s with linear easing

4a. Program List (Mobile)

List-Based Rendering:

  • Vertical list of channels (ProgramListMobile)
  • Each channel rendered as ChannelRowMobile component
  • Programs displayed in channel-specific sections
  • Scroll container independent from timeline scroll

Channel Row Structure:

  • Channel logo and name header
  • Programs listed vertically within channel section
  • Programs filtered by timeline range (same logic as desktop)
  • Click interaction opens program detail dialog (shared component)

5. Current Time Indicator

Desktop Implementation:

  • Red vertical line (bg-destructive) with triangle indicator at top
  • Position calculated using useTimeToGridColumn(currentTime) hook
  • Grid columns from useGridColumns() hook
  • Updates every 60 seconds via setInterval
  • Hidden when current time is outside timeline range
  • Uses Motion layout prop for smooth position updates
  • Positioned in grid column matching timeline structure

Mobile Implementation:

  • CurrentTimeIndicatorMobile: Similar red line indicator in timeline
  • Uses useTimeToGridColumn() and useGridColumns() hooks
  • Timeline width from useTimelineWidth() hook
  • CenteredTimeIndicator: Additional indicator showing time at viewport center
  • Centered time updates on scroll (not time-based)
  • Both indicators use same visual style as desktop

6. Date Navigation

Features:

  • Format: "DayName, DD.MM.YYYY" (e.g., "Today, 27.11.2025")
  • Previous/next day buttons with ChevronLeft/ChevronRight icons (lucide-react)
  • Calendar popover for date selection (Radix UI Popover + Calendar)
  • "Now" button: Jumps to today and centers current time in viewport

Jump to Now Logic:

  • Calculates target date (yesterday if current time < startTime)
  • Calculates current time position in pixels
  • Scrolls to center current time: scrollPosition = totalCurrentTimePosition - viewportWidth / 2
  • Uses startTransition for non-blocking scroll
  • Accounts for channel logo column width (116px) in position calculation

7. Program Detail Dialog

Implementation:

  • Uses Radix UI Dialog component
  • Mobile: Bottom sheet (max-sm:bottom-0 max-sm:rounded-t-2xl)
  • Desktop: Centered modal (max-w-2xl)
  • Displays: title, time range, thumbnail, genre tags, description
  • Dismissible: outside click, close button, ESC key
  • Dialog structure uses compound component pattern (Root, Trigger, Content)

8. Context APIs

TimelineContext (Shared):

  • Exported directly: TimelineContext (no Provider component)
  • Provides config: TimelineConfig containing { gridGap, start, end }
  • Config created via createTimelineConfig(selectedDate, startTime, gridGap)
  • Timeline spans 24 hours from start time
  • Used by: Timeline, ProgramTrack, CurrentTimeIndicator (both variants), all timeline utilities
  • Hook: useTimeline() returns TimelineConfig, throws error if used outside provider
  • All utility hooks consume TimelineContext internally:
    • useTimeToPosition() - Time → pixels conversion
    • useTimeToGridColumn() - Time → grid column conversion
    • usePositionToTime() - Pixels → time conversion
    • useGridColumns() - Get total grid columns
    • useTimelineWidth() - Get timeline width
  • Shared between desktop and mobile implementations
  • Components create config and provide via <TimelineContext.Provider value={{ config }}>

ScrollContainerContext (Desktop Only):

  • Provides shared scroll container ref
  • Used for:
    • Scroll position tracking
    • Programmatic scrolling (Jump to Now, scroll buttons)
    • Hover expansion calculations
  • Context provider wraps scroll container in TVGuide component
  • Not used in mobile (separate scroll containers)

CenteredTimeContext (Mobile Only):

  • Tracks which time is currently centered in viewport
  • Provides centeredTime Date and setCenteredTime function
  • Used by: CenteredTimeIndicator, TimelineMobileWrapper (in TVGuideMobile)
  • Context provider: CenteredTimeProvider
  • Hook: useCenteredTime() throws error if used outside provider
  • Updates on timeline scroll to reflect viewport center
  • Initial value: current time if within range, otherwise timeline start

9. Data Loading

Route Integration:

  • Uses React Router clientLoader for data fetching
  • URL parameter: ?date=yyyy-MM-dd for date selection
  • Simulated loading delay: 500ms
  • Suspense boundary with SkeletonLoader fallback
  • Generates 12 channels and 36 hours of programs

Test Data:

  • Uses @faker-js/faker for realistic data
  • Seeded based on date for reproducibility: faker.seed(dateSeed)
  • Programs: 15min-4hr duration, non-overlapping within channel
  • Includes gaps and overlaps between programs:
    • 30% consecutive (no gap)
    • 40% gap (5min-2hr gap)
    • 30% overlap (5-30min overlap)
  • Channel logos: Picsum photos with seeded URLs
  • Program thumbnails: Picsum photos with program ID seeds

10. Constants

// Shared constants
CHANNEL_LOGO_WIDTH = 116px
TRACK_HEIGHT = 106px
// Note: Grid columns calculated dynamically via useGridColumns() hook

// Desktop-specific
DESKTOP_TIMELINE_GRID_GAP = 7px (per minute)
DESKTOP_SCROLL_INCREMENT = 3 * 60 * 7 = 1260px (3 hours)

// Mobile-specific
MOBILE_TIMELINE_GRID_GAP = 3px (per minute)

Key Implementation Decisions

1. Minute-Based Grid vs 30-Minute Intervals

  • Decision: Use minute-based grid for precise positioning
  • Rationale: Allows programs to start/end at any minute, not just 30-minute boundaries
  • Trade-off: More grid columns (1439 vs ~48), but enables accurate positioning
  • Benefit: Programs can have exact start/end times without rounding

2. Motion for Animations

  • Decision: Use motion/react (Motion library) instead of CSS transitions
  • Rationale: More control over animation timing and layout animations
  • Usage: Program card hover expansion, current time indicator position updates
  • Package: motion@^12.23.24 (imported as motion from motion/react)

3. Context API for Shared State

  • Decision: Use React Context for scroll container, timeline config, and centered time
  • Rationale: Avoids prop drilling, enables components to access shared refs/state
  • Components: Timeline, ProgramTrack, CurrentTimeIndicator all use contexts
  • Pattern: Context exported directly, components provide via <TimelineContext.Provider>
  • Contexts: Three contexts total - TimelineContext (shared, provides config + hooks), ScrollContainerContext (desktop), CenteredTimeContext (mobile)
  • Utility Hooks: All timeline conversion utilities are hooks in TimelineContext.tsx
  • Timeline Component: Uses config prop primarily, falls back to context for flexibility

4. CSS Grid for Layout

  • Decision: Extensive use of CSS Grid for timeline and tracks
  • Rationale: Natural alignment, no manual pixel calculations for positioning
  • Benefits: Responsive, maintainable, precise alignment
  • Implementation: Grid columns match timeline structure exactly
  • Grid Columns: Calculated dynamically from time range via useGridColumns() hook
  • Consistency: All components use same grid structure via shared hooks

5. Program Filtering

  • Decision: Filter programs by timeline range before rendering
  • Rationale: Only render visible programs, improves performance
  • Implementation: usePositionInTrackGrid returns null if program outside range
  • Calculation: Uses differenceInMinutes to check if program intersects timeline

6. Channel Logo Integration

  • Decision: Integrate channel logos directly into main grid layout (desktop)
  • Rationale: Simplifies layout, ensures perfect alignment with tracks
  • Implementation: Logos in first column, sticky left positioning
  • Mobile: Logos integrated into ChannelRowMobile component headers
  • Note: shared/ChannelStack.tsx exists but is not used (logos integrated in TVGuide.tsx)

7. Separate Mobile Architecture

  • Decision: Fully separate mobile component (TVGuideMobile) vs responsive CSS
  • Rationale: Mobile UX requires fundamentally different layout (list vs grid, separate scrolls)
  • Trade-off: Code duplication, but enables optimized mobile experience
  • Benefits: Independent optimization, cleaner mobile code, better performance
  • Pattern: Route-level selection using useIsMobile() hook

8. Centralized Timeline Logic

  • Decision: Unified Timeline component with configuration-driven approach
  • Rationale: Eliminates duplication, centralizes timeline logic, enables shared utilities
  • Implementation:
    • Single Timeline component in shared/timeline/
    • All utility hooks in TimelineContext.tsx
    • Desktop/Mobile use wrappers for platform-specific behavior
  • Benefits:
    • Single source of truth for timeline calculations
    • Shared utilities for time/position conversions
    • Easy to maintain and extend
    • Type-safe with TypeScript
  • Pattern: Config prop first, context fallback for flexibility

Performance Considerations

Desktop:

  • Memoization: programsByChannel computed with useMemo
  • Program Filtering: Only programs intersecting timeline are rendered
  • Hover Calculations: useMemo for position calculations, only recalculates on hover state change
  • Scroll Tracking: Efficient boundary detection with event listeners
  • Time Updates: Current time indicator updates every 60 seconds (not every second)
  • Grid Rendering: CSS Grid handles layout efficiently without JavaScript calculations

Mobile:

  • Separate Scroll Containers: Timeline and program list scroll independently, reducing layout thrashing
  • List-Based Rendering: ProgramListMobile uses vertical list (more efficient than grid for vertical scrolling)
  • CenteredTimeIndicator: Updates only on scroll events (not time-based), reducing re-renders
  • Program Filtering: Same filtering logic as desktop (only visible programs rendered)
  • Memoization: programsByChannel computed with useMemo (same as desktop)

Browser Compatibility

  • Modern browsers with CSS Grid support
  • CSS sticky positioning required
  • Smooth scroll behavior supported
  • ES2020+ features (optional chaining, nullish coalescing)

Dependencies

Production

  • react@^19.1.1
  • react-router@^7.9.2
  • date-fns@^4.1.0
  • @faker-js/faker@^10.1.0
  • @radix-ui/react-dialog@^1.1.15
  • @radix-ui/react-popover@^1.1.15
  • motion@^12.23.24 (Motion library)
  • lucide-react@^0.553.0 (icons)
  • tailwindcss@^4.1.13
  • clsx@^2.1.1 & tailwind-merge@^3.3.1 (utility functions)

Development

  • typescript@^5.9.2
  • @react-router/dev@^7.9.2
  • vite@^7.1.7

Testing Notes

Desktop:

  • Test data generation is deterministic (seeded by date)
  • Programs span midnight boundaries correctly
  • Scroll boundaries work correctly
  • Hover expansion handles all edge cases
  • Current time indicator updates correctly
  • Date navigation updates URL and triggers data reload
  • Channel logos align perfectly with tracks
  • Timeline and tracks scroll together synchronously

Mobile:

  • Component selection works correctly (desktop vs mobile based on viewport width)
  • Timeline scrolls independently from program list
  • CenteredTimeIndicator updates correctly on scroll
  • CurrentTimeIndicatorMobile shows correct position
  • Program list renders correctly with channel grouping
  • Jump to Now works correctly in mobile layout
  • Date navigation updates URL and triggers data reload (same as desktop)

Code Quality

  • ✅ TypeScript strict mode enabled
  • ✅ No any types used
  • ✅ Proper error handling in context hooks
  • ✅ Memoization where appropriate
  • ✅ Event handlers properly cleaned up
  • ✅ Accessibility: ARIA labels on buttons
  • ✅ Semantic HTML structure
  • ✅ Shared props interface between desktop/mobile components
  • ✅ Type safety maintained across component variants

Future Enhancements

Potential improvements:

  1. Virtual Scrolling: For very large datasets (50+ channels, 2000+ programs)
  2. Keyboard Navigation: Enhanced keyboard support for accessibility
  3. Program Filtering: Filter by genre, channel, or time range
  4. Search: Search programs by title
  5. Favorites: Save favorite programs
  6. Program Reminders: Set reminders for upcoming programs
  7. Live Indicator: Visual indicator for currently airing programs
  8. Mobile Optimizations: Further optimize mobile scroll performance, consider intersection observer for program visibility
  9. Timeline Enhancements: Add configurable mark intervals, custom time formats, or timezone support