@readr-media/react-election-widgets
Version:
551 lines (491 loc) • 17.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _api = _interopRequireDefault(require("./utils/api.js"));
var _errors = _interopRequireDefault(require("@twreporter/errors"));
var _events = _interopRequireDefault(require("events"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/**
* @typedef {'error'|'councilMember'|'mayor'|'president'|'legislator'|'referendum'} SupportEventType
* @typedef {import('../react-components/votes-comparison/typedef').CouncilMemberElection} CouncilMemberElection
* @typedef {import('../react-components/votes-comparison/typedef').CountyMayorElection} CountyMayorElection
* @typedef {import('../react-components/votes-comparison/typedef').LegislatorElection} LegislatorElection
* @typedef {import('../react-components/votes-comparison/typedef').LegislatorPartyElection} LegislatorPartyElection
* @typedef {import('../react-components/votes-comparison/typedef').LegislatorIndigenousElection} LegislatorIndigenousElection
* @typedef {import('../react-components/votes-comparison/typedef').PresidentElection} PresidentElection
* @typedef {import('../react-components/votes-comparison/typedef').ReferendumElection} ReferendumElection
*/
/**
* Example: 抓 2018 年台北市市議員的選舉結果
*
* let dataLoader = new Loader({
* apiUrl: 'https://whoareyou-gcs.readr.tw/elections',
* version: 'v2',
* })
*
* // For server side rendering,
* // load data once.
* try {
* // fetch data once
* const data = await dataLoader.loadCouncilMemberData({
* year: '2018',
* district: 'taipeiCity',
* })
* } catch(err) {
* // handle error
* }
*
* // For client side rendering,
* // load data periodically and make React component re-render
* useEffect(() => {
* const handleError = (err) => {
* // do something for loading error
* }
*
* const handleData = (data) => {
* // call React component `setState`
* setState(data)
* }
*
* dataLoader.addEventListener('error', handleError)
* dataLoader.addEventListener('councilMember', setState)
*
* // after register event listener
* // start to load data periodically
* dataLoader.loadCouncilMemberData({
* year: '2018',
* district: 'taipeiCity',
* toLoadPeriodically: true,
* loadInterval: 300, // seconds
* })
*
* return () => {
* dataLoader.removeEventListener('error', handleError)
* dataLoader.removeEventListener('councilMember', setState)
* dataLoader = null
* }
* }, [])
*
*/
let Loader = /*#__PURE__*/function () {
/** @type events.EventEmitter */
/**
* @constructor
* @param {Object} props
* @param {string} [props.apiUrl='https://whoareyou-gcs.readr.tw']
* @param {string} [props.version=v2]
*/
function Loader({
apiUrl = 'https://whoareyou-gcs.readr.tw/elections',
version = 'v2'
}) {
_classCallCheck(this, Loader);
_defineProperty(this, "eventEmitter", null);
_defineProperty(this, "apiUrl", 'https://whoareyou-gcs.readr.tw/elections');
_defineProperty(this, "version", 'v2');
_defineProperty(this, "timers", {});
this.eventEmitter = new _events.default.EventEmitter();
this.apiUrl = apiUrl;
this.version = version;
}
/**
* Load data from web service.
* @param {Object} props
* @param {string} props.year
* @param {string} props.type
* @param {string} [props.subType]
* @param {string} props.district
* @throws Error
* @returns {Promise<Object>}
*/
_createClass(Loader, [{
key: "loadData",
value: async function loadData({
year,
type,
subtype,
district
}) {
try {
const dataUrl = subtype ? `${this.apiUrl}/${this.version}/${year}/${type}/${subtype}/${district}.json` : `${this.apiUrl}/${this.version}/${year}/${type}/${district}.json`;
const axiosRes = await _api.default.get(dataUrl);
return axiosRes === null || axiosRes === void 0 ? void 0 : axiosRes.data;
} catch (err) {
const annotatedErr = _errors.default.helpers.annotateAxiosError(err);
throw annotatedErr;
}
}
/**
* @typedef {'all'|'normal'|'indigenous'|'mountainIndigenous'|'plainIndigenous'} CouncilMemberType
*/
/**
* Load data from web service.
* @param {Object} props
* @param {string} props.year
* @param {string} props.district - county/city name, see `Loader.electionDistricts` for more info
* @param {CouncilMemberType[]} [props.includes=['all']]
* @param {number} [props.periodicalLoading=-1]
* @throws Error
* @returns {Promise<CouncilMemberElection>}
*/
}, {
key: "loadCouncilMemberDataForElectionMapProject",
value: async function loadCouncilMemberDataForElectionMapProject({
year,
district,
includes: _includes = ['all']
}) {
var _includes2, _data, _data$districts;
let data;
data = await this.loadData({
type: 'councilMember',
year,
district
});
let includes = _includes;
if (((_includes2 = includes) === null || _includes2 === void 0 ? void 0 : _includes2.indexOf('all')) > -1) {
includes = ['normal', 'indigenous', 'mountainIndigenous', 'plainIndigenous'];
}
const districts = [];
(_data = data) === null || _data === void 0 ? void 0 : (_data$districts = _data.districts) === null || _data$districts === void 0 ? void 0 : _data$districts.forEach(d => {
if (includes.indexOf(d === null || d === void 0 ? void 0 : d.type) > -1) {
switch (d === null || d === void 0 ? void 0 : d.type) {
case 'plainIndigenous':
{
d.fullDistrictName = `第${d.districtName}選區(平地)`;
districts.push(d);
break;
}
case 'mountainIndigenous':
{
d.fullDistrictName = `第${d.districtName}選區(山地)`;
districts.push(d);
break;
}
case 'indigenous':
case 'normal':
{
d.fullDistrictName = `第${d.districtName}選區`;
districts.push(d);
break;
}
default:
{// do nothing
}
}
}
});
data.districts = districts;
return data;
}
/**
* Load data from web service.
* @param {Object} props
* @param {string} props.year
* @param {string} props.district - county/city name, see `Loader.electionDistricts` for more info
* @param {boolean} [props.toLoadPeriodically=false]
* @param {number} [props.loadInterval] - available only when `toLoadPeriodically=true`, and its value must be greater than 30. Unit is second.
* @throws Error
* @returns {Promise<void|CouncilMemberElection>}
*/
}, {
key: "loadCouncilMemberData",
value: function loadCouncilMemberData({
year,
district,
toLoadPeriodically = false,
loadInterval
}) {
if (toLoadPeriodically) {
return this.loadDataPeriodically({
type: 'councilMember',
year,
district,
interval: loadInterval
});
}
return this.loadData({
type: 'councilMember',
year,
district
});
}
/**
* @param {Object} props
* @param {string} props.year
* @param {'plainIndigenous' | 'mountainIndigenous' | 'party' | 'district'} props.subtype
* @param {string} [props.district] - available only when `type` is `district`
* @param {boolean} [props.toLoadPeriodically=false]
* @param {number} [props.loadInterval] - available only when `toLoadPeriodically=true`, and its value must be greater than 30. Unit is second.
* @throws Error
* @returns {Promise<void|LegislatorElection|LegislatorPartyElection|LegislatorIndigenousElection}
*/
}, {
key: "loadLegislatorData",
value: function loadLegislatorData({
year,
subtype,
district: _district,
toLoadPeriodically = false,
loadInterval
}) {
let district = '';
switch (subtype) {
case 'party':
case 'plainIndigenous':
case 'mountainIndigenous':
{
district = 'all';
break;
}
case 'district':
{
district = _district;
break;
}
case 'recall':
{
district = 'all';
break;
}
default:
{
throw new Error('subtype should be either "plainIndigenous", "mountainIndigenous", "party" or "district"');
}
}
if (toLoadPeriodically) {
return this.loadDataPeriodically({
type: 'legislator',
subtype,
year,
district,
interval: loadInterval
});
}
return this.loadData({
type: 'legislator',
subtype,
year,
district
});
}
/**
* @param {Object} props
* @param {string} props.year
* @param {string} props.recallType
* @param {string} props.district
* @throws Error
* @returns {Promise<void|LegislatorElection|LegislatorPartyElection|LegislatorIndigenousElection}
*/
}, {
key: "loadRecallData",
value: function loadRecallData({
year,
recallType = 'recall',
district
}) {
return this.loadData({
year,
type: recallType,
district: `district/${district}`
});
}
/**
* @param {Object} props
* @param {string} props.year
* @param {boolean} [props.toLoadPeriodically=false]
* @param {number} [props.loadInterval] - available only when `toLoadPeriodically=true`, and its value must be greater than 30. Unit is second.
* @throws Error
* @returns {Promise<void|PresidentElection>}
*/
}, {
key: "loadPresidentData",
value: function loadPresidentData({
year,
toLoadPeriodically = false,
loadInterval
}) {
if (toLoadPeriodically) {
return this.loadDataPeriodically({
type: 'president',
year,
district: 'all',
interval: loadInterval
});
}
return this.loadData({
type: 'president',
year,
district: 'all'
});
}
/**
* @param {Object} props
* @param {string} props.year
* @param {boolean} [props.toLoadPeriodically=false]
* @param {number} [props.loadInterval] - available only when `toLoadPeriodically=true`, and its value must be greater than 30. Unit is second.
* @throws Error
* @returns {Promise<void|CountyMayorElection>}
*/
}, {
key: "loadMayorData",
value: function loadMayorData({
year,
toLoadPeriodically = false,
loadInterval
}) {
if (toLoadPeriodically) {
return this.loadDataPeriodically({
type: 'mayor',
year,
district: 'all',
interval: loadInterval
});
}
return this.loadData({
type: 'mayor',
year,
district: 'all'
});
}
/**
* @param {Object} props
* @param {string} props.year
* @param {boolean} [props.toLoadPeriodically=false]
* @param {number} [props.loadInterval] - available only when `toLoadPeriodically=true`, and its value must be greater than 30. Unit is second.
* @throws Error
* @returns {Promise<void|ReferendumElection>}
*/
}, {
key: "loadReferendumData",
value: function loadReferendumData({
year,
toLoadPeriodically = false,
loadInterval
}) {
if (toLoadPeriodically) {
return this.loadDataPeriodically({
type: 'referendum',
year,
district: 'all',
interval: loadInterval
});
}
return this.loadData({
type: 'referendum',
year,
district: 'all'
});
}
/**
* Load data from web service,
* and take advantage of event emitter to pass data to the event subscribers.
* This function will take `props.interval` first.
* If `props.interval` not provided, the function
* will take web service response header `Cache-Control`'s `max-age` into account.
* Value of `max-age` will be used to `setTimeout` a timer.
* When time is up, the timer will load data,
* and emit it again.
* if `max-age` is not defined, the default timeout will be 3600.
*
* @param {Object} props
* @param {string} props.year
* @param {string} props.type
* @param {string} props.district
* @param {number} [props.interval]
* @returns {Promise<void>}
*/
}, {
key: "loadDataPeriodically",
value: async function loadDataPeriodically({
year,
type,
district,
interval
}) {
const url = `${this.apiUrl}/${this.version}/${year}/${type}/${district}.json`;
let axiosRes;
let annotatedErr;
try {
axiosRes = await _api.default.get(url);
} catch (err) {
annotatedErr = _errors.default.helpers.annotateAxiosError(err);
annotatedErr = _errors.default.helpers.wrap(annotatedErr, 'DataLoaderError', `Error to load data from ${url}`);
}
const minimumInterval = 0; // seconds
const defaultMaxAge = 3600; // seconds
let maxAge = defaultMaxAge;
if (annotatedErr) {
this.eventEmitter.emit('error', annotatedErr);
} else {
var _axiosRes, _axiosRes2, _axiosRes2$headers, _cacheControl$match;
this.eventEmitter.emit(type, (_axiosRes = axiosRes) === null || _axiosRes === void 0 ? void 0 : _axiosRes.data);
const cacheControl = (_axiosRes2 = axiosRes) === null || _axiosRes2 === void 0 ? void 0 : (_axiosRes2$headers = _axiosRes2.headers) === null || _axiosRes2$headers === void 0 ? void 0 : _axiosRes2$headers['cache-control'];
maxAge = interval > minimumInterval ? interval : parseInt((_cacheControl$match = cacheControl.match(/max-age=([\d]+)/)) === null || _cacheControl$match === void 0 ? void 0 : _cacheControl$match[1]);
if (isNaN(maxAge)) {
maxAge = defaultMaxAge;
}
}
this.timers[type] = setTimeout(async () => {
await this.loadDataPeriodically({
year,
type,
district,
interval
});
}, maxAge * 1000);
}
}, {
key: "clearTimer",
value: function clearTimer(eventType) {
var _this$timers;
if ((_this$timers = this.timers) !== null && _this$timers !== void 0 && _this$timers.hasOwnProperty(eventType)) {
clearTimeout(this.timers[eventType]);
delete this.timers[eventType];
}
}
/**
* @param {SupportEventType} eventType
* @param {(...args: any[]) => void} cb
* @returns void
*/
}, {
key: "addEventListener",
value: function addEventListener(eventType, cb) {
if (Loader.supportTypes.indexOf(eventType) > -1) {
this.eventEmitter.addListener(eventType, cb);
}
}
/**
* @param {SupportEventType} eventType
* @param {(...args: any[]) => void} cb
* @returns void
*/
}, {
key: "removeEventListener",
value: function removeEventListener(eventType, cb) {
if (Loader.supportTypes.indexOf(eventType) > -1) {
this.eventEmitter.removeListener(eventType, cb);
if (this.eventEmitter.listenerCount(eventType) === 0) {
this.clearTimer(eventType);
}
}
}
}]);
return Loader;
}(); // Event support types could be added or removed if necessarily
exports.default = Loader;
Loader.supportTypes = ['error', 'councilMember', 'mayor', 'president', 'legislator', 'referendum'];
Loader.electionTypes = ['president', // 總統
'legislator', // 立法委員
'mayor', // 縣市首長
'councilMember' // 縣市議員
];
Loader.electionYears = ['1994', '1997', '1998', '2001', '2002', '2005', '2006', '2009', '2010', '2012', '2014', '2016', '2018', '2020', '2022', '2024'];
Loader.electionDistricts = ['taipeiCity', 'newTaipeiCity', 'taoyuanCity', 'taichungCity', 'tainanCity', 'kaohsiungCity', 'hsinchuCounty', 'miaoliCounty', 'changhuaCounty', 'nantouCounty', 'yunlinCounty', 'chiayiCounty', 'pingtungCounty', 'yilanCounty', 'hualienCounty', 'taitungCounty', 'penghuCounty', 'kinmenCounty', 'lienchiangCounty', 'keelungCity', 'hsinchuCity', 'chiayiCity'];