React Masterclass: Building an Interactive Calendar View
Tags: react, coding, programming, webdev
Published: 2023-09-16 12:57:45

✨ [**Watch on YouTube**](https://youtu.be/2rRChK47Z0M)  |  🐙 [GitHub](https://github.com/radzionc/reactkit)

In this post, we'll conduct a React masterclass, during which I'll share how I've created this attractive timeline, alternatively known as a calendar view, to display work sessions overview. While the code for [Increaser](https://increaser.org) sits in a private repository, you can locate all the reusable components, hooks, and utilities featured in this article at the [ReactKit repository](https://github.com/radzionc/reactkit). I trust you'll find lots of useful content in this masterclass, particularly when it comes to absolute positioning and time management.

![Timeline from Increaser](https://cdn-images-1.medium.com/proxy/1*K5BiQSa2StSR6YzCBvxItA.jpeg)

## DayOverviewProvider

Our component is constructed from four principal sections:

- Navigation between days of the week
- Summary of work sessions with a projects breakdown
- A timeline featuring blocks of work, which also presents the current time, and shows the amount of a workday that remains
- Lastly, a flow to add a work session, but we won't be covering this feature in this video.

```tsx
import styled from "styled-components"
import { Panel } from "@increaser/ui/ui/Panel/Panel"
import { AmountOverview } from "./AmountOverview"
import { DayTimeline } from "./DayTimeline"
import { AddSession } from "./AddSession"
import { horizontalPaddingInPx } from "./config"
import { WeekNavigation } from "./WeekNavigation"
import { DayOverviewProvider } from "./DayOverviewProvider"

const Container = styled(Panel)`
  height: 100%;
`

export const DayOverview = () => {
  return (
    <DayOverviewProvider>
      <Container padding={horizontalPaddingInPx} withSections kind="secondary">
        <WeekNavigation />
        <AmountOverview />
        <DayTimeline />
        <AddSession />
      </Container>
    </DayOverviewProvider>
  )
}
```

We'll need to reuse some state in multiple components within `DayOverview`, so let's create a provider using React Context.

`DayOverviewProvider` will effectively deliver:

- Sets for the current day
- Current time
- Start and end times of the timeline
- Start of the current day
- End of the workday
- A function to change the current day

```tsx
import { Set } from "@increaser/entities/User"
import { ComponentWithChildrenProps } from "@increaser/ui/props"
import { createContextHook } from "@increaser/ui/state/createContextHook"
import { useRhythmicRerender } from "@increaser/ui/hooks/useRhythmicRerender"
import { getLastItem } from "@increaser/utils/array/getLastItem"
import { MS_IN_MIN } from "@increaser/utils/time"
import { startOfHour, endOfHour, isToday } from "date-fns"
import { useFocus } from "focus/hooks/useFocus"
import { createContext, useEffect, useMemo, useState } from "react"
import { startOfDay } from "date-fns"
import { useAssertUserState } from "user/state/UserStateContext"
import { getDaySets } from "sets/helpers/getDaySets"
import { useStartOfDay } from "@increaser/ui/hooks/useStartOfDay"

interface DayOverviewContextState {
  sets: Set[]
  currentTime: number

  timelineStartsAt: number
  timelineEndsAt: number

  dayStartedAt: number
  workdayEndsAt: number
  setCurrentDay: (timestamp: number) => void
}

const DayOverviewContext = createContext<DayOverviewContextState | undefined>(
  undefined
)

export const useDayOverview = createContextHook(
  DayOverviewContext,
  "DayOverview"
)

export const DayOverviewProvider = ({
  children,
}: ComponentWithChildrenProps) => {
  const currentTime = useRhythmicRerender()
  const todayStartedAt = useStartOfDay()
  const [currentDay, setCurrentDay] = useState(todayStartedAt)

  const dayStartedAt = startOfDay(currentDay).getTime()

  const { currentSet } = useFocus()
  useEffect(() => {
    if (currentSet && dayStartedAt !== todayStartedAt) {
      setCurrentDay(todayStartedAt)
    }
  }, [currentSet, dayStartedAt, todayStartedAt])

  const { sets: allSets } = useAssertUserState()

  const sets = useMemo(() => {
    const result = getDaySets(allSets, dayStartedAt)
    if (currentSet && isToday(dayStartedAt)) {
      result.push({
        start: currentSet.startedAt,
        end: currentTime,
        projectId: currentSet.projectId,
      })
    }

    return result
  }, [allSets, currentSet, currentTime, dayStartedAt])

  const { goalToStartWorkAt, goalToFinishWorkBy } = useAssertUserState()

  const workdayEndsAt = dayStartedAt + goalToFinishWorkBy * MS_IN_MIN

  const timelineStartsAt = useMemo(() => {
    if (sets.length) {
      return startOfHour(sets[0].start).getTime()
    }

    const workdayStartsAt = dayStartedAt + goalToStartWorkAt * MS_IN_MIN

    if (currentTime < workdayStartsAt) {
      return startOfHour(currentTime).getTime()
    }

    return workdayStartsAt
  }, [currentTime, dayStartedAt, goalToStartWorkAt, sets])

  const timelineEndsAt = useMemo(() => {
    if (!sets.length) {
      return workdayEndsAt
    }
    const lastSetEnd = getLastItem(sets).end
    if (workdayEndsAt > lastSetEnd) {
      return workdayEndsAt
    }

    return endOfHour(lastSetEnd).getTime()
  }, [sets, workdayEndsAt])

  return (
    <DayOverviewContext.Provider
      value={{
        sets,
        currentTime,
        timelineStartsAt,
        timelineEndsAt,
        workdayEndsAt,
        dayStartedAt,
        setCurrentDay,
      }}
    >
      {children}
    </DayOverviewContext.Provider>
  )
}
```

We will interact with the context using the `useDayOverview` hook, which will generate an error if we attempt to utilize it outside of the provider.

```tsx
import { Context as ReactContext, useContext } from "react"

export function createContextHook<T>(
  Context: ReactContext<T | undefined>,
  contextName: string
) {
  return () => {
    const context = useContext(Context)

    if (!context) {
      throw new Error(`${contextName} is not provided`)
    }

    return context
  }
}
```

To obtain the current time and rerender the component, we will employ the `useRhythmicRerender` hook.

```ts
import { useEffect, useState } from "react"

export const useRhythmicRerender = (durationInMs = 1000) => {
  const [time, setTime] = useState<number>(Date.now())

  useEffect(() => {
    const interval = setInterval(() => setTime(Date.now()), durationInMs)
    return () => clearInterval(interval)
  }, [setTime, durationInMs])

  return time
}
```

We will record the current day as a timestamp in the `currentDay` state and distribute the `setCurrentDay` function to provider consumer to modify it.

We will filter the sets to include only those within the current day, and if the user is in focus mode, we will add a current set to the list.

Based on the current time, sets, and the user's preferred start and end of the workday, we will compute the start and end of the timeline.

## Panel Container

We encapsulate our component in a `Panel` container, which spaces out the content inside and outlines the component with a border.

```tsx
import styled, { css } from "styled-components"

import { defaultBorderRadiusCSS } from "../borderRadius"
import { getCSSUnit } from "../utils/getCSSUnit"
import { getColor } from "../theme/getters"
import { match } from "@increaser/utils/match"

type PanelKind = "regular" | "secondary"

export interface PanelProps {
  width?: React.CSSProperties["width"]
  padding?: React.CSSProperties["padding"]
  direction?: React.CSSProperties["flexDirection"]

  kind?: PanelKind

  withSections?: boolean
}

const panelPaddingCSS = css<{ padding?: React.CSSProperties["padding"] }>`
  padding: ${({ padding }) => getCSSUnit(padding || 20)};
`

export const Panel = styled.div<PanelProps>`
  ${defaultBorderRadiusCSS};
  width: ${({ width }) => (width ? getCSSUnit(width) : undefined)};
  overflow: hidden;

  ${({ withSections, direction = "column", kind = "regular", theme }) => {
    const contentBackground = match(kind, {
      secondary: () => theme.colors.background.toCssValue(),
      regular: () => theme.colors.mist.toCssValue(),
    })

    const contentCSS = css`
      ${panelPaddingCSS}
      background: ${contentBackground};
    `

    return withSections
      ? css`
          display: flex;
          flex-direction: ${direction};
          gap: 1px;

          > * {
            ${contentCSS}
          }
        `
      : contentCSS
  }}

  ${({ kind }) =>
    kind === "secondary" &&
    css`
      border: 2px solid ${getColor("mist")};
    `}
`
```

We will continue to maintain all the hardcoded sizes and distances in the `config.ts` file.

```ts
export const horizontalPaddingInPx = 20
export const timeLabelWidthInPx = 40
export const timeLabelGapInPx = 8
export const botomPlaceholderHeightInPx = 28
export const topPlaceholderHeightInPx = horizontalPaddingInPx
export const minimumHourHeightInPx = 40
```

## WeekNavigation

The `WeekNavigation` component presents the specific day of the week and enables the user to navigate between days.

```tsx
import { WEEKDAYS } from "@increaser/utils/time"
import styled from "styled-components"

import { useDayOverview } from "../DayOverviewProvider"
import { useStartOfDay } from "@increaser/ui/hooks/useStartOfDay"
import { useStartOfWeek } from "@increaser/ui/hooks/useStartOfWeek"
import { convertDuration } from "@increaser/utils/time/convertDuration"
import { useFocus } from "focus/hooks/useFocus"
import { verticalPadding } from "@increaser/ui/css/verticalPadding"
import { horizontalPadding } from "@increaser/ui/css/horizontalPadding"
import { SameWidthChildrenRow } from "@increaser/ui/ui/Layout/SameWidthChildrenRow"
import { horizontalPaddingInPx } from "../config"
import { WeekdayOption } from "./WeekdayOption"
import { InvisibleHTMLRadio } from "@increaser/ui/ui/inputs/InvisibleHTMLRadio"

const Container = styled(SameWidthChildrenRow)`
  ${verticalPadding(2)}
  ${horizontalPadding(horizontalPaddingInPx * 0.6)}
`

export const WeekNavigation = () => {
  const todayStartedAt = useStartOfDay()
  const weekStartedAt = useStartOfWeek()
  const { setCurrentDay, dayStartedAt } = useDayOverview()
  const { currentSet } = useFocus()

  if (currentSet) {
    return null
  }

  return (
    <Container rowHeight={horizontalPaddingInPx * 1.6} gap={1} fullWidth>
      {WEEKDAYS.map((weekday, index) => {
        const weekdayStartsAt =
          weekStartedAt + convertDuration(index, "d", "ms")
        const isActive = dayStartedAt === weekdayStartsAt
        const isEnabled = weekdayStartsAt <= todayStartedAt
        return (
          <WeekdayOption key={index} isActive={isActive} isEnabled={isEnabled}>
            {isEnabled && (
              <InvisibleHTMLRadio
                isSelected={isActive}
                groupName="week-navigation"
                value={weekdayStartsAt}
                onSelect={() => setCurrentDay(weekdayStartsAt)}
              />
            )}
            {weekday.slice(0, 3)}
          </WeekdayOption>
        )
      })}
    </Container>
  )
}
```

We will position the weekdays inside a CSS Grid container abstracted behind the `SameWidthChildrenRow` component.

```tsx
import styled, { css } from "styled-components"
import { getCSSUnit } from "../utils/getCSSUnit"

interface Props {
  gap: number
  minChildrenWidth?: number
  childrenWidth?: number
  rowHeight?: number
  fullWidth?: boolean
  maxColumns?: number
}

const getColumnMax = (maxColumns: number | undefined, gap: number) => {
  if (!maxColumns) return `0px`

  const gapCount = maxColumns - 1
  const totalGapWidth = `calc(${gapCount} * ${getCSSUnit(gap)})`

  return `calc((100% - ${totalGapWidth}) / ${maxColumns})`
}

const getColumnWidth = ({
  minChildrenWidth,
  maxColumns,
  gap,
  childrenWidth,
}: Props) => {
  if (childrenWidth !== undefined) {
    return getCSSUnit(childrenWidth)
  }

  return `
    minmax(
      max(
        ${getCSSUnit(minChildrenWidth || 0)},
        ${getColumnMax(maxColumns, gap)}
      ),
      1fr
  )`
}

export const SameWidthChildrenRow = styled.div<Props>`
  display: grid;
  grid-template-columns: repeat(auto-fit, ${getColumnWidth});
  gap: ${({ gap }) => getCSSUnit(gap)};
  ${({ rowHeight }) =>
    rowHeight &&
    css`
      grid-auto-rows: ${getCSSUnit(rowHeight)};
    `}
  ${({ fullWidth }) =>
    fullWidth &&
    css`
      width: 100%;
    `}
`
```

To cycle over weekdays, we will utilize the `WEEKDAYS` array that contains the names of the days of the week. To procure the commencement of a weekday, we do simple math by adding days converted to milliseconds to the start of the week timestamp.

The active day will be the one that corresponds with the `dayStartedAt` timestamp, and we will disable all the days that fall in the future.

For a better accessibility experience and keyword support, we will render an invisible radio input inside every option.

```tsx
import { centerContent } from "@increaser/ui/css/centerContent"
import { interactive } from "@increaser/ui/css/interactive"
import { transition } from "@increaser/ui/css/transition"
import { getColor } from "@increaser/ui/ui/theme/getters"
import styled, { css } from "styled-components"

interface WeekdayOptionProps {
  isActive: boolean
  isEnabled: boolean
}

export const WeekdayOption = styled.label<WeekdayOptionProps>`
  ${centerContent}
  ${transition}
  font-size: 12px;
  font-weight: 500;
  border-radius: 4px;
  border: 2px solid transparent;

  ${({ isEnabled }) =>
    isEnabled
      ? css`
          ${interactive}
          color: ${getColor("textSupporting")};
        `
      : css`
          pointer-events: none;
          color: ${getColor("textShy")};
        `}

  ${({ isActive }) =>
    isActive
      ? css`
          color: ${getColor("contrast")};
          background: ${getColor("mist")};
        `
      : css`
          :hover {
            color: ${getColor("text")};
            border-color: ${getColor("mist")};
          }
        `}
`
```

The `WeekdayOption` component will style the label component, changing the text border and background color based on the `isActive` and `isEnabled` props.

## AmountOverview

The `AmountOverview` component provides an overview of the current day's work sessions and projects breakdown.

```tsx
import { HStack, VStack } from "@increaser/ui/ui/Stack"
import {
  HStackSeparatedBy,
  slashSeparator,
} from "@increaser/ui/ui/StackSeparatedBy"
import { WEEKDAYS } from "@increaser/utils/time"
import { ProjectTotal } from "projects/components/ProjectTotal"
import { ProjectsAllocationLine } from "projects/components/ProjectsAllocationLine"
import { getProjectColor } from "projects/utils/getProjectColor"
import { getProjectName } from "projects/utils/getProjectName"
import { useDayOverview } from "./DayOverviewProvider"
import { Text } from "@increaser/ui/ui/Text"
import { formatDuration } from "@increaser/utils/time/formatDuration"
import { getSetsSum } from "sets/helpers/getSetsSum"
import { useWeekTimeAllocation } from "weekTimeAllocation/hooks/useWeekTimeAllocation"
import { useProjects } from "projects/hooks/useProjects"
import { getProjectsTotalRecord } from "projects/helpers/getProjectsTotalRecord"
import { useTheme } from "styled-components"
import { getWeekday } from "@increaser/utils/time/getWeekday"

export const AmountOverview = () => {
  const theme = useTheme()
  const { sets, dayStartedAt } = useDayOverview()
  const setsTotal = getSetsSum(sets)
  const { allocation } = useWeekTimeAllocation()
  const weekday = getWeekday(new Date(dayStartedAt))
  const { projectsRecord } = useProjects()

  const allocatedMinutes = allocation ? allocation[weekday] : 0
  const projectsTotal = getProjectsTotalRecord(sets)

  return (
    <VStack fullWidth gap={8}>
      <VStack gap={4}>
        <HStack alignItems="center" justifyContent="space-between">
          <Text weight="semibold" color="supporting" size={14}>
            {WEEKDAYS[weekday]}
          </Text>
          <HStackSeparatedBy
            separator={<Text color="shy">{slashSeparator}</Text>}
          >
            <Text size={14} weight="semibold">
              {formatDuration(setsTotal, "ms")}
            </Text>
            <Text size={14} weight="semibold" color="shy">
              {formatDuration(allocatedMinutes, "min")}
            </Text>
          </HStackSeparatedBy>
        </HStack>
        <ProjectsAllocationLine
          projectsRecord={projectsRecord}
          sets={sets}
          allocatedMinutes={allocatedMinutes}
        />
      </VStack>

      <VStack gap={4} fullWidth>
        {Object.entries(projectsTotal)
          .sort((a, b) => b[1] - a[1])
          .map(([projectId]) => (
            <ProjectTotal
              key={projectId}
              name={getProjectName(projectsRecord, projectId)}
              color={getProjectColor(projectsRecord, theme, projectId)}
              value={projectsTotal[projectId]}
            />
          ))}
      </VStack>
    </VStack>
  )
}
```

First, we display the name of the current day along with the total time spent on work sessions compared to the designated time for the day. Normally, you would opt to work less over the weekends, so the allocated time will be different for Saturday and Sunday.

To visualize the breakdown of projects, we will employ the `ProjectsAllocationLine` component.

To estimate the total time spent on each project, we will exploit the `getProjectsTotalRecord` helper.

```tsx
import { getSetDuration } from "sets/helpers/getSetDuration"
import { Set } from "sets/Set"

export const getProjectsTotalRecord = (sets: Set[]) =>
  sets.reduce(
    (acc, set) => ({
      ...acc,
      [set.projectId]: (acc[set.projectId] || 0) + getSetDuration(set),
    }),
    {} as Record<string, number>
  )
```

## DayTimeline

The `DayTimeline` component forms the most complex part of our display, heavily stocking up on the absolutely positioned elements. The major segments of the component are:

- A block highlighting the time left before the end of the workday
- A timeline marker showing the hours of the day
- A current time indicator
- A workday end status showing how much time left before the end of the workday
- Work blocks displaying the work sessions
- A floating button permitting edits to the last work session

```tsx
import styled from "styled-components"

import { TimelineMarks } from "./TimelineMarks"
import { WorkdayEndStatus } from "./WorkdayEndStatus"
import { CurrentTime } from "./CurrentTime"
import { WorkdayLeftBlock } from "./WorkdayLeftBlock"
import { WorkBlocks } from "./WorkBlocks"
import { ManageLastSession } from "./ManageLastSession"
import {
  botomPlaceholderHeightInPx,
  minimumHourHeightInPx,
  topPlaceholderHeightInPx,
} from "./config"
import { useDayOverview } from "./DayOverviewProvider"
import { convertDuration } from "@increaser/utils/time/convertDuration"
import { takeWholeSpaceAbsolutely } from "@increaser/ui/css/takeWholeSpaceAbsolutely"
import { takeWholeSpace } from "@increaser/ui/css/takeWholeSpace"

const Wrapper = styled.div`
  flex: 1;
  padding: 0;
  position: relative;
  min-height: 320px;
`

const Container = styled.div`
  ${takeWholeSpaceAbsolutely}
  overflow: auto;
  padding-bottom: ${botomPlaceholderHeightInPx}px;
  padding-top: ${topPlaceholderHeightInPx}px;
`

const Content = styled.div`
  ${takeWholeSpace};
  position: relative;
`

export const DayTimeline = () => {
  const { timelineStartsAt, timelineEndsAt } = useDayOverview()
  const timespan = timelineEndsAt - timelineStartsAt
  const minHeight = convertDuration(timespan, "ms", "h") * minimumHourHeightInPx

  return (
    <Wrapper>
      <Container>
        <Content style={{ minHeight }}>
          <WorkdayLeftBlock />
          <TimelineMarks />
          <CurrentTime />
          <WorkdayEndStatus />
          <WorkBlocks />
          <ManageLastSession />
        </Content>
      </Container>
    </Wrapper>
  )
}
```

The `Wrapper` uses `flex: 1` to accommodate all the available space, and we use `min-height` to guarantee the component is at least 320px tall.

The `Container` will be an absolutely positioned element with `overflow: auto` to permit scrolling when space is inadequate.

The `Content` will be a relatively positioned element occupying all available space within the `Container`.

Since a lengthy timeline may not fit into the screen, we set a `min-height` for the `Content` element based on the timeline duration so that one hour is at least `40px` tall.

## WorkdayLeftBlock

The `WorkdayLeftBlock` component will visualize the remaining time until the end of the workday. It only makes sense for today, so it will hide if the user is looking at a past weekday.

```tsx
import styled from "styled-components"
import { useDayOverview } from "./DayOverviewProvider"
import { getColor } from "@increaser/ui/ui/theme/getters"
import { toPercents } from "@increaser/utils/toPercents"

const Container = styled.div`
  width: 100%;
  background: ${getColor("foreground")};
  position: absolute;
  left: 0;
`

export const WorkdayLeftBlock = () => {
  const { workdayEndsAt, timelineEndsAt, timelineStartsAt, currentTime } =
    useDayOverview()
  const workEndsIn = workdayEndsAt - currentTime
  const timespan = timelineEndsAt - timelineStartsAt

  if (currentTime > workdayEndsAt) {
    return null
  }

  return (
    <Container
      style={{
        top: toPercents((currentTime - timelineStartsAt) / timespan),
        height: toPercents(workEndsIn / timespan),
      }}
    />
  )
}
```

To convert ratios for the `top` and `height` attributes to percentages, we employ the `toPercents` helper.

```ts
type Format = "round"

export const toPercents = (value: number, format?: Format) => {
  const number = value * 100

  return `${format === "round" ? Math.round(number) : number}%`
}
```

## TimelineMarks

The `TimelineMarks` component will portray the hours of the day.

```tsx
import { useMemo } from "react"
import { useDayOverview } from "./DayOverviewProvider"
import { PositionAbsolutelyCenterHorizontally } from "@increaser/ui/ui/PositionAbsolutelyCenterHorizontally"
import { toPercents } from "@increaser/utils/toPercents"
import styled from "styled-components"
import {
  horizontalPaddingInPx,
  timeLabelGapInPx,
  timeLabelWidthInPx,
} from "./config"
import { Text } from "@increaser/ui/ui/Text"
import { formatTime } from "@increaser/utils/time/formatTime"
import { getColor } from "@increaser/ui/ui/theme/getters"
import { getHoursInRange } from "@increaser/utils/time/getHoursInRange"
import { takeWholeSpace } from "@increaser/ui/css/takeWholeSpace"
import { horizontalPadding } from "@increaser/ui/css/horizontalPadding"
import { centerContent } from "@increaser/ui/css/centerContent"
import { transition } from "@increaser/ui/css/transition"

const Container = styled.div`
  display: grid;
  grid-template-columns: ${timeLabelWidthInPx}px 1fr;
  align-items: center;
  ${horizontalPadding(horizontalPaddingInPx)};
  gap: ${timeLabelGapInPx}px;
`

const Time = styled.div`
  ${centerContent};
  ${takeWholeSpace};
`

const Line = styled.div`
  width: 100%;
  height: 1px;
  background: ${getColor("mist")};
  ${transition};
`

export const TimelineMarks = () => {
  const { timelineStartsAt, timelineEndsAt } = useDayOverview()
  const marks = useMemo(() => {
    return getHoursInRange(timelineStartsAt, timelineEndsAt)
  }, [timelineEndsAt, timelineStartsAt])

  const timespan = timelineEndsAt - timelineStartsAt

  return (
    <>
      {marks.map((mark) => {
        return (
          <PositionAbsolutelyCenterHorizontally
            fullWidth
            top={toPercents((mark - timelineStartsAt) / timespan)}
            key={mark}
          >
            <Container>
              <Time>
                <Text color="shy" size={14}>
                  {formatTime(mark)}
                </Text>
              </Time>
              <Line />
            </Container>
          </PositionAbsolutelyCenterHorizontally>
        )
      })}
    </>
  )
}
```

We rely on a recursive function `getHoursInRange` to acquire the hours of the day between two timestamps.

```ts
import { startOfHour } from "date-fns"
import { MS_IN_HOUR } from "."

export const getHoursInRange = (start: number, end: number) => {
  const recursive = (time: number): number[] => {
    if (time > end) {
      return []
    }

    const nextHour = time + MS_IN_HOUR
    if (time < start) {
      return recursive(nextHour)
    }

    return [time, ...recursive(nextHour)]
  }

  return recursive(startOfHour(start).getTime())
}
```

To center the text with the time and line, we use the `PositionAbsolutelyCenterHorizontally` abstract component.

```tsx
import styled from "styled-components"
import { ComponentWithChildrenProps } from "../props"

interface PositionAbsolutelyCenterHorizontallyProps
  extends ComponentWithChildrenProps {
  top: React.CSSProperties["top"]
  fullWidth?: boolean
}

const Wrapper = styled.div`
  position: absolute;
  left: 0;
`

const Container = styled.div`
  position: relative;
  display: flex;
  align-items: center;
`

const Content = styled.div`
  position: absolute;
  left: 0;
`

export const PositionAbsolutelyCenterHorizontally = ({
  top,
  children,
  fullWidth,
}: PositionAbsolutelyCenterHorizontallyProps) => {
  const width = fullWidth ? "100%" : undefined
  return (
    <Wrapper style={{ top, width }}>
      <Container style={{ width }}>
        <Content style={{ width }}>{children}</Content>
      </Container>
    </Wrapper>
  )
}
```

It combines a `Wrapper` with 0 height, `Container` with horizontally aligned content, and `Content` with absolute positioning. Briefly, it involves a lot of wrapping, but as a user of the component, you access a pleasing API for absolute positioning.

## CurrentTime

The `CurrentTime` component strongly resembles one of the markers in the `TimelineMarks` component.

```tsx
import { PositionAbsolutelyCenterHorizontally } from "@increaser/ui/ui/PositionAbsolutelyCenterHorizontally"
import { toPercents } from "@increaser/utils/toPercents"
import { useDayOverview } from "./DayOverviewProvider"
import styled from "styled-components"
import { getColor } from "@increaser/ui/ui/theme/getters"
import { horizontalPaddingInPx, timeLabelWidthInPx } from "./config"
import { formatTime } from "@increaser/utils/time/formatTime"
import { Text } from "@increaser/ui/ui/Text"
import { useStartOfDay } from "@increaser/ui/hooks/useStartOfDay"
import { centerContent } from "@increaser/ui/css/centerContent"
import { absoluteOutline } from "@increaser/ui/css/absoluteOutline"

const Line = styled.div`
  width: 100%;
  height: 2px;
  background: ${getColor("primary")};
`

const Wrapper = styled.div`
  width: ${timeLabelWidthInPx}px;
  margin-left: ${horizontalPaddingInPx}px;
  position: relative;
  ${centerContent}
  height: 20px;
`

const Time = styled(Text)`
  position: absolute;
`

const Outline = styled.div`
  ${absoluteOutline(6, 6)};
  background: ${getColor("background")};
  border-radius: 8px;
  border: 2px solid ${getColor("primary")};
`

export const CurrentTime = () => {
  const {
    currentTime,
    timelineStartsAt,
    timelineEndsAt,
    workdayEndsAt,
    dayStartedAt,
  } = useDayOverview()

  const todayStartedAt = useStartOfDay()
  if (dayStartedAt !== todayStartedAt) {
    return null
  }

  if (currentTime > workdayEndsAt && timelineEndsAt === workdayEndsAt) {
    return null
  }

  const timespan = timelineEndsAt - timelineStartsAt

  const top = toPercents((currentTime - timelineStartsAt) / timespan)

  return (
    <>
      <PositionAbsolutelyCenterHorizontally fullWidth top={top}>
        <Line />
      </PositionAbsolutelyCenterHorizontally>
      <PositionAbsolutelyCenterHorizontally fullWidth top={top}>
        <Wrapper>
          <Outline />
          <Time size={14} weight="bold">
            {formatTime(currentTime)}
          </Time>
        </Wrapper>
      </PositionAbsolutelyCenterHorizontally>
    </>
  )
}
```

An interesting CSS trick here involves the `absoluteOutline` helper to create a border around the current time.

```tsx
import { css } from "styled-components"
import { toSizeUnit } from "./toSizeUnit"

export const absoluteOutline = (
  horizontalOffset: number | string,
  verticalOffset: number | string
) => {
  return css`
    pointer-events: none;
    position: absolute;
    left: -${toSizeUnit(horizontalOffset)};
    top: -${toSizeUnit(verticalOffset)};
    width: calc(100% + ${toSizeUnit(horizontalOffset)} * 2);
    height: calc(100% + ${toSizeUnit(verticalOffset)} * 2);
  `
}
```

As the `Outline` is an absolutely positioned element, we have to turn the `Time` component into an absolute as well.

## WorkdayEndStatus

When the user observes the current day and it's not yet the end of the workday, we present the `WorkdayEndStatus` component to display how much time remains before work concludes.

```tsx
import { Text } from "@increaser/ui/ui/Text"
import { formatDuration } from "@increaser/utils/time/formatDuration"
import styled from "styled-components"
import { useDayOverview } from "./DayOverviewProvider"
import { useStartOfDay } from "@increaser/ui/hooks/useStartOfDay"

const Container = styled.div`
  position: absolute;
  bottom: -20px;
  width: 100%;
  display: flex;
  justify-content: center;
  font-size: 14px;
  line-height: 1;
`

export const WorkdayEndStatus = () => {
  const { workdayEndsAt, timelineEndsAt, currentTime, dayStartedAt } =
    useDayOverview()
  const workEndsIn = workdayEndsAt - currentTime

  const todayStartedAt = useStartOfDay()
  if (dayStartedAt !== todayStartedAt) {
    return null
  }

  if (timelineEndsAt > workdayEndsAt) {
    return null
  }

  return (
    <Container>
      {currentTime < workdayEndsAt && (
        <Text color="contrast" weight="semibold" size={14}>
          <Text as="span" color="supporting">
            workday ends in
          </Text>{" "}
          {formatDuration(workEndsIn, "ms")}
        </Text>
      )}
    </Container>
  )
}
```

To format, we use the proficient `formatDuration` helper.

```ts
import { padWithZero } from "../padWithZero"
import { H_IN_DAY, MIN_IN_HOUR, S_IN_HOUR, S_IN_MIN } from "."
import { DurationUnit, convertDuration } from "./convertDuration"

export const formatDuration = (duration: number, unit: DurationUnit) => {
  const minutes = Math.round(convertDuration(duration, unit, "min"))

  if (minutes < MIN_IN_HOUR) return `${minutes}m`

  const hours = Math.floor(minutes / S_IN_MIN)

  if (hours < H_IN_DAY) {
    const minutesPart = Math.round(minutes % S_IN_MIN)
    if (!minutesPart) {
      return `${hours}h`
    }
    return `${hours}h ${minutesPart}m`
  }

  const days = Math.floor(hours / H_IN_DAY)
  const hoursPart = Math.round(hours % H_IN_DAY)
  if (!hoursPart) {
    return `${days}d`
  }

  return `${days}d ${hoursPart}h`
}
```

## WorkBlocks

A work block is an assembly of sets that are close together. As per Andrew Huberman, one of the productivity secrets involves organizing work into 90-minute blocks. That's why we desire to categorize sessions into blocks and highlight their duration to the user.

```tsx
import { useDayOverview } from "./DayOverviewProvider"
import { getBlocks } from "@increaser/entities-utils/block"
import { WorkBlock } from "./WorkBlock"

export const WorkBlocks = () => {
  const { sets } = useDayOverview()
  const blocks = getBlocks(sets)

  return (
    <>
      {blocks.map((block, index) => (
        <WorkBlock key={index} block={block} />
      ))}
    </>
  )
}
```

Since I also incorporate blocks on the server-side to create a scoreboard of the most productive users, the `getBlocks` function is contained in the `entities-utils` package.

```ts
export const getBlocks = (sets: Set[]): Block[] => {
  const blocks: Block[] = []

  sets.forEach((set, index) => {
    const prevSet = sets[index - 1]

    if (!prevSet) {
      blocks.push({ sets: [set] })
      return
    }

    const distance = getDistanceBetweenSets(prevSet, set)
    if (distance > blockDistanceInMinutes * MS_IN_MIN) {
      blocks.push({ sets: [set] })
      return
    }

    getLastItem(blocks).sets.push(set)
  })

  return blocks
}
```

## WorkBlock

To show a dashed line around the block, we'll once again use the `absoluteOutline` helper.

```tsx
import { Block } from "@increaser/entities/Block"
import styled from "styled-components"
import { useDayOverview } from "../DayOverviewProvider"
import {
  getBlockBoundaries,
  getBlockWorkDuration,
} from "@increaser/entities-utils/block"
import { toPercents } from "@increaser/utils/toPercents"
import { takeWholeSpace } from "@increaser/ui/css/takeWholeSpace"
import { getColor } from "@increaser/ui/ui/theme/getters"
import {
  horizontalPaddingInPx,
  timeLabelWidthInPx,
  timeLabelGapInPx,
} from "../config"
import { convertDuration } from "@increaser/utils/time/convertDuration"
import { Text } from "@increaser/ui/ui/Text"
import { formatDuration } from "@increaser/utils/time/formatDuration"
import { WorkSession } from "./WorkSession"
import { getSetDuration } from "@increaser/entities-utils/set/getSetDuration"
import { transition } from "@increaser/ui/css/transition"
import { absoluteOutline } from "@increaser/ui/css/absoluteOutline"

interface WorkBlockProps {
  block: Block
}

const leftOffset =
  horizontalPaddingInPx + timeLabelWidthInPx + timeLabelGapInPx * 2

const Container = styled.div`
  width: calc(100% - ${leftOffset}px - ${horizontalPaddingInPx}px);
  left: ${leftOffset}px;
  position: absolute;
  ${transition};
`

const Content = styled.div`
  position: relative;
  ${takeWholeSpace}
`

const Outline = styled.div`
  ${absoluteOutline(2, 2)};
  border-radius: 4px;
  border: 1px dashed ${getColor("textSupporting")};
`

const Duration = styled(Text)`
  position: absolute;
  top: 1px;
  right: 4px;
`

export const WorkBlock = ({ block }: WorkBlockProps) => {
  const { timelineStartsAt, timelineEndsAt } = useDayOverview()
  const timespan = timelineEndsAt - timelineStartsAt
  const { start, end } = getBlockBoundaries(block)
  const blockDuration = end - start
  const showDuration = blockDuration > convertDuration(25, "min", "ms")

  return (
    <Container
      style={{
        top: toPercents((start - timelineStartsAt) / timespan),
        height: toPercents(blockDuration / timespan),
      }}
    >
      <Content>
        <Outline />
        {block.sets.map((set) => (
          <WorkSession
            set={set}
            style={{
              top: toPercents((set.start - start) / blockDuration),
              height: toPercents(getSetDuration(set) / blockDuration),
            }}
          />
        ))}
        {showDuration && (
          <Duration color="supporting" size={14} weight="semibold">
            {formatDuration(getBlockWorkDuration(block), "ms")}
          </Duration>
        )}
      </Content>
    </Container>
  )
}
```

If the session is too short, we won't display the duration. To alternate between different time units, we use the `convertDuration` helper.

```ts
import { MS_IN_DAY, MS_IN_HOUR, MS_IN_MIN, MS_IN_SEC, MS_IN_WEEK } from "."

export type DurationUnit = "ms" | "s" | "min" | "h" | "d" | "w"

const msInUnit: Record<DurationUnit, number> = {
  ms: 1,
  s: MS_IN_SEC,
  min: MS_IN_MIN,
  h: MS_IN_HOUR,
  d: MS_IN_DAY,
  w: MS_IN_WEEK,
}

export const convertDuration = (
  value: number,
  from: DurationUnit,
  to: DurationUnit
) => {
  const result = value * (msInUnit[from] / msInUnit[to])

  return result
}
```

To illustrate a work session, we have a designated component.

```tsx
import { Set } from "@increaser/entities/User"
import { transition } from "@increaser/ui/css/transition"
import { UIComponentProps } from "@increaser/ui/props"
import { getColor } from "@increaser/ui/ui/theme/getters"
import { useFocus } from "focus/hooks/useFocus"
import { useProjects } from "projects/hooks/useProjects"
import { getProjectColor } from "projects/utils/getProjectColor"
import styled, { useTheme } from "styled-components"

interface WorkSessionProps extends UIComponentProps {
  set: Set
}

const Container = styled.div`
  border-radius: 2px;
  background: ${getColor("mist")};
  overflow: hidden;
  position: absolute;
  width: 100%;
  ${transition};
`

const Identifier = styled.div`
  width: 4px;
  height: 100%;
  ${transition};
`

export const WorkSession = ({ set, ...rest }: WorkSessionProps) => {
  const { projectsRecord } = useProjects()
  const { currentSet } = useFocus()

  const theme = useTheme()

  const color = getProjectColor(projectsRecord, theme, set.projectId)

  return (
    <Container {...rest}>
      {!currentSet && <Identifier style={{ background: color.toCssValue() }} />}
    </Container>
  )
}
```

We assign a session a `mist` background using the `getColor` helper, which simplifies interaction with styled components themes.

```tsx
import { DefaultTheme } from "styled-components"
import { ThemeColors } from "./ThemeColors"

interface ThemeGetterParams {
  theme: DefaultTheme
}

type ColorName = keyof Omit<ThemeColors, "getLabelColor">

export const getColor =
  (color: ColorName) =>
  ({ theme }: ThemeGetterParams) => {
    return theme.colors[color].toCssValue()
  }
```

To link a session with a project, we'll display a small identifier in the form of a vertical line. Here we utilize the `getProjectColor` helper to retrieve the project's color.

```tsx
import { EnhancedProject } from "projects/Project"
import { DefaultTheme } from "styled-components"

export const getProjectColor = (
  projectsRecord: Record<string, EnhancedProject>,
  theme: DefaultTheme,
  projectId = ""
) => {
  const project = projectsRecord[projectId]

  if (!project) return theme.colors.mist

  return theme.colors.getLabelColor(project.color)
}
```

## ManageLastSession

Lastly, we'll introduce a floating button to modify the last work session. The `ManageLastSession` component will generate a menu component with options to either edit or remove the last session.

```tsx
import { useStartOfDay } from "@increaser/ui/hooks/useStartOfDay"
import { useFocus } from "focus/hooks/useFocus"
import { useDayOverview } from "../DayOverviewProvider"
import { getLastItem } from "@increaser/utils/array/getLastItem"
import { Menu } from "@increaser/ui/ui/Menu"
import { MenuOptionProps, MenuOption } from "@increaser/ui/ui/Menu/MenuOption"
import { EditIcon } from "@increaser/ui/ui/icons/EditIcon"
import { MoveIcon } from "@increaser/ui/ui/icons/MoveIcon"
import { TrashBinIcon } from "@increaser/ui/ui/icons/TrashBinIcon"
import { useState } from "react"
import { Match } from "@increaser/ui/ui/Match"
import { ChangeLastSetIntervalOverlay } from "focus/components/ChangeLastSetInvervalOverlay"
import { ChangeLastSetProjectOverlay } from "focus/components/ChangeLastSetProjectOverlay"
import { useDeleteLastSetMutation } from "sets/hooks/useDeleteLastSetMutation"
import { ManageSetOpener } from "./ManageSetOpener"

type MenuOptionType = "editInterval" | "changeProject"

export const ManageLastSession = () => {
  const [selectedOption, setSelectedOption] = useState<MenuOptionType | null>(
    null
  )

  const { currentSet } = useFocus()
  const todayStartedAt = useStartOfDay()
  const { dayStartedAt, sets } = useDayOverview()

  const { mutate: deleteLastSet } = useDeleteLastSetMutation()

  if (currentSet || dayStartedAt !== todayStartedAt || !sets.length) {
    return null
  }

  return (
    <>
      <Menu
        title="Manage last session"
        renderContent={({ view, onClose }) => {
          const options: MenuOptionProps[] = [
            {
              icon: <EditIcon />,
              text: "Change project",
              onSelect: () => {
                setSelectedOption("changeProject")
              },
            },
            {
              icon: <MoveIcon />,
              text: "Edit interval",
              onSelect: () => {
                setSelectedOption("editInterval")
              },
            },
            {
              icon: <TrashBinIcon />,
              text: "Delete session",
              kind: "alert",
              onSelect: () => {
                deleteLastSet()
              },
            },
          ]

          return options.map(({ text, icon, onSelect, kind }) => (
            <MenuOption
              text={text}
              key={text}
              icon={icon}
              view={view}
              kind={kind}
              onSelect={() => {
                onClose()
                onSelect()
              }}
            />
          ))
        }}
        renderOpener={(openerProps) => (
          <ManageSetOpener set={getLastItem(sets)} openerProps={openerProps} />
        )}
      />
      {selectedOption && (
        <Match
          value={selectedOption}
          editInterval={() => (
            <ChangeLastSetIntervalOverlay
              onClose={() => setSelectedOption(null)}
            />
          )}
          changeProject={() => (
            <ChangeLastSetProjectOverlay
              onClose={() => setSelectedOption(null)}
            />
          )}
        />
      )}
    </>
  )
}
```

To display the opener, we use the `ManageSetOpener` component which retrieves a set and essential props for the Menu component, to activate and position the menu. Here we position the opener absolutely with a `4px` offset from the end of the session.

```tsx
import { interactive } from "@increaser/ui/css/interactive"
import { HStack } from "@increaser/ui/ui/Stack"
import { defaultTransitionCSS } from "@increaser/ui/ui/animations/transitions"
import { MoreHorizontalIcon } from "@increaser/ui/ui/icons/MoreHorizontalIcon"
import { getColor } from "@increaser/ui/ui/theme/getters"
import { centerContentCSS } from "@increaser/ui/ui/utils/centerContentCSS"
import { toPercents } from "@increaser/utils/toPercents"
import { getProjectEmoji } from "projects/utils/getProjectEmoji"
import styled, { useTheme } from "styled-components"
import { horizontalPaddingInPx } from "../config"
import { Text } from "@increaser/ui/ui/Text"
import { useProjects } from "projects/hooks/useProjects"
import { RenderOpenerProps } from "@increaser/ui/ui/Menu/PopoverMenu"
import { Set } from "@increaser/entities/User"
import { useDayOverview } from "../DayOverviewProvider"
import { formatDuration } from "@increaser/utils/time/formatDuration"
import { getProjectColor } from "projects/utils/getProjectColor"

const offsetInPx = 4

const Container = styled.div`
  ${interactive}
  position: absolute;
  right: ${horizontalPaddingInPx}px;
  border-radius: 8px;
  padding: 4px 8px;
  ${centerContentCSS};
  font-size: 14px;
  border: 2px solid;
  background: ${getColor("background")};
  color: ${getColor("text")};
  ${defaultTransitionCSS};
  :hover {
    color: ${getColor("contrast")};
  }
`

interface ManageSetOpener {
  set: Set
  openerProps: RenderOpenerProps
}

export const ManageSetOpener = ({ set, openerProps }: ManageSetOpener) => {
  const { projectsRecord } = useProjects()
  const { start, end, projectId } = set
  const { timelineEndsAt, timelineStartsAt } = useDayOverview()
  const timespan = timelineEndsAt - timelineStartsAt

  const theme = useTheme()
  const color = getProjectColor(projectsRecord, theme, projectId)

  return (
    <Container
      {...openerProps}
      style={{
        top: `calc(${toPercents(
          (end - timelineStartsAt) / timespan
        )} + ${offsetInPx}px)`,
        borderColor: color.toCssValue(),
      }}
    >
      <HStack alignItems="center" gap={8}>
        <Text>{getProjectEmoji(projectsRecord, projectId)}</Text>
        <Text weight="semibold">{formatDuration(end - start, "ms")}</Text>
        <MoreHorizontalIcon />
      </HStack>
    </Container>
  )
}
```