mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-28 01:39:29 +08:00
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:
parent
8e96570b7e
commit
f02649ae9f
12 changed files with 330 additions and 4 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1203,8 +1203,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
#tagsWrapper {
|
||||
#tagsEdit {
|
||||
#tagsWrapper,
|
||||
#newResultFilterPresetPopupWrapper {
|
||||
#tagsEdit,
|
||||
#newResultFilterPresetPopup {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ export const defaultSnap: MonkeyTypes.Snapshot = {
|
|||
quoteRatings: undefined,
|
||||
quoteMod: false,
|
||||
favoriteQuotes: {},
|
||||
filterPresets: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
81
frontend/src/ts/popups/new-result-filter-preset-popup.ts
Normal file
81
frontend/src/ts/popups/new-result-filter-preset-popup.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
3
frontend/src/ts/types/types.d.ts
vendored
3
frontend/src/ts/types/types.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue