legacy/src/vms/projects-explore-vm.ts
type ExtendedWindow = {
replaceDiacritics(inputText : string): string;
}
import _ from 'underscore'
import { catarse } from '../api'
import models from '../models'
import h from '../h'
import projectFilters from './project-filters-vm'
import userVM from '../vms/user-vm'
import { SequencePaginationVM } from '../utils/sequence-pagination-vm'
import { Project } from '../@types/project'
import { ViewModel } from '../@types/view-model'
import { City } from '../@types/city'
import { State } from '../@types/state'
import { CityState } from '../@types/city-state'
import { searchCitiesGroupedByState } from '../vms/cities-search-vm'
import { SinglePageViewModel } from '../utils/single-page-vm'
const { replaceDiacritics } = window as any as ExtendedWindow;
interface Observer<T> {
next(data: T): void;
error?(err: Error): void;
};
const projectFiltersVM = projectFilters();
type Mode = 'all_modes' | 'sub' | 'not_sub' | 'covid_19';
export type Category = {
name: string;
id: number;
}
const ALL_CATEGORIES : Category = { name: 'Todas as categorias', id: null };
const filtersMap = projectFiltersVM.filters;
const filters = catarse.filtersVM;
export type ProjectsExploreVMSearchParams = {
searchParam?: string;
mode?: Mode;
cityState?: CityState;
category_id?: number;
filter?: string;
}
export type Filter = {
title: string;
nicename?: string;
filter: any;
isContextual: boolean;
keyName: string;
}
export type Query = {
mode?: string,
category_id?: number,
state_acronym?: string,
state_name?: string,
city_name?: string,
filter?: string,
}
export class ProjectsExploreViewModel {
private _observer : Observer<Query>;
private _categories : Category[];
private _mode : Mode;
private _category : Category;
private _category_id : number;
private _filter : string;
private _cityState : CityState;
private _searchParam : string;
private _amountFoundOnLocation : number;
private _projectsView : ViewModel<Project>;
private _foundCityStates : CityState[];
private _isLoadingLocationsSearch : boolean;
private _lastQuery : Query;
constructor(params : ProjectsExploreVMSearchParams) {
this._observer = null;
this._categories = [ALL_CATEGORIES];
this._mode = params.mode || 'all_modes';
this._category = this._categories[0];
this._category_id = params.category_id || null;
this._filter = params.filter || 'projects_we_love';
this._cityState = params.cityState || null;
this._searchParam = params.searchParam || '';
this._amountFoundOnLocation = 0;
this._projectsView = {
collection: () => [],
isLastPage: () => true,
isLoading: () => false,
nextPage: () => new Promise<Project[]>(() => {}),
total: () => 0,
firstPage: (p = {}) => new Promise<Project[]>(() => {}),
};
this._foundCityStates = [];
this._lastQuery = this.mountQuery();
this._isLoadingLocationsSearch = false;
this.loadCategories();
this.executeSearch();
}
async search(params : ProjectsExploreVMSearchParams) {
this._mode = params.mode || 'all_modes';
this._category_id = params.category_id || null;
this._filter = params.filter || 'projects_we_love';
this._cityState = params.cityState || null;
this._searchParam = params.searchParam || '';
if (this._category_id) {
try {
this._category = await this.getCategoryById(this._category_id);
h.redraw();
} catch(e) {
this.category = ALL_CATEGORIES;
this.dispatchNewQuery();
}
} else {
this._category = ALL_CATEGORIES;
h.redraw();
}
this.executeSearch();
}
subscribe(observer : Observer<Query>) {
this._observer = observer;
}
get projectsView() : ViewModel<Project> {
return this._projectsView;
}
async searchLocations(inputText : string) {
let isLoaded = false;
const loaderTimeout = setTimeout(() => {
if (!isLoaded) {
this._isLoadingLocationsSearch = true;
h.redraw();
}
}, 100);
this._foundCityStates = [];
this._foundCityStates = await searchCitiesGroupedByState(inputText);
isLoaded = true;
clearTimeout(loaderTimeout);
this._isLoadingLocationsSearch = false;
h.redraw();
}
get foundLocations() : CityState[] {
return this._foundCityStates;
}
get isLoadingLocationsSearch() : boolean {
return this._isLoadingLocationsSearch;
}
get isTextSearch() : boolean {
return this._searchParam !== '' && this._searchParam.length > 0;
}
set searchParam(value : string) {
this._searchParam = value
this.dispatchNewQuery();
}
get searchParam() : string {
return this._searchParam;
}
set mode(mode : Mode) {
this._mode = mode;
if (mode === 'sub') {
this._filter = 'all';
} else if (mode === 'all_modes') {
this._filter = 'projects_we_love';
}
this.dispatchNewQuery();
}
get mode() : Mode {
return this._mode;
}
get modeName() : string {
return filtersMap[this._mode].title;
}
set category(category : Category) {
this._category = category;
this._category_id = category.id;
this.dispatchNewQuery();
h.redraw();
}
get category() : Category {
return this._category;
}
set category_id(category_id : number) {
this._category_id = category_id;
(async () => {
try {
this.category = await this.getCategoryById(category_id);
} catch(e) {
this.category = ALL_CATEGORIES;
}
})();
}
get category_id() {
return this._category_id;
}
get categories() : Category[] {
return this._categories;
}
set cityState(cityState : CityState) {
this._cityState = cityState;
this.dispatchNewQuery();
}
get cityState() : CityState{
return this._cityState;
}
get amountFoundOnLocation() : number {
return this._amountFoundOnLocation;
}
set filter(filter : string) {
this._filter = filter;
this.dispatchNewQuery();
}
get filter() {
return this._filter;
}
get filterName() : string {
return filtersMap[this._filter].nicename;
}
get filters() : Filter[] {
return projectFiltersVM.getContextFilters();
}
private async getCategoryById(category_id : number) : Promise<Category> {
return new Promise<Category>((resolve, reject) => {
const category = this.findCagetoryById(category_id);
if (category) {
resolve(category);
} else {
const intervalWaitingCategoriesToLoad = setInterval(() => {
if (this._categories.length > 1) {
const category = this.findCagetoryById(category_id);
if (category) {
resolve(category);
} else {
reject(new Error('Category not found'));
}
clearInterval(intervalWaitingCategoriesToLoad);
}
}, 100);
}
});
}
private findCagetoryById(category_id : number) : Category {
return this._categories.find(c => c.id === category_id);
}
private async loadCategories() {
models.category.pageSize(100);
const params = filters({}).order({ name: 'asc' }).parameters();
const categories = await models.category.getPageWithToken(params);
this._categories = [ALL_CATEGORIES].concat(categories);
const category = this._categories.find(c => c.id === this._category_id);
if (category) {
this._category = category;
}
h.redraw();
}
private dispatchNewQuery() {
const newQuery = this.mountQuery();
const queryIsDifferentFromLast = !_.isEqual(this._lastQuery, newQuery);
if (queryIsDifferentFromLast) {
this._lastQuery = newQuery;
if (this._observer) {
this._observer.next(newQuery);
}
}
}
private mountQuery() {
const query : Query = { }
if (this._mode !== 'all_modes') {
query.mode = this._mode;
}
if (this._category_id) {
query.category_id = this._category_id;
}
if (this._cityState) {
query.state_acronym = this._cityState.state.acronym;
query.state_name = this._cityState.state.state_name;
if (this._cityState.city) {
query.city_name = this._cityState.city.name;
}
}
if (this._filter !== 'projects_we_love') {
query.filter = this._filter;
}
return query;
}
private async executeSearch() {
this.resetContextFilter();
if (this._mode === 'sub') {
projectFiltersVM.removeContextFilter(projectFiltersVM.filters.finished);
projectFiltersVM.removeContextFilter(projectFiltersVM.filters.expiring);
this._filter = 'all';
}
const model = this.getModelBasedOnFilter();
const parameters = this.getParametersBaserOnFilter();
this._projectsView = this.loadProjects(model, parameters);
this.countProjectsOnCity(model, parameters);
this._lastQuery = this.mountQuery();
h.redraw();
}
private resetContextFilter() {
const loggedInContextFilters = ['finished', 'projects_we_love', 'all', 'saved_projects', 'contributed_by_friends', 'expiring', 'recent'];
const notLoggedInContextFilters = ['finished', 'projects_we_love', 'all', 'expiring', 'recent'];
const contextFilters = userVM.isLoggedIn ? loggedInContextFilters : notLoggedInContextFilters;
projectFiltersVM.setContextFilters(contextFilters);
}
private loadProjects(model, parameters : Object = {}) : ViewModel<Project> {
model.pageSize(9);
if (this._searchParam) {
return new SinglePageViewModel(async () => {
const projectsFound = await this.makeProjectsSearch()
h.redraw()
return projectsFound
});
} else if (this._cityState?.city?.name) {
const cityOnlyVmInstance = catarse.paginationVM(model, null, { Prefer: 'count=exact' });
const stateOnlyVmInstance = catarse.paginationVM(model, null, { Prefer: 'count=exact' });
const cityOnlyPages = h.createBasicPaginationVMWithAutoRedraw(cityOnlyVmInstance);
const stateOnlyPages = h.createBasicPaginationVMWithAutoRedraw(stateOnlyVmInstance);
const cityOnlyParameters = {
...parameters,
...filters({ city_name: 'eq' }).city_name(this._cityState.city.name).parameters(),
};
const stateOnlyParameters = {
...parameters,
...filters({ state_acronym: 'eq', city_name: 'not.eq' }).state_acronym(this._cityState.state.acronym).city_name(this._cityState.city.name).parameters(),
};
cityOnlyPages.firstPage(cityOnlyParameters);
stateOnlyPages.firstPage(stateOnlyParameters);
const pageSize = 9;
const vms = [cityOnlyPages, stateOnlyPages];
return new SequencePaginationVM<Project>(vms, pageSize, model);
} else {
const vmInstance = catarse.paginationVM(model, null, { Prefer: 'count=exact' });
const pages = h.createBasicPaginationVMWithAutoRedraw(vmInstance);
pages.firstPage(parameters);
return pages;
}
}
private async makeProjectsSearch() : Promise<Project[]> {
try {
const response = await models.projectSearch.postWithToken({ query: replaceDiacritics(this._searchParam) })
return response as Project[]
} catch(e) {
if (this._observer) {
this._observer.error(e)
}
}
}
private async countProjectsOnCity(model, filterParameters : Object = {}) {
try {
if (this._cityState?.city?.name && _.isEmpty(this._searchParam)) {
const parametersWithOnlyCityNotState = _.extend(
filterParameters,
filters({ city_name: 'eq' }).city_name(this._cityState.city.name).parameters()
);
this._amountFoundOnLocation = await this.countProjects(model, parametersWithOnlyCityNotState);
}
} catch(e) {
this._amountFoundOnLocation = 0;
} finally {
h.redraw();
}
}
private async countProjects(model, filterParameters: Object = {}) {
model.pageSize(1);
const selectMinimalFieldsFilterVM = catarse.filtersVM({ selectFields: 'select' });
selectMinimalFieldsFilterVM.selectFields('project_id');
const pages = catarse.paginationVM(model, null, { Prefer: 'count=exact' });
const countParameters = _.extend(filterParameters, selectMinimalFieldsFilterVM.parameters());
await pages.firstPage(countParameters);
return pages.total();
}
private getModelBasedOnFilter() {
return this._filter === 'finished' ? models.finishedProject : models.project;
}
private getParametersBaserOnFilter() {
const modeFilter = filtersMap[this._mode];
const filterFilter = filtersMap[this._filter];
const parametersFilter = this.getParametersFromLocationSearchAndCategory();
const order = this.filterOrderBasedOnFilter();
return _.extend(
modeFilter.filter.parameters(),
filterFilter.filter.order(order).parameters(),
this.setOpenForContribution(),
parametersFilter
);
}
private filterOrderBasedOnFilter() {
if (this._filter === 'finished') {
return {
state_order: 'asc',
state: 'desc',
pledged: 'desc'
};
} else {
return {
open_for_contributions: 'desc',
state_order: 'asc',
state: 'desc',
score: 'desc',
pledged: 'desc'
};
}
}
private setOpenForContribution() {
if (this._filter !== 'finished') {
return filters({ open_for_contributions: 'eq' }).open_for_contributions(true).parameters();
} else {
return {};
}
}
private getParametersFromLocationSearchAndCategory() {
let parametersFilter = {};
if (this._category_id) {
parametersFilter = Object.assign(parametersFilter, filters({ category_id : 'eq' }).category_id(this._category_id).parameters());
}
const cityName = this._cityState?.city?.name;
const stateAcronym = this._cityState?.state?.acronym;
if (!cityName && stateAcronym) {
parametersFilter = Object.assign(parametersFilter, filters({ state_acronym: 'eq' }).state_acronym(stateAcronym).parameters());
}
parametersFilter = Object.assign(parametersFilter, this.getOrParameters());
return parametersFilter;
}
private getOrParameters() {
if (this._searchParam) {
return filters({
textSearch: 'or'
})
.textSearch({
full_text_index: {
plfts: this._searchParam,
},
project_name: {
plfts: this._searchParam,
}
}).parameters();
} else {
return {};
}
}
}