diff --git a/interfaces/carddata.ts b/interfaces/carddata.ts index fa29742..4e5feed 100644 --- a/interfaces/carddata.ts +++ b/interfaces/carddata.ts @@ -13,4 +13,7 @@ export interface ICard { imageClass?: string; meta: Record; + + faq?: number; + errata?: number; } diff --git a/search/operators/errata.ts b/search/operators/errata.ts new file mode 100644 index 0000000..f0b9d25 --- /dev/null +++ b/search/operators/errata.ts @@ -0,0 +1,30 @@ +import { type ICardHelp } from '../../interfaces'; +import { numericalOperator } from './_helpers'; + +export const errata = numericalOperator(['errata', 'e'], 'errata'); + +export const errataDescription: ICardHelp = { + name: 'Errata', + id: 'errata', + + icon: 'flask-outline', + + color: '#aa3c06', + + help: ` +You can find cards that have errata entries with the \`errata:\` or \`e:\` operator. + +This operator is a numeric operator, meaning you'll by default want to search for \`errata:>0\` to find entries with any errata entries. +`, + + examples: [ + { + example: '`errata:>0`', + explanation: 'Cards that have any number of errata entries.', + }, + { + example: '`errata:=0`', + explanation: 'Cards with no errata entries.', + }, + ], +}; diff --git a/search/operators/faq.ts b/search/operators/faq.ts new file mode 100644 index 0000000..771627f --- /dev/null +++ b/search/operators/faq.ts @@ -0,0 +1,30 @@ +import { type ICardHelp } from '../../interfaces'; +import { numericalOperator } from './_helpers'; + +export const faq = numericalOperator(['faq', 'f'], 'faq'); + +export const faqDescription: ICardHelp = { + name: 'FAQ', + id: 'faq', + + icon: 'chatbubbles-outline', + + color: '#aa063c', + + help: ` +You can find cards that have FAQ entries with the \`faq:\` or \`f:\` operator. + +This operator is a numeric operator, meaning you'll by default want to search for \`faq:>0\` to find entries with any FAQs. +`, + + examples: [ + { + example: '`faq:>0`', + explanation: 'Cards that have any number of FAQ entries.', + }, + { + example: '`faq:=0`', + explanation: 'Cards with no FAQ entries.', + }, + ], +}; diff --git a/search/operators/index.ts b/search/operators/index.ts index 06712ba..b9218a4 100644 --- a/search/operators/index.ts +++ b/search/operators/index.ts @@ -1,5 +1,7 @@ export * from './bare'; export * from './card'; +export * from './errata'; +export * from './faq'; export * from './name'; export * from './product'; export * from './subproduct'; diff --git a/search/search.ts b/search/search.ts index cb9c210..dacd711 100644 --- a/search/search.ts +++ b/search/search.ts @@ -4,13 +4,25 @@ import * as parser from 'search-query-parser'; import { type ICard } from '../interfaces'; -import { bare, card, name, product, subproduct, tag, text } from './operators'; +import { + bare, + card, + errata, + faq, + name, + product, + subproduct, + tag, + text, +} from './operators'; const allKeywords = [ ['id'], // exact text ['name', 'n'], // loose text ['cardtext', 't'], // loose text ['game', 'g'], // exact text + ['faq', 'f'], // numerical + ['errata', 'e'], // numerical ['product', 'expansion', 'p', 'e'], // exact text ['tag'], // array search ]; @@ -24,6 +36,8 @@ const operators: ParserOperator[] = [ card, name, text, + faq, + errata, product, subproduct, tag, @@ -80,6 +94,24 @@ const allQueryFormatters = [ return `"${value}"`; }, }, + { + key: 'errata', + includes: 'is', + excludes: 'is not', + formatter: (result: Record) => { + const value = result['errata'] ?? 0; + return `${value}`; + }, + }, + { + key: 'faq', + includes: 'is', + excludes: 'is not', + formatter: (result: Record) => { + const value = result['faq'] ?? 0; + return `${value}`; + }, + }, /* { key: 'game', @@ -138,35 +170,41 @@ export function queryToText(query: string, isPlural = true): string { const result = properOperatorsInsteadOfAliases(firstResult); - const text = []; + const textArrayEntries = []; const gameResult = result['game']; if (gameResult) { - text.push(`in ${gameResult}`); + textArrayEntries.push(`in ${gameResult}`); } allQueryFormatters.forEach((queryFormatter) => { const { key, includes, excludes, formatter } = queryFormatter; if (result[key]) { - text.push(`${key} ${includes} ${formatter(result)}`); + textArrayEntries.push(`${key} ${includes} ${formatter(result)}`); } if (result.exclude?.[key]) { - text.push(`${key} ${excludes} ${formatter(result.exclude)}`); + textArrayEntries.push(`${key} ${excludes} ${formatter(result.exclude)}`); } }); if (result['in']) { - text.push(`in ${result['in']}`); + textArrayEntries.push(`in ${result['in']}`); } if (result['text']) { - text.push(`"${result['text']}" is in name or card id`); + textArrayEntries.push(`"${result['text']}" is in name or card id`); + } + + if (query.includes('game:')) { + return `${cardText} ${textArrayEntries[0]} ${ + textArrayEntries.length > 1 ? 'where' : '' + } ${textArrayEntries.slice(1).join(' and ')}`; } - return `${cardText} ${text[0]} ${text.length > 1 ? 'where' : ''} ${text - .slice(1) - .join(' and ')}`; + return `${cardText} ${ + textArrayEntries.length > 0 ? 'where' : '' + } ${textArrayEntries.join(' and ')}`; } export function getGameFromQuery(query: string): string | undefined { diff --git a/src/app/_shared/components/search-cards/search-cards.component.html b/src/app/_shared/components/search-cards/search-cards.component.html index 2100888..2e046ab 100644 --- a/src/app/_shared/components/search-cards/search-cards.component.html +++ b/src/app/_shared/components/search-cards/search-cards.component.html @@ -20,7 +20,7 @@

{{ 'Pages.SearchResults.NoResults.Header' | translate }}

{{ searchService.displayCurrent() | number }} - {{ searchService.displayMaximum() | number }} - of  {{ searchService.displayTotal() | number }} {{ searchService.queryDesc() }} + of {{ searchService.displayTotal() | number }} {{ searchService.queryDesc() }} diff --git a/src/app/_shared/helpers/navigation.ts b/src/app/_shared/helpers/navigation.ts index 510fbd2..9999b8b 100644 --- a/src/app/_shared/helpers/navigation.ts +++ b/src/app/_shared/helpers/navigation.ts @@ -1,5 +1,4 @@ export function tryNavigateToHash() { - console.log(document.location.hash); if (!document.location.hash) return; setTimeout(() => { diff --git a/src/app/advanced/advanced.page.ts b/src/app/advanced/advanced.page.ts index c6aa04c..1cde0d1 100644 --- a/src/app/advanced/advanced.page.ts +++ b/src/app/advanced/advanced.page.ts @@ -118,6 +118,15 @@ export class AdvancedPage implements OnInit { if (this.searchQuery.meta[filter.prop]) return; if (filter.type === 'number') { + if (['FAQ', 'Errata'].includes(filter.name)) { + this.searchQuery.meta[filter.prop] = { + operator: '>', + value: undefined, + }; + + return; + } + this.searchQuery.meta[filter.prop] = { operator: '=', value: undefined, @@ -165,6 +174,13 @@ export class AdvancedPage implements OnInit { this.visibleFilters = this.metaService.getFiltersByProductId(product); this.visibleTags = this.tagsByProduct[product]; } + + this.visibleFilters.unshift( + ...([ + { name: 'FAQ', prop: 'faq', type: 'number' }, + { name: 'Errata', prop: 'errata', type: 'number' }, + ] as IProductFilter[]) + ); } getSearchQuery() { diff --git a/src/app/cards.service.ts b/src/app/cards.service.ts index fe5bb7a..6b55ab9 100644 --- a/src/app/cards.service.ts +++ b/src/app/cards.service.ts @@ -9,6 +9,8 @@ import { type ICard, type IProductFilter } from '../../interfaces'; import { numericalOperator } from '../../search/operators/_helpers'; import { parseQuery, type ParserOperator } from '../../search/search'; import { environment } from '../environments/environment'; +import { ErrataService } from './errata.service'; +import { FAQService } from './faq.service'; import { LocaleService } from './locale.service'; import { MetaService } from './meta.service'; @@ -23,6 +25,8 @@ export class CardsService { private http = inject(HttpClient); private localeService = inject(LocaleService); private metaService = inject(MetaService); + private faqService = inject(FAQService); + private errataService = inject(ErrataService); public get allCards(): ICard[] { return this.cards; @@ -59,6 +63,15 @@ export class CardsService { } // card utilities + private reformatCardsWithErrataAndFAQ(): ICard[] { + return this.cards.map((card) => ({ + ...card, + faq: this.faqService.getCardFAQ(card.game, card.name).length ?? 0, + errata: + this.errataService.getCardErrata(card.game, card.name).length ?? 0, + })); + } + public getCardByIdOrName(codeOrName: string): ICard | undefined { return ( this.cardsById[codeOrName] ?? this.cardsByName[codeOrName] ?? undefined @@ -66,7 +79,8 @@ export class CardsService { } public searchCards(query: string): ICard[] { - return parseQuery(this.cards, query, this.getExtraFilterOperators()); + const formattedCards = this.reformatCardsWithErrataAndFAQ(); + return parseQuery(formattedCards, query, this.getExtraFilterOperators()); } public getExtraFilterOperators() { diff --git a/src/app/syntax/syntax.page.ts b/src/app/syntax/syntax.page.ts index 39a275b..091325e 100644 --- a/src/app/syntax/syntax.page.ts +++ b/src/app/syntax/syntax.page.ts @@ -8,6 +8,8 @@ import { type ICardHelp } from '../../../interfaces'; import { TranslateService } from '@ngx-translate/core'; import { cardDescription, + errataDescription, + faqDescription, nameDescription, productDescription, subproductDescription, @@ -28,6 +30,8 @@ export class SyntaxPage implements OnInit { public allOperators: ICardHelp[] = [ cardDescription, textDescription, + errataDescription, + faqDescription, nameDescription, productDescription, subproductDescription,