One of the first coding projects I ever wrote was a chrome extension to replace the new tab page with a grid of my most used sites. It was purely HTML and CSS and was a hardcoded table with images I had made in photoshop. It was honestly really handy but it was way too much effort to create a new tile image, change the code and redeploy so it was never updated.

This project is a revival in tribute of my first attempt. It is built with a modern development stack with ease of customisation as the main goal.

Technology

Built with:

  • Webpack (4.x)
  • Babel (7.x)
  • Typescript (3.x)
  • React (16.x)
  • Redux (4.x)
  • Reselect
  • Sass

Development server using:

  • webpack-dev-server

Style-checking:

  • Prettier
  • Tslint
  • Stylelint & stylelint-config-sass-guidelines

Tested with:

  • Jest
  • Enzyme
  • redux-mock-store

State Management

There is one global state for the project and reselect and redux are used to manage it.

reselect is used to define selectors.

selectors/grid.ts

/**
 * Returns the grid state from the root state
 * @param {IState} state The root state of the app
 */
export const getGrid = (state: IState) => state.grid

export const getTiles = createSelector([getGrid], s => s.tiles)

Which are then used to pass some of the state to the ui components via the props of their container.

containers/EditableGrid.tsx

const mapStateToProps = (state: IState) => ({
  tiles: getTiles(state),
})

In order to alter the state, the components can dispatch an action via functions which are also passed through their container’s props.

containers/EditableGrid.tsx

const mapDispatchToProps = (dispatch: any) => ({
  handleReorderColumn: (
    columnOrder: string[],
    startIndex: number,
    endIndex: number
  ) => dispatch(reorderColumn(columnOrder, startIndex, endIndex)),
})

These actions are simply an identifier for the reducer function and a payload containing the information needed to perform the state modification.

actions/grid.ts

/**
 * Reorder the column order of the grid.
 * @param {string[]} columnOrder The current columnOrder array
 * @param {number} startIndex The index the column started at before being dragged
 * @param {number} endIndex The index at which the column was dropped
 */
export function reorderColumn(
  columnOrder: string[],
  startIndex: number,
  endIndex: number
): IReorderColumnAction {
  return {
    type: REORDER_COLUMN,
    payload: {
      columnOrder,
      startIndex,
      endIndex,
    },
  }
}

The reducer then performs the required modifications to the state.

reducers/grid.ts

/**
 * Reducer function for the grid state
 * @param {IGridState} state The current state of the grid, initialState if none provided
 * @param {Action} action The action to handle. The possible actions are declared in src/actions/grid.ts
 */
export function reducer(state: IGridState = initialState, action: Action) {
  switch (action.type) {
    case REORDER_COLUMN: {
      /**
       * Rearranges the column order within the grid moving the column from startIndex to endIndex.
       */
      const { columnOrder, startIndex, endIndex } = action.payload
      const newColumnOrder = Array.from(columnOrder)
      const [removed] = newColumnOrder.splice(startIndex, 1)
      newColumnOrder.splice(endIndex, 0, removed)

      return {
        ...state,
        columnOrder: newColumnOrder,
      }
    }
  }
}

Testing

This method of state management allows the state logic to be isolated and tested seperately from the UI code.

  • Using Jest it is easy to pass a state into the reducer and test that the expected result is returned.
  • Using redux-mock-store it is easy to test that actions are being correctly dispatched.
  • Enzyme can then be used to ensure that the UI components are being rendered as expected.

Combining these three methods of testing allows for excellent test coverage without much overhead.

Functionality

Opening the sidebar puts the grid into edit mode whereas closing the sidebar saves any changes made to localStorage.

When creating or editing a tile there are a range of display options available including a solid background colour, a gradient or an image.

The page background has a couple extra options allowing you to choose an animated particle system or an image from Unsplash randomly selected from the results of a user-specified search query.

The tiles are simply dragged and dropped in order to arrange them as desired.

Finally, the extension allows you to import or export your settings to back them up, transfer between computers or share with friends.