election-live

repository·master·Indexed 19 days ago

https://github.com/electinth/election-live

A live scoreboard application for the Thailand General Election 2562 (2019), built with Gatsby and Node.js. It features a high-traffic architecture supporting 100,000 active users on a single server using static site generation (SSG), an ETL process for live data, and an immutable caching strategy. The project includes utilities for calculating party statistics, managing election zone data, and implementing a mobile-first responsive design using Emotion object styles.

Tokens
4.7K
Snippets
22
Records
27
Agent score
66%

What's inside election-live

  1. Data architecture and caching strategy

    master

    The application uses a static site generation (SSG) approach with an ETL process for live data to handle high traffic efficiently.

    Data Loading Pattern

    To balance initial load time and interactivity, data is downloaded per province rather than per zone. This prevents downloading all data at once while avoiding frequent network requests when switching between zones in the same province.

    Caching and Immutability

    • Static Files: Served via Apache/Cloudflare with high cacheability.
    • Live Data Reference (/data/latest.json): A small file (<1kb) that points to specific data files. It has a short cache time (max-age=30) to allow updates.
    • Actual Data Files: These are immutable. Every update writes to a new file location. This eliminates the need for cache invalidation and allows for 'time travel' by loading specific historical data files.

    Cache-Control Headers:

    • Reference file: public, max-age=30, stale-while-revalidate=30, stale-if-error=300, must-revalidate
    • Data files: public, max-age=31536000, stale-while-revalidate=30, stale-if-error=300, immutable
  2. Implement responsive design

    master

    The project follows a mobile-first approach. Avoid using @media (max-width). Instead, define mobile styles first and use @media (min-width) to enhance for desktop.

    There are two primary patterns used:

    1. CSS-based (Pre-renderable)

    Uses the same HTML for all screen sizes, applying different styles via CSS. This is best for performance as it allows pre-rendering.

    import { media } from "../styles"
    
    function Thing() {
      return (
        <div
          css={{
            display: "block", // Mobile-first
            [media(600)]: { display: "inline-block" }, // Desktop enhancement
          }}
        />
      )
    }

    2. React-based (Non-pre-renderable)

    Renders different markup based on window size using the <Responsive /> component. This is simpler for complex UI changes but cannot be pre-rendered.

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

    Note: To prevent layout jumping when using the React-based approach, reserve space for the component in the parent container using CSS heights.

    import { media } from "../styles"
    
    function Thing() {
      return (
        <div
          css={{
            // Mobile-first:
            display: "block",
            // then enhance to desktop:
            [media(600)]: { display: "inline-block" },
          }}
        />
      )
    }
  3. Release a new version

    master

    Releasing a new version is handled via a Slack slash command (available only to collaborators). Running /updateelectliveversion <version> performs the following actions:

    1. Updates the package.json file.
    2. Deploys the changes to the live website.
    3. Triggers an update bar on users' screens, prompting them to refresh the page.
    /updateelectliveversion 1.0.0-beta.5
  4. Define component props using JSDoc

    master

    Instead of propTypes, use JSDoc to define component props. This provides better type expression and enhanced refactoring support in VS Code.

    Single line definition:

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

    Detailed definition:

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

    master

    The project uses emotion for styling. It is recommended to use the css prop with object styles rather than string styles or inline style props. Object styles provide better VS Code autocomplete and are compatible with Prettier.

    Example:

    import { media } from "../styles"
    
    function Component() {
      return (
        <div
          css={{
            fontSize: 18,
            [media(600)]: {
              fontSize: 20,
            },
          }}
        />
      )
    }
  6. Set up the development environment

    master

    To develop this project, ensure you have the following prerequisites installed:

    • Node.js (10.x)
    • Yarn

    Follow these steps to get started:

    1. Install dependencies: yarn install
    2. Start the development server: yarn develop

    Note: If you are running the app for the first time, you may see a 'curtain' (countdown) because it is not yet election time. To bypass this for development:

    1. Navigate to /dev (e.g., http://localhost:8000/dev).
    2. Toggle the ELECT_DISABLE_CURTAIN flag.
    yarn install
    yarn develop
  7. Use @todo comments for issue tracking

    master

    The project uses 0pdd to convert @todo markers in source code into GitHub issues.

    To add a todo:

    1. Use the format // @todo #[issue_number] [description].
    2. If no issue exists, use #1.
    3. If the @todo spans multiple lines, indent subsequent lines with exactly one extra space.

    Example:

    // @todo #1 Add Analytics, e.g. Google Analytics.
    //  Check out Gatsby docs for how to add analytics
  8. Understand the PartyStatsRow data structure

    master

    The output of the party statistics functions is an array of PartyStatsRow objects. Each object represents a single party's performance:

    • party: The IParty object representing the party.
    • constituencySeats: The number of seats won in constituency elections (subject to the provided filter).
    • partyListSeats: The number of seats won via the party list system.
    • score: The total vote count for the party.
    • seatsCeiling: A calculated value representing the theoretical maximum seats the party could hold based on their vote share (voteCount / totalVoteCount) * 500.
  9. Determine if a zone's data is ready for display

    master

    The project uses two utility functions to decide when election data for a specific zone is significant enough to be shown to users:

    • isZoneFinished(zoneStats): Returns true if the zone is marked as finished, has reached 80% progress, or has reported 80% of eligible votes.
    • shouldDisplayZoneData(zoneStats): A more lenient check used for UI rendering. Returns true if the zone is finished, has at least 1% progress, or has reported at least 1% of eligible votes.