# Project export: Friction

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2024
- Tagline: Use your energy to do work, don't just create Friction.
- Devpost: https://devpost.com/software/friction
- GitHub: https://github.com/maggie-j-liu/friction
- Demo: https://github.com/sampoder/treehacks-backend
- Video: https://www.youtube.com/embed/TC2Bw1xrA6I?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Sam Poder (30 commits), Maggie Liu (23 commits), Janet (2 commits)

## Devpost submission (written by the team)

### Overview

Find yourself on Instagram Reels all day? Can't escape the Twitter drug? We'll help you break that habit with a bit of social friction. Here's how it works: Get all your friends together in a group. You can set your group's timezone as well as a designated no-friction rest period. You can set your group's timezone as well as a designated no-friction rest period. Anytime someone scrolls on a website like Twitter or Instagram, the distance they scrolled is tracked. The more distance that the group scrolls, the more "friction" is added to everyone's trackpad. This means over time, if people are doomscrolling a lot, it becomes harder & harder to scroll.... This means over time, if people are doomscrolling a lot, it becomes harder & harder to scroll.... And if you're addicted to Instagram Reels or YouTube Shorts, we'll also make them slower and blurrier (+ greyscale them!) over time. You can customize all the settings. Choose whether to make your reels and shorts blur, slow down, turn black-and-white...or all of the above! Also, specify a list of websites to apply friction on. Spend too much time scrolling Hacker News? Just add it to the blocklist. That way you put your energy towards other things, not just creating friction. Behind-the-scenes We've got a couple of things happening behind the scenes to make this happen: A PostgreSQL database being accessed / edited by a collection of Vercel Serverless Functions (via Prisma). We're hosting this database on Vercel's Serverless Postgres. We're hosting this database on Vercel's Serverless Postgres. A Chrome extension built using React This Chrome extension hijacks the wheel and onScroll events to produce the friction effect This Chrome extension hijacks the wheel and onScroll events to produce the friction effect Built at TreeHacks 2024 by @janetguo, @sampoder, and @maggie-j-liu

## README (from the GitHub repository)


<p align="center">
  <h1 align="center">
    <img src="https://github.com/maggie-j-liu/friction/assets/39828164/e0996d12-2cf0-4471-8d6d-f53ee6acdcc5" height="80px" /> <br />
    Use your energy to do work, don't just create <i>Friction</i>.
  </h1>
</p>

Find yourself on Instagram Reels all day? Can't escape the Twitter drug? We'll help you break that habit with a bit of social friction.

<img width="542" alt="Screenshot 2024-02-17 at 11 33 57 PM" src="https://github.com/maggie-j-liu/friction/assets/39828164/67544dd4-1f50-4b24-845a-f62e962251dc">

Here's how it works:

* Get all your friends together in a group.
  * You can set your group's timezone as well as a designated no-fricition rest period.
* Anytime someone scrolls on a website like Twitter or Instagram, the distance they scrolled is tracked.
* The more distance that the group scrolls, the more "friction" is added to everyone's trackpad.
  * This means over time, if people are doomscrolling a lot, it becomes harder & harder to scroll....
* And if you're addicted to Instagram Reels or YouTube Shorts, we'll also make them slower and blurrier (+ greyscale them!) over time.

That way you put your energy towards other things, not just creating friction.

## Behind-the-scenes

We've got a couple of things happening behind the scenes to make this happen:

* A PostgreSQL database being accessed / edited by a collection of Vercel Serverless Functions (via Prisma).
  * We're hosting this database on Vercel's Serverless Postgres.
* A Chrome extension built using React
  * This Chrome extension hijacks the `wheel` and `onScroll` events to produce the friction effect
 
Built at [TreeHacks 2024](https://www.treehacks.com/) by [@janetguo](https://github.com/janetguo), [@sampoder](https://github.com/sampoder), and [@maggie-j-liu](https://github.com/maggie-j-liu)

Here's our mascot MITtens and our project!

![IMG_5692](https://github.com/maggie-j-liu/friction/assets/39828164/90a2c7a5-b699-4868-8fc2-7c1891204f71)

And a team photo!

![IMG_5694](https://github.com/maggie-j-liu/friction/assets/39828164/caf6ce41-2703-4b67-a8ea-ce184aa2d806)




## Detected evidence (automated analysis)

Indexed codebase: 20 recognized source files, 59 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (28 of 28)

```
.babelrc
.gitignore
.prettierrc
.vscode/settings.json
LICENSE
package.json
README.md
src/constants.js
src/containers/Greetings/Greetings.jsx
src/manifest.json
src/pages/Content/content.styles.css
src/pages/Content/index.js
src/pages/Options/index.css
src/pages/Options/index.html
src/pages/Options/index.jsx
src/pages/Options/Options.css
src/pages/Options/Options.jsx
src/pages/Popup/index.css
src/pages/Popup/index.html
src/pages/Popup/index.jsx
src/pages/Popup/Popup.css
src/pages/Popup/Popup.jsx
src/pages/Popup/theme.js
tsconfig.json
utils/build.js
utils/env.js
utils/webserver.js
webpack.config.js
```

### Dependencies

- package.json: @babel/core@^7.20.12, @babel/plugin-proposal-class-properties@^7.18.6, @babel/preset-env@^7.20.2, @babel/preset-react@^7.18.6, @emotion/react@^11.11.3, @emotion/styled@^11.11.0, @pmmmwh/react-refresh-webpack-plugin@^0.5.10, @types/chrome@^0.0.202, @types/react@^18.0.26, @types/react-dom@^18.0.10, babel-eslint@^10.1.0, babel-loader@^9.1.2, babel-preset-react-app@^10.0.1, clean-webpack-plugin@^4.0.0, copy-webpack-plugin@^11.0.0, css-loader@^6.7.3, eslint@^8.31.0, eslint-config-react-app@^7.0.1, eslint-plugin-flowtype@^8.0.3, eslint-plugin-import@^2.27.4, eslint-plugin-jsx-a11y@^6.7.1, eslint-plugin-react@^7.32.0, eslint-plugin-react-hooks@^4.6.0, file-loader@^6.2.0, fs-extra@^11.1.0, html-loader@^4.2.0, html-webpack-plugin@^5.5.0, palx@^1.0.3, prettier@^2.8.3, react@^18.2.0, react-dom@^18.2.0, react-hot-toast@^2.4.1, react-refresh@^0.14.0, react-refresh-typescript@^2.0.7, sass@^1.57.1, sass-loader@^13.2.0, source-map-loader@^3.0.1, style-loader@^3.3.1, terser-webpack-plugin@^5.3.6, theme-ui@^0.16.1, toastify-js@^1.12.0, ts-loader@^9.4.2, type-fest@^3.5.2, typescript@^4.9.4, webpack@^5.75.0, webpack-cli@^4.10.0, webpack-dev-server@^4.11.1, zip-webpack-plugin@^4.0.1

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Merge branch 'react'
- cleanup repo
- Update index.js
- add some snarky comments
- Merge branch 'react' of https://github.com/maggie-j-liu/friction into react
- fix all the problems
- check for undefined session
- don't notify multiple times for same checkpoint
- Merge branch 'react' of github.com:maggie-j-liu/friction into react
- add toasts past certain checkpoints of distance scrolled
- oops
- Merge branch 'react' of https://github.com/maggie-j-liu/friction into react
- Merge branch 'react' of https://github.com/maggie-j-liu/friction into react

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "chrome-extension-boilerplate-react",
  "version": "5.0.4",
  "description": "A chrome extension boilerplate built with React 18, Webpack 5, and Webpack Dev Server 4",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/lxieyang/chrome-extension-boilerplate-react.git"
  },
  "scripts": {
    "build": "node utils/build.js",
    "start": "node utils/webserver.js",
    "prettier": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
  },
  "dependencies": {
    "@emotion/react": "^11.11.3",
    "@emotion/styled": "^11.11.0",
    "palx": "^1.0.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-hot-toast": "^2.4.1",
    "theme-ui": "^0.16.1",
    "toastify-js": "^1.12.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.12",
    "@babel/plugin-proposal-class-properties": "^7.18.6",
    "@babel/preset-env": "^7.20.2",
    "@babel/preset-react": "^7.18.6",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
    "@types/chrome": "^0.0.202",
    "@types/react": "^18.0.26",
    "@types/react-dom": "^18.0.10",
    "babel-eslint": "^10.1.0",
    "babel-loader": "^9.1.2",
    "babel-preset-react-app": "^10.0.1",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^11.0.0",
    "css-loader": "^6.7.3",
    "eslint": "^8.31.0",
    "eslint-config-react-app": "^7.0.1",
    "eslint-plugin-flowtype": "^8.0.3",
    "eslint-plugin-import": "^2.27.4",
    "eslint-plugin-jsx-a11y": "^6.7.1",
    "eslint-plugin-react": "^7.32.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "file-loader": "^6.2.0",
    "fs-extra": "^11.1.0",
    "html-loader": "^4.2.0",
    "html-webpack-plugin": "^5.5.0",
    "prettier": "^2.8.3",
    "react-refresh": "^0.14.0",
    "react-refresh-typescript": "^2.0.7",
    "sass": "^1.57.1",
    "sass-loader": "^13.2.0",
    "source-map-loader": "^3.0.1",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.3.6",
    "ts-loader": "^9.4.2",
    "type-fest": "^3.5.2",
    "typescript": "^4.9.4",
    "webpack": "^5.75.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.11.1",
    "zip-webpack-plugin": "^4.0.1"
  }
}

```

### src/pages/Popup/index.jsx

```javascript
import React from 'react';
import { createRoot } from 'react-dom/client';

import Popup from './Popup';
import './index.css';

const container = document.getElementById('app-container');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<Popup />);

```

### src/pages/Options/index.jsx

```javascript
import React from 'react';
import { createRoot } from 'react-dom/client';

import Options from './Options';
import './index.css';

const container = document.getElementById('app-container');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<Options title={'Settings'} />);

```

### src/pages/Content/index.js

```javascript
import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';
import { DEFAULT_BLOCKED_SITES } from '../../constants';

let scrollY = 0;
const SCROLL_TARGET = 1000;
let scrollingToNext = false;
let activeVidId = 0;
let lastScrollPos = window.scrollY;
let totalScrollDist = 0;
let instanceScroll = 0;
let session = null;

let slowdownVideo = false,
  blurVideo = false,
  grayscaleVideo = false;

let checkpoints = [1000, 3000, 5000, 10000];
let prevCheckpoint = 0;

const IS_YOUTUBE =
  (window.location.hostname === 'youtube.com' ||
    window.location.hostname === 'www.youtube.com') &&
  window.location.pathname.startsWith('/shorts/');
const IS_INSTAGRAM =
  (window.location.host === 'instagram.com' ||
    window.location.host === 'www.instagram.com') &&
  window.location.pathname.startsWith('/reels/');

const progressBarContainer = document.createElement('div');

progressBarContainer.style =
  'position: absolute; top: 0; bottom: 0; right: 0; width: 12px; z-index: 9999; display: flex; flex-direction: column';

const spacer = document.createElement('div');

const progressBar = document.createElement('div');
progressBar.style = 'width: 100%;';

progressBarContainer.appendChild(spacer);
progressBarContainer.appendChild(progressBar);

document.body.appendChild(progressBarContainer);

const styleEl = document.createElement('style');
document.head.appendChild(styleEl);

const YOUTUBE_VIDEO_SELECTOR =
  '.reel-video-in-sequence video, .reel-video-in-sequence #player-container';
const INSTAGRAM_VIDEO_SELECTOR = 'main video';

const updateVideoFilter = (scrollDist, selector) => {
  const multiplier = Math.pow(
    Math.E,
    -(scrollDist / 1000) / (SCROLL_TARGET * 30)
  );
  const blurMultiplier = Math.pow(
    Math.E,
    -(scrollDist / 1000) / (SCROLL_TARGET * 60)
  );
  styleEl.innerHTML = `${selector} { filter: ${
    grayscaleVideo ? `grayscale(${1 - multiplier})` : ''
  } ${blurVideo ? `blur(${(1 - blurMultiplier) * 5}px)` : ''}}`;
};

const findPassedCheckpoint = () => {
  let largestCheckpointPassed = 0;
  if (totalScrollDist <= checkpoints[checkpoints.length - 1]) {
    for (let i = checkpoints.length - 1; i >= 0; i--) {
      if (totalScrollDist >= checkpoints[i]) {
        largestCheckpointPassed = checkpoints[i];
        break;
      }
    }
  } else {
    largestCheckpointPassed = Math.floor(totalScrollDist / 10000) * 10000;
  }
  return largestCheckpointPassed;
};

const snarkyComments = [
  'Starting to feel the friction?',
  "You're creating friction in the friend group.",
  'Things are getting heated... slow down the scrolling.',
  'Put your energy towards other things, not just creating friction.',
  'Scroll... scroll... scroll.... more friction I guess?',
  "Your friends aren't going to like this extra friction.",
];

const processScroll = () => {
  chrome.storage.local.set({ scrollDist: totalScrollDist });
  let largestCheckpointPassed = findPassedCheckpoint(totalScrollDist);
  if (prevCheckpoint < largestCheckpointPassed) {
    prevCheckpoint = largestCheckpointPassed;
    Toastify({
      text: snarkyComments[
        Math.floor(Math.random() * (snarkyComments.length - 1))
      ],
      duration: 3000,
      close: false,
      gravity: 'bottom', // `top` or `bottom`
      position: 'right', // `left`, `center` or `right`
      style: {
        background: '#000',
        color: 'white',
        borderRadius: '8px',
        fontFamily: `system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Helvetica, sans-serif`,
        boxShadow: 'none',
        border: '1px solid white',
      },
    }).showToast();
  }
};

const handleYoutubeScroll = (event) => {
  event.preventDefault();

  const multiplier = Math.pow(
    Math.E,
    -(totalScrollDist / 1000) / (SCROLL_TARGET * 30)
  );

  updateVideoFilter(totalScrollDist, YOUTUBE_VIDEO_SELECTOR);

  if (slowdownVideo) {
    const vids = document.querySelectorAll('video');
    for (const vid of vids) {
      vid.playbackRate = multiplier;
    }
  }

  const percentScrolled = Math.abs(scrollY) / SCROLL_TARGET;
  progressBar.style.height = `${percentScrolled * 100}%`;
  if (scrollY < 0) {
    progressBarContainer.style.justifyContent = 'space-between';
  } else {
    progressBarContainer.style.justifyContent = 'flex-start';
  }

  const videos = [...document.querySelectorAll('.reel-video-in-sequence')];
  const currentId = videos.find(
    (v) =>
      v.hasAttribute('is-active') &&
      v.querySelector("#shorts-container video[tabindex='-1']")
  )?.id;
  if (!currentId) {
    return;
  }

  if (currentId && activeVidId === Number(currentId)) {
    scrollingToNext = false;
  }

  if (scrollingToNext) {
    return;
  }
  scrollY += event.deltaY * Math.pow(Math.E, -(totalScrollDist / 1000) / 8000);
  totalScrollDist += Math.abs(scrollY - lastScrollPos);
  instanceScroll += Math.abs(scrollY - lastScrollPos);
  lastScrollPos = scrollY;

  if (scrollY > SCROLL_TARGET || scrollY < -SCROLL_TARGET) {
    let scrollDir = scrollY > 0 ? 1 : -1;
    const currentParent = videos.find(
      (v) =>
        v.hasAttribute('is-active') &&
        v.querySelector("#shorts-container video[tabindex='-1']")
    );
    const nextVid = document.getElementById(
      Number(currentParent.id) + scrollDir
    );
    if (nextVid) {
      processScroll();
      scrollY = 0;
      activeVidId = Number(currentParent.id) + scrollDir;
      scrollingToNext = true;
      nextVid.scrollIntoView({
        behavior: 'smooth',
        block: 'center',
        inline: 'center',
      });
    } else {
      scrollY = Math.max(Math.min(scrollY, SCROLL_TARGET), -SCROLL_TARGET);
    }
  }
};

const handleInstagramScroll = (event) => {
  event.preventDefault();

  const multiplier = Math.pow(
    Math.E,
    -(totalScrollDist / 1000) / (SCROLL_TARGET * 30)
  );

  updateVideoFilter(totalScrollDist, INSTAGRAM_VIDEO_SELECTOR);

  if (slowdownVideo) {
    // slow down videos as more are scrolled thru
    const vids = document.querySelec
[truncated — 4313 more characters]
```

### webpack.config.js

```javascript
var webpack = require('webpack'),
  path = require('path'),
  fileSystem = require('fs-extra'),
  env = require('./utils/env'),
  CopyWebpackPlugin = require('copy-webpack-plugin'),
  HtmlWebpackPlugin = require('html-webpack-plugin'),
  TerserPlugin = require('terser-webpack-plugin');
var { CleanWebpackPlugin } = require('clean-webpack-plugin');
var ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
var ReactRefreshTypeScript = require('react-refresh-typescript');

const ASSET_PATH = process.env.ASSET_PATH || '/';

var alias = {};

// load the secrets
var secretsPath = path.join(__dirname, 'secrets.' + env.NODE_ENV + '.js');

var fileExtensions = [
  'jpg',
  'jpeg',
  'png',
  'gif',
  'eot',
  'otf',
  'svg',
  'ttf',
  'woff',
  'woff2',
];

if (fileSystem.existsSync(secretsPath)) {
  alias['secrets'] = secretsPath;
}

const isDevelopment = process.env.NODE_ENV !== 'production';

var options = {
  mode: process.env.NODE_ENV || 'development',
  entry: {
    options: path.join(__dirname, 'src', 'pages', 'Options', 'index.jsx'),
    popup: path.join(__dirname, 'src', 'pages', 'Popup', 'index.jsx'),
    contentScript: path.join(__dirname, 'src', 'pages', 'Content', 'index.js'),
  },
  chromeExtensionBoilerplate: {
    notHotReload: ['contentScript'],
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'build'),
    clean: true,
    publicPath: ASSET_PATH,
  },
  module: {
    rules: [
      {
        // look for .css or .scss files
        test: /\.(css|scss)$/,
        // in the `src` directory
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
      {
        test: new RegExp('.(' + fileExtensions.join('|') + ')$'),
        type: 'asset/resource',
        exclude: /node_modules/,
        // loader: 'file-loader',
        // options: {
        //   name: '[name].[ext]',
        // },
      },
      {
        test: /\.html$/,
        loader: 'html-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: require.resolve('ts-loader'),
            options: {
              getCustomTransformers: () => ({
                before: [isDevelopment && ReactRefreshTypeScript()].filter(
                  Boolean
                ),
              }),
              transpileOnly: isDevelopment,
            },
          },
        ],
      },
      {
        test: /\.(js|jsx)$/,
        use: [
          {
            loader: 'source-map-loader',
          },
          {
            loader: require.resolve('babel-loader'),
            options: {
              plugins: [
                isDevelopment && require.resolve('react-refresh/babel'),
              ].filter(Boolean),
            },
          },
        ],
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    alias: alias,
    extensions: fileExtensions
      .map((extension) => '.' + extension)
      .concat(['.js', '.jsx', '.ts', '.tsx', '.css']),
  },
  plugins: [
    isDevelopment && new ReactRefreshWebpackPlugin(),
    new CleanWebpackPlugin({ verbose: false }),
    new webpack.ProgressPlugin(),
    // expose and write the allowed env vars on the compiled bundle
    new webpack.EnvironmentPlugin(['NODE_ENV']),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'src/manifest.json',
          to: path.join(__dirname, 'build'),
          force: true,
          transform: function (content, path) {
            // generates the manifest file using the package.json informations
            return Buffer.from(
              JSON.stringify({
                description: process.env.npm_package_description,
                version: process.env.npm_package_version,
                ...JSON.parse(content.toString()),
              })
            );
          },
        },
      ],
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'src/pages/Content/content.styles.css',
          to: path.join(__dirname, 'build'),
          force: true,
        },
      ],
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'src/assets/img/icon-128.png',
          to: path.join(__dirname, 'build'),
          force: true,
        },
      ],
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'src/assets/img/icon-34.png',
          to: path.join(__dirname, 'build'),
          force: true,
        },
      ],
    }),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src', 'pages', 'Options', 'index.html'),
      filename: 'options.html',
      chunks: ['options'],
      cache: false,
    }),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'src', 'pages', 'Popup', 'index.html'),
      filename: 'popup.html',
      chunks: ['popup'],
      cache: false,
    })
  ].filter(Boolean),
  infrastructureLogging: {
    level: 'info',
  },
};

if (env.NODE_ENV === 'development') {
  options.devtool = 'cheap-module-source-map';
} else {
  options.optimization = {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ],
  };
}

module.exports = options;

```

### utils/env.js

```javascript
// tiny wrapper with default env vars
module.exports = {
  NODE_ENV: process.env.NODE_ENV || 'development',
  PORT: process.env.PORT || 3000,
};

```

### src/constants.js

```javascript
export const DEFAULT_BLOCKED_SITES = [
  'https://www.instagram.com/reels/',
  'https://www.youtube.com/shorts/',
  'https://twitter.com',
  'https://news.ycombinator.com',
];

```

### utils/build.js

```javascript
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
process.env.ASSET_PATH = '/';

var webpack = require('webpack'),
  path = require('path'),
  fs = require('fs'),
  config = require('../webpack.config'),
  ZipPlugin = require('zip-webpack-plugin');

delete config.chromeExtensionBoilerplate;

config.mode = 'production';

var packageInfo = JSON.parse(fs.readFileSync('package.json', 'utf-8'));

config.plugins = (config.plugins || []).concat(
  new ZipPlugin({
    filename: `${packageInfo.name}-${packageInfo.version}.zip`,
    path: path.join(__dirname, '../', 'zip'),
  })
);

webpack(config, function (err) {
  if (err) throw err;
});

```

### utils/webserver.js

```javascript
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
process.env.ASSET_PATH = '/';

var WebpackDevServer = require('webpack-dev-server'),
  webpack = require('webpack'),
  config = require('../webpack.config'),
  env = require('./env'),
  path = require('path');

var options = config.chromeExtensionBoilerplate || {};
var excludeEntriesToHotReload = options.notHotReload || [];

for (var entryName in config.entry) {
  if (excludeEntriesToHotReload.indexOf(entryName) === -1) {
    config.entry[entryName] = [
      'webpack/hot/dev-server',
      `webpack-dev-server/client?hot=true&hostname=localhost&port=${env.PORT}`,
    ].concat(config.entry[entryName]);
  }
}

delete config.chromeExtensionBoilerplate;

var compiler = webpack(config);

var server = new WebpackDevServer(
  {
    https: false,
    hot: true,
    liveReload: false,
    client: {
      webSocketTransport: 'ws',
    },
    webSocketServer: 'ws',
    host: 'localhost',
    port: env.PORT,
    static: {
      directory: path.join(__dirname, '../build'),
    },
    devMiddleware: {
      publicPath: `wss://localhost:${env.PORT}/`,
      writeToDisk: true,
    },
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    allowedHosts: 'all',
  },
  compiler
);

(async () => {
  await server.start();
})();

```

### src/pages/Options/Options.css

```css
.OptionsContainer {
  width: 100%;
  height: 50vh;
  font-size: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

```

[8 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]