Skip to content

Commit

Permalink
closes #3
Browse files Browse the repository at this point in the history
  • Loading branch information
seiyria committed Apr 25, 2024
1 parent 7d357f9 commit a43fb03
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 52 deletions.
1 change: 1 addition & 0 deletions interfaces/product.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface IProductFilter {
name: string;
prop: string;
type: 'number';
}

export interface IProductDefinition {
Expand Down
28 changes: 6 additions & 22 deletions search/operators/_helpers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { isArray } from 'lodash';
import { get, isArray } from 'lodash';
import type * as parser from 'search-query-parser';

import { type ICard } from '../../interfaces';

function getValueFromCard<T>(card: ICard, prop: keyof ICard): T {
return card[prop] as T;
return get(card, prop) as T;
}

function cardMatchesNumberCheck(value: number, numberCheck: string): boolean {
Expand Down Expand Up @@ -38,11 +38,7 @@ function cardMatchesNumberCheck(value: number, numberCheck: string): boolean {
// this operator works on number fields
// it supports exact matching, as well as >, >=, <, <=
export function numericalOperator(aliases: string[], key: keyof ICard) {
return (
cards: ICard[],
results: parser.SearchParserResult,
extraData = {}
) => {
return (cards: ICard[], results: parser.SearchParserResult) => {
// if we have no cards, short-circuit because we can't filter it anymore
if (cards.length === 0) {
return [];
Expand Down Expand Up @@ -95,11 +91,7 @@ export function numericalOperator(aliases: string[], key: keyof ICard) {
// it also checks case-insensitively
// it also supports "none" as a value for empty arrays
export function arraySearchOperator(aliases: string[], key: keyof ICard) {
return (
cards: ICard[],
results: parser.SearchParserResult,
extraData = {}
) => {
return (cards: ICard[], results: parser.SearchParserResult) => {
// if we have no cards, short-circuit because we can't filter it anymore
if (cards.length === 0) {
return [];
Expand Down Expand Up @@ -175,11 +167,7 @@ export function partialWithOptionalExactTextOperator(
aliases: string[],
key: keyof ICard
) {
return (
cards: ICard[],
results: parser.SearchParserResult,
extraData = {}
) => {
return (cards: ICard[], results: parser.SearchParserResult) => {
// if we have no cards, short-circuit because we can't filter it anymore
if (cards.length === 0) {
return [];
Expand Down Expand Up @@ -252,11 +240,7 @@ export function partialWithOptionalExactTextOperator(
// most properties can use this sufficiently
// it still checks case-insensitively
export function exactTextOperator(aliases: string[], key: keyof ICard) {
return (
cards: ICard[],
results: parser.SearchParserResult,
extraData = {}
) => {
return (cards: ICard[], results: parser.SearchParserResult) => {
// if we have no cards, short-circuit because we can't filter it anymore
if (cards.length === 0) {
return [];
Expand Down
2 changes: 1 addition & 1 deletion search/operators/bare.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type ICard } from '../../interfaces';

export function bare(cards: ICard[], query: string, extraData = {}): ICard[] {
export function bare(cards: ICard[], query: string): ICard[] {
const sQueries = query.toLowerCase().split(' ');

const matches = (card: ICard, term: string) => {
Expand Down
32 changes: 20 additions & 12 deletions search/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ const allKeywords = [
['tag'], // array search
];

const operators = [card, name, product, subproduct, tag];
export type ParserOperator = (
cards: ICard[],
results: parser.SearchParserResult
) => ICard[];

const operators: ParserOperator[] = [card, name, product, subproduct, tag];

export function properOperatorsInsteadOfAliases(
result: parser.SearchParserResult
Expand Down Expand Up @@ -130,37 +135,40 @@ export function queryToText(query: string): string {
export function parseQuery(
cards: ICard[],
query: string,
extraData = {}
extraOperators: Array<{ aliases: string[]; operator: ParserOperator }> = []
): ICard[] {
query = query.toLowerCase().trim();

const validKeywords = [
allKeywords,
...extraOperators.map((o) => o.aliases),
].flat(2);

const result = parser.parse(query, {
keywords: allKeywords.flat(),
keywords: validKeywords,
offsets: false,
});

// the parser returns a string if there's nothing interesting to do, for some reason
// so we have a bare words parser
if (isString(result)) {
return bare(cards, query, extraData);
return bare(cards, query);
}

const resultText = (result as parser.SearchParserResult).text as string;

let returnCards = cards;

if (resultText) {
returnCards = bare(returnCards, resultText, extraData);
returnCards = bare(returnCards, resultText);
}

// check all the operators
operators.forEach((operator) => {
returnCards = operator(
returnCards,
result as parser.SearchParserResult,
extraData
);
});
[operators, extraOperators.map((o) => o.operator)]
.flat()
.forEach((operator) => {
returnCards = operator(returnCards, result as parser.SearchParserResult);
});

return returnCards;
}
44 changes: 43 additions & 1 deletion src/app/advanced/advanced.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@
</ion-col>
</ion-row>

@for(filter of visibleFilters; track index) {
<ion-row>
@switch(filter.type) {
@case ('number') {
<ng-container *ngTemplateOutlet="number; context: { filter: filter }"></ng-container>
}
}
</ion-row>
}

<!-- search! -->
<ion-row class="actions">
<ion-col class="criteria" [sizeXs]="2" [sizeMd]="2" [offsetMd]="2"></ion-col>
Expand All @@ -76,4 +86,36 @@
</div>

<app-below-the-fold></app-below-the-fold>
</ion-content>
</ion-content>

<ng-template #number let-filter="filter">
<ion-col class="criteria" [sizeXs]="12" [sizeMd]="2" [offsetMd]="2">
<ion-icon name="bookmark-outline" color="primary"></ion-icon>
<span>{{ filter.name }}</span>
</ion-col>

<ion-col class="search" [sizeXs]="12" [sizeMd]="6">
<ion-row>
<ion-col size="6">
<ion-select interface="popover" [(ngModel)]="searchQuery.meta[filter.prop].operator" (ionChange)="saveQuery()">
<ion-select-option *ngFor="let operator of allOperators" [value]="operator.value">
{{ operator.label }}
</ion-select-option>
</ion-select>
</ion-col>

<ion-col size="2"></ion-col>

<ion-col size="4">
<ion-input (ionChange)="saveQuery()" [placeholder]="filter.name"
[(ngModel)]="searchQuery.meta[filter.prop].value"></ion-input>
</ion-col>
</ion-row>

<ion-row class="note">
<ion-col>
If left unspecified, will not be included in search query.
</ion-col>
</ion-row>
</ion-col>
</ng-template>
39 changes: 37 additions & 2 deletions src/app/advanced/advanced.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Component, inject, type OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { LocalStorage } from 'ngx-webstorage';

import { sortBy, uniqBy } from 'lodash';
import { isNumber, sortBy, uniqBy } from 'lodash';
import type { IProductFilter } from '../../../interfaces';
import { CardsService } from '../cards.service';
import { MetaService } from '../meta.service';

Expand All @@ -11,6 +12,7 @@ const defaultQuery = () => ({
product: undefined,
subproduct: [],
tags: [],
meta: {},
});

interface DropdownForProductItem {
Expand All @@ -27,7 +29,7 @@ interface DropdownForProductItem {
export class AdvancedPage implements OnInit {
private router = inject(Router);
private cardsService = inject(CardsService);
private metaService = inject(MetaService);
public metaService = inject(MetaService);

public allOperators = [
{ value: '=', label: 'Equal To' },
Expand All @@ -45,6 +47,8 @@ export class AdvancedPage implements OnInit {
public visibleSubproducts: DropdownForProductItem[] = [];
public visibleTags: string[] = [];

public visibleFilters: IProductFilter[] = [];

@LocalStorage()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public searchQuery: any;
Expand Down Expand Up @@ -84,6 +88,7 @@ export class AdvancedPage implements OnInit {
this.visibleTags = this.allTags;

this.setSubproductsBasedOnProduct();
this.setBasicMeta();
}

saveQuery() {
Expand All @@ -94,8 +99,23 @@ export class AdvancedPage implements OnInit {
changeProduct() {
this.searchQuery.subproducts = [];
this.searchQuery.tags = [];
this.searchQuery.meta = {};

this.setSubproductsBasedOnProduct();
this.setBasicMeta();
}

setBasicMeta() {
this.visibleFilters.forEach((filter) => {
if (this.searchQuery.meta[filter.prop]) return;

if (filter.type === 'number') {
this.searchQuery.meta[filter.prop] = {
operator: '=',
value: undefined,
};
}
});
}

setSubproductsBasedOnProduct() {
Expand All @@ -105,6 +125,10 @@ export class AdvancedPage implements OnInit {
this.visibleSubproducts = this.allSubproducts.filter(
(sp) => sp.product === this.searchQuery.product.value
);

this.visibleFilters = this.metaService.getFiltersByProductId(
this.searchQuery.product.value
);
}
}

Expand All @@ -130,6 +154,17 @@ export class AdvancedPage implements OnInit {
queryAttributes.push(`tag:"${this.searchQuery.tags.join(',')}"`);
}

this.visibleFilters.forEach((filter) => {
if (!this.searchQuery.meta[filter.prop]) return;

if (filter.type === 'number') {
const { operator, value } = this.searchQuery.meta[filter.prop];
if (isNumber(+value) && !isNaN(+value)) {
queryAttributes.push(`${filter.prop}:${operator}${value}`);
}
}
});

const query = queryAttributes.join(' ');

return query;
Expand Down
41 changes: 28 additions & 13 deletions src/app/cards.service.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { Injectable } from '@angular/core';
import { inject, Injectable } from '@angular/core';

import { decompress } from 'compress-json';
import { sortBy } from 'lodash';

import { type ICard } from '../../interfaces';
import { parseQuery } from '../../search/search';
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 { MetaService } from './meta.service';

@Injectable({
providedIn: 'root',
})
export class CardsService {
private cards: ICard[] = [];
private cardsByName: Record<string, ICard> = {};
private cardsByCode: Record<string, ICard> = {};
private cardsById: Record<string, ICard> = {};

private collection: Record<string, number> = {};

public get cardCollection() {
return this.collection;
}
private metaService = inject(MetaService);

public async init() {
const cardData = await fetch(`${environment.baseUrl}/cards.min.json`);
Expand All @@ -34,19 +32,36 @@ export class CardsService {

this.cards.forEach((card) => {
this.cardsByName[card.name] = card;
this.cardsByCode[card.id] = card;
this.cardsById[card.id] = card;
});
}

// card utilities
public getCardByCodeOrName(codeOrName: string): ICard | undefined {
public getCardByIdOrName(codeOrName: string): ICard | undefined {
return (
this.cardsByCode[codeOrName] ?? this.cardsByName[codeOrName] ?? undefined
this.cardsById[codeOrName] ?? this.cardsByName[codeOrName] ?? undefined
);
}

public searchCards(query: string): ICard[] {
return parseQuery(this.cards, query, { collection: this.cardCollection });
const extraFilters = this.metaService.getAllFilters();
const extraFilterOperators = extraFilters.map((filter) => {
const mapped: Record<
IProductFilter['type'],
(f: IProductFilter) => ParserOperator
> = {
number: (f: IProductFilter) => {
return numericalOperator(
[filter.prop],
`meta.${f.prop}` as keyof ICard
);
},
};

return { operator: mapped[filter.type](filter), aliases: [filter.prop] };
});

return parseQuery(this.cards, query, extraFilterOperators);
}

public getCardById(id: string): ICard | undefined {
Expand Down
Loading

0 comments on commit a43fb03

Please sign in to comment.