Start typing to search...
Docs
Blueprints
Vote counter template
Search

Vote counter template

Follow this blueprint to build a template that counts votes from multiple users in real-time. You will implement a database to record the users and votes and real-time dispatch to provide users with an immediate, multi-user experience. Then, you will publish the template on Koji.

Make a vote counter template on Koji

Prerequisites

  • Familiarity with web development. React and ES6 basics are a plus.

  • Familiarity with the Koji editor and remix process. For an overview, go through the Koji quick-start tutorial.

  • Basic understanding of backend communication.

  • Basic understanding of the Koji service map. For an overview, see add-service.html.

  • Basic understanding of the Koji database. See koji-database.html.

Level

  • Koji: Intermediate

  • Developer: Intermediate – Advanced

  • Time: 45 minutes

Building blocks

Remix the scaffold

Remix an existing web application on Koji that implements basic elements of your favorite framework.

React
Vanilla JS

Add the backend service

This template relies on a backend service to store the total number of votes from different users and sessions.

Create backend/package.json to define the dependencies for your backend service.

Tip
Click the plus (+) next to any folder in the source code, and enter the full path: backend/package.json. This path creates a backend directory and a package.json file in it.
{
  "name": "koji-project-backend",
  "version": "1.0.0",
  "scripts": {
    "compile": "babel src -d dist --copy-files --ignore \"node_modules/**/*.js\"",
    "prestart": "koji-vcc watch &",
    "start-dev": "koji-vcc watch & NODE_ENV=development babel-watch -L --watch ../.koji/ src/server.js",
    "start": "NODE_ENV=production node dist/server.js"
  },
  "dependencies": {
    "@withkoji/database": "^1.0.19",
    "@withkoji/vcc": "^1.1.27",
    "babel-polyfill": "^6.26.0",
    "body-parser": "^1.18.3",
    "cors": "^2.8.5",
    "express": "4.16.3"
  },
  "devDependencies": {
    "@babel/cli": "7.2.3",
    "@babel/core": "7.2.2",
    "@babel/preset-env": "7.3.1",
    "babel-watch": "git+https://github.com/kmagiera/babel-watch.git"
  }
}

Install the dependencies.

cd backend
npm install

Create backend/.babelrc to define the Babel runtime configuration and to add extra syntax that Babel can transpile.

{
  "presets": ["@babel/preset-env"],
  "plugins": ["@babel/plugin-proposal-object-rest-spread"]
}

Create backend/src/votes.js and implement the counting logic by using the asynchronous get and set commands in the @withkoji/database package.

import Database from '@withkoji/database';

export default function (app) {

    //Get total votes by fetching all database entries and summing them up
    app.get('/votes', async (req, res) => {
        const database = new Database();
        const rawResults = await database.get('votes');

        let sum = 0;

        for (let i = 0; i < rawResults.length; i++) {
            sum += rawResults[i].amount;
        }

        res.status(200).json({
            success: true,
            votes: sum
        });
    });

    //Submit votes
    //Handling each user (userId) as a separate entry in order to decentralize vote submission
    //This helps prevent errors when syncing votes in the app
    //body: {userId: string, votes: number}
    app.put('/votes/add', async (req, res) => {
        const database = new Database();
        const promises = [];

        const userId = req.body.userId;
        const votes = req.body.votes;

        const rawResults = await database.get('votes');

        const results = rawResults.filter((e) => e.userId === userId);

        //If there's no existing entry/user, create a new one
        const entry = results.length > 0 ? results[0] : { userId: userId, amount: 0 };
        entry.amount += votes;

        promises.push(async () => {
            await database.set('votes', userId, entry);
        });

        await Promise.all(promises.map(async p => p()));

        res.status(200).json({
            success: true
        });
    });
}

Add backend/src/server.js to create an express server that acts as an entry point to handle backend requests from the frontend.

import 'babel-polyfill';
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import { VccMiddleware } from '@withkoji/vcc';
import votes from './votes';

// Create server
const app = express();

// Specifically enable CORS for pre-flight options requests
app.options('*', cors())

// Enable body parsers for reading POST data. We set up this app to
// accept JSON bodies and x-www-form-urlencoded bodies. If you wanted to
// process other request types, like form-data or graphql, you would need
// to include the appropriate parser middlewares here.
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  limit: '2mb',
  extended: true,
}));

// CORS allows these API routes to be requested directly by browsers
app.use(cors());
app.use(VccMiddleware.express);

// Disable caching
app.use((req, res, next) => {
  res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
  res.header('Expires', '-1');
  res.header('Pragma', 'no-cache');
  next();
});

// Enable routes we want to use
votes(app);

// Start server
app.listen(process.env.PORT || 3333, null, async err => {
  if (err) {
    console.log(err.message);
  }
  console.log('[koji] backend started');
});

Add the backend service to .koji/project/develop.json so Koji can detect the service.

"backend": {
  "path": "backend",
  "port": 3333,
  "startCommand": "npm run start-dev",
  "events": {
    "started": "[koji] backend started",
    "log": "[koji-log]"
  }
}

Add the backend service to .koji/project/deploy.json to ensure the backend is built and deployed when you publish the template.

"backend": {
  "output": "backend",
  "type": "dynamic",
  "commands": [
    "cd backend",
    "npm install",
    "export NODE_ENV=production && npm run compile"
  ]
}

Restart your project environment to allow Koji to add the new service to the service map.

  • On the left side of the editor, go to Advanced > Remote environment and click Force restart project.

    Note
    Your editor might disconnect during the reboot. You can reconnect when it has completed. You should see that a backend terminal has been added at the bottom.

Install the packages

Install @withkoji/dispatch to add real time, multi-user functionality in your frontend Koji application.

npm install --save @withkoji/dispatch

Install @withkoji/vcc to expose Visual Customization Controls (VCCs), dynamically update custom values, and display your template correctly in the Koji feed.

npm install --save @withkoji/vcc
Note
To use this package, you must configure the package.json file to run the watcher (koji-vcc watch) concurrently with your development server.

Add the template logic

Create frontend/utils/DataHandler.js as a utility file to manage the data handling for the application.

import { v4 as uuidv4 } from 'uuid'
import Dispatch from '@withkoji/dispatch'

class DataHandler {
    constructor(instantRemixing) {
        this.totalVotes = 0;
        this.votesToSend = 0;
        this.instantRemixing = instantRemixing; //Reference to an already existing instantRemixing
        this.backendPath = this.instantRemixing.get(['serviceMap', 'backend']);

        //Custom user id helps prevent syncing problems
        this.userId = uuidv4();
        localStorage.setItem('userId', this.userId);

        this.submissionTimeout = null;
        this.isVerifyingResults = false;
        this.isSubmitting = false;

        //Fetch votes from the backend periodically just in case of any desyncs
        setInterval(() => this.fetchVotes(), 5000);
    }

    //Call this from the parent component on mount
    initialize() {
        this.initializeDispatch();
        this.fetchVotes();
        this.scheduleSubmission();
    }

    setVotesCallback(setVotes) {
        this.setVotes = setVotes; //Callback to set the vote amount on the component
    }

    //Use dispatch to monitor tap events for all connected users

    //Queue up frontend votesToSend locally in the background

    // Submit votes from frontend to backend

    // Fetch counts from backend database
}

export default DataHandler;

Initialize Dispatch with the projectId metadata to identify your Koji application. Add a listener to monitor the new_tap event, which fires whenever a user taps or clicks the icon.

initializeDispatch() {
    this.dispatch = new Dispatch({
        projectId: this.instantRemixing.get(['metadata', 'projectId']),
    });
    this.dispatch.connect();
    this.dispatch.on('new_tap', () => { this.addTapFromDispatch() });
}

addTapFromDispatch() {
    this.totalVotes++;
    this.setVotes(this.totalVotes)
}

Periodically get the data from the backend to ensure you have the most up-to-date counts from the database.

fetchVotes() {
  if (this.backendPath)
  fetch(`${this.backendPath}/votes`, {
      method: "get",
      headers: {
          "Accept": "application/json",
          "Content-Type": "application/json"
      }
  })
      .then((response) => response.json())
      .then((responseJson) => {
          //Only update votes if the fetched votes are higher than local
          if (responseJson.votes > this.totalVotes) this.totalVotes = responseJson.votes;
          this.setVotes(this.totalVotes);
      })
      .catch(err => {
          console.log(err);
      })
}

Submit vote counts to the backend for long-term storage in the database.

//Using this to schedule submission to make sure we only have one check in the queue at any time
scheduleSubmission(timeout = 1000) {
    if (this.isSubmitting) return;

    this.isSubmitting = true;

    clearTimeout(this.submissionTimeout);
    this.submissionTimeout = setTimeout(() => {
        this.submitVotes();
    }, timeout);
}

submitVotes = () => {
  const body = JSON.stringify({
      userId: this.userId,
      votes: this.votesToSend
  });

  //Reset vote pool so we start piling up a new batch of votes
  this.votesToSend = 0;
  if (this.backendPath)
  fetch(`${this.backendPath}/votes/add`, {
      method: "put",
      headers: {
          "Accept": "application/json",
          "Content-Type": "application/json"
      },
      body: body,
  })
      .then((response) => response.json())
      .then((responseJson) => {
          this.isSubmitting = false;
          this.scheduleSubmission();
      })
      .catch(err => {
          console.log(err);
          this.isSubmitting = false;
          this.scheduleSubmission();
      });
}

When a user taps the icon, increment the counter and emit the new_tap event with dispatch to allow other clients to respond to the event.

addVote() {
    this.votesToSend++;
    this.dispatch.emitEvent('new_tap');
}

Update the file that contains the main application code.

React
Vanilla JS
React

frontend/common/App.js

Vanilla JS

frontend/index.js

Import the required libraries.

React
Vanilla JS
React
import { FeedSdk, InstantRemixing } from '@withkoji/vcc';
import DataHandler from '../utils/DataHandler';
Vanilla JS
import { FeedSdk, InstantRemixing } from '@withkoji/vcc';
import DataHandler from './utils/DataHandler';

Use FeedSdk to display the template in the Koji feed.

const feed = new FeedSdk();

Initialize DataHandler.

const dataHandler = new DataHandler(instantRemixing);
dataHandler.initialize();

Track the remix state and the number of votes.

React
Vanilla JS
React
const [remix, setRemix] = useState(false);
const [votes, setVotes] = useState(-1);
Vanilla JS
var remixing = false;
var votes = -1;

// Add a listener to handle state changes between remixing and not
instantRemixing.onSetRemixing((isRemixing) => {
    remixing = isRemixing;
    if (isRemixing) {
        startRemix();
    } else {
        stopRemix();
    }
});

// Callback to set the vote count value for display
const setVotes = (voteCount) => {
    votes = voteCount;
    let voteDisplay = document.getElementById('voteCount');
    voteDisplay.textContent = votes;
};

Set the callback for DataHandler to update the total votes. Set instant remixing to ready to allow remixes and use FeedSdk so your application can be displayed in the Koji feed.

React
Vanilla JS
React
useEffect(() => {
    dataHandler.setVotesCallback(setVotes);
    instantRemixing.ready();
    feed.load();

    instantRemixing.onSetRemixing((isRemixing) => {
        setRemix(isRemixing)
    });
});
Vanilla JS
instantRemixing.ready();
feed.load();
dataHandler.setVotesCallback(setVotes);

Update the rendered output to display the current vote count, and implement tap events for instant remixing and for counting votes. Use the instantRemix.onPresentControl callback to display the VCC controls when users are remixing your application.

React
Vanilla JS
React
<Title onClick={()=>{
    if (remix)instantRemixing.onPresentControl(['strings', 'title']);
}} remixing={remix?1:0}>{title}</Title>
<Icon onClick={()=>{
    if (remix) {
        instantRemixing.onPresentControl(['images', 'icon']);
    } else {
        dataHandler.addVote();
    }
}}remixing={remix?1:0} style={{ icon }} />
{votes === -1 &&
    <p>Fetching...</p>
}
{votes !== -1 &&
    <p>This icon has been clicked {votes} times</p>
}
Vanilla JS
// Render application
const render = () => {
    document.body.innerHTML = `
        <h1 style="font-size:${titleSize}px" id="title">${title}</h1>
        <img id="logo" src="${optimizeURL(logo)}"/>
        <p>This icon has been clicked <span id="voteCount">${votes}</span> times</p>
    `;
};

// Add click event listener to expose title VCC
document.addEventListener('click', function (event) {
    if (!event.target.matches('#title')) return;
    if (remixState !== 'text') return;
    event.preventDefault();

    instantRemixing.onPresentControl(['settings', 'titleOptions']);
});

// Add click event listener to expose logo VCC
document.addEventListener('click', function (event) {
    if (!event.target.matches('#logo')) return;
    if (remixState === 'logo') {
        event.preventDefault();
        instantRemixing.onPresentControl(['settings', 'logo']);
    } else {
        dataHandler.addVote();
    }
});

Apply conditional styling so remixers can easily tell what can be customized.

React
Vanilla JS
Example 1. React
const Container = styled.div`
    background-color: ${({ style: { backgroundColor }}) => backgroundColor};
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: ${({ style: { textColor }}) => textColor};
    text-align: center;
`;

const edit = 'border: 3px dashed lightgrey; cursor: pointer;';
const normal = 'border: 3px solid transparent';

const Title = styled.h1`
    ${props=>props.remixing ? edit : normal};
`;

...

const Icon = styled.div`
    animation: ${AppLogoSpin} infinite 20s linear;
    height: 50vmin;
    width: 50vmin;
    background-image: url(${({ style: { icon }}) => icon});
    background-size: contain;
    background-repeat: no-repeat;
    margin-bottom: 16px;
    ${props=>props.remixing ? edit : normal};
    cursor: pointer;
`;
Note
Remove the pointer-events: none; property definition from the Icon component.
Vanilla JS
.edit {
    border: 2px dashed grey;
}
p {
    color: white;
}

Add entitlements

Create .koji/project/entitlements.json and enable instant remix support and listing in the Koji feed.

{
  "entitlements": {
    "InstantRemixing": true,
    "FeedEvents": true
  }
}

Test

Use the tools in the Koji editor to test template functionality. For example:

  • Multi-user experience – Use the remote staging button Remote staging button in the preview pane to open the template in multiple tabs and ensure the vote counter remains synchronized.

  • Template default view – Refresh the Live preview tab.

  • Conditional styling of editable elements – In the live preview, switch between Default and Remix mode.

  • VCC targeting – In Remix mode, click each editable element. The corresponding VCC should open.

  • Remix functionality – In the JSON file, switch to Visual view, and use VCC editor to customize values. The template should update immediately.

  • Template styles in another browser tab or device – To open a preview in a new browser tab, click the remote staging button Remote staging button in the preview pane. To open a preview on a mobile device, click the QR code button QR code button in the preview pane to display a QR code, then scan the QR code with your mobile device.

Tip
If you want to reset the counter in the published template, open it in the Koji debugger, and then delete the test entries from the votes database view. See Using the Koji debugger.

Publish

Click Publish Now and enter a Name (defines the permalink URL to your template), Description (displays along with your template on Koji), and other publish settings, as desired. Then, click Publish New Version.

When publishing is completed, click the link in the message to view your live template on Koji. Your template is now available to remix and share anywhere on the web. You can create a fun version for yourself and share it on your favorite site to see what your friends make.

"As Built" sample code

To see this blueprint as a completed template on Koji, visit the following link. From there, you can view the source code or remix the template into your own project.

The following code is a completed sample of the template logic described in this blueprint.

React
Vanilla JS
DataHandler.js
React
import React, { useEffect, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import { FeedSdk, InstantRemixing } from '@withkoji/vcc';
import DataHandler from '../utils/DataHandler';

const Container = styled.div`
    background-color: ${({ style: { backgroundColor }}) => backgroundColor};
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: ${({ style: { textColor }}) => textColor};
    text-align: center;
`;

const edit = 'border: 3px dashed lightgrey; cursor: pointer;';
const normal = 'border: 3px solid transparent';

const Title = styled.h1`
    ${props=>props.remixing ? edit : normal};
`;

const AppLogoSpin = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`;

const Icon = styled.div`
    animation: ${AppLogoSpin} infinite 20s linear;
    height: 50vmin;
    width: 50vmin;
    background-image: url(${({ style: { icon }}) => icon});
    background-size: contain;
    background-repeat: no-repeat;
    margin-bottom: 16px;
    ${props=>props.remixing ? edit : normal};
    cursor: pointer;
`;

const instantRemixing = new InstantRemixing();
const feed = new FeedSdk();
const dataHandler = new DataHandler(instantRemixing);
dataHandler.initialize();

const App = () => {
    const [title, setTitle] = useState(instantRemixing.get(['strings', 'title']));
    const [icon, setIcon] = useState(instantRemixing.get(['images', 'icon']));
    const [backgroundColor, setBackgroundColor] = useState(instantRemixing.get(['colors', 'background']));
    const [textColor, setTextColor] = useState(instantRemixing.get(['colors', 'text']));
    const [remix, setRemix] = useState(false);
    const [votes, setVotes] = useState(-1);

    useEffect(() => {
        dataHandler.setVotesCallback(setVotes);
        instantRemixing.ready();
        feed.load();

        instantRemixing.onSetRemixing((isRemixing) => {
            setRemix(isRemixing)
        });
    });
    useEffect(() => {
        instantRemixing.onValueChanged(([scope = '', key = ''], value) => {
            if (scope === 'strings' && key === 'title') setTitle(value);
            if (scope === 'images' && key === 'icon') setIcon(value);
            if (scope === 'colors' && key === 'background') setBackgroundColor(value);
            if (scope === 'colors' && key === 'text') setTextColor(value);
        });
    });


    return (
        <Container style={{ backgroundColor, textColor }}>
            <Title onClick={()=>{
                if (remix)instantRemixing.onPresentControl(['strings', 'title']);
            }} remixing={remix?1:0}>{title}</Title>
            <Icon onClick={()=>{
                if (remix) {
                    instantRemixing.onPresentControl(['images', 'icon']);
                } else {
                    dataHandler.addVote();
                }
            }}remixing={remix?1:0} style={{ icon }} />
            {votes === -1 &&
                <p>Fetching...</p>
            }
            {votes !== -1 &&
                <p>This icon has been clicked {votes} times</p>
            }
        </Container>
    );
};

export default App;
Vanilla JS
import './styles.css';
import { LoadingIndicator } from 'skytree-koji';
import { FeedSdk, InstantRemixing } from '@withkoji/vcc';
import DataHandler from './utils/DataHandler';

// initialize the loading indicator
const loadHandle = LoadingIndicator.ofDocument().init();

const optimizeURL = url => `${url}?fit=bounds&width=${window.innerWidth/2}&height=${window.innerHeight/2}&optimize=medium`;

var instantRemixing = new InstantRemixing();

// Alert Koji we are ready to use instantRemixing
instantRemixing.ready();

var title = instantRemixing.get(['settings', 'titleOptions', 'title']);
var titleSize = instantRemixing.get(['settings', 'titleOptions', 'fontSize']);
var logo = instantRemixing.get(['settings', 'logo']);

var remixState = 'text';
var remixing = false;
var votes = -1;

// Preload images before calling render
var imagesLoaded;
const preload = () => {
    let images = [logo];
    imagesLoaded = 0;
    for (let i = 0; i < images.length; i++) {
        let imagePreload = new Image();
        imagePreload.onload = () => {
            imagesLoaded++;
            if (imagesLoaded === images.length) {
                loadHandle.release();
                render();
                if (remixing) {
                    startRemix();
                }
            }
        };
        imagePreload.src = optimizeURL(logo);
    }
};

// Render application
const render = () => {
    document.body.innerHTML = `
        <h1 style="font-size:${titleSize}px" id="title">${title}</h1>
        <img id="logo" src="${optimizeURL(logo)}"/>
        <p>This icon has been clicked <span id="voteCount">${votes}</span> times</p>
    `;
};

// Inform Koji Feed that the app is ready to be displayed
const feed = new FeedSdk();
feed.load();

// Initialize the data handler
const dataHandler = new DataHandler(instantRemixing);
dataHandler.initialize();

// Add a listener to the on change event of instant remixing to update the title
instantRemixing.onValueChanged((path, newValue) => {
    if (path[0] && path[0] === 'settings' && path[1] && path[1] === 'titleOptions') {
        let titleElement = document.getElementById('title');
        titleElement.textContent = newValue.title;
        titleElement.style.fontSize = newValue.fontSize+"px";
    }
    if (path[0] && path[0] === 'settings' && path[1] && path[1] === 'logo') {
        let logoImage = document.getElementById('logo');
        logoImage.src = optimizeURL(newValue);
    }
});

// Add a listener to handle state changes between remixing and not
instantRemixing.onSetRemixing((isRemixing) => {
    remixing = isRemixing;
    if (isRemixing) {
        startRemix();
    } else {
        stopRemix();
    }
});

// Callback to set the vote count value for display
const setVotes = (voteCount) => {
    votes = voteCount;
    let voteDisplay = document.getElementById('voteCount');
    voteDisplay.textContent = votes;
};

dataHandler.setVotesCallback(setVotes);

// Modify elements to display state for remixing
const startRemix = () => {
    if (remixState === 'text') {
      let titleElement = document.getElementById('title');
      titleElement.classList.add('edit');
    } else if (remixState === 'logo') {
      let logoImage = document.getElementById('logo');
      logoImage.classList.add('edit');
    }

};

// Modify elements to display state for previewing
const stopRemix = () => {
    let titleElement = document.getElementById('title');
    let logoImage = document.getElementById('logo');
    titleElement.classList.remove('edit');
    logoImage.classList.remove('edit');
};

// Add click event listener to expose title VCC
document.addEventListener('click', function (event) {
    if (!event.target.matches('#title')) return;
    if (remixState !== 'text') return;
    event.preventDefault();

    instantRemixing.onPresentControl(['settings', 'titleOptions']);
});

// Add click event listener to expose logo VCC
document.addEventListener('click', function (event) {
    if (!event.target.matches('#logo')) return;
    if (remixState === 'logo') {
        event.preventDefault();
        instantRemixing.onPresentControl(['settings', 'logo']);
    } else {
        dataHandler.addVote();
    }
});

// Set current remixing state
instantRemixing.onSetCurrentState((templateState) => {
  remixState = templateState;
  if (remixing) {
    stopRemix();
    startRemix();
  }
});

// Add feed event listener to modify title class
feed.onPlaybackStateChanged((isPlaying) => {
  let titleElement = document.getElementById('title');
  if (isPlaying) {
    titleElement.classList.add('animate');
  } else {
    titleElement.classList.remove('animate');
  }
});
DataHandler.js
import { v4 as uuidv4 } from 'uuid'
import Dispatch from '@withkoji/dispatch'

class DataHandler {
    constructor(instantRemixing) {
        this.totalVotes = 0;
        this.votesToSend = 0;
        this.instantRemixing = instantRemixing; //Reference to an already existing instantRemixing
        this.backendPath = this.instantRemixing.get(['serviceMap', 'backend']);

        //Custom user id helps prevent syncing problems
        this.userId = uuidv4();
        localStorage.setItem('userId', this.userId);

        this.submissionTimeout = null;
        this.isVerifyingResults = false;
        this.isSubmitting = false;

        //Fetch votes from the backend periodically just in case of any desyncs
        setInterval(() => this.fetchVotes(), 5000);
    }

    //Call this from the parent component on mount
    initialize() {
        this.initializeDispatch();
        this.fetchVotes();
        this.scheduleSubmission();
    }

    setVotesCallback(setVotes) {
        this.setVotes = setVotes; //Callback to set the vote amount on the component
    }

    //Use dispatch to monitor tap events for all connected users
    initializeDispatch() {
        this.dispatch = new Dispatch({
            projectId: this.instantRemixing.get(['metadata', 'projectId']),
        });
        this.dispatch.connect();
        this.dispatch.on('new_tap', () => { this.addTapFromDispatch() });
    }

    addTapFromDispatch() {
        this.totalVotes++;
        this.setVotes(this.totalVotes)
    }

    //Queue up frontend votesToSend locally in the background
    addVote() {
        this.votesToSend++;
        this.dispatch.emitEvent('new_tap');
    }

    //Using this to schedule submission to make sure we only have one check in the queue at any time
    scheduleSubmission(timeout = 1000) {
        if (this.isSubmitting) return;

        this.isSubmitting = true;

        clearTimeout(this.submissionTimeout);
        this.submissionTimeout = setTimeout(() => {
            this.submitVotes();
        }, timeout);
    }

    // Submit votes from frontend to backend
    submitVotes = () => {
      const body = JSON.stringify({
          userId: this.userId,
          votes: this.votesToSend
      });

      //Reset vote pool so we start piling up a new batch of votes
      this.votesToSend = 0;
      if (this.backendPath)
      fetch(`${this.backendPath}/votes/add`, {
          method: "put",
          headers: {
              "Accept": "application/json",
              "Content-Type": "application/json"
          },
          body: body,
      })
          .then((response) => response.json())
          .then((responseJson) => {
              this.isSubmitting = false;
              this.scheduleSubmission();
          })
          .catch(err => {
              console.log(err);
              this.isSubmitting = false;
              this.scheduleSubmission();
          });
    }

    // Fetch counts from backend database
    fetchVotes() {
      if (this.backendPath)
      fetch(`${this.backendPath}/votes`, {
          method: "get",
          headers: {
              "Accept": "application/json",
              "Content-Type": "application/json"
          }
      })
          .then((response) => response.json())
          .then((responseJson) => {
              //Only update votes if the fetched votes are higher than local
              if (responseJson.votes > this.totalVotes) this.totalVotes = responseJson.votes;
              this.setVotes(this.totalVotes);
          })
          .catch(err => {
              console.log(err);
          })
    }
}

export default DataHandler;