UNPKG

@readr-media/react-election-widgets

Version:
551 lines (491 loc) 17.7 kB
"use strict"; 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'];