mirror of
https://github.com/tcgdex/cards-database.git
synced 2025-04-22 19:02:10 +00:00
feat: Add status Dashboard (#187)
* feat: Add status Dashboard Still need some polishing like using the compiler instead of the live DB Signed-off-by: Avior <github@avior.me> * refactor: Simplified compiler files generators Signed-off-by: Avior <florian.bouillon@delta-wings.net> * chore: Add step to compiler for stats and optimize Signed-off-by: Avior <github@avior.me> * refactor: Remove unused variable Signed-off-by: Avior <github@avior.me>
This commit is contained in:
parent
8ef099273e
commit
762ce389c1
@ -6,7 +6,7 @@ const bw11: Set = {
|
||||
|
||||
name: {
|
||||
en: "Legendary Treasures",
|
||||
fr: "Legendary Treasures",
|
||||
// fr: "Trésors Légendaires", // ONLY PTCGO
|
||||
pt: "Tesouros Lendários"
|
||||
},
|
||||
|
||||
|
@ -5,8 +5,7 @@ const bog: Set = {
|
||||
id: "bog",
|
||||
|
||||
name: {
|
||||
en: "Best of game",
|
||||
fr: "Best of game"
|
||||
en: "Best of game"
|
||||
},
|
||||
|
||||
serie: serie,
|
||||
|
@ -5,8 +5,7 @@ const sp: Set = {
|
||||
id: "sp",
|
||||
|
||||
name: {
|
||||
en: "Sample",
|
||||
fr: "Sample"
|
||||
en: "Sample"
|
||||
},
|
||||
|
||||
serie: serie,
|
||||
|
1
meta/definitions/api.d.ts
vendored
1
meta/definitions/api.d.ts
vendored
@ -23,6 +23,7 @@ interface variants {
|
||||
reverse?: boolean;
|
||||
holo?: boolean;
|
||||
firstEdition?: boolean;
|
||||
wPromo?: boolean
|
||||
}
|
||||
|
||||
export interface SetResume {
|
||||
|
@ -121,6 +121,7 @@ type Variants {
|
||||
holo: Boolean!
|
||||
normal: Boolean!
|
||||
reverse: Boolean!
|
||||
wPromo: Boolean!
|
||||
}
|
||||
|
||||
##################
|
||||
@ -135,6 +136,7 @@ type Set {
|
||||
name: String!
|
||||
symbol: String
|
||||
serie: Serie!
|
||||
releaseDate: String!
|
||||
}
|
||||
|
||||
type CardCount {
|
||||
|
@ -779,6 +779,7 @@ components:
|
||||
- holo
|
||||
- normal
|
||||
- reverse
|
||||
- wPromo
|
||||
type: object
|
||||
properties:
|
||||
normal:
|
||||
@ -789,6 +790,8 @@ components:
|
||||
type: boolean
|
||||
firstEdition:
|
||||
type: boolean
|
||||
wPromo:
|
||||
type: boolean
|
||||
hp:
|
||||
type: number
|
||||
example: 80
|
||||
|
10
server/compiler/compilerInterfaces.d.ts
vendored
10
server/compiler/compilerInterfaces.d.ts
vendored
@ -1,7 +1,3 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export interface Endpoint<Index extends {} = {}, Item extends {} = {}, SubItem extends {} = {}, C = undefined> {
|
||||
index(common: C): Promise<Index | undefined>
|
||||
item(common: C): Promise<Record<string, Item> | undefined>
|
||||
sub?(common: C, item: string): Promise<Record<string, SubItem> | undefined>
|
||||
common?(): Promise<C>
|
||||
}
|
||||
import { SupportedLanguages } from '../../interfaces'
|
||||
|
||||
export type FileFunction = (lang: SupportedLanguages) => Promise<any>
|
||||
|
@ -1,30 +1,10 @@
|
||||
import { Card as CardSingle, CardResume } from '../../../meta/definitions/api'
|
||||
import { Card, Languages } from '../../../interfaces'
|
||||
import { Endpoint } from '../compilerInterfaces'
|
||||
import { cardToCardSimple, cardToCardSingle, getCards } from '../utils/cardUtil'
|
||||
|
||||
type CardList = Array<CardResume>
|
||||
|
||||
export default class implements Endpoint<CardList, CardSingle, Record<string, unknown>, Array<[string, Card]>> {
|
||||
|
||||
public constructor(
|
||||
private lang: keyof Languages
|
||||
) {}
|
||||
|
||||
public async index(common: Array<[string, Card]>): Promise<CardList> {
|
||||
return Promise.all(common.map((c) => cardToCardSimple(c[0], c[1], this.lang)))
|
||||
}
|
||||
|
||||
public async item(common: Array<[string, Card]>): Promise<Record<string, CardSingle>> {
|
||||
const items: Record<string, CardSingle> = {}
|
||||
for await (const card of common) {
|
||||
items[`${card[1].set.id}-${card[0]}`] = await cardToCardSingle(card[0], card[1], this.lang)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
public async common(): Promise<Array<[string, Card]>> {
|
||||
return getCards(this.lang)
|
||||
}
|
||||
import { SupportedLanguages } from '../../../interfaces'
|
||||
import { FileFunction } from '../compilerInterfaces'
|
||||
import { cardToCardSingle, getCards } from '../utils/cardUtil'
|
||||
|
||||
const fn: FileFunction = async (lang: SupportedLanguages) => {
|
||||
const common = await getCards(lang)
|
||||
return await Promise.all(common.map((card) => cardToCardSingle(card[0], card[1], lang)))
|
||||
}
|
||||
|
||||
export default fn
|
||||
|
@ -1,32 +1,10 @@
|
||||
import { Serie as SerieSingle, SerieResume } from '../../../meta/definitions/api'
|
||||
import { Languages, Serie } from '../../../interfaces'
|
||||
import { Endpoint } from '../compilerInterfaces'
|
||||
import { getSeries, serieToSerieSimple, serieToSerieSingle } from '../utils/serieUtil'
|
||||
|
||||
type SerieList = Array<SerieResume>
|
||||
|
||||
export default class implements Endpoint<SerieList, SerieSingle, Record<string, any>, Array<Serie>> {
|
||||
|
||||
public constructor(
|
||||
private lang: keyof Languages
|
||||
) {}
|
||||
|
||||
public async index(common: Array<Serie>): Promise<Array<SerieResume>> {
|
||||
return Promise.all(common.map((s) => serieToSerieSimple(s, this.lang)))
|
||||
}
|
||||
|
||||
public async item(common: Array<Serie>): Promise<Record<string, SerieSingle>> {
|
||||
const items: Record<string, SerieSingle> = {}
|
||||
for await (const val of common) {
|
||||
const gen = await serieToSerieSingle(val, this.lang)
|
||||
const name = val.name[this.lang] as string
|
||||
items[val.id] = gen
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
public async common(): Promise<Array<Serie>> {
|
||||
return getSeries(this.lang)
|
||||
}
|
||||
import { SupportedLanguages } from '../../../interfaces'
|
||||
import { FileFunction } from '../compilerInterfaces'
|
||||
import { getSeries, serieToSerieSingle } from '../utils/serieUtil'
|
||||
|
||||
const fn: FileFunction = async (lang: SupportedLanguages) => {
|
||||
const common = await getSeries(lang)
|
||||
return await Promise.all(common.map((val) => serieToSerieSingle(val, lang)))
|
||||
}
|
||||
|
||||
export default fn
|
||||
|
@ -1,56 +1,11 @@
|
||||
import { Set as SetSingle, Card as CardSingle, SetResume } from '../../../meta/definitions/api'
|
||||
import { getSets, isSetAvailable, setToSetSimple, setToSetSingle } from '../utils/setUtil'
|
||||
import { Languages, Set } from '../../../interfaces'
|
||||
import { Endpoint } from '../compilerInterfaces'
|
||||
import { cardToCardSingle, getCards } from '../utils/cardUtil'
|
||||
import { getSets, setToSetSingle } from '../utils/setUtil'
|
||||
import { SupportedLanguages } from '../../../interfaces'
|
||||
import { FileFunction } from '../compilerInterfaces'
|
||||
|
||||
type SetList = Array<SetResume>
|
||||
|
||||
export default class implements Endpoint<SetList, SetSingle, CardSingle, Array<Set>> {
|
||||
|
||||
public constructor(
|
||||
private lang: keyof Languages
|
||||
) {}
|
||||
|
||||
public async index(common: Array<Set>): Promise<SetList> {
|
||||
const sets = common
|
||||
.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1)
|
||||
|
||||
const tmp: SetList = await Promise.all(sets.map((el) => setToSetSimple(el, this.lang)))
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
public async item(common: Array<Set>): Promise<Record<string, SetSingle>> {
|
||||
const sets = await Promise.all(common
|
||||
.map((set) => setToSetSingle(set, this.lang)))
|
||||
const res: Record<string, SetSingle> = {}
|
||||
|
||||
for (const set of sets) {
|
||||
res[set.id] = set
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
public async common(): Promise<Array<Set>> {
|
||||
return getSets(undefined, this.lang)
|
||||
}
|
||||
|
||||
public async sub(common: Array<Set>, item: string): Promise<Record<string, CardSingle> | undefined> {
|
||||
const set = common.find((s) => s.name[this.lang] === item || s.id === item)
|
||||
|
||||
if (!set || !isSetAvailable(set, this.lang)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const lit = await getCards(this.lang, set)
|
||||
const list: Record<string, CardSingle> = {}
|
||||
for await (const card of lit) {
|
||||
list[card[0]] = await cardToCardSingle(card[0], card[1], this.lang)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
const fn: FileFunction = async (lang: SupportedLanguages) => {
|
||||
const common = await getSets(undefined, lang)
|
||||
return await Promise.all(common.map((set) => setToSetSingle(set, lang)))
|
||||
}
|
||||
|
||||
export default fn
|
||||
|
42
server/compiler/endpoints/stats.ts
Normal file
42
server/compiler/endpoints/stats.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { getSets, setToSetSingle } from '../utils/setUtil'
|
||||
import { SupportedLanguages } from '../../../interfaces'
|
||||
import { FileFunction } from '../compilerInterfaces'
|
||||
import { getCards } from '../utils/cardUtil'
|
||||
import { getSeries } from '../utils/serieUtil'
|
||||
|
||||
interface Stats {
|
||||
count: number
|
||||
total: number
|
||||
images: number
|
||||
sets: Record<string, Record<string, {name: string, count: number, images: number}>>
|
||||
}
|
||||
|
||||
const fn: FileFunction = async (lang: SupportedLanguages) => {
|
||||
const stats: Partial<Stats> = {}
|
||||
stats.count = (await getCards(lang)).length
|
||||
|
||||
const langSets = await Promise.all(await getSets(undefined, lang).then((sets) => sets.map(async (set) => await setToSetSingle(set, lang))))
|
||||
const englishSets = await Promise.all(await getSets(undefined, 'en').then((sets) => sets.map(async (set) => await setToSetSingle(set, 'en'))))
|
||||
stats.total = langSets.reduce((p, set) => p + (englishSets.find((s) => set.id === s.id)?.cardCount?.total ?? 0), 0)
|
||||
stats.images = langSets.reduce((p1, set) => p1 + (set.cards.reduce((p2, card) => p2 + (card.image ? 1 : 0), 0)), 0)
|
||||
stats.sets = {}
|
||||
|
||||
const series = await getSeries(lang)
|
||||
|
||||
for (const serie of series) {
|
||||
stats.sets[serie.id] = {}
|
||||
for (const set of langSets.filter((set) => set.serie.id === serie.id)) {
|
||||
stats.sets[serie.id][set.id] = {
|
||||
name: set.name,
|
||||
count: set.cards.length,
|
||||
images: set.cards.reduce((p, card) => p + (card.image ? 1 : 0), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//const sts = await Promise.all(sets.map((set) => getCards(lang, set)))
|
||||
return stats
|
||||
}
|
||||
|
||||
export default fn
|
@ -1,16 +1,18 @@
|
||||
/* eslint-disable max-statements */
|
||||
import { Endpoint } from './compilerInterfaces'
|
||||
import { FileFunction } from './compilerInterfaces'
|
||||
import { promises as fs } from 'fs'
|
||||
import { fetchRemoteFile } from './utils/util'
|
||||
import { objectValues } from '@dzeio/object-util'
|
||||
import { SupportedLanguages } from '../../interfaces'
|
||||
|
||||
const LANGS = ['en', 'fr', 'es', 'it', 'pt', 'de']
|
||||
const LANGS: Array<SupportedLanguages> = ['en', 'fr', 'es', 'it', 'pt', 'de']
|
||||
|
||||
const DIST_FOLDER = './generated'
|
||||
|
||||
;(async () => {
|
||||
const paths = (await fs.readdir('./compiler/endpoints')).filter((p) => p.endsWith('.ts'))
|
||||
|
||||
// Prefetch the pictures at the start as it can bug because of bad connection
|
||||
console.log('Prefetching pictures')
|
||||
await fetchRemoteFile('https://assets.tcgdex.net/datas.json')
|
||||
|
||||
@ -35,26 +37,18 @@ const DIST_FOLDER = './generated'
|
||||
await fs.mkdir(folder, {recursive: true})
|
||||
|
||||
// Import the """Endpoint"""
|
||||
const Ep = (await import(`./endpoints/${file}`)).default
|
||||
const fn = (await import(`./endpoints/${file}`)).default as FileFunction
|
||||
|
||||
const endpoint = new Ep(lang) as Endpoint
|
||||
|
||||
console.log(file, 'Running Common')
|
||||
let common: any | null = null
|
||||
|
||||
if (endpoint.common) {
|
||||
common = await endpoint.common()
|
||||
}
|
||||
|
||||
console.log(file, 'Running Item')
|
||||
const item = await endpoint.item(common)
|
||||
// Run the function
|
||||
console.log(file, 'Running...')
|
||||
const item = await fn(lang)
|
||||
|
||||
// Write to file
|
||||
await fs.writeFile(`${folder}/${file.replace('.ts', '')}.json`, JSON.stringify(
|
||||
objectValues(item)
|
||||
item
|
||||
))
|
||||
|
||||
console.log(file, 'Finished Item')
|
||||
console.log(file, 'Finished!')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,8 @@ export async function cardToCardSingle(localId: string, card: Card, lang: Suppor
|
||||
firstEdition: typeof card.variants?.firstEdition === 'boolean' ? card.variants.firstEdition : false,
|
||||
holo: typeof card.variants?.holo === 'boolean' ? card.variants.holo : true,
|
||||
normal: typeof card.variants?.normal === 'boolean' ? card.variants.normal : true,
|
||||
reverse: typeof card.variants?.reverse === 'boolean' ? card.variants.reverse : true
|
||||
reverse: typeof card.variants?.reverse === 'boolean' ? card.variants.reverse : true,
|
||||
wPromo: typeof card.variants?.wPromo === 'boolean' ? card.variants.wPromo : false
|
||||
},
|
||||
|
||||
|
||||
@ -120,6 +121,12 @@ export async function getCard(serie: string, setName: string, id: string): Promi
|
||||
return (await import(`../../${DB_PATH}/data/${serie}/${setName}/${id}.js`)).default
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cards filtered by the language they are available in
|
||||
* @param lang the language of the cards
|
||||
* @param set the set to filter in (optional)
|
||||
* @returns An array with the 0 = localId, 1 = Card Object
|
||||
*/
|
||||
export async function getCards(lang: SupportedLanguages, set?: Set): Promise<Array<[string, Card]>> {
|
||||
const cards = await smartGlob(`${DB_PATH}/data/${(set && set.serie.name.en) ?? '*'}/${(set && set.name.en) ?? '*'}/*.js`)
|
||||
const list: Array<[string, Card]> = []
|
||||
|
@ -57,6 +57,8 @@ export async function serieToSerieSingle(serie: Serie, lang: SupportedLanguages)
|
||||
.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1)
|
||||
.map((el) => setToSetSimple(el, lang)))
|
||||
const logo = sets.find((set) => set.logo)?.logo
|
||||
|
||||
// Final data
|
||||
return {
|
||||
id: serie.id,
|
||||
logo,
|
||||
|
@ -15,7 +15,7 @@ export function isSetAvailable(set: Set, lang: SupportedLanguages): boolean {
|
||||
|
||||
/**
|
||||
* Return the set
|
||||
* @param name the name of the set (don't include.js/.ts)
|
||||
* @param name the name of the set
|
||||
*/
|
||||
export async function getSet(name: string, serie = '*'): Promise<Set> {
|
||||
if (!setCache[name]) {
|
||||
|
@ -11,6 +11,11 @@ export const DB_PATH = "../"
|
||||
|
||||
const fileCache: fileCacheInterface = {}
|
||||
|
||||
/**
|
||||
* Fetch a JSON file from a remote location
|
||||
* @param url the URL to fetch
|
||||
* @returns the JSON file content
|
||||
*/
|
||||
export async function fetchRemoteFile<T = any>(url: string): Promise<T> {
|
||||
if (!fileCache[url]) {
|
||||
const resp = await fetch(url, {
|
||||
@ -32,6 +37,13 @@ export async function smartGlob(query: string): Promise<Array<string>> {
|
||||
return globCache[query]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a card is currently Legal
|
||||
* @param type the type of legality
|
||||
* @param card the card to check
|
||||
* @param localId the card localid
|
||||
* @returns {boolean} if the card is currently in the legal type
|
||||
*/
|
||||
export function cardIsLegal(type: 'standard' | 'expanded', card: Card, localId: string): boolean {
|
||||
const legal = legals[type]
|
||||
if (
|
||||
@ -47,6 +59,12 @@ export function cardIsLegal(type: 'standard' | 'expanded', card: Card, localId:
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a set is currently Legal
|
||||
* @param type the type of legality
|
||||
* @param set the set to check
|
||||
* @returns {boolean} if the set is currently in the legal type
|
||||
*/
|
||||
export function setIsLegal(type: 'standard' | 'expanded', set: Set): boolean {
|
||||
const legal = legals[type]
|
||||
if (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import express from 'express'
|
||||
import graphql from './V2/graphql'
|
||||
import jsonEndpoints from './V2/endpoints/jsonEndpoints'
|
||||
|
||||
import status from './status'
|
||||
// Current API version
|
||||
const VERSION = 2
|
||||
|
||||
@ -30,6 +30,9 @@ server.use(`/v${VERSION}/graphql`, graphql)
|
||||
// Setup JSON endpoints
|
||||
server.use(`/v${VERSION}`, jsonEndpoints)
|
||||
|
||||
// Status page
|
||||
server.use('/status', status)
|
||||
|
||||
// Start server
|
||||
server.listen(3000)
|
||||
console.log(`🚀 Server ready at localhost:3000`);
|
||||
|
310
server/src/status.ts
Normal file
310
server/src/status.ts
Normal file
@ -0,0 +1,310 @@
|
||||
import { objectLoop, objectMap } from '@dzeio/object-util'
|
||||
import { SupportedLanguages } from '@tcgdex/sdk'
|
||||
import express from 'express'
|
||||
import Serie from './V2/Components/Serie'
|
||||
import Set from './V2/Components/Set'
|
||||
|
||||
import enStats from '../generated/en/stats.json'
|
||||
import frStats from '../generated/fr/stats.json'
|
||||
import deStats from '../generated/de/stats.json'
|
||||
import esStats from '../generated/es/stats.json'
|
||||
import itStats from '../generated/it/stats.json'
|
||||
import ptStats from '../generated/pt/stats.json'
|
||||
|
||||
/**
|
||||
* This file is meant to contains the TCGdex Project status page.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Simple calculation of maximum and current count globally
|
||||
*/
|
||||
const totalStats = {
|
||||
count: enStats.count + frStats.count + deStats.count + itStats.count + ptStats.count + esStats.count,
|
||||
total: enStats.total + frStats.total + deStats.total + itStats.total + ptStats.total + esStats.total,
|
||||
images: enStats.images + frStats.images + deStats.images + itStats.images + ptStats.images + esStats.images,
|
||||
}
|
||||
|
||||
/**
|
||||
* Array containing data for sets, it also allow to display non english available sets
|
||||
* Serie
|
||||
* Set
|
||||
* Array of langs
|
||||
*/
|
||||
const setsData: Record<string, Record<string, Array<string>>> = {}
|
||||
|
||||
function preProcessSets(t: any, lang: SupportedLanguages) {
|
||||
objectLoop(t.sets, (sets, serieId) => {
|
||||
if (!(serieId in setsData)) {
|
||||
setsData[serieId] = {}
|
||||
}
|
||||
objectLoop(sets, (_, set) => {
|
||||
if (!(set in setsData[serieId])) {
|
||||
setsData[serieId][set] = []
|
||||
}
|
||||
setsData[serieId][set].push(lang)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
preProcessSets(enStats, 'en')
|
||||
preProcessSets(frStats, 'fr')
|
||||
preProcessSets(esStats, 'es')
|
||||
preProcessSets(itStats, 'it')
|
||||
preProcessSets(ptStats, 'pt')
|
||||
preProcessSets(deStats, 'de')
|
||||
|
||||
|
||||
export default express.Router().get('/', (_, res): void => {
|
||||
|
||||
res.send(`
|
||||
<style>
|
||||
td, th {
|
||||
border-left: black 1px solid;
|
||||
border-right: black 1px solid;
|
||||
border-top: black 1px solid;
|
||||
text-align: center;
|
||||
}
|
||||
tr:last-child td {
|
||||
border-bottom: black 1px solid;
|
||||
}
|
||||
th.notop {
|
||||
border-top: none;
|
||||
}
|
||||
th {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 1em 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
.serie td:not(:first-child), .completed {
|
||||
background: gold;
|
||||
}
|
||||
.serie td.na, .na {
|
||||
background: black;
|
||||
}
|
||||
.serie td.missing-cards, .missing-cards {
|
||||
background: #3f17ff4f;
|
||||
}
|
||||
.serie td.missing-img, .missing-img {
|
||||
background: #67e301;
|
||||
}
|
||||
.serie td.empty, .empty {
|
||||
background: #ff000070;
|
||||
}
|
||||
h1, h2 {
|
||||
margin: 1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<h1>TCGdex Progress</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="na"></th>
|
||||
<th>English</th>
|
||||
<th>French</th>
|
||||
<th>German</th>
|
||||
<th>Italian</th>
|
||||
<th>Portuguese</th>
|
||||
<th>Spanish</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th colspan="8">Card Progress</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cards</td>
|
||||
<td>${enStats.count} of ${enStats.total}</td>
|
||||
<td>${frStats.count} of ${frStats.total}</td>
|
||||
<td>${deStats.count} of ${deStats.total}</td>
|
||||
<td>${itStats.count} of ${itStats.total}</td>
|
||||
<td>${ptStats.count} of ${ptStats.total}</td>
|
||||
<td>${esStats.count} of ${esStats.total}</td>
|
||||
<td>${totalStats.count} of ${totalStats.total}</td>
|
||||
</tr><tr>
|
||||
<td>Percentage</td>
|
||||
<td>${(100 * enStats.count / enStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * frStats.count / frStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * deStats.count / deStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * itStats.count / itStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * ptStats.count / ptStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * esStats.count / esStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * totalStats.count / totalStats.total).toFixed(2)}%</td>
|
||||
</tr><tr>
|
||||
<td>Remaining</td>
|
||||
<td>${enStats.total - enStats.count}</td>
|
||||
<td>${frStats.total - frStats.count}</td>
|
||||
<td>${deStats.total - deStats.count}</td>
|
||||
<td>${itStats.total - itStats.count}</td>
|
||||
<td>${ptStats.total - ptStats.count}</td>
|
||||
<td>${esStats.total - esStats.count}</td>
|
||||
<td>${totalStats.total - totalStats.count}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th colspan="8">Images Progress</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cards</td>
|
||||
<td>${enStats.images} of ${enStats.total}</td>
|
||||
<td>${frStats.images} of ${frStats.total}</td>
|
||||
<td>${deStats.images} of ${deStats.total}</td>
|
||||
<td>${itStats.images} of ${itStats.total}</td>
|
||||
<td>${ptStats.images} of ${ptStats.total}</td>
|
||||
<td>${esStats.images} of ${esStats.total}</td>
|
||||
<td>${totalStats.images} of ${totalStats.total}</td>
|
||||
</tr><tr>
|
||||
<td>Percentage</td>
|
||||
<td>${(100 * enStats.images / enStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * frStats.images / frStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * deStats.images / deStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * itStats.images / itStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * ptStats.images / ptStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * esStats.images / esStats.total).toFixed(2)}%</td>
|
||||
<td>${(100 * totalStats.images / totalStats.total).toFixed(2)}%</td>
|
||||
</tr><tr>
|
||||
<td>Remaining</td>
|
||||
<td>${enStats.total - enStats.images}</td>
|
||||
<td>${frStats.total - frStats.images}</td>
|
||||
<td>${deStats.total - deStats.images}</td>
|
||||
<td>${itStats.total - itStats.images}</td>
|
||||
<td>${ptStats.total - ptStats.images}</td>
|
||||
<td>${esStats.total - esStats.images}</td>
|
||||
<td>${totalStats.total - totalStats.images}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th colspan="8">Total Progress</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cards</td>
|
||||
<td>${enStats.images + enStats.count} of ${enStats.total}</td>
|
||||
<td>${frStats.images + frStats.count} of ${frStats.total}</td>
|
||||
<td>${deStats.images + deStats.count} of ${deStats.total}</td>
|
||||
<td>${itStats.images + itStats.count} of ${itStats.total}</td>
|
||||
<td>${ptStats.images + ptStats.count} of ${ptStats.total}</td>
|
||||
<td>${esStats.images + esStats.count} of ${esStats.total}</td>
|
||||
<td>${totalStats.images + totalStats.count} of ${totalStats.total * 2}</td>
|
||||
</tr><tr>
|
||||
<td>Percentage</td>
|
||||
<td>${(100 * (enStats.images + enStats.count) / (enStats.total * 2)).toFixed(2)}%</td>
|
||||
<td>${(100 * (frStats.images + frStats.count) / (frStats.total * 2)).toFixed(2)}%</td>
|
||||
<td>${(100 * (deStats.images + deStats.count) / (deStats.total * 2)).toFixed(2)}%</td>
|
||||
<td>${(100 * (itStats.images + itStats.count) / (itStats.total * 2)).toFixed(2)}%</td>
|
||||
<td>${(100 * (ptStats.images + ptStats.count) / (ptStats.total * 2)).toFixed(2)}%</td>
|
||||
<td>${(100 * (esStats.images + esStats.count) / (esStats.total * 2)).toFixed(2)}%</td>
|
||||
<td>${(100 * (totalStats.images + totalStats.count) / (totalStats.total * 2)).toFixed(2)}%</td>
|
||||
</tr><tr>
|
||||
<td>Remaining</td>
|
||||
<td>${enStats.total * 2 - (enStats.images + enStats.count)}</td>
|
||||
<td>${frStats.total * 2 - (frStats.images + frStats.count)}</td>
|
||||
<td>${deStats.total * 2 - (deStats.images + deStats.count)}</td>
|
||||
<td>${itStats.total * 2 - (itStats.images + itStats.count)}</td>
|
||||
<td>${ptStats.total * 2 - (ptStats.images + ptStats.count)}</td>
|
||||
<td>${esStats.total * 2 - (esStats.images + esStats.count)}</td>
|
||||
<td>${totalStats.total * 2 - (totalStats.images + totalStats.count)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Status</h2>
|
||||
<ul>
|
||||
<li class="completed">Completed</li>
|
||||
<li class="missing-cards">Missing some cards informations</li>
|
||||
<li class="missing-img">Missing some cards images</li>
|
||||
<li class="empty">No Data</li>
|
||||
<li class="na" style="color: white">Not Available</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<table class="serie">
|
||||
${objectMap(setsData, (serie, serieId) => {
|
||||
// Loop through every series and name them
|
||||
const name = Serie.findOne('en', {id: serieId})?.name
|
||||
return `
|
||||
<thead>
|
||||
<tr><th class="notop" colspan="13"><h2>${name} (${serieId})</h2></th></tr>
|
||||
<tr>
|
||||
<th rowspan="2">Set Name</th>
|
||||
<th colspan="2">English</th>
|
||||
<th colspan="2">French</th>
|
||||
<th colspan="2">German</th>
|
||||
<th colspan="2">Italian</th>
|
||||
<th colspan="2">Portuguese</th>
|
||||
<th colspan="2">Spanish</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Cards</th>
|
||||
<th>Images</th>
|
||||
|
||||
<th>Cards</th>
|
||||
<th>Images</th>
|
||||
|
||||
<th>Cards</th>
|
||||
<th>Images</th>
|
||||
|
||||
<th>Cards</th>
|
||||
<th>Images</th>
|
||||
|
||||
<th>Cards</th>
|
||||
<th>Images</th>
|
||||
|
||||
<th>Cards</th>
|
||||
<th>Images</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${objectMap(serie, (data, setId) => {
|
||||
// loop through every sets
|
||||
|
||||
// find the set in the first available language (Should be English globally)
|
||||
const setTotal = Set.findOne(data[0] as 'en', {id: setId})
|
||||
let str = '<tr>' + `<td>${setTotal?.name} (${setId}) <br />${setTotal?.cardCount.total ?? 1} cards</td>`
|
||||
|
||||
// Loop through every languages
|
||||
const l = ['en', 'fr', 'de', 'it', 'pt', 'es']
|
||||
l.map((it) => {
|
||||
|
||||
// Change the stats file depending on the language
|
||||
let stats: any = enStats
|
||||
switch (it) {
|
||||
case 'fr': stats = frStats; break
|
||||
case 'de': stats = deStats; break
|
||||
case 'it': stats = itStats; break
|
||||
case 'pt': stats = ptStats; break
|
||||
case 'es': stats = esStats; break
|
||||
}
|
||||
|
||||
// Get the stats we want
|
||||
const item = stats.sets[serieId]?.[setId] as {count: number, images: number} | undefined
|
||||
|
||||
// if item dont exist for the language skip it
|
||||
if (!item) {
|
||||
str += `
|
||||
<td class="na"></td>
|
||||
<td class="na"></td>`
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate percentages and status
|
||||
const percent = 100 * item.count / (setTotal?.cardCount.total ?? 1)
|
||||
const imgPercent = 100 * item.images / (setTotal?.cardCount.total ?? 1)
|
||||
|
||||
// append to string :D
|
||||
str +=`<td class="${percent === 100 ? '' : percent === 0 ? 'empty' : 'missing-cards'}">${percent.toFixed(2)}% <br />(${item.count})</td>
|
||||
<td class="${imgPercent === 100 ? '' : imgPercent === 0 ? 'empty' : 'missing-img'}">${imgPercent.toFixed(2)}% <br />(${item.images})</td>`
|
||||
})
|
||||
|
||||
// finish Row
|
||||
return str + '</tr>'
|
||||
}).join('')}
|
||||
</tbody>
|
||||
`}).join('')}
|
||||
</table>`)
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user