Skip to content

Commit 05090ed

Browse files
feat: add search and filter to 'From Ingredients' tab (#259)
1 parent 9fe5d76 commit 05090ed

File tree

19 files changed

+313
-131
lines changed

19 files changed

+313
-131
lines changed

.vscode/extensions.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
{
22
"recommendations": [
33
"behzad88.aurelia",
4+
"codeium.codeium",
45
"dbaeumer.vscode-eslint",
5-
"github.copilot",
66
"esbenp.prettier-vscode",
7-
"vscode-icons-team.vscode-icons",
8-
"mikeburgh.xml-format",
7+
"bradlc.vscode-tailwindcss",
98
]
109
}

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ android {
66
applicationId "com.moimob.drinkable"
77
minSdkVersion rootProject.ext.minSdkVersion
88
targetSdkVersion rootProject.ext.targetSdkVersion
9-
versionCode 12400
10-
versionName "1.24.0"
9+
versionCode 12500
10+
versionName "1.25.0"
1111
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1212
aaptOptions {
1313
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

cypress/e2e/cocktails.cy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('Cocktails', () => {
3838
it('Should display only result from search', () => {
3939
cy.visit('#/cocktails');
4040

41-
cy.getByDataAttribute('cocktails-search').type('Gin & Tonic');
41+
cy.getByDataAttribute('all-cocktails-filter').find('input').type('Gin & Tonic');
4242
cy.getByDataAttribute('cocktails-wrapper').children().should('have.length', '1');
4343
});
4444
});
@@ -47,7 +47,7 @@ describe('Cocktails', () => {
4747
it('Category - Should display only result from filter', () => {
4848
cy.visit('#/cocktails');
4949

50-
cy.getByDataAttribute('open-filters').click();
50+
cy.get('[data-cy=all-cocktails-filter] [data-cy=open-filters]').click();
5151
cy.getByDataAttribute('select-category').select('Shot');
5252
cy.getByDataAttribute('filter-dialog-close').click();
5353
cy.getByDataAttribute('active-filters').should('contain', '1');
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
• Added Search and Filters to 'From Ingredients' tab
2+
• 3 new cocktails. White Sangria, Ruussian Spring Punch and French Martini
3+
• 4 new ingredients. Apple, Lemon, Lime, Raspberry Liqueur

src/components/cocktail-list-item.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@
77
<img else class="small-round-image" src="/static/images/no-image.png" />
88
<div>
99
<p>${cocktail.name}</p>
10-
<div class="flex">
10+
<div if.bind="!cocktail.missingIngredient.name" class="flex">
1111
<div if.bind="cocktail.rating" class="rating rating-xs pr-1" data-cy="cocktail-item-rating">
1212
<div
1313
repeat.for="i of 5"
1414
type="radio"
1515
class="${cocktail.rating > i ? '' : 'bg-opacity-20'} mask mask-star-2 bg-warning w-3 h-3"></div>
1616
</div>
1717
</div>
18+
<div else>
19+
<ul>
20+
<li style="font-weight: bold">${cocktail.missingIngredient.name}</li>
21+
</ul>
22+
</div>
1823
</div>
1924
<div class="flex-1"></div>
2025
<icon-heart

src/components/dialogs/cocktail-filter-dialog.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,11 @@ export class CocktailFilterDialogModel {
6464
spiritFilter: SpiritType;
6565
ingredientFilter: string;
6666
favoriteFilter: boolean;
67+
68+
constructor() {
69+
this.categoryFilter = null;
70+
this.spiritFilter = null;
71+
this.ingredientFilter = null;
72+
this.favoriteFilter = null;
73+
}
6774
}

src/data/cocktail-data.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export function toCocktailWithMissingIngredients(
1919
ingredientGroups: cocktail.ingredientGroups,
2020
instructions: cocktail.instructions,
2121
missingIngredient: ingredient,
22-
name: cocktail.name
22+
name: cocktail.name,
23+
isFavorite: cocktail.isFavorite,
24+
rating: cocktail.rating
2325
};
2426
}
2527

@@ -1811,5 +1813,51 @@ const cocktails: Cocktail[] = [
18111813
{ amount: '', ingredientId: '102', unit: '' },
18121814
{ amount: '', ingredientId: '103', unit: '' }
18131815
]
1816+
},
1817+
{
1818+
id: '128',
1819+
imageSrc: 'images/white_wine_sangria.jpg',
1820+
isImagePortrait: false,
1821+
name: 'White Sangria',
1822+
category: DrinkCategory.Other,
1823+
instructions:
1824+
'Chop the Lemon, Lime and other fruits into large chunks.\nFill the Pitcher with the white wine and mix in the Apple Brandy.\nTop to taste with soda water.',
1825+
ingredientGroups: [
1826+
{ amount: '7.5', ingredientId: '87', unit: Unit.DL },
1827+
{ amount: '120', ingredientId: '65', unit: Unit.ML },
1828+
{ amount: '', ingredientId: '4', unit: '' },
1829+
{ amount: '150', ingredientId: '20', unit: Unit.G },
1830+
{ amount: '1', ingredientId: '104', unit: '' },
1831+
{ amount: '1', ingredientId: '105', unit: '' },
1832+
{ amount: '1', ingredientId: '106', unit: '' }
1833+
]
1834+
},
1835+
{
1836+
id: '129',
1837+
imageSrc: 'images/french_martini.jpg',
1838+
isImagePortrait: false,
1839+
name: 'French Martini',
1840+
category: DrinkCategory.Cocktail,
1841+
instructions:
1842+
'Pour all ingredients into shaker with ice cubes.\nShake well and strain into a chilled cocktail glass.\nSqueeze oil from lemon peel onto the drink',
1843+
ingredientGroups: [
1844+
{ amount: '45', ingredientId: '8', unit: Unit.ML },
1845+
{ amount: '15', ingredientId: '107', unit: Unit.ML },
1846+
{ amount: '15', ingredientId: '26', unit: Unit.ML }
1847+
]
1848+
},
1849+
{
1850+
id: '130',
1851+
imageSrc: 'images/russian_spring_punch.jpg',
1852+
isImagePortrait: false,
1853+
name: 'Russian Spring Punch',
1854+
category: DrinkCategory.Cocktail,
1855+
instructions: 'Pour the ingredients into an highball glass with ice',
1856+
ingredientGroups: [
1857+
{ amount: '30', ingredientId: '8', unit: Unit.ML },
1858+
{ amount: '15', ingredientId: '68', unit: Unit.ML },
1859+
{ amount: '10', ingredientId: '3', unit: Unit.ML },
1860+
{ amount: '30', ingredientId: '10', unit: Unit.ML }
1861+
]
18141862
}
18151863
];

src/data/ingredient-data.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ export function getStaticIngredients() {
77

88
const currentIngredients: StaticIngredient[] = [
99
{ id: '1', translation: 'light-rum', spiritType: SpiritType.Rum },
10-
{ id: '2', translation: 'lime-juice', spiritType: SpiritType.None },
10+
{ id: '2', translation: 'lime-juice', spiritType: SpiritType.None, replacementIds: ['106'] },
1111
{ id: '3', translation: 'simple-syrup', spiritType: SpiritType.None, recipeId: '114' },
1212
{ id: '4', translation: 'soda-water', spiritType: SpiritType.None },
1313
{ id: '5', translation: 'mint', spiritType: SpiritType.None },
1414
{ id: '6', translation: 'gin', spiritType: SpiritType.Gin },
1515
{ id: '7', translation: 'tonic-water', spiritType: SpiritType.None },
1616
{ id: '8', translation: 'vodka', spiritType: SpiritType.Vodka },
1717
{ id: '9', translation: 'tomato-juice', spiritType: SpiritType.None },
18-
{ id: '10', translation: 'lemon-juice', spiritType: SpiritType.None },
18+
{ id: '10', translation: 'lemon-juice', spiritType: SpiritType.None, replacementIds: ['105'] },
1919
{ id: '11', translation: 'tabasco-sauce', spiritType: SpiritType.None },
2020
{ id: '12', translation: 'worcestershire-sauce', spiritType: SpiritType.None },
2121
{ id: '13', translation: 'black-pepper', spiritType: SpiritType.None },
@@ -108,5 +108,9 @@ const currentIngredients: StaticIngredient[] = [
108108
{ id: '100', translation: 'cachaca', spiritType: SpiritType.Rum },
109109
{ id: '101', translation: 'red-wine', spiritType: SpiritType.None },
110110
{ id: '102', translation: 'cloves', spiritType: SpiritType.None },
111-
{ id: '103', translation: 'cinnamon', spiritType: SpiritType.None }
111+
{ id: '103', translation: 'cinnamon', spiritType: SpiritType.None },
112+
{ id: '104', translation: 'apple', spiritType: SpiritType.None },
113+
{ id: '105', translation: 'lemon', spiritType: SpiritType.None },
114+
{ id: '106', translation: 'lime', spiritType: SpiritType.None },
115+
{ id: '107', translation: 'raspberry-liqueur', spiritType: SpiritType.None }
112116
];

src/locales/en/ingredients.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,9 @@
101101
"cachaca": "Cachaça",
102102
"red-wine": "Red wine",
103103
"cloves": "Cloves",
104-
"cinnamon": "Cinnamon"
104+
"cinnamon": "Cinnamon",
105+
"apple": "Apple",
106+
"lime": "Lime",
107+
"lemon": "Lemon",
108+
"raspberry-liqueur": "Raspberry Liqueur"
105109
}
Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<template>
2+
<require from="./../cocktail-filter-component"></require>
23
<div class="absolute h-full w-full overflow-y-scroll pb-12" data-cy="cocktails-wrapper">
34
<cocktail-list-item
45
repeat.for="cocktail of filteredCocktails"
@@ -13,32 +14,8 @@
1314
</button>
1415
</div>
1516

16-
<div class="search-container-new bg-base-300">
17-
<div style="min-height: 0.65em"></div>
18-
<div class="flex justify-between max-w-md m-auto relative">
19-
<input
20-
type="text"
21-
class="input input-bordered input-sm w-full"
22-
autocomplete="off"
23-
ref="searchElement"
24-
type="search"
25-
placeholder="${'search' | t}"
26-
id="searchInput"
27-
value.bind="searchFilter"
28-
data-cy="cocktails-search" />
29-
<div>
30-
<label for="searchInput" aria-label="Search"></label>
31-
</div>
32-
<div click.trigger="openFilters()" class="flex" data-cy="open-filters">
33-
<icon-filter class="flex w-6 h-6 self-center mx-2"></icon-filter>
34-
</div>
35-
<div
36-
if.bind="activeFilters"
37-
class="absolute w-5 h-5 right-2.5 -top-1 text-sm bg-accent rounded-badge text-center font-light pointer-events-none"
38-
data-cy="active-filters">
39-
${activeFilters}
40-
</div>
41-
</div>
42-
<div style="min-height: 0.4em"></div>
43-
</div>
17+
<cocktail-filter-component
18+
callback.call="update(data)"
19+
params.bind="params"
20+
data-cy="all-cocktails-filter"></cocktail-filter-component>
4421
</template>

src/modules/cocktails/all-cocktails/all-cocktails.ts

Lines changed: 24 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
1-
import { inject, observable } from 'aurelia-framework';
1+
import { inject } from 'aurelia-framework';
22
import { Cocktail } from 'domain/entities/cocktail';
33
import { CocktailDialog } from 'components/dialogs/cocktail-dialog';
44
import { DialogService } from 'aurelia-dialog';
55
import { CocktailService } from 'services/cocktail-service';
66
import { createCocktailDeleteToast } from 'functions/toast-functions';
7-
import { CocktailFilterDialog, CocktailFilterDialogModel } from 'components/dialogs/cocktail-filter-dialog';
7+
import { CocktailFilterDialogModel } from 'components/dialogs/cocktail-filter-dialog';
88
import { IngredientService } from 'services/ingredient-service';
99
import { CocktailsParams } from '../cocktails';
10+
import { filterCocktailList } from '../filter-cocktails-helper';
11+
import { CocktailFilterCallbackData } from '../cocktail-filter-component';
1012

1113
@inject(CocktailService, DialogService, IngredientService)
1214
export class AllCocktails {
13-
@observable public searchFilter: string;
14-
1515
public filteredCocktails: Cocktail[] = [];
16-
public activeFilters: number | undefined;
17-
1816
private _cocktails: Cocktail[] = [];
19-
private _filterDialogModel: CocktailFilterDialogModel = {
20-
categoryFilter: null,
21-
spiritFilter: null,
22-
favoriteFilter: null,
23-
ingredientFilter: null
24-
};
17+
private _latestCallback: CocktailFilterCallbackData;
18+
19+
public params: CocktailsParams;
2520

2621
constructor(
2722
private _cocktailService: CocktailService,
@@ -30,73 +25,33 @@ export class AllCocktails {
3025
) {}
3126

3227
activate(model: CocktailsParams) {
33-
if (model?.filter === 'favorites') {
34-
this._filterDialogModel.favoriteFilter = true;
35-
}
28+
this.params = model;
3629
}
3730

3831
bind() {
3932
this._cocktails = this._cocktailService.getCocktails();
40-
this.filterCocktails();
41-
}
42-
43-
searchFilterChanged() {
44-
this.filterCocktails();
45-
}
46-
47-
openFilters() {
48-
this._dialogService
49-
.open({ viewModel: CocktailFilterDialog, model: this._filterDialogModel, lock: false })
50-
.whenClosed(response => {
51-
if (response.wasCancelled) {
52-
return;
53-
}
54-
this._filterDialogModel = response.output;
55-
this.filterCocktails();
56-
});
57-
}
58-
59-
filterCocktails() {
60-
let filterCount = 0;
6133

62-
const searchFilter = this.searchFilter === undefined ? '' : this.searchFilter;
63-
let cocktails = this._cocktails.filter(x => x.name.toLowerCase().includes(searchFilter.toLowerCase()));
34+
let data: CocktailFilterCallbackData = {
35+
filterDialogModel: this._latestCallback?.filterDialogModel || new CocktailFilterDialogModel(),
36+
searchText: this._latestCallback?.searchText || ''
37+
};
6438

65-
if (searchFilter !== '') {
66-
cocktails.sort(a => (a.name.toLowerCase().startsWith(searchFilter.toLowerCase()) ? -1 : 1));
39+
if (this.params?.filter === 'favorites') {
40+
data.filterDialogModel.favoriteFilter = true;
6741
}
6842

69-
if (this._filterDialogModel.categoryFilter !== null) {
70-
cocktails = cocktails.filter(x => x.category === this._filterDialogModel.categoryFilter);
71-
filterCount++;
72-
}
73-
74-
if (this._filterDialogModel.spiritFilter !== null) {
75-
const ingredientIds = this._ingredientService.getIngredientsBySpiritType(
76-
this._filterDialogModel.spiritFilter
77-
);
78-
79-
cocktails = cocktails.filter(x =>
80-
x.ingredientGroups.some(y => ingredientIds.map(y => y.id).includes(y.ingredientId))
81-
);
82-
filterCount++;
83-
}
84-
85-
if (this._filterDialogModel.ingredientFilter !== null) {
86-
let ingredientIds = this._ingredientService.getIngredientAndReplacementIds(
87-
this._filterDialogModel.ingredientFilter
88-
);
89-
90-
cocktails = cocktails.filter(x => x.ingredientGroups.some(x => ingredientIds.includes(x.ingredientId)));
91-
filterCount++;
92-
}
43+
this.update(data);
44+
}
9345

94-
if (this._filterDialogModel.favoriteFilter !== null) {
95-
cocktails = cocktails.filter(x => x.isFavorite === true);
96-
filterCount++;
97-
}
46+
update(data: CocktailFilterCallbackData) {
47+
this._latestCallback = data;
48+
let { cocktails } = filterCocktailList({
49+
cocktails: this._cocktails,
50+
filterDialogModel: data.filterDialogModel,
51+
ingredientService: this._ingredientService,
52+
searchText: data.searchText
53+
});
9854

99-
this.activeFilters = filterCount > 0 ? filterCount : undefined;
10055
this.filteredCocktails = cocktails;
10156
}
10257

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<template>
2+
<div class="search-container-new bg-base-300">
3+
<div style="min-height: 0.65em"></div>
4+
<div class="flex justify-between max-w-md m-auto relative">
5+
<input
6+
type="text"
7+
class="input input-bordered input-sm w-full"
8+
autocomplete="off"
9+
type="search"
10+
placeholder="${'search' | t}"
11+
id="searchInput"
12+
value.bind="searchFilter" />
13+
<div>
14+
<label for="searchInput" aria-label="Search"></label>
15+
</div>
16+
<div click.trigger="openFilters()" class="flex" data-cy="open-filters">
17+
<icon-filter class="flex w-6 h-6 self-center mx-2"></icon-filter>
18+
</div>
19+
<div
20+
if.bind="activeFilters"
21+
class="absolute w-5 h-5 right-2.5 -top-1 text-sm bg-accent rounded-badge text-center font-light pointer-events-none"
22+
data-cy="active-filters">
23+
${activeFilters}
24+
</div>
25+
</div>
26+
<div style="min-height: 0.4em"></div>
27+
</div>
28+
</template>

0 commit comments

Comments
 (0)