add retry to fetching google sheets

This commit is contained in:
Daniel 2021-08-16 19:47:41 -04:00
parent d87403032b
commit 1359d840dc
23 changed files with 360 additions and 321 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
@media only screen and (min-width:975px){.pack{max-width:90%}.pack .card{width:20%}}@media only screen and (max-width:975px){.pack{max-width:90%}.pack .card{width:40%}}.packsim input[type=number]::-webkit-inner-spin-button,.packsim input[type=number]::-webkit-outer-spin-button{opacity:1}.enterthecode .card{width:250px;height:350px}.pack{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:center;margin:auto}.pack .card{margin:5px}.pack .card:hover{width:250px;height:350px}.pack .card>div{width:150px;height:210px;background-size:cover;box-sizing:content-box;-webkit-transition:-webkit-transform 1s,opacity 1s,background 1s,width 1s,height 1s,font-size 1s,top 1s,left 1s;-webkit-border-radius:5px;-o-transition-property:width,height,-o-transform,background,font-size,opacity,top,left;-o-transition-duration:1s,1s,1s,1s,1s,1s,1s,1s;-moz-transition-property:width,height,-o-transform,background,font-size,opacity,top,left;-moz-transition-duration:1s,1s,1s,1s,1s,1s,1s,1s;transition-property:width,height,transform,background,font-size,opacity,top,left;transition-duration:1s,1s,1s,1s,1s,1s,1s,1s}.pack .card>div:hover{width:250px;height:350px}.pack .card.locations{width:150px;height:210px}.pack .card.locations:hover{width:250px;height:350px}.pack .card.locations div{width:210px;height:150px;transform-origin:50% 70%;transform:rotate(270deg)}.pack .card.locations div:hover{width:350px;height:250px}.pack .stats{height:100%;width:100%;text-align:left}.pack .stats span{position:relative;color:#000;display:block;left:10px;font-size:12px}.pack .stats span:first-of-type{top:130px}.pack .stats span:nth-of-type(2){top:128px}.pack .stats span:nth-of-type(3){top:126px}.pack .stats span:nth-of-type(4){top:124px}.pack .stats span:nth-of-type(5){text-align:right;top:125px;left:-11px;font-size:14px}.pack .card:hover .stats span{left:20px;font-size:14px;font-weight:700}.pack .card:hover .stats span:first-of-type{top:220px}.pack .card:hover .stats span:nth-of-type(2){top:229px}.pack .card:hover .stats span:nth-of-type(3){top:237px}.pack .card:hover .stats span:nth-of-type(4){top:245px}.pack .card:hover .stats span:nth-of-type(5){text-align:right;top:258px;left:-21px;font-size:20px}
@media only screen and (min-width:975px){.pack{max-width:90%}.pack .card{width:20%}}@media only screen and (max-width:975px){.pack{max-width:90%}.pack .card{width:40%}}.packsim input[type=number]::-webkit-inner-spin-button,.packsim input[type=number]::-webkit-outer-spin-button{opacity:1}.enterthecode .card{height:350px;width:250px}.pack{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:center;margin:auto}.pack .card{margin:5px}.pack .card:hover{height:350px;width:250px}.pack .card>div{background-size:cover;-webkit-border-radius:5px;box-sizing:content-box;height:210px;-webkit-transition:-webkit-transform 1s,opacity 1s,background 1s,width 1s,height 1s,font-size 1s,top 1s,left 1s;-o-transition-duration:1s,1s,1s,1s,1s,1s,1s,1s;-moz-transition-duration:1s,1s,1s,1s,1s,1s,1s,1s;transition-duration:1s,1s,1s,1s,1s,1s,1s,1s;-o-transition-property:width,height,-o-transform,background,font-size,opacity,top,left;-moz-transition-property:width,height,-o-transform,background,font-size,opacity,top,left;transition-property:width,height,transform,background,font-size,opacity,top,left;width:150px}.pack .card>div:hover{height:350px;width:250px}.pack .card.locations{height:210px;width:150px}.pack .card.locations:hover{height:350px;width:250px}.pack .card.locations div{height:150px;transform:rotate(270deg);transform-origin:50% 70%;width:210px}.pack .card.locations div:hover{height:250px;width:350px}.pack .stats{height:100%;text-align:left;width:100%}.pack .stats span{color:#000;display:block;font-size:12px;left:10px;position:relative}.pack .stats span:first-of-type{top:130px}.pack .stats span:nth-of-type(2){top:128px}.pack .stats span:nth-of-type(3){top:126px}.pack .stats span:nth-of-type(4){top:124px}.pack .stats span:nth-of-type(5){font-size:14px;left:-11px;text-align:right;top:125px}.pack .card:hover .stats span{font-size:14px;font-weight:700;left:20px}.pack .card:hover .stats span:first-of-type{top:220px}.pack .card:hover .stats span:nth-of-type(2){top:229px}.pack .card:hover .stats span:nth-of-type(3){top:237px}.pack .card:hover .stats span:nth-of-type(4){top:245px}.pack .card:hover .stats span:nth-of-type(5){font-size:20px;left:-21px;text-align:right;top:258px}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.icon14{height:14px;padding-bottom:2px}.icon16{height:16px}.icon20{height:20px}.icon24{height:24px}.bigger{font-size:14px}.name{-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all}.name .subname:before{content:"\a ";white-space:pre}.name .subname{font-size:13px;padding-bottom:4px;display:inherit}.lore{text-align:left;padding-left:10%;padding-right:10%}.lore .block,.lore .title{margin-top:6px;text-align:center;font-weight:700}.lore .title{font-size:18px}.lore .block{font-size:24px}.lore div{margin:0 0 6px;line-height:22px!important}.lore .set{padding-left:5%;padding-right:5%}.donate{margin-bottom:6px}.donate form a{border-bottom:none}@-webkit-keyframes love{to{-webkit-transform:scale(1.1)}}@-moz-keyframes love{to{-moz-transform:scale(1.1)}}@keyframes love{to{transform:scale(1.1)}}@media only screen and (min-width:975px){.with-love{color:#333}}.with-love div,.with-love span{font-size:14px!important;line-height:18px!important}.with-love div{padding-bottom:4px}.with-love .heart{font-size:1.4em;color:#ff79c6;-webkit-transform:scale(.9);-moz-transform:scale(.9);transform:scale(.9);-webkit-animation:love .5s linear infinite alternate-reverse;-moz-animation:love .5s infinite linear alternate-reverse;animation:love .5s linear infinite alternate-reverse}.with-love a{text-decoration:none;border-bottom:1px dotted}
.icon14{height:14px;padding-bottom:2px}.icon16{height:16px}.icon20{height:20px}.icon24{height:24px}.bigger{font-size:14px}.name{-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all}.name .subname:before{content:"\a ";white-space:pre}.name .subname{display:inherit;font-size:13px;padding-bottom:4px}.lore{padding-left:10%;padding-right:10%;text-align:left}.lore .block,.lore .title{font-weight:700;margin-top:6px;text-align:center}.lore .title{font-size:18px}.lore .block{font-size:24px}.lore div{line-height:22px!important;margin:0 0 6px}.lore .set{padding-left:5%;padding-right:5%}.donate{margin-bottom:6px}.donate form a{border-bottom:none}@-webkit-keyframes love{to{-webkit-transform:scale(1.1)}}@-moz-keyframes love{to{-moz-transform:scale(1.1)}}@keyframes love{to{transform:scale(1.1)}}@media only screen and (min-width:975px){.with-love{color:#333}}.with-love div,.with-love span{font-size:14px!important;line-height:18px!important}.with-love div{padding-bottom:4px}.with-love .heart{-webkit-animation:love .5s linear infinite alternate-reverse;-moz-animation:love .5s linear infinite alternate-reverse;animation:love .5s linear infinite alternate-reverse;color:#ff79c6;font-size:1.4em;-webkit-transform:scale(.9);-moz-transform:scale(.9);transform:scale(.9)}.with-love a{border-bottom:1px dotted;text-decoration:none}

View File

@ -1 +1 @@
(self.webpackChunkchaoticbackup=self.webpackChunkchaoticbackup||[]).push([[640],{8640:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(2122),a=n(6610),l=n(5991),o=n(379),c=n(6089),i=n(7608),s=n(7294),u=n(3727),f=n(2195);n(2188);function m(e){var t=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=(0,i.Z)(e);if(t){var a=(0,i.Z)(this).constructor;n=Reflect.construct(r,arguments,a)}else n=r.apply(this,arguments);return(0,c.Z)(this,n)}}var h=function(){return s.createElement("a",{href:"https://github.com/chaoticbackup",className:"name",rel:"noreferrer noopener",target:"_blank"},"Chaotic Backup Project")},d=function(e){var t=e.block,n=e.text,r=e.sets;return s.createElement("div",{className:"lore"},s.createElement("div",{className:"block"},t),n.map((function(e,t){return s.createElement("div",{key:t,dangerouslySetInnerHTML:{__html:e}})})),r.map((function(e,t){if(e.text&&e.text.length>0)return s.createElement("div",{className:"set",key:t},s.createElement("div",{className:"title"},e.title),e.text.map((function(e,t){return s.createElement("div",{key:t},e)})))})))},p=function(e){(0,o.Z)(n,e);var t=m(n);function n(){var e;(0,a.Z)(this,n);for(var r=arguments.length,l=new Array(r),o=0;o<r;o++)l[o]=arguments[o];return(e=t.call.apply(t,[this].concat(l))).state={lore:[]},e}return(0,l.Z)(n,[{key:"componentDidMount",value:function(){var e=this;fetch("/public/json/starter_lore.json").then((function(e){return e.json()})).then((function(t){e.setState({lore:t})})).catch((function(){e.setState({lore:[{block:"Unable to load lore...",text:[]}]})}))}},{key:"render",value:function(){return s.createElement("div",null,s.createElement("br",null),s.createElement("div",{className:"with-love"},s.createElement("div",null,"Welcome to the ",s.createElement(h,null),"."),s.createElement("span",null,"Built by fans for fans."),s.createElement("br",null),s.createElement("br",null),s.createElement("div",null,"Made with ",s.createElement("span",{className:"heart"},"♥")," by",s.createElement("br",null),"Danude Sandstorm (Project Lead)",s.createElement("br",null),"Chiodosin1 (Database Contributions)",s.createElement("br",null),"Afjak and Blitser (Art and Knowledge)"),s.createElement("div",null,"Do you like the site? You can donate to support it!"),s.createElement("div",{className:"donate"},s.createElement(f.U9,null)),s.createElement("div",{className:"lore"},"We were unsatisfied with the options on how to search for cards. I took the design of the old Chaotic website and added my own modernizations. With an extensive lists of search options in the ",s.createElement(u.rU,{to:"/collection"},"collection"),", you'll find deck building mores streamlined than ever before. Chaotic is full of rich lore, but unfortunately the best database of official lore disapeared when the ",s.createElement(u.rU,{to:"/portal"},"Portal to Perim")," disapeared along with the official site. You can again explore the official lore and information!"),s.createElement("br",null),this.state.lore.length>0?this.state.lore.map((function(e,t){return s.createElement(d,(0,r.Z)({key:t},e))})):"Loading lore entries..."))}}]),n}(s.Component)}}]);
"use strict";(self.webpackChunkchaoticbackup=self.webpackChunkchaoticbackup||[]).push([[640],{8640:(e,t,n)=>{n.r(t),n.d(t,{default:()=>u});var a=n(7462),l=n(5603),r=n(7294),o=n(3727),c=n(2195),i=(n(2188),function(){return r.createElement("a",{href:"https://github.com/chaoticbackup",className:"name",rel:"noreferrer noopener",target:"_blank"},"Chaotic Backup Project")}),s=function(e){var t=e.block,n=e.text,a=e.sets;return r.createElement("div",{className:"lore"},r.createElement("div",{className:"block"},t),n.map((function(e,t){return r.createElement("div",{key:t,dangerouslySetInnerHTML:{__html:e}})})),a.map((function(e,t){if(e.text&&e.text.length>0)return r.createElement("div",{className:"set",key:t},r.createElement("div",{className:"title"},e.title),e.text.map((function(e,t){return r.createElement("div",{key:t},e)})))})))},u=function(e){function t(){for(var t,n=arguments.length,a=new Array(n),l=0;l<n;l++)a[l]=arguments[l];return(t=e.call.apply(e,[this].concat(a))||this).state={lore:[]},t}(0,l.Z)(t,e);var n=t.prototype;return n.componentDidMount=function(){var e=this;fetch("/public/json/starter_lore.json").then((function(e){return e.json()})).then((function(t){e.setState({lore:t})})).catch((function(){e.setState({lore:[{block:"Unable to load lore...",text:[]}]})}))},n.render=function(){return r.createElement("div",null,r.createElement("br",null),r.createElement("div",{className:"with-love"},r.createElement("div",null,"Welcome to the ",r.createElement(i,null),"."),r.createElement("span",null,"Built by fans for fans."),r.createElement("br",null),r.createElement("br",null),r.createElement("div",null,"Made with ",r.createElement("span",{className:"heart"},"♥")," by",r.createElement("br",null),"Danude Sandstorm (Project Lead)",r.createElement("br",null),"Chiodosin1 (Database Contributions)",r.createElement("br",null),"Afjak and Blitser (Art and Knowledge)"),r.createElement("div",null,"Do you like the site? You can donate to support it!"),r.createElement("div",{className:"donate"},r.createElement(c.U9,null)),r.createElement("div",{className:"lore"},"We were unsatisfied with the options on how to search for cards. I took the design of the old Chaotic website and added my own modernizations. With an extensive lists of search options in the ",r.createElement(o.rU,{to:"/collection"},"collection"),", you'll find deck building mores streamlined than ever before. Chaotic is full of rich lore, but unfortunately the best database of official lore disapeared when the ",r.createElement(o.rU,{to:"/portal"},"Portal to Perim")," disapeared along with the official site. You can again explore the official lore and information!"),r.createElement("br",null),this.state.lore.length>0?this.state.lore.map((function(e,t){return r.createElement(s,(0,a.Z)({key:t},e))})):"Loading lore entries..."))},t}(r.Component)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

37
package-lock.json generated
View File

@ -3405,6 +3405,11 @@
"@types/react": "*"
}
},
"@types/retry": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
"integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g=="
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -10174,12 +10179,12 @@
}
},
"p-retry": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
"integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
"dev": true,
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz",
"integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==",
"requires": {
"retry": "^0.12.0"
"@types/retry": "^0.12.0",
"retry": "^0.13.1"
}
},
"p-try": {
@ -11336,10 +11341,9 @@
"dev": true
},
"retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
"dev": true
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="
},
"reusify": {
"version": "1.0.4",
@ -13225,6 +13229,15 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"p-retry": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
"integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
"dev": true,
"requires": {
"retry": "^0.12.0"
}
},
"resolve-cwd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
@ -13234,6 +13247,12 @@
"resolve-from": "^3.0.0"
}
},
"retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
"dev": true
},
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",

View File

@ -23,6 +23,7 @@
"lokijs": "^1.5.12",
"mobx": "^5.15.7",
"mobx-react": "^6.3.1",
"p-retry": "^4.6.1",
"prop-types": "^15.7.2",
"react": "^16.14.0",
"react-collapsible": "^2.8.3",

View File

@ -1,274 +0,0 @@
import 'whatwg-fetch';
import loki from 'lokijs';
import { observable, observe, action } from "mobx";
import Cookies from 'universal-cookie';
const cookies = new Cookies();
class CollectionDB {
// Keeps track of what collections have been populated
@observable building = {};
constructor(API, format) {
this.api = API;
this.format = format;
this.setupDB(format);
}
async getSpreadsheetData(spreadsheet, type, callback) {
this.api.getSpreadsheet(spreadsheet, (data) => {
callback(data.map((item) => {
const temp = {};
delete item.content;
for (const key of Object.keys(item)) {
temp[key] = item[key].$t;
}
temp["gsx$type"] = type;
return temp;
}));
});
}
// example format
// this.setup(this.api.urls.Attacks["portal"], "Attack", (data) => {});
@action
async setupType(type, resolve) {
if (this.building.hasOwnProperty(type)) {
const uc_type = type.charAt(0).toUpperCase() + type.slice(1);
if (this.building[type].get() == "built") {
// Check if data has been updated
this.getSpreadsheetData(this.api.urls[uc_type][this.format], uc_type, (data) => {
const cookie = cookies.get(`${this.format}_${type}`);
if (cookie) {
if ((new Date(data[0].updated)) > (new Date(cookie))) {
this[type].clear();
this[type].insert(data);
cookies.set(`${this.format}_${type}`, data[0].updated, { path: '/' });
}
}
else {
cookies.set(`${this.format}_${type}`, data[0].updated, { path: '/' });
}
});
return resolve();
}
if (this.building[type].get() == "building") {
const disposer = observe(this.building[type], (change) => {
disposer();
resolve();
});
return disposer;
}
if (this.building[type].get() == "setup") {
this.building[type].set("building");
// check if the collection already exists in memory
if (this[type].data.length == 0) {
return this.getSpreadsheetData(this.api.urls[uc_type][this.format], uc_type, (data) => {
this[type].insert(data);
this.building[type].set("built");
return resolve();
});
}
else {
this.building[type].set("built");
return resolve();
}
}
}
else {
// Wait until the database is initialized
this.building[type] = observable.box("wait");
const disposer = observe(this.building[type], () => {
disposer();
return this.setupType(type, resolve);
});
return disposer;
}
}
@action
setupDB(format) {
const db = new loki(`chaotic_${format}.db`, {
autosave: true,
autoload: true,
autoloadCallback: databaseInitialize.bind(this),
autosaveInterval: 4000,
persistenceMethod: 'localStorage'
});
this.db = db;
function databaseInitialize() {
["attacks","battlegear", "creatures", "locations", "mugic"]
.forEach((type) => {
// check if the db already exists in memory
const entries = db.getCollection(type);
if (entries === null || entries.data.length === 0) {
this[type] = db.addCollection(type);
if (this.building[type])
this.building[type].set("setup");
else
this.building[type] = observable.box("setup");
}
else {
this[type] = entries;
if (this.building[type])
this.building[type].set("built");
else
this.building[type] = observable.box("built");
}
});
}
}
purgeDB = () => {
this.db.deleteDatabase();
}
}
class API {
@observable portal = null;
@observable cards = null;
@observable urls = null;
instance = null;
static base_url = "https://spreadsheets.google.com/feeds/list/";
static data_format = "/od6/public/values?alt=json";
// + "/od6/public/basic?alt=json"; // Alternate data format
get base_image() { return "https://drive.google.com/uc?id=" }
get thumb_missing() { return "1JYjPzkv74IhzlHTyVh2niTDyui73HSfp" }
get card_back() { return "https://i.imgur.com/xbeDBRJ.png" }
// Singleton
static getInstance() {
if (!this.instance) { this.instance = new API() }
return this.instance;
}
static path(spreadsheetID) {
return API.base_url + spreadsheetID + API.data_format;
}
// Wrapper
path(spreadsheetID) {
return API.path(spreadsheetID);
}
constructor() {
this.setupDB();
}
async getSpreadsheet(spreadsheet, callback) {
fetch(spreadsheet)
.then((response) => {
return response.json();
}).then((json) => {
return callback(json.feed.entry);
}).catch((err) => {
console.error('parsing failed', err);
return callback(null);
});
}
// This sets up urls and kicks off db
setupDB() {
// let base_spreadsheet = "1cUNmwV693zl2zqbH_IG4Wz8o9Va_sOHe7pAZF6M59Es";
try {
const urls = {};
const data = require('./meta_spreadsheet.json');
// this.getSpreadsheet(API.path(API.base_spreadsheet), (data) => {
// if (data == null) throw "no data from base_spreadsheet";
data.forEach((d) => {
if (!urls[d.gsx$type.$t]) urls[d.gsx$type.$t] = {};
urls[d.gsx$type.$t][d.gsx$subtype.$t] = API.path(d.gsx$url.$t);
});
this.urls = urls;
// });
this.portal = new CollectionDB(this, 'portal');
this.cards = new CollectionDB(this, 'cards');
}
catch (err) {
console.error('setting up database failed', err);
}
}
// Input format
// [{cards: 'attacks'}, {portal: 'attacks'}]
async LoadDB(collection) {
return new Promise((resolve, reject) => {
if (this.urls !== null &&
this.portal !== null &&
this.cards !== null
) {
this.buildCollection(collection)
.then(() => {
resolve();
})
.catch(() => {});
}
else resolve();
});
}
// Input format
// [{cards: 'attacks'}, {portal: 'attacks'}]
async buildCollection(input) {
return await Promise.all(input.map((item) => {
return new Promise((resolve, reject) => {
if ('cards' in item)
return this.cards.setupType(item.cards, resolve);
if ('portal' in item)
return this.portal.setupType(item.portal, resolve);
console.error('cards or portal');
return reject();
});
}));
}
/* Wrappers for images */
cardImage(card) {
if (card.gsx$ic && card.gsx$ic !== '') {
return card.gsx$ic;
} else if (card.gsx$image && card.gsx$image !== '') {
return this.base_image + card.gsx$image;
} else {
return this.card_back;
}
}
get tribes() {
return ["Danian", "Generic", "Mipedian", "M'arrillian", "OverWorld", "UnderWorld"];
}
// For the conversion of shorthand in database
get sets() {
return {
"DOP": "Dawn of Perim",
"ZOTH": "Zenith of the Hive",
"SS": "Silent Sands",
"MI": "Beyond the Doors",
"ROTO": "Rise of the Oligarch",
"TOTT": "Turn of the Tide",
"FUN": "Forged Unity",
"AU": "Alliance Unraveled",
"FAS": "Fire and Stone",
"OP1": "Organized Play 1",
"PE1": "Premium Edition 1",
"SAS": "Storm and Sea",
"EE": "Elemental Emperors",
"BR": "Beyond Rare",
"LR": "League Rewards",
"PROMO": "Promotional",
"PROTO": "Prototype"
};
}
purgeDB() {
this.cards.purgeDB();
this.portal.purgeDB();
window.location.reload();
}
}
export default API.getInstance();

View File

@ -0,0 +1,145 @@
import 'whatwg-fetch';
import { observable } from "mobx";
import pRetry from 'p-retry';
import CollectionDB from './CollectionDB';
import spreadsheet_data from './meta_spreadsheet.json';
import { Card } from '../common/definitions';
type card_type = 'attacks' | 'battlegear' | 'creatures' | 'locations' | 'mugic';
type data_type = 'cards' | 'portal';
class API {
@observable portal;
@observable cards;
@observable urls;
private static instance: API;
static base_url = "https://spreadsheets.google.com/feeds/list/";
static data_format = "/od6/public/values?alt=json";
// + "/od6/public/basic?alt=json"; // Alternate data format
get base_image() { return "https://drive.google.com/uc?id=" }
get thumb_missing() { return "1JYjPzkv74IhzlHTyVh2niTDyui73HSfp" }
get card_back() { return "https://i.imgur.com/xbeDBRJ.png" }
private constructor () {
// This sets up urls and kicks off db
// let base_spreadsheet = "1cUNmwV693zl2zqbH_IG4Wz8o9Va_sOHe7pAZF6M59Es";
try {
const urls = {};
// this.getSpreadsheet(API.path(API.base_spreadsheet), (data) => {
// if (data == null) throw "no data from base_spreadsheet";
spreadsheet_data.forEach((d) => {
if (!urls[d.gsx$type.$t]) urls[d.gsx$type.$t] = {};
urls[d.gsx$type.$t][d.gsx$subtype.$t] = this.path(d.gsx$url.$t);
});
this.urls = urls;
// });
this.portal = new CollectionDB(this, 'portal');
this.cards = new CollectionDB(this, 'cards');
}
catch (err) {
this.portal = null;
this.cards = null;
this.urls = null;
console.error('setting up database failed', err);
}
}
// Singleton
static getInstance(): API {
if (!API.instance) { API.instance = new API() }
return API.instance;
}
// Wrapper
path(spreadsheetID: string) {
return API.base_url + spreadsheetID + API.data_format;
}
async getSpreadsheet(spreadsheet: string, retry: boolean, callback: (data: any) => any) {
await pRetry(async () => {
return fetch(spreadsheet)
.then((response) => {
if (response.status === 404) throw new Error("Can't Open File");
return response.json();
})
.then((json) => {
callback(json.feed.entry);
})
.catch((err) => {
throw new pRetry.AbortError(err);
});
}, { retries: retry ? 3 : 0 });
}
// Input format
// [{cards: 'attacks'}, {portal: 'attacks'}]
async LoadDB(input: { [key in data_type]?: card_type }[]) {
if (this.urls && this.portal && this.cards) {
return Promise.all(input.map((item) => {
return new Promise((resolve, reject) => {
if ('cards' in item) {
return this.cards!.setupType(item.cards, resolve);
}
else if ('portal' in item) {
return this.portal!.setupType(item.portal, resolve);
}
else {
console.error('key must be cards or portal');
return reject();
}
});
}));
}
else return Promise.reject();
}
/* Wrappers for images */
cardImage(card: Card) {
if (card.gsx$ic && card.gsx$ic !== '') {
return card.gsx$ic;
} else if (card.gsx$image && card.gsx$image !== '') {
return this.base_image + card.gsx$image;
} else {
return this.card_back;
}
}
get tribes() {
return ["Danian", "Generic", "Mipedian", "M'arrillian", "OverWorld", "UnderWorld"];
}
// For the conversion of shorthand in database
get sets() {
return {
"DOP": "Dawn of Perim",
"ZOTH": "Zenith of the Hive",
"SS": "Silent Sands",
"MI": "Beyond the Doors",
"ROTO": "Rise of the Oligarch",
"TOTT": "Turn of the Tide",
"FUN": "Forged Unity",
"AU": "Alliance Unraveled",
"FAS": "Fire and Stone",
"OP1": "Organized Play 1",
"PE1": "Premium Edition 1",
"SAS": "Storm and Sea",
"EE": "Elemental Emperors",
"BR": "Beyond Rare",
"LR": "League Rewards",
"PROMO": "Promotional",
"PROTO": "Prototype"
};
}
purgeDB() {
if (this.cards) this.cards.purgeDB();
if (this.portal) this.portal.purgeDB();
window.location.reload();
}
}
export default API;

View File

@ -0,0 +1,128 @@
import loki from 'lokijs';
import { observable, observe, action } from "mobx";
import Cookies from 'universal-cookie';
const cookies = new Cookies();
class CollectionDB {
// Keeps track of what collections have been populated
@observable building = {};
constructor(API, format) {
this.api = API;
this.format = format;
this.setupDB(format);
}
// Wrapper that transforms spreadsheet data into expected object
async getSpreadsheetData(spreadsheet, type, retry, callback) {
this.api.getSpreadsheet(spreadsheet, retry, (data) => {
callback(data.map((item) => {
const temp = {};
delete item.content;
for (const key of Object.keys(item)) {
temp[key] = item[key].$t;
}
temp["gsx$type"] = type;
return temp;
}));
});
}
// example format
// this.setup(this.api.urls.Attacks["portal"], "Attack", (data) => {});
@action
async setupType(type, resolve) {
if (this.building.hasOwnProperty(type)) {
const uc_type = type.charAt(0).toUpperCase() + type.slice(1);
if (this.building[type].get() == "built") {
// Check if data has been updated
this.getSpreadsheetData(this.api.urls[uc_type][this.format], uc_type, false, (data) => {
const cookie = cookies.get(`${this.format}_${type}`);
if (cookie) {
if ((new Date(data[0].updated)) > (new Date(cookie))) {
this[type].clear();
this[type].insert(data);
cookies.set(`${this.format}_${type}`, data[0].updated, { path: '/' });
}
}
else {
cookies.set(`${this.format}_${type}`, data[0].updated, { path: '/' });
}
});
return resolve();
}
if (this.building[type].get() == "building") {
const disposer = observe(this.building[type], (change) => {
disposer();
resolve();
});
return disposer;
}
if (this.building[type].get() == "setup") {
this.building[type].set("building");
// check if the collection already exists in memory
if (this[type].data.length == 0) {
return this.getSpreadsheetData(this.api.urls[uc_type][this.format], uc_type, true, (data) => {
this[type].insert(data);
this.building[type].set("built");
return resolve();
});
}
else {
this.building[type].set("built");
return resolve();
}
}
}
else {
// Wait until the database is initialized
this.building[type] = observable.box("wait");
const disposer = observe(this.building[type], () => {
disposer();
return this.setupType(type, resolve);
});
return disposer;
}
}
@action
setupDB(format) {
const db = new loki(`chaotic_${format}.db`, {
autosave: true,
autoload: true,
autoloadCallback: databaseInitialize.bind(this),
autosaveInterval: 4000,
persistenceMethod: 'localStorage'
});
this.db = db;
function databaseInitialize() {
["attacks","battlegear", "creatures", "locations", "mugic"]
.forEach((type) => {
// check if the db already exists in memory
const entries = db.getCollection(type);
if (entries === null || entries.data.length === 0) {
this[type] = db.addCollection(type);
if (this.building[type])
this.building[type].set("setup");
else
this.building[type] = observable.box("setup");
}
else {
this[type] = entries;
if (this.building[type])
this.building[type].set("built");
else
this.building[type] = observable.box("built");
}
});
}
}
purgeDB = () => {
this.db.deleteDatabase();
}
}
export default CollectionDB;

View File

@ -0,0 +1,3 @@
import API from './API';
export default API.getInstance();

View File

@ -9,6 +9,7 @@ import search_api from './search';
@inject((stores, props, context) => props) @observer
export default class SearchCollection extends React.Component {
@observable loaded = false;
@observable loading = false;
@observable input;
@observable collapsed;
list = ["sets", "types", "rarity", "tribes", "elements", "mull", "gender"];
@ -187,12 +188,16 @@ export default class SearchCollection extends React.Component {
render() {
if (this.loaded == false) {
API.LoadDB([{ 'cards': 'attacks' }, { 'cards': 'battlegear' }, { 'cards': 'creatures' }, { 'cards': 'locations' }, { 'cards': 'mugic' }])
if (this.loading == false) {
this.loading = true;
API.LoadDB([{ 'cards': 'attacks' }, { 'cards': 'battlegear' }, { 'cards': 'creatures' }, { 'cards': 'locations' }, { 'cards': 'mugic' }])
.then(() => {
this.loaded = true;
this.loading = false;
this.search();
})
.catch(() => {});
}
return (<Loading />);
}

View File

@ -41,7 +41,7 @@ const EnterTheCode = () => {
});
useEffect(() => {
API.getSpreadsheet(API.path("1hzSojB76Me-P1qppxYR0oiHSU56jyK59x3DKm660ntc"), (data: any) => {
API.getSpreadsheet(API.path("1hzSojB76Me-P1qppxYR0oiHSU56jyK59x3DKm660ntc"), false, (data: any) => {
setFanData(data);
});
}, []);

View File

@ -13,6 +13,7 @@ import { sortCardName, thumb_link } from './common';
@inject((stores, props, context) => props) @observer
export default class Tribes extends React.Component {
@observable loaded = false;
@observable loading = false;
constructor() {
super();
@ -25,11 +26,15 @@ export default class Tribes extends React.Component {
// -> /{Tribe}/Mugic || /{Tribe}/Creatures
render() {
if (this.loaded == false) {
API.LoadDB([{ 'cards': 'creatures' }, { 'portal': 'creatures' }, { 'cards': 'mugic' }, { 'portal': 'mugic' }])
.then(() => {
this.loaded = true;
})
.catch(() => {});
if (this.loading == false) {
this.loading = true;
API.LoadDB([{ 'cards': 'creatures' }, { 'portal': 'creatures' }, { 'cards': 'mugic' }, { 'portal': 'mugic' }])
.then(() => {
this.loaded = true;
this.loading = false;
})
.catch(() => {});
}
return (<Loading />);
}

View File

@ -10,6 +10,7 @@ import { sortCardName, thumb_link } from './common';
@inject((stores, props, context) => props) @observer
export default class Category extends React.Component {
@observable loaded = false;
@observable loading = false;
scrollLeft(amount) {
document.getElementsByClassName('bottom_nav')[0].scrollLeft = (amount);
@ -19,11 +20,15 @@ export default class Category extends React.Component {
const type = this.props.type.toLowerCase();
if (this.loaded == false) {
API.LoadDB([{ 'cards': type }, { 'portal': type }])
.then(() => {
this.loaded = true;
})
.catch(() => {});
if (this.loading == false) {
this.loading = true;
API.LoadDB([{ 'cards': type }, { 'portal': type }])
.then(() => {
this.loaded = true;
this.loading = false;
})
.catch(() => {});
}
return (<Loading />);
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import API from '../SpreadsheetData';
import { Loading } from '../Snippets';
import { observable } from "mobx";
import { observer, inject } from 'mobx-react';
import loki from 'lokijs';
@ -39,6 +40,7 @@ export default class SearchPortal extends React.Component {
@inject((stores, props, context) => props) @observer
class DBSearch extends React.Component {
@observable loaded = false;
@observable loading = false;
constructor() {
super();
@ -47,14 +49,18 @@ class DBSearch extends React.Component {
render() {
if (this.loaded == false) {
API.LoadDB([
{ 'portal': 'attacks' }, { 'portal': 'battlegear' }, { 'portal': 'creatures' }, { 'portal': 'locations' }, { 'portal': 'mugic' },
{ 'cards': 'attacks' }, { 'cards': 'battlegear' }, { 'cards': 'creatures' }, { 'cards': 'locations' }, { 'cards': 'mugic' }
]).then(() => {
this.loaded = true;
})
.catch(() => {});
return (<span>Loading...</span>);
if (this.loading == false) {
this.loading = true;
API.LoadDB([
{ 'portal': 'attacks' }, { 'portal': 'battlegear' }, { 'portal': 'creatures' }, { 'portal': 'locations' }, { 'portal': 'mugic' },
{ 'cards': 'attacks' }, { 'cards': 'battlegear' }, { 'cards': 'creatures' }, { 'cards': 'locations' }, { 'cards': 'mugic' }
]).then(() => {
this.loaded = true;
this.loading = false;
})
.catch(() => {});
}
return (<Loading />);
}
const { string } = this.props;

View File

@ -84,14 +84,10 @@ export default class SingleCreature extends React.Component {
<hr />
<div>
<strong>Disciplines: </strong>
{card_data.gsx$courage}
<Discipline discipline="courage" />&nbsp;
{card_data.gsx$power}
<Discipline discipline="power" />&nbsp;
{card_data.gsx$speed}
<Discipline discipline="speed" />&nbsp;
{card_data.gsx$wisdom}
<Discipline discipline="wisdom" />
{card_data.gsx$courage}<Discipline discipline="courage" />&nbsp;
{card_data.gsx$power}<Discipline discipline="power" />&nbsp;
{card_data.gsx$wisdom}<Discipline discipline="wisdom" />&nbsp;
{card_data.gsx$speed}<Discipline discipline="speed" />
</div>
<hr />
<div>