Custom Filters [FrontEnd] (#3106) nocommentcode

* Add Create and Delete functions for Filter Presets to `Users` ape endpoint
- deleteFilterPreset
- addFilterPreset

* Added name and _id fields to the `ResultFilter` interface in the front end

This matches the `ResultFilter` interface in the backend
Added the new fields for the default filter
Added checks in result-filter.ts to not update the ui based on these fields

* Added new-filter-preset-popup

This popup is displayed when creating a new filter preset
It allows the user to select a name.

* Added Filter Preset Buttons in account page html

- Added the Filter Preset button
- Added the filter preset region
- Added the new and delete buttons

* Added Filter Presets to DB Snapshot

* Implemented Custom Fileter Creation, Selection and Deletion flow

- Users can now create a custom filter based on the current settings
- Users will be prompted to select a name for the filter
- User can click on any custom filter and the current filter will be updated
- User can delete a custom filter while it is selected

* Implemented Filter Preset Creation, Selection and Deletion flow

- Users can now create a filter preset based on the current settings
- Users will be prompted to select a name for the filter
- User can click on any filter preset and the current filter will be updated
- User can delete a filter preset while it is selected

* adjusted styling

* removed margin

* removed icon

* added media query

* popup wording

* automatically replacing _ when displaying and creating preset

* using loader and notifications

* fixed bork

* fixed sometimes not being able to delete preset

* make preset popup look like tags popup

Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
Malo Hamon 2022-06-15 22:47:52 +10:00 committed by GitHub
parent 8e96570b7e
commit f02649ae9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 330 additions and 4 deletions

View file

@ -61,6 +61,12 @@
}
}
.presetFilterButtons .buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.miniResultChartWrapper {
// pointer-events: none;
z-index: 999;
@ -295,6 +301,19 @@
}
.pageAccount {
.group.presetFilterButtons {
.buttons.filter-btns {
.filter-presets {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
grid-template-columns: 1fr auto;
width: 100%;
gap: 0.5rem;
}
}
}
.group.filterButtons {
gap: 1rem;
display: grid;

View file

@ -1203,8 +1203,10 @@
}
}
#tagsWrapper {
#tagsEdit {
#tagsWrapper,
#newResultFilterPresetPopupWrapper {
#tagsEdit,
#newResultFilterPresetPopup {
background: var(--bg-color);
border-radius: var(--roundness);
padding: 2rem;

View file

@ -248,6 +248,9 @@
}
@media only screen and (max-width: 700px) {
.pageAccount .presetFilterButtons .buttons {
grid-template-columns: 1fr;
}
.pageAccount {
.triplegroup {
grid-template-columns: 1fr 1fr;

View file

@ -2,8 +2,13 @@ import * as Misc from "../utils/misc";
import * as DB from "../db";
import Config from "../config";
import * as Notifications from "../elements/notifications";
import Ape from "../ape/index";
import * as Loader from "../elements/loader";
import { showNewResultFilterPresetPopup } from "../popups/new-result-filter-preset-popup";
export const defaultResultFilters: MonkeyTypes.ResultFilters = {
_id: "default-result-filters-id",
name: "default result filters",
difficulty: {
normal: true,
expert: true,
@ -60,13 +65,14 @@ export const defaultResultFilters: MonkeyTypes.ResultFilters = {
},
};
// current activated filter
export let filters = defaultResultFilters;
function save(): void {
window.localStorage.setItem("resultFilters", JSON.stringify(filters));
}
export function load(): void {
export async function load(): Promise<void> {
console.log("loading filters");
try {
const newResultFilters = window.localStorage.getItem("resultFilters");
@ -96,7 +102,7 @@ export function load(): void {
});
filters.tags = newTags;
await updateFilterPresets();
save();
} catch {
console.log("error in loading result filters");
@ -105,6 +111,133 @@ export function load(): void {
}
}
export async function updateFilterPresets(): Promise<void> {
// remove all previous filter preset buttons
$(".pageAccount .presetFilterButtons .filter-btns").html("");
const filterPresets = DB.getSnapshot().filterPresets.map((filter) => {
filter.name = filter.name.replace(/_/g, " ");
return filter;
});
console.log(filterPresets);
// if user has filter presets
if (filterPresets.length > 0) {
// show region
$(".pageAccount .presetFilterButtons").show();
// add button for each filter
DB.getSnapshot().filterPresets.forEach((filter) => {
$(".pageAccount .group.presetFilterButtons .filter-btns").append(
`<div class="filter-presets">
<div class="select-filter-preset button" data-id="${filter._id}">${filter.name} </div>
<div class="button delete-filter-preset" data-id="${filter._id}">
<i class="fas fa-fw fa-trash"></i>
</div>
</div>`
);
});
} else {
$(".pageAccount .presetFilterButtons").hide();
}
}
// sets the current filter to be a user custom filter
export async function setFilterPreset(id: string): Promise<void> {
const filter = DB.getSnapshot().filterPresets.find(
(filter) => filter._id === id
);
if (filter) {
// deep copy filter
filters = deepCopyFilter(filter);
save();
updateActive();
}
// make all filter preset butons inactive
$(
`.pageAccount .group.presetFilterButtons .filter-btns .filter-presets .select-filter-preset`
).removeClass("active");
// make current filter presest button active
$(
`.pageAccount .group.presetFilterButtons .filter-btns .filter-presets .select-filter-preset[data-id=${id}]`
).addClass("active");
}
function deepCopyFilter(
filter: MonkeyTypes.ResultFilters
): MonkeyTypes.ResultFilters {
return JSON.parse(JSON.stringify(filter));
}
function addFilterPresetToSnapshot(filter: MonkeyTypes.ResultFilters): void {
const snapshot = DB.getSnapshot();
DB.setSnapshot({
...snapshot,
filterPresets: [...snapshot.filterPresets, deepCopyFilter(filter)],
});
}
// callback function called by popup once user inputs name
async function createFilterPresetCallback(name: string): Promise<void> {
name = name.replace(/ /g, "_");
Loader.show();
const result = await Ape.users.addResultFilterPreset({ ...filters, name });
Loader.hide();
if (result.status === 200) {
addFilterPresetToSnapshot({ ...filters, name, _id: result.data });
updateFilterPresets();
Notifications.add("Filter preset created", 1);
} else {
Notifications.add("Error creating filter preset: " + result.message, -1);
console.log("error creating filter preset: " + result.message);
}
}
// shows popup for user to select name
export async function startCreateFilterPreset(): Promise<void> {
showNewResultFilterPresetPopup((name: string) =>
createFilterPresetCallback(name)
);
}
function removeFilterPresetFromSnapshot(id: string): void {
const snapshot = DB.getSnapshot();
const filterPresets = [...snapshot.filterPresets];
const toDeleteIx = filterPresets.findIndex((filter) => filter._id === id);
if (toDeleteIx > -1) {
filterPresets.splice(toDeleteIx, 1);
}
DB.setSnapshot({ ...snapshot, filterPresets });
}
// deletes the currently selected filter preset
export async function deleteFilterPreset(id: string): Promise<void> {
Loader.show();
const result = await Ape.users.removeResultFilterPreset(id);
Loader.hide();
if (result.status === 200) {
removeFilterPresetFromSnapshot(id);
updateFilterPresets();
reset();
Notifications.add("Filter preset deleted", 1);
} else {
Notifications.add("Error deleting filter preset: " + result.message, -1);
console.log("error deleting filter preset", result.message);
}
}
function deSelectFilterPreset(): void {
// make all filter preset buttons inactive
$(
".pageAccount .group.presetFilterButtons .filter-btns .filter-presets .select-filter-preset"
).removeClass("active");
}
export function getFilters(): MonkeyTypes.ResultFilters {
return filters;
}
@ -150,6 +283,11 @@ type AboveChartDisplay = MonkeyTypes.PartialRecord<
export function updateActive(): void {
const aboveChartDisplay: AboveChartDisplay = {};
(Object.keys(getFilters()) as MonkeyTypes.Group[]).forEach((group) => {
// id and name field do not correspond to any ui elements, no need to update
if (group === "_id" || group === "name") {
return;
}
aboveChartDisplay[group] = {
all: true,
array: [],
@ -296,6 +434,9 @@ export function toggle<G extends MonkeyTypes.Group>(
group: G,
filter: MonkeyTypes.Filter<G>
): void {
// user is changing the filters -> current filter is no longer a filter preset
deSelectFilterPreset();
try {
if (group === "date") {
(Object.keys(getGroup("date")) as MonkeyTypes.Filter<"date">[]).forEach(
@ -355,6 +496,11 @@ $(
const filter = $(e.target).attr("filter") as MonkeyTypes.Filter<typeof group>;
if ($(e.target).hasClass("allFilters")) {
(Object.keys(getFilters()) as MonkeyTypes.Group[]).forEach((group) => {
// id and name field do not correspond to any ui elements, no need to update
if (group === "_id" || group === "name") {
return;
}
(
Object.keys(getGroup(group)) as MonkeyTypes.Filter<typeof group>[]
).forEach((filter) => {
@ -371,6 +517,11 @@ $(
filters["date"]["all"] = true;
} else if ($(e.target).hasClass("noFilters")) {
(Object.keys(getFilters()) as MonkeyTypes.Group[]).forEach((group) => {
// id and name field do not correspond to any ui elements, no need to update
if (group === "_id" || group === "name") {
return;
}
if (group !== "date") {
(
Object.keys(getGroup(group)) as MonkeyTypes.Filter<typeof group>[]
@ -403,7 +554,15 @@ $(
});
$(".pageAccount .topFilters .button.allFilters").on("click", () => {
// user is changing the filters -> current filter is no longer a filter preset
deSelectFilterPreset();
(Object.keys(getFilters()) as MonkeyTypes.Group[]).forEach((group) => {
// id and name field do not correspond to any ui elements, no need to update
if (group === "_id" || group === "name") {
return;
}
(
Object.keys(getGroup(group)) as MonkeyTypes.Filter<typeof group>[]
).forEach((filter) => {
@ -424,7 +583,15 @@ $(".pageAccount .topFilters .button.allFilters").on("click", () => {
});
$(".pageAccount .topFilters .button.currentConfigFilter").on("click", () => {
// user is changing the filters -> current filter is no longer a filter preset
deSelectFilterPreset();
(Object.keys(getFilters()) as MonkeyTypes.Group[]).forEach((group) => {
// id and name field do not correspond to any ui elements, no need to update
if (group === "_id" || group === "name") {
return;
}
(
Object.keys(getGroup(group)) as MonkeyTypes.Filter<typeof group>[]
).forEach((filter) => {
@ -536,3 +703,15 @@ Misc.getFunboxList().then((funboxModes) => {
);
});
});
$(".pageAccount .topFilters .button.createFilterPresetBtn").on("click", () => {
startCreateFilterPreset();
});
$(document).on(
"click",
".pageAccount .group.presetFilterButtons .filter-btns .filter-presets .delete-filter-preset",
(e) => {
deleteFilterPreset($(e.currentTarget).data("id"));
}
);

View file

@ -64,6 +64,20 @@ export default class Users {
return await this.httpClient.delete(`${BASE_PATH}/personalBests`);
}
async addResultFilterPreset(
filter: MonkeyTypes.ResultFilters
): Ape.EndpointData {
return await this.httpClient.post(`${BASE_PATH}/resultFilterPresets`, {
payload: filter,
});
}
async removeResultFilterPreset(id: string): Ape.EndpointData {
return await this.httpClient.delete(
`${BASE_PATH}/resultFilterPresets/${id}`
);
}
async getTags(): Ape.EndpointData {
return await this.httpClient.get(`${BASE_PATH}/tags`);
}

View file

@ -24,4 +24,5 @@ export const defaultSnap: MonkeyTypes.Snapshot = {
quoteRatings: undefined,
quoteMod: false,
favoriteQuotes: {},
filterPresets: [],
};

View file

@ -89,6 +89,7 @@ export async function initSnapshot(): Promise<
};
if (userData.quoteMod === true) snap.quoteMod = true;
snap.favoriteQuotes = userData.favoriteQuotes ?? {};
snap.filterPresets = userData.resultFilterPresets ?? [];
snap.quoteRatings = userData.quoteRatings;
snap.favouriteThemes =
userData.favouriteThemes === undefined ? [] : userData.favouriteThemes;

View file

@ -1060,6 +1060,15 @@ $(document).on(
}
);
$(document).on(
"click",
".pageAccount .group.presetFilterButtons .filter-btns .filter-presets .select-filter-preset",
(e) => {
ResultFilters.setFilterPreset($(e.target).data("id"));
update();
}
);
$(".pageAccount .content .below .smoothing input").on("input", () => {
applyHistorySmoothing();
});

View file

@ -0,0 +1,81 @@
// the function to call after name is inputed by user
let callbackFunc: ((name: string) => void) | null = null;
export function show(): void {
if ($("#newResultFilterPresetPopupWrapper").hasClass("hidden")) {
$("#newResultFilterPresetPopupWrapper")
.stop(true, true)
.css("opacity", 0)
.removeClass("hidden")
.animate({ opacity: 1 }, 100, () => {
$("#newResultFilterPresetPopup input").trigger("focus").select();
});
}
}
export function hide(): void {
if (!$("#newResultFilterPresetPopupWrapper").hasClass("hidden")) {
$("#newResultFilterPresetPopupWrapper")
.stop(true, true)
.css("opacity", 1)
.animate(
{
opacity: 0,
},
100,
() => {
$("#newResultFilterPresetPopupWrapper").addClass("hidden");
}
);
}
}
function apply(): void {
const name = $("#newResultFilterPresetPopup input").val() as string;
if (callbackFunc) {
callbackFunc(name);
}
hide();
}
$("#newResultFilterPresetPopupWrapper").on("click", (e) => {
if ($(e.target).attr("id") === "newResultFilterPresetPopupWrapper") {
hide();
}
});
$("#newResultFilterPresetPopup input").on("keypress", (e) => {
if (e.key === "Enter") {
apply();
}
});
$("#newResultFilterPresetPopup .button").on("click", () => {
apply();
});
// this function is called to display the popup,
// it must specify the callback function to call once the name is selected
export function showNewResultFilterPresetPopup(
callback: (name: string) => void
): void {
callbackFunc = callback;
show();
}
$(document).on("click", "#top .config .wordCount .text-button", (e) => {
const wrd = $(e.currentTarget).attr("wordCount");
if (wrd == "custom") {
show();
}
});
$(document).on("keydown", (event) => {
if (
event.key === "Escape" &&
!$("#newResultFilterPresetPopupWrapper").hasClass("hidden")
) {
hide();
event.preventDefault();
}
});

View file

@ -466,6 +466,7 @@ declare namespace MonkeyTypes {
favoriteQuotes: FavoriteQuotes;
needsToChangeName?: boolean;
discordAvatar?: string;
filterPresets: ResultFilters[];
}
type FavoriteQuotes = Record<string, string[]>;
@ -476,6 +477,8 @@ declare namespace MonkeyTypes {
};
interface ResultFilters {
_id: string;
name: string;
difficulty: {
normal: boolean;
expert: boolean;

View file

@ -135,6 +135,12 @@
<div id="ad_account" class="hidden"></div>
<div class="group presetFilterButtons" style="display: none">
<div class="buttonsAndTitle">
<div class="title">filter presets</div>
<div class="buttons filter-btns" style="grid-column: 1/3"></div>
</div>
</div>
<div class="group topFilters">
<!-- <div
class="button"
@ -149,6 +155,7 @@
<div class="button allFilters">all</div>
<div class="button currentConfigFilter">current settings</div>
<div class="button toggleAdvancedFilters">advanced</div>
<div class="button createFilterPresetBtn">save as preset</div>
</div>
</div>
<div

View file

@ -956,3 +956,10 @@
<input type="text" class="input" placeholder="input" />
</div>
</div>
<div id="newResultFilterPresetPopupWrapper" class="popupWrapper hidden">
<div id="newResultFilterPresetPopup">
<div class="title">Add new filter preset</div>
<input type="text" value="new preset" />
<div class="button">add</div>
</div>
</div>