From 762ce389c167293a0e7ca2b51587454975f79fbd Mon Sep 17 00:00:00 2001 From: Avior Date: Tue, 23 Nov 2021 15:12:50 +0000 Subject: [PATCH] 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 * refactor: Simplified compiler files generators Signed-off-by: Avior * chore: Add step to compiler for stats and optimize Signed-off-by: Avior * refactor: Remove unused variable Signed-off-by: Avior --- data/Black & White/Legendary Treasures.ts | 2 +- data/E-Card/Best of game.ts | 3 +- data/E-Card/Sample.ts | 3 +- meta/definitions/api.d.ts | 1 + meta/definitions/graphql.gql | 2 + meta/definitions/openapi.yaml | 3 + server/compiler/compilerInterfaces.d.ts | 10 +- server/compiler/endpoints/cards.ts | 36 +-- server/compiler/endpoints/series.ts | 38 +-- server/compiler/endpoints/sets.ts | 61 +---- server/compiler/endpoints/stats.ts | 42 +++ server/compiler/index.ts | 26 +- server/compiler/utils/cardUtil.ts | 9 +- server/compiler/utils/serieUtil.ts | 2 + server/compiler/utils/setUtil.ts | 2 +- server/compiler/utils/util.ts | 18 ++ server/src/index.ts | 5 +- server/src/status.ts | 310 ++++++++++++++++++++++ 18 files changed, 431 insertions(+), 142 deletions(-) create mode 100644 server/compiler/endpoints/stats.ts create mode 100644 server/src/status.ts diff --git a/data/Black & White/Legendary Treasures.ts b/data/Black & White/Legendary Treasures.ts index 15595cee3..b1e3a1ebd 100644 --- a/data/Black & White/Legendary Treasures.ts +++ b/data/Black & White/Legendary Treasures.ts @@ -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" }, diff --git a/data/E-Card/Best of game.ts b/data/E-Card/Best of game.ts index c1aa5ee82..ce9fd2cbd 100644 --- a/data/E-Card/Best of game.ts +++ b/data/E-Card/Best of game.ts @@ -5,8 +5,7 @@ const bog: Set = { id: "bog", name: { - en: "Best of game", - fr: "Best of game" + en: "Best of game" }, serie: serie, diff --git a/data/E-Card/Sample.ts b/data/E-Card/Sample.ts index 9b926f937..b6e58292e 100644 --- a/data/E-Card/Sample.ts +++ b/data/E-Card/Sample.ts @@ -5,8 +5,7 @@ const sp: Set = { id: "sp", name: { - en: "Sample", - fr: "Sample" + en: "Sample" }, serie: serie, diff --git a/meta/definitions/api.d.ts b/meta/definitions/api.d.ts index b89d52fe1..f2ca522a1 100644 --- a/meta/definitions/api.d.ts +++ b/meta/definitions/api.d.ts @@ -23,6 +23,7 @@ interface variants { reverse?: boolean; holo?: boolean; firstEdition?: boolean; + wPromo?: boolean } export interface SetResume { diff --git a/meta/definitions/graphql.gql b/meta/definitions/graphql.gql index 88ffce658..1b432eae6 100644 --- a/meta/definitions/graphql.gql +++ b/meta/definitions/graphql.gql @@ -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 { diff --git a/meta/definitions/openapi.yaml b/meta/definitions/openapi.yaml index 7f0ff83dd..5ecfd9709 100644 --- a/meta/definitions/openapi.yaml +++ b/meta/definitions/openapi.yaml @@ -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 diff --git a/server/compiler/compilerInterfaces.d.ts b/server/compiler/compilerInterfaces.d.ts index cc9fb1f84..16a7fa6be 100644 --- a/server/compiler/compilerInterfaces.d.ts +++ b/server/compiler/compilerInterfaces.d.ts @@ -1,7 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-types -export interface Endpoint { - index(common: C): Promise - item(common: C): Promise | undefined> - sub?(common: C, item: string): Promise | undefined> - common?(): Promise -} +import { SupportedLanguages } from '../../interfaces' + +export type FileFunction = (lang: SupportedLanguages) => Promise diff --git a/server/compiler/endpoints/cards.ts b/server/compiler/endpoints/cards.ts index 68e9e5748..967c90191 100644 --- a/server/compiler/endpoints/cards.ts +++ b/server/compiler/endpoints/cards.ts @@ -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 - -export default class implements Endpoint, Array<[string, Card]>> { - - public constructor( - private lang: keyof Languages - ) {} - - public async index(common: Array<[string, Card]>): Promise { - return Promise.all(common.map((c) => cardToCardSimple(c[0], c[1], this.lang))) - } - - public async item(common: Array<[string, Card]>): Promise> { - const items: Record = {} - 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> { - 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 diff --git a/server/compiler/endpoints/series.ts b/server/compiler/endpoints/series.ts index 9937794f8..7c65c0f91 100644 --- a/server/compiler/endpoints/series.ts +++ b/server/compiler/endpoints/series.ts @@ -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 - -export default class implements Endpoint, Array> { - - public constructor( - private lang: keyof Languages - ) {} - - public async index(common: Array): Promise> { - return Promise.all(common.map((s) => serieToSerieSimple(s, this.lang))) - } - - public async item(common: Array): Promise> { - const items: Record = {} - 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> { - 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 diff --git a/server/compiler/endpoints/sets.ts b/server/compiler/endpoints/sets.ts index 9d0ee02bc..21ba3ed97 100644 --- a/server/compiler/endpoints/sets.ts +++ b/server/compiler/endpoints/sets.ts @@ -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 - -export default class implements Endpoint> { - - public constructor( - private lang: keyof Languages - ) {} - - public async index(common: Array): Promise { - 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): Promise> { - const sets = await Promise.all(common - .map((set) => setToSetSingle(set, this.lang))) - const res: Record = {} - - for (const set of sets) { - res[set.id] = set - } - - return res - } - - public async common(): Promise> { - return getSets(undefined, this.lang) - } - - public async sub(common: Array, item: string): Promise | 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 = {} - 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 diff --git a/server/compiler/endpoints/stats.ts b/server/compiler/endpoints/stats.ts new file mode 100644 index 000000000..e902c36f7 --- /dev/null +++ b/server/compiler/endpoints/stats.ts @@ -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> +} + +const fn: FileFunction = async (lang: SupportedLanguages) => { + const stats: Partial = {} + 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 diff --git a/server/compiler/index.ts b/server/compiler/index.ts index 7e48e0a31..cdb6dfdd0 100644 --- a/server/compiler/index.ts +++ b/server/compiler/index.ts @@ -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 = ['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!') } } diff --git a/server/compiler/utils/cardUtil.ts b/server/compiler/utils/cardUtil.ts index ee370b401..af784550c 100644 --- a/server/compiler/utils/cardUtil.ts +++ b/server/compiler/utils/cardUtil.ts @@ -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> { const cards = await smartGlob(`${DB_PATH}/data/${(set && set.serie.name.en) ?? '*'}/${(set && set.name.en) ?? '*'}/*.js`) const list: Array<[string, Card]> = [] diff --git a/server/compiler/utils/serieUtil.ts b/server/compiler/utils/serieUtil.ts index 78002e038..85a0b0448 100644 --- a/server/compiler/utils/serieUtil.ts +++ b/server/compiler/utils/serieUtil.ts @@ -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, diff --git a/server/compiler/utils/setUtil.ts b/server/compiler/utils/setUtil.ts index 83858cb92..69b7f990d 100644 --- a/server/compiler/utils/setUtil.ts +++ b/server/compiler/utils/setUtil.ts @@ -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 { if (!setCache[name]) { diff --git a/server/compiler/utils/util.ts b/server/compiler/utils/util.ts index 981029862..25977828a 100644 --- a/server/compiler/utils/util.ts +++ b/server/compiler/utils/util.ts @@ -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(url: string): Promise { if (!fileCache[url]) { const resp = await fetch(url, { @@ -32,6 +37,13 @@ export async function smartGlob(query: string): Promise> { 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 ( diff --git a/server/src/index.ts b/server/src/index.ts index 972653b9b..aaa5dde98 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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`); diff --git a/server/src/status.ts b/server/src/status.ts new file mode 100644 index 000000000..1f0f12ff4 --- /dev/null +++ b/server/src/status.ts @@ -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>> = {} + +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(` + +

TCGdex Progress

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EnglishFrenchGermanItalianPortugueseSpanishTotal
Card Progress
Cards${enStats.count} of ${enStats.total}${frStats.count} of ${frStats.total}${deStats.count} of ${deStats.total}${itStats.count} of ${itStats.total}${ptStats.count} of ${ptStats.total}${esStats.count} of ${esStats.total}${totalStats.count} of ${totalStats.total}
Percentage${(100 * enStats.count / enStats.total).toFixed(2)}%${(100 * frStats.count / frStats.total).toFixed(2)}%${(100 * deStats.count / deStats.total).toFixed(2)}%${(100 * itStats.count / itStats.total).toFixed(2)}%${(100 * ptStats.count / ptStats.total).toFixed(2)}%${(100 * esStats.count / esStats.total).toFixed(2)}%${(100 * totalStats.count / totalStats.total).toFixed(2)}%
Remaining${enStats.total - enStats.count}${frStats.total - frStats.count}${deStats.total - deStats.count}${itStats.total - itStats.count}${ptStats.total - ptStats.count}${esStats.total - esStats.count}${totalStats.total - totalStats.count}
Images Progress
Cards${enStats.images} of ${enStats.total}${frStats.images} of ${frStats.total}${deStats.images} of ${deStats.total}${itStats.images} of ${itStats.total}${ptStats.images} of ${ptStats.total}${esStats.images} of ${esStats.total}${totalStats.images} of ${totalStats.total}
Percentage${(100 * enStats.images / enStats.total).toFixed(2)}%${(100 * frStats.images / frStats.total).toFixed(2)}%${(100 * deStats.images / deStats.total).toFixed(2)}%${(100 * itStats.images / itStats.total).toFixed(2)}%${(100 * ptStats.images / ptStats.total).toFixed(2)}%${(100 * esStats.images / esStats.total).toFixed(2)}%${(100 * totalStats.images / totalStats.total).toFixed(2)}%
Remaining${enStats.total - enStats.images}${frStats.total - frStats.images}${deStats.total - deStats.images}${itStats.total - itStats.images}${ptStats.total - ptStats.images}${esStats.total - esStats.images}${totalStats.total - totalStats.images}
Total Progress
Cards${enStats.images + enStats.count} of ${enStats.total}${frStats.images + frStats.count} of ${frStats.total}${deStats.images + deStats.count} of ${deStats.total}${itStats.images + itStats.count} of ${itStats.total}${ptStats.images + ptStats.count} of ${ptStats.total}${esStats.images + esStats.count} of ${esStats.total}${totalStats.images + totalStats.count} of ${totalStats.total * 2}
Percentage${(100 * (enStats.images + enStats.count) / (enStats.total * 2)).toFixed(2)}%${(100 * (frStats.images + frStats.count) / (frStats.total * 2)).toFixed(2)}%${(100 * (deStats.images + deStats.count) / (deStats.total * 2)).toFixed(2)}%${(100 * (itStats.images + itStats.count) / (itStats.total * 2)).toFixed(2)}%${(100 * (ptStats.images + ptStats.count) / (ptStats.total * 2)).toFixed(2)}%${(100 * (esStats.images + esStats.count) / (esStats.total * 2)).toFixed(2)}%${(100 * (totalStats.images + totalStats.count) / (totalStats.total * 2)).toFixed(2)}%
Remaining${enStats.total * 2 - (enStats.images + enStats.count)}${frStats.total * 2 - (frStats.images + frStats.count)}${deStats.total * 2 - (deStats.images + deStats.count)}${itStats.total * 2 - (itStats.images + itStats.count)}${ptStats.total * 2 - (ptStats.images + ptStats.count)}${esStats.total * 2 - (esStats.images + esStats.count)}${totalStats.total * 2 - (totalStats.images + totalStats.count)}
+ +

Status

+
    +
  • Completed
  • +
  • Missing some cards informations
  • +
  • Missing some cards images
  • +
  • No Data
  • +
  • Not Available
  • +
+ + + + ${objectMap(setsData, (serie, serieId) => { + // Loop through every series and name them + const name = Serie.findOne('en', {id: serieId})?.name + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${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 = '' + `` + + // 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 += ` + + ` + 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 +=` + ` + }) + + // finish Row + return str + '' + }).join('')} + + `}).join('')} +

${name} (${serieId})

Set NameEnglishFrenchGermanItalianPortugueseSpanish
CardsImagesCardsImagesCardsImagesCardsImagesCardsImagesCardsImages
${setTotal?.name} (${setId})
${setTotal?.cardCount.total ?? 1} cards
${percent.toFixed(2)}%
(${item.count})
${imgPercent.toFixed(2)}%
(${item.images})
`) +})