Commit a94610ca authored by peterh-gr's avatar peterh-gr
Browse files

Refactored campaign counters

I wanted to reduce the amount of props passing between the components
and combine the character component into one, simpler component.

We want the counter to keep updating indefinitely on the page. It should
check for new totals every 5 seconds.

Issue #48277
parent 9aebaac0
import React, {useState} from 'react';
import {Counter} from './counter';
import {useInterval} from './use_interval';
export function CampaignTotals(props) {
const {showCounter, donateProccessorBaseUrl} = props;
const [donation, setDonation] = useState(0);
const [totalDonation, setTotalDonation] = useState(0);
const [totalMatchedDonation, setTotalMatchedDonation] = useState(0);
const [countersState, setCountersState] = useState('flashing');
const [resolvedIndex, setResolvedIndex] = useState(6);
if (showCounter != "True") {
return null;
const counters = [
{
'cssClass': 'supporters',
'field': 'totalDonations',
'label': 'Number of Donations',
},
{
'cssClass': 'total-donated',
'field': 'totalAmount',
'label': 'Total Donated',
},
{
'cssClass': 'total-matched',
'field': 'amountWithMozilla',
'label': "Total Raised with Mozilla's Match",
}
];
export function CampaignTotals(props) {
const {donateProccessorBaseUrl} = props;
const [amounts, setAmounts] = useState({
'totalDonations': 0,
'totalAmount': 0,
'amountWithMozilla': 0
});
const requestTotal = async () => {
const options = {
......@@ -26,82 +41,20 @@ export function CampaignTotals(props) {
if (response_data['errors'].length > 0) {
console.log(`Error fetching campaign totals from ${donateProccessorBaseUrl}/campaign-totals`, response_data);
} else {
handleCampaignTotalsResponseData(response_data['data']);
setAmounts(response_data['data']);
}
}
};
const convertAmountToArrayOfStringToDisplay = (amount) => {
var amountArray = convertNumberToArrayOfString(amount);
while (amountArray.length < 6) {
amountArray.unshift("0");
}
return amountArray;
}
const convertNumberToArrayOfString = (number) => {
return parseInt(number).toString().split("");
}
const counters = [
{
'type': 'supporters',
'label': 'Number of Donations',
'amountArrayWithoutZeroPrefixes': convertNumberToArrayOfString(donation).length,
'amountArray': convertAmountToArrayOfStringToDisplay(donation)
},
{
'type': 'total-donated',
'label': 'Total Donated',
'amountArrayWithoutZeroPrefixes': convertNumberToArrayOfString(totalDonation).length,
'amountArray': convertAmountToArrayOfStringToDisplay(totalDonation)
},
{
'type': 'total-matched',
'label': "Total Raised with Mozilla's Match",
'amountArrayWithoutZeroPrefixes': convertNumberToArrayOfString(totalMatchedDonation).length,
'amountArray': convertAmountToArrayOfStringToDisplay(totalMatchedDonation)
}
];
const getMaxAmountArray = () => {
const lengthOfArrays = counters.map(counter => counter.amountArray.length);
return Math.max(...lengthOfArrays);
}
const handleCampaignTotalsResponseData = (data) => {
setDonation(data['totalDonations']);
setTotalDonation(data['totalAmount']);
setTotalMatchedDonation(data['amountWithMozilla']);
setResolvedIndex(getMaxAmountArray());
setCountersState('resolving');
}
if (donation == 0 && totalDonation == 0 && totalMatchedDonation == 0) {
requestTotal();
}
if (countersState == 'resolving') {
if (resolvedIndex >= 0) {
setTimeout(() => {
setResolvedIndex(resolvedIndex - 1);
}, 500);
} else {
setCountersState('resolved');
}
}
useInterval(requestTotal, 5000, true);
function renderCounters(item) {
return (
<Counter
key={item.type}
type={item.type}
key={item.cssClass}
cssClass={item.cssClass}
label={item.label}
amountArray={item.amountArray}
amountArrayWithoutZeroPrefixes={item.amountArrayWithoutZeroPrefixes}
countersState={countersState}
resolvedIndex={resolvedIndex}
maxArrayLength={getMaxAmountArray()}
amount={amounts[item.field]}
/>
);
}
......
import React from 'react';
import React, {useState} from 'react';
import {CounterCharacter} from './counter_character';
import {FlashingCounterCharacter} from './flashing_counter_character';
import {useInterval} from './use_interval';
function convertNumberToArrayOfString(number) {
let characterArray = parseInt(number).toString().split("");
while (characterArray.length < 6) {
characterArray.unshift('0');
}
return characterArray;
}
function createCharacterStates(amount, state) {
let characterArray = convertNumberToArrayOfString(amount);
let characterStates = [];
for (const character of characterArray) {
characterStates.push({character: character, state: state});
}
return characterStates;
}
export function Counter(props) {
const {type, label, amountArray, countersState, resolvedIndex, maxArrayLength, amountArrayWithoutZeroPrefixes} = props;
const {amount, label, cssClass} = props;
const [counterState, setCounterState] = useState('flashing');
const [characterStates, setCharacterStates] = useState(() => createCharacterStates(0, 'flashing'));
const [resolvedCharacterCount, setResolvedCharacterCount] = useState(0);
const counterResolvedIndex= resolvedIndex-(maxArrayLength-amountArray.length);
if (counterState == 'flashing' && amount != 0) {
setCounterState('resolving');
setCharacterStates(createCharacterStates(amount, 'resolving'));
}
let delay = null;
if (counterState == 'resolving') {
delay = 500;
}
useInterval(() => {
if (resolvedCharacterCount == characterStates.length) {
setCounterState('resolved');
return;
}
const index = characterStates.length - resolvedCharacterCount - 1;
const newCharacterStates = [...characterStates];
newCharacterStates[index].state = 'resolved';
setCharacterStates(newCharacterStates);
setResolvedCharacterCount(resolvedCharacterCount + 1);
}, delay);
function renderLetters(item, index) {
const renderCharacter = (character, index) => {
return (
<CounterCharacter
key={index}
amountArrayLength={amountArray.length}
characterIndex={index}
number={item}
amountArrayWithoutZeroPrefixes={amountArrayWithoutZeroPrefixes}
counterResolvedIndex={counterResolvedIndex}
countersState={countersState}
character={character.character}
state={character.state}
/>
);
}
return (
<div className={type}>
<div className={cssClass}>
<div className="characters">
{amountArray.map((item, index) => renderLetters(item, index))}
{characterStates.map((character, index) => renderCharacter(character, index))}
</div>
<div className="label">{label}</div>
</div>
......
import React, {useState} from 'react';
import React, {useEffect, useState} from 'react';
import {useInterval} from './use_interval';
export function CounterCharacter(props) {
const {number, characterIndex, amountArrayWithoutZeroPrefixes, amountArrayLength, counterResolvedIndex, countersState} = props;
const [characterFlashingState, setCharacterFlashingState] = useState('covered');
if (countersState == 'flashing' || (countersState == 'resolving' && characterIndex < counterResolvedIndex)) {
// Flashing Characters
setTimeout(() => {
var newState = '';
if (Math.random() <= 0.5) {
newState = 'covered';
}
setCharacterFlashingState(newState);
}, 100);
const characterClasses = ['character', characterFlashingState];
const {character, state} = props;
const [flash, setFlash] = useState(false);
const renderCover = () => {
if (state == 'resolved') {
return null;
} else {
return (
<div className={characterClasses.join(" ")}>
<div className="cover"></div>
0
</div>
);
} else {
// Resolved Characters
var characterState = 'covered';
const lastResolvedIndex = amountArrayLength - amountArrayWithoutZeroPrefixes;
var label = 0;
const isCharacterResolved = () => {
if (characterIndex >= lastResolvedIndex) {
return true;
}
return false;
};
let delay = null;
if (state == 'flashing' || state == 'resolving') {
delay = 100;
}
if (isCharacterResolved()) {
characterState = 'resolved';
label = number;
useInterval(() => {
if (Math.random() <= 0.5) {
setFlash(true);
} else {
characterState = '';
setFlash(false);
}
}, delay);
const characterClasses = ['character', characterState];
const renderCover = () => {
if (isCharacterResolved()) {
return null;
} else {
return (
<div className="covered"></div>
);
let characterClasses = ['character', state];
if (flash) {
characterClasses.push('covered');
}
let displayCharacter = '0';
if (state == 'resolved') {
displayCharacter = character;
}
return (
<div className={characterClasses.join(" ")}>
{renderCover()}
{label}
{displayCharacter}
</div>
);
}
}
import React, {useState} from 'react';
export function FlashingCounterCharacter(props) {
const {countersState} = props;
const [characterState, setCharacterState] = useState('covered');
if (countersState == 'resolved') {
return null;
}
setTimeout(() => {
var newState = '';
if (Math.random() <= 0.5) {
newState = 'covered';
}
setCharacterState(newState);
}, 100);
const characterClasses = ['character', characterState];
return (
<div className={characterClasses.join(" ")}>
<div className="cover"></div>
0
</div>
);
}
import React, { useState, useEffect, useRef } from 'react';
export function useInterval(callback, delay, immediate = false) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (immediate) {
tick();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment