electinth / election-live

Live Scoreboard for Thai General Election 2562 (2019)
https://elect.thematter.co/
MIT License
436 stars 145 forks source link
elect

ELECT LIVE CircleCI Code Climate technical debt Code Climate maintainability

Live Scoreboard for Thailand General Election 2562 (2019).

Screenshot

Development

This web application is built using Gatsby. We chose to use Gatsby because:

@todo comments PDD status

0pdd helps us keep track of TODOs by converting @todo markers in source code into GitHub issue. The created issue will be closed automatically when the corresponding @todo marker is removed from the source code.

To add a todo, put a comment in the source code beginning with @todo, followed by issue number. If there’s no associated issue, just use the catch-all issue #1. Example:

// @todo #1 Add Analytics, e.g. Google Analytics.
//  Check out Gatsby docs for how to add analytics
//  - Main Guide: https://www.gatsbyjs.org/docs/adding-analytics/
//  - Google Analytics: https://www.gatsbyjs.org/docs/adding-analytics/#using-gatsby-plugin-google-analytics
//  - Google Tag Manager: https://www.gatsbyjs.org/packages/gatsby-plugin-google-tagmanager/

Note that if the @todo spans multiple lines, subsequent lines must be indented with 1 extra space. See @todo formatting rules.

Prerequisites

You need to install these tools first in order to develop this project:

Install dependencies

Before you can start the development server, you have to install the dependencies first.

yarn install

Running development server

yarn develop

Disable the curtain

When you run the app for the first time, if it’s not yet time to count the election results, the application will be disabled and you will see a curtain with a countdown. To develop this website, you have to disable the curtain first.

  1. Go to /dev, e.g. http://localhost:8000/dev
  2. Toggle the flag ELECT_DISABLE_CURTAIN

Resources for developers

How to add new pages

Just add a new file in src/pages — each .js file becomes a new page automatically.

Client-side data architecture

The goals to achieve:

Data architecture

Folder structure

While the React Community these days tends to recommend structuring your project around features (map, search, filter, chart) rather than types (components, pages, models), this project uses types-based folder structure.

Since some developers may not be too familiar with React, it is therefore more useful to break the apps into logical parts (components, models, pages, styles, utils) to make them more obvious to new developers, rather than functional parts, which is easier to grasp if there are not too many.

Component naming

When we set up a skeleton and implement a design, before we start coding, it’s useful to step back a bit, and look at the design and try to name each component in a consistent way first.

Component naming sketch

After we have made the conscious effort to have the core components named in a consistent manner, most components created after that are named ad-hoc.

Styling

We use emotion to style the website. It allows us to keep the CSS code close to the component that uses it.

Responsive Design

Development of the UI is done using mobile-first approach. The main benefit is that it allows many component to be reused easily. Most components for mobile can be use as-is on the desktop (just position it in a way that makes sense), while usually components for desktop must be re-implemented for mobile from scratch.

This results in one simple rule: @media (max-width) should not be used. Instead, put in the mobile styling first, then use @media (min-width) to enhance the component for desktop.

When it come to pre-rendered React applications, there are two main approaches to responsive design: CSS-based and React-based. Each approach has its own pros and cons. We use both approaches in this project, its trade-offs are discussed below:

  1. CSS-based. We render the same HTML, but use CSS to apply styling.

    • Pro: The same markup can be shared.
    • Pro: Can be pre-rendered. This makes the component appear immediately while page is loading.
    • Con: Usually results in a more complex code, especially when a component looks very different on different screen sizes.

    Usage:

    import { media } from "../styles"
    
    function Thing() {
     return (
       <div
         css={{
           // Mobile-first:
           display: "block",
           // then enhance to desktop:
           [media(600)]: { display: "inline-block" },
         }}
       >
         ...
       </div>
     )
    }
  2. React-based. Different markup is rendered based on window size.

    • Pro: Can use different markup for different screen sizes. This usually results in simpler code.
    • Con: Cannot be pre-rendered, because the server cannot send different HTML code based on screen size. Our <Responsive /> component will only be mounted after JavaScript is loaded.

    Usage:

    import { Responsive } from "../styles"
    
    function Thing() {
     return (
       <Responsive
         breakpoint={600}
         narrow={<ComponentForMobile />}
         wide={<ComponentForDesktop />}
       />
     )
    }

    Note that <Responsive /> will not be pre-rendered. This means that sometimes you might need to use both approaches together to prevent layout jumping. For example, if a component is 50 pixels high on mobile and 100 pixels high on desktop:

    import { Responsive } from "../styles"
    
    function Thing() {
     const breakpoint = 600
     return (
       <div
         css={{
           // Reserve the space for component to be mounted.
           height: 50,
           [media(breakpoint)]: { height: 100 },
         }}
       >
         <Responsive
           breakpoint={breakpoint}
           narrow={<ComponentForMobile />}
           wide={<ComponentForDesktop />}
         />
       </div>
     )
    }

Use JSDoc instead of propTypes

Using JSDoc allows us to specify types of component props more expressively, and allows enhanced integration (and refactoring capability) with Visual Studio Code.

You can define all props in one line:

/**
 * @param {{ party: IParty, hidden: boolean }} props
 */
export default function Unimplemented(props) {

...or you can also spell out each prop as a @param (where some props needs extra elaboration):

/**
 * @param {object} props
 * @param {IParty} props.party
 * @param {number} props.changeRate - The rate of change in score
 */
export default function MyComponent(props) {

How we handled 100,000 active users on a single \$10/mo server

After the election, we have more than 100,000 simultaneous active users watching the counting progress live (updated every 1 minute). In total, 1.5 million users visited the website on the election day.

The website runs on a single DigitalOcean machine which costs \$10/mo, serving static files on Apache Web Server, with Cloudflare in front. With a good Cache-Control setting, we were able to have 99% cache rate.

Cloudflare stats

  1. We generated a static site (using Gatsby), so, there’s no need to run server-side code, as everything is pre-built by the CI.

    We follow Gatsby’s caching guide (they have it documented and so we didn’t have to figure it out on our own. In fact, we didn’t even have to implement it as gatsby-plugin-htaccess will automatically generate an .htaccess file that follows this best practice already.)

    One deviation we made is that instead of setting max-age=0, we set it to 30 seconds in order to better utilize Cloudflare’s caching.

  2. For live data, there’s no API either. We use an ETL process to put data files on the web server.

    1. /data/latest.json is a very small file (< 1kb) that has references to larger “data files”. This file is updated in-place and has a very short cache time.

      Cache-Control: public, max-age=30, stale-while-revalidate=30, stale-if-error=300, must-revalidate
    2. The actual data files. These files are immutable. Each time a data file is generated, it is written to a new location. So, there’s no need to do cache invalidation.

      Cache-Control: public, max-age=31536000, stale-while-revalidate=30, stale-if-error=300, immutable

      Due to immutability and keeping old data files around, this also allows us to time travel and see what our webpage would look like using data at different point in time, simply by loading up a past data file.

Build the project into a static web page

yarn build

Releasing new version

To release a new version, run /updateelectliveversion 1.0.0-beta.5 slash command in our development Slack channel (only available to collaborators). This will update package.json file and deploy to the live website, and will also cause an update bar to display on user’s screen, asking them to refresh.

Contributors

We developed this website just 8 days before the election date. It wouldn’t be possible without a lot of help from our volunteer contributors!

Also, thanks to Cleverse for Thailand party list calculation algorithm implementation in JavaScript. It saved us a lot of time.