diff --git a/.gitignore b/.gitignore index b24fc61..d0441a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ .env .idea/ +.eslintrc +.vscode/ \ No newline at end of file diff --git a/app/common/enums.js b/app/common/enums.js index f52cf4f..33cb41e 100644 --- a/app/common/enums.js +++ b/app/common/enums.js @@ -104,6 +104,13 @@ const AD_CATEGORY = { id: "FLAT", title: "Stan", hasGardenSize: false, + hasAccesRoadType: true, + hasBalconyProp: true, + hasNewBuildingProp: true, + hasElevatorProp: true, + hasNumberOfRoom: true, + hasNumberOfFloors: false, + hasFloorProp: true, priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS @@ -112,6 +119,13 @@ const AD_CATEGORY = { id: "HOUSE", title: "Kuća", hasGardenSize: true, + hasAccesRoadType: true, + hasBalconyProp: true, + hasNewBuildingProp: true, + hasElevatorProp: false, + hasNumberOfRoom: true, + hasNumberOfFloors: true, + hasFloorProp: false, priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS, @@ -121,6 +135,13 @@ const AD_CATEGORY = { id: "OFFICE", title: "Kancelarija", hasGardenSize: false, + hasAccesRoadType: true, + hasBalconyProp: false, + hasNewBuildingProp: true, + hasElevatorProp: true, + hasNumberOfRoom: true, + hasNumberOfFloors: false, + hasFloorProp: true, priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS @@ -129,6 +150,13 @@ const AD_CATEGORY = { id: "LAND", title: "Zemljište", hasGardenSize: false, + hasAccesRoadType: true, + hasBalconyProp: false, + hasNewBuildingProp: false, + hasElevatorProp: false, + hasNumberOfRoom: false, + hasNumberOfFloors: false, + hasFloorProp: false, priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: LAND_SIZE_SLIDER_OPTIONS @@ -137,6 +165,13 @@ const AD_CATEGORY = { id: "APARTMENT", title: "Apartman", hasGardenSize: false, + hasAccesRoadType: true, + hasBalconyProp: true, + hasNewBuildingProp: true, + hasElevatorProp: true, + hasNumberOfRoom: true, + hasNumberOfFloors: false, + hasFloorProp: true, priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS @@ -145,6 +180,13 @@ const AD_CATEGORY = { id: "GARAGE", title: "Garaža", hasGardenSize: false, + hasAccesRoadType: true, + hasBalconyProp: false, + hasNewBuildingProp: false, + hasElevatorProp: false, + hasNumberOfRoom: false, + hasNumberOfFloors: false, + hasFloorProp: false, priceSliderOptionsSale: GARAGE_PRICE_SLIDER_OPTIONS_SALE, priceSliderOptionsRent: GARAGE_PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: GARAGE_SIZE_SLIDER_OPTIONS @@ -153,6 +195,13 @@ const AD_CATEGORY = { id: "COTTAGE", title: "Vikendica", hasGardenSize: true, + hasAccesRoadType: true, + hasBalconyProp: true, + hasNewBuildingProp: true, + hasElevatorProp: false, + hasNumberOfRoom: true, + hasNumberOfFloors: true, + hasFloorProp: false, priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS, @@ -199,6 +248,10 @@ const EMAIL_FREQUENCY = { }; const HEATING_TYPE = { + ANY: { + id: "ANY", + title: "Svi" + }, NO_HEATING: { id: "NO_HEATING", title: "Nije uvedeno" @@ -238,6 +291,10 @@ const HEATING_TYPE = { }; const ACCESS_ROAD_TYPE = { + ANY: { + id: "ANY", + title: "Svi" + }, ASPHALT: { id: "ASPHALT", title: "Asfalt" diff --git a/app/common/filterEnums.js b/app/common/filterEnums.js new file mode 100644 index 0000000..608e8d3 --- /dev/null +++ b/app/common/filterEnums.js @@ -0,0 +1,110 @@ +const { AD_CATEGORY, ACCESS_ROAD_TYPE, HEATING_TYPE } = require("./enums"); + +const ADVANCED_BOOLEAN_FILTERS = [ + { + dbField: "balcony", + title: "Balkon", + categoriesToShow: [ + AD_CATEGORY.FLAT, + AD_CATEGORY.HOUSE, + AD_CATEGORY.APARTMENT, + AD_CATEGORY.COTTAGE + ] + }, + { + dbField: "elevator", + title: "Lift", + categoriesToShow: [ + AD_CATEGORY.FLAT, + AD_CATEGORY.APARTMENT, + AD_CATEGORY.OFFICE + ] + }, + { + dbField: "newBuilding", + title: "Novogradnja", + categoriesToShow: [ + AD_CATEGORY.FLAT, + AD_CATEGORY.HOUSE, + AD_CATEGORY.APARTMENT, + AD_CATEGORY.COTTAGE, + AD_CATEGORY.OFFICE + ] + } +]; + +const ADVANCED_SEGMENT_SELECT_FILTERS = [ + { + dbField: "accessRoadType", + title: "Pristupni put", + values: Object.keys(ACCESS_ROAD_TYPE).map(key => ACCESS_ROAD_TYPE[key]), + categoriesToShow: [ + AD_CATEGORY.FLAT, + AD_CATEGORY.HOUSE, + AD_CATEGORY.APARTMENT, + AD_CATEGORY.COTTAGE, + AD_CATEGORY.OFFICE, + AD_CATEGORY.LAND, + AD_CATEGORY.GARAGE + ] + } + // { + // dbField: "heatingType", + // title: "Vrsta grijanja", + // values: Object.keys(HEATING_TYPE).map(key => HEATING_TYPE[key]), + // categoriesToShow: [ + // AD_CATEGORY.FLAT, + // AD_CATEGORY.HOUSE, + // AD_CATEGORY.APARTMENT, + // AD_CATEGORY.COTTAGE, + // AD_CATEGORY.OFFICE + // ] + // } +]; + +const ADVANCED_RANGE_FILTERS = [ + { + id: "numberOfFloors", + title: "Broj spratova", + dbFieldMin: "numberOfFloorsMin", + dbFieldMax: "numberOfFloorsMax", + validValueMin: -1, + validValueMax: 50, + categoriesToShow: [AD_CATEGORY.HOUSE, AD_CATEGORY.COTTAGE] + }, + { + id: "floor", + title: "Sprat", + dbFieldMin: "floorMin", + dbFieldMax: "floorMax", + validValueMin: -10, + validValueMax: 50, + categoriesToShow: [ + AD_CATEGORY.FLAT, + AD_CATEGORY.APARTMENT, + AD_CATEGORY.OFFICE + ] + }, + { + id: "numberOfRooms", + title: "Broj soba", + dbFieldMin: "numberOfRoomsMin", + dbFieldMax: "numberOfRoomsMax", + decimalPlaces: 1, + validValueMin: 0, + validValueMax: 200, + categoriesToShow: [ + AD_CATEGORY.FLAT, + AD_CATEGORY.HOUSE, + AD_CATEGORY.APARTMENT, + AD_CATEGORY.COTTAGE, + AD_CATEGORY.OFFICE + ] + } +]; + +module.exports = { + ADVANCED_BOOLEAN_FILTERS, + ADVANCED_SEGMENT_SELECT_FILTERS, + ADVANCED_RANGE_FILTERS +}; diff --git a/app/controllers/realEstateFilters.js b/app/controllers/realEstateFilters.js index 8949cc1..17f5e58 100644 --- a/app/controllers/realEstateFilters.js +++ b/app/controllers/realEstateFilters.js @@ -1,5 +1,10 @@ const { currentSearchRequest } = require("../helpers/url"); -const { AD_CATEGORY, AD_TYPE } = require("../common/enums"); +const { AD_CATEGORY, AD_TYPE, ACCESS_ROAD_TYPE } = require("../common/enums"); +const { + ADVANCED_BOOLEAN_FILTERS, + ADVANCED_SEGMENT_SELECT_FILTERS, + ADVANCED_RANGE_FILTERS +} = require("../common/filterEnums"); const getFilters = async (req, res) => { const searchRequest = await currentSearchRequest(req); @@ -19,7 +24,18 @@ const getFilters = async (req, res) => { sizeMin, sizeMax, gardenSizeMin, - gardenSizeMax + gardenSizeMax, + numberOfRoomsMin, + numberOfRoomsMax, + numberOfFloorsMin, + numberOfFloorsMax, + floorMin, + floorMax, + includeIncompleteAds, + balcony, + elevator, + newBuilding, + accessRoadType } = searchRequest; const category = AD_CATEGORY[realEstateType] || AD_CATEGORY.FLAT; @@ -41,6 +57,40 @@ const getFilters = async (req, res) => { return; } + // TODO: Maybe this is slow, pay attention to this + const filterFilters = filterObject => { + const filterCategories = filterObject.categoriesToShow; + return filterCategories.indexOf(category) !== -1; + }; + + const advancedBooleanFilterObjects = ADVANCED_BOOLEAN_FILTERS.filter( + filterFilters + ); + const advancedSegmentSelectFilterObjects = ADVANCED_SEGMENT_SELECT_FILTERS.filter( + filterFilters + ); + const advancedRangeFilterObjects = ADVANCED_RANGE_FILTERS.filter( + filterFilters + ); + + const advancedBooleanFilterValues = { + includeIncompleteAds, + balcony, + elevator, + newBuilding + }; + const advancedSegmentSelectFilterValues = { + accessRoadType + }; + const advancedRangeFilterValues = { + numberOfFloorsMin, + numberOfFloorsMax, + numberOfRoomsMin, + numberOfRoomsMax, + floorMin, + floorMax + }; + if (priceMin || priceMax) { priceSliderOptions.start = [priceMin, priceMax]; } @@ -58,7 +108,14 @@ const getFilters = async (req, res) => { hasGardenSize, priceSliderOptions: JSON.stringify(priceSliderOptions), sizeSliderOptions: JSON.stringify(sizeSliderOptions), - gardenSizeSliderOptions: JSON.stringify(gardenSizeSliderOptions) + gardenSizeSliderOptions: JSON.stringify(gardenSizeSliderOptions), + advancedBooleanFilterObjects, + advancedBooleanFilterValues, + advancedSegmentSelectFilterObjects, + advancedSegmentSelectFilterValues, + advancedRangeFilterObjects, + advancedRangeFilterValues, + includeIncompleteAds }); }; @@ -78,13 +135,91 @@ const postFilters = async (req, res) => { const sizeMin = parseInt(req.body.sizeMin) || 0; const sizeMax = parseInt(req.body.sizeMax) || 0; - //TODO: Filter validation + const advancedRangeFilters = {}; + ADVANCED_RANGE_FILTERS.forEach(filter => { + let parsingFunction = parseInt; + if (filter.decimalPlaces) { + parsingFunction = parseFloat; + } + + advancedRangeFilters[filter.dbFieldMin] = parsingFunction( + req.body[filter.dbFieldMin] + ); + advancedRangeFilters[filter.dbFieldMax] = parsingFunction( + req.body[filter.dbFieldMax] + ); + + advancedRangeFilters[filter.dbFieldMin] = isNaN( + advancedRangeFilters[filter.dbFieldMin] + ) + ? null + : advancedRangeFilters[filter.dbFieldMin]; + advancedRangeFilters[filter.dbFieldMax] = isNaN( + advancedRangeFilters[filter.dbFieldMax] + ) + ? null + : advancedRangeFilters[filter.dbFieldMax]; + + try { + if (filter.decimalPlaces) { + advancedRangeFilters[filter.dbFieldMin] = advancedRangeFilters[ + filter.dbFieldMin + ].toFixed(filter.decimalPlaces); + advancedRangeFilters[filter.dbFieldMax] = advancedRangeFilters[ + filter.dbFieldMax + ].toFixed(filter.decimalPlaces); + } + } catch (e) { + advancedRangeFilters[filter.dbFieldMin] = null; + advancedRangeFilters[filter.dbFieldMax] = null; + } + + if ( + advancedRangeFilters[filter.dbFieldMin] < filter.validValueMin || + advancedRangeFilters[filter.dbFieldMin] > filter.validValueMax + ) { + advancedRangeFilters[filter.dbFieldMin] = filter.validValueMin; + } + + if ( + advancedRangeFilters[filter.dbFieldMax] < filter.validValueMin || + advancedRangeFilters[filter.dbFieldMax] > filter.validValueMax + ) { + advancedRangeFilters[filter.dbFieldMax] = filter.validValueMax; + } + }); + + const includeIncompleteAds = req.body.includeIncompleteAds === "on"; + + const balcony = req.body.balcony === "on"; + const elevator = req.body.elevator === "on"; + const newBuilding = req.body.newBuilding === "on"; + + const accessRoadType = req.body.accessRoadType; + if (!ACCESS_ROAD_TYPE[accessRoadType]) { + res.render("notFound", { title: " Greška !" }); + return; + } + + //TODO: Filter validation searchRequest.priceMin = priceMin; searchRequest.priceMax = priceMax; searchRequest.sizeMin = sizeMin; searchRequest.sizeMax = sizeMax; + for (const filter of Object.keys(advancedRangeFilters)) { + searchRequest[filter] = advancedRangeFilters[filter]; + } + + searchRequest.balcony = balcony; + searchRequest.elevator = elevator; + searchRequest.newBuilding = newBuilding; + + searchRequest.includeIncompleteAds = includeIncompleteAds; + + searchRequest.accessRoadType = accessRoadType; + if ( req.body.gardenSizeMin !== undefined && req.body.gardenSizeMax !== undefined @@ -97,7 +232,6 @@ const postFilters = async (req, res) => { searchRequest.gardenSizeMin = gardenSizeMin; searchRequest.gardenSizeMax = gardenSizeMax; } - await searchRequest.save(); res.redirect(nextStepUrl); diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index 0282645..ebeb84c 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -2,7 +2,6 @@ const db = require("../../models/index"); const sequelize = require("sequelize"); const Op = sequelize.Op; - const bulkUpsertRealEstates = async realEstateData => { try { const fieldsToUpdateIfDuplicate = [ @@ -87,7 +86,20 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { sizeMax, adType, realEstateType, - areaToSearch + areaToSearch, + gardenSizeMin, + gardenSizeMax, + numberOfRoomsMin, + numberOfRoomsMax, + numberOfFloorsMin, + numberOfFloorsMax, + floorMin, + floorMax, + includeIncompleteAds, + balcony, + elevator, + newBuilding, + accessRoadType } = searchRequest; const longitudeColumn = sequelize.col("locationLong"); @@ -116,12 +128,20 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { const geoSearchQueryPart = sequelize.where(contains, true); + //General queries contain only attributes that are defined for every searchreq + + //Query for case of complete ads const query = { adType, realEstateType, price: { - [Op.lte]: priceMax, - [Op.gte]: priceMin + [Op.or]: { + [Op.and]: { + [Op.lte]: priceMax, + [Op.gte]: priceMin + }, + [Op.is]: null + } }, area: { [Op.lte]: sizeMax, @@ -130,10 +150,148 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { [Op.and]: geoSearchQueryPart }; + //Query for case of incomplete ads + const queryIncludeIncomplete = { + adType, + realEstateType, + price: { + [Op.or]: { + [Op.and]: { + [Op.lte]: priceMax, + [Op.gte]: priceMin + }, + [Op.is]: null + } + }, + area: { + [Op.or]: { + [Op.and]: { + [Op.lte]: sizeMax, + [Op.gte]: sizeMin + }, + [Op.is]: null + } + }, + [Op.and]: geoSearchQueryPart + }; + + //Every other attribute is checked separately and included in query only if it is defined + if (gardenSizeMax && gardenSizeMin) { + query.gardenSize = { + [Op.lte]: gardenSizeMax, + [Op.gte]: gardenSizeMin + }; + queryIncludeIncomplete.gardenSize = { + [Op.or]: { + [Op.and]: { + [Op.lte]: gardenSizeMax, + [Op.gte]: gardenSizeMin + }, + [Op.is]: null + } + }; + } + + if (numberOfRoomsMin && numberOfRoomsMax) { + query.numberOfRooms = { + [Op.lte]: numberOfRoomsMax, + [Op.gte]: numberOfRoomsMin + }; + queryIncludeIncomplete.numberOfRooms = { + [Op.or]: { + [Op.and]: { + [Op.lte]: numberOfRoomsMax, + [Op.gte]: numberOfRoomsMin + }, + [Op.is]: null + } + }; + } + + if (numberOfFloorsMin && numberOfFloorsMax) { + query.numberOfFloors = { + [Op.lte]: numberOfFloorsMax, + [Op.gte]: numberOfFloorsMin + }; + queryIncludeIncomplete.numberOfFloors = { + [Op.or]: { + [Op.and]: { + [Op.lte]: numberOfFloorsMax, + [Op.gte]: numberOfFloorsMin + }, + [Op.is]: null + } + }; + } + + if (floorMin && floorMax) { + query.floor = { + [Op.lte]: floorMax, + [Op.gte]: floorMin + }; + queryIncludeIncomplete.floor = { + [Op.or]: { + [Op.and]: { + [Op.lte]: floorMax, + [Op.gte]: floorMin + }, + [Op.is]: null + } + }; + } + + if (balcony) { + query.balcony = { + [Op.eq]: balcony + }; + queryIncludeIncomplete.balcony = { + [Op.or]: { + [Op.eq]: balcony, + [Op.is]: null + } + }; + } + + if (newBuilding) { + query.newBuilding = { + [Op.eq]: newBuilding + }; + queryIncludeIncomplete.newBuilding = { + [Op.or]: { + [Op.eq]: newBuilding, + [Op.is]: null + } + }; + } + + if (elevator) { + query.elevator = { + [Op.eq]: elevator + }; + queryIncludeIncomplete.elevator = { + [Op.or]: { + [Op.eq]: elevator, + [Op.is]: null + } + }; + } + + if (accessRoadType !== "ANY") { + query.accessRoadType = { + [Op.eq]: accessRoadType + }; + queryIncludeIncomplete.accessRoadType = { + [Op.or]: { + [Op.eq]: accessRoadType, + [Op.is]: null + } + }; + } + const order = [["updatedAt", "desc"]]; - return await db.RealEstate.findAll({ - where: query, + return db.RealEstate.findAll({ + where: includeIncompleteAds ? queryIncludeIncomplete : query, limit: maxResults, order }); diff --git a/app/helpers/db/searchRequest.js b/app/helpers/db/searchRequest.js index 6e47b5d..808637a 100644 --- a/app/helpers/db/searchRequest.js +++ b/app/helpers/db/searchRequest.js @@ -2,11 +2,13 @@ const db = require("../../models/index"); const sequelize = require("sequelize"); const Op = sequelize.Op; +const { AD_CATEGORY } = require("../../common/enums"); const getSearchRequest = async searchRequestId => { try { return await db.SearchRequest.findByPk(searchRequestId); } catch (error) { + console.log("searchrequest.js", error); return null; } }; @@ -22,7 +24,15 @@ const findSearchRequestsForRealEstate = async realEstate => { adType, realEstateType, locationLat, - locationLong + locationLong, + accessRoadType, + balcony, + newBuilding, + elevator, + gardenSize, + numberOfRooms, + numberOfFloors, + floor } = realEstate; if (!locationLat || !locationLong) { @@ -39,12 +49,20 @@ const findSearchRequestsForRealEstate = async realEstate => { const geoSearchQueryPart = sequelize.where(contains, true); + //General query contains only attributes that are defined for every RealEstate - not null const query = { adType, realEstateType, subscribed: true, [Op.and]: geoSearchQueryPart }; + //Needed for defining which attribute should exist or not + const realEstateTypeObject = AD_CATEGORY[realEstateType]; + //Needed to decide on including incomplete RealEstates data + let checkForIncompleteWanted = false; + + //Attributes are checked separately and included in query only if defined + //Price and area should be defined for every property if (price) { query.priceMin = { @@ -62,8 +80,96 @@ const findSearchRequestsForRealEstate = async realEstate => { query.sizeMax = { [Op.gte]: area }; + } else { + checkForIncompleteWanted = true; + } + //Other attributes can be defined or not depending on RealEstate type + if (gardenSize) { + query.gardenSizeMin = { + [Op.lte]: gardenSize + }; + query.gardenSizeMax = { + [Op.gte]: gardenSize + }; + } else if (realEstateTypeObject.hasGardenSize) { + checkForIncompleteWanted = true; } + if (numberOfRooms) { + query.numberOfRoomsMin = { + [Op.lte]: numberOfRooms + }; + query.numberOfRoomsMax = { + [Op.gte]: numberOfRooms + }; + } else if (realEstateTypeObject.hasNumberOfRoom) { + checkForIncompleteWanted = true; + } + + if (numberOfFloors) { + query.numberOfFloorsMin = { + [Op.lte]: numberOfFloors + }; + query.numberOfFloorsMax = { + [Op.gte]: numberOfFloors + }; + } else if (realEstateTypeObject.hasNumberOfFloors) { + checkForIncompleteWanted = true; + } + + if (floor) { + query.floorMin = { + [Op.lte]: floor + }; + query.floorMax = { + [Op.gte]: floor + }; + } else if (realEstateTypeObject.hasFloorProp) { + checkForIncompleteWanted = true; + } + + if (accessRoadType) { + query.accessRoadType = { + [Op.or]: { + [Op.eq]: "ANY", + [Op.eq]: accessRoadType + } + }; + } else if (realEstateTypeObject.hasAccesRoadType) { + checkForIncompleteWanted = true; + } + + if (balcony) { + query.balcony = { + [Op.eq]: balcony + }; + } else if (realEstateTypeObject.hasBalconyProp) { + checkForIncompleteWanted = true; + } + + if (newBuilding) { + query.newBuilding = { + [Op.eq]: newBuilding + }; + } else if (realEstateTypeObject.hasNewBuildingProp) { + checkForIncompleteWanted = true; + } + + if (elevator) { + query.elevator = { + [Op.eq]: elevator + }; + } else if (realEstateTypeObject.hasElevatorProp) { + checkForIncompleteWanted = true; + } + + //If one of the attributes that exists for property type is null + //we include in query to check if incomplete real estates are accepted + if (checkForIncompleteWanted) { + query.includeIncompleteAds = { + [Op.eq]: true + }; + } return await db.SearchRequest.findAll({ where: query }); }; diff --git a/app/migrations/20191118105541-add-additional-fields-to-searchRequests-table.js b/app/migrations/20191118105541-add-additional-fields-to-searchRequests-table.js new file mode 100644 index 0000000..0deaa9b --- /dev/null +++ b/app/migrations/20191118105541-add-additional-fields-to-searchRequests-table.js @@ -0,0 +1,64 @@ +"use strict"; +const { ACCESS_ROAD_TYPE, HEATING_TYPE } = require("../common/enums"); + +module.exports = { + up: (queryInterface, Sequelize) => { + return Promise.all([ + queryInterface.addColumn("SearchRequests", "includeIncompleteAds", { + type: Sequelize.BOOLEAN + }), + queryInterface.addColumn("SearchRequests", "balcony", { + type: Sequelize.BOOLEAN + }), + queryInterface.addColumn("SearchRequests", "newBuilding", { + type: Sequelize.BOOLEAN + }), + queryInterface.addColumn("SearchRequests", "elevator", { + type: Sequelize.BOOLEAN + }), + queryInterface.addColumn("SearchRequests", "numberOfRoomsMin", { + type: Sequelize.REAL + }), + queryInterface.addColumn("SearchRequests", "numberOfRoomsMax", { + type: Sequelize.REAL + }), + queryInterface.addColumn("SearchRequests", "numberOfFloorsMin", { + type: Sequelize.INTEGER + }), + queryInterface.addColumn("SearchRequests", "numberOfFloorsMax", { + type: Sequelize.INTEGER + }), + queryInterface.addColumn("SearchRequests", "floorMin", { + type: Sequelize.INTEGER + }), + queryInterface.addColumn("SearchRequests", "floorMax", { + type: Sequelize.INTEGER + }), + queryInterface.addColumn("SearchRequests", "accessRoadType", { + type: Sequelize.TEXT, + defaultValue: ACCESS_ROAD_TYPE.ANY.id + }), + queryInterface.addColumn("SearchRequests", "heatingType", { + type: Sequelize.TEXT, + defaultValue: HEATING_TYPE.ANY.id + }) + ]); + }, + + down: (queryInterface, Sequelize) => { + return Promise.all([ + queryInterface.removeColumn("SearchRequests", "includeIncompleteAds"), + queryInterface.removeColumn("SearchRequests", "balcony"), + queryInterface.removeColumn("SearchRequests", "newBuilding"), + queryInterface.removeColumn("SearchRequests", "elevator"), + queryInterface.removeColumn("SearchRequests", "numberOfRoomsMin"), + queryInterface.removeColumn("SearchRequests", "numberOfRoomsMax"), + queryInterface.removeColumn("SearchRequests", "numberOfFloorsMin"), + queryInterface.removeColumn("SearchRequests", "numberOfFloorsMax"), + queryInterface.removeColumn("SearchRequests", "floorMin"), + queryInterface.removeColumn("SearchRequests", "floorMax"), + queryInterface.removeColumn("SearchRequests", "accessRoadType"), + queryInterface.removeColumn("SearchRequests", "heatingType") + ]); + } +}; diff --git a/app/models/searchRequest.js b/app/models/searchRequest.js index b1fd229..8a04593 100644 --- a/app/models/searchRequest.js +++ b/app/models/searchRequest.js @@ -69,7 +69,19 @@ module.exports = (sequelize, DataTypes) => { }, deletedEmail: { type: DataTypes.TEXT - } + }, + includeIncompleteAds: DataTypes.BOOLEAN, + balcony: DataTypes.BOOLEAN, + elevator: DataTypes.BOOLEAN, + newBuilding: DataTypes.BOOLEAN, + numberOfRoomsMin: DataTypes.REAL, + numberOfRoomsMax: DataTypes.REAL, + numberOfFloorsMin: DataTypes.INTEGER, + numberOfFloorsMax: DataTypes.INTEGER, + floorMin: DataTypes.INTEGER, + floorMax: DataTypes.INTEGER, + accessRoadType: DataTypes.TEXT, + heatingType: DataTypes.TEXT }); return SearchRequest; diff --git a/app/public/main.css b/app/public/main.css index 2e0d7d4..8d128b7 100644 --- a/app/public/main.css +++ b/app/public/main.css @@ -110,3 +110,47 @@ h3 { .collection a.collection-item:not(.active):hover { background-color: rgba(2, 173, 186, 0.2); } + +.tabs .tab a { + color: #02adba; + -webkit-transition: color 0.28s ease, background-color 0.28s ease; + transition: color 0.28s ease, background-color 0.28s ease; +} +.tabs .tab a:focus, +.tabs .tab a:focus.active { + background-color: rgba(2, 173, 186, 0.2); +} +.tabs .tab a:hover, +.tabs .tab a.active { + color: #02adba; +} +.tabs .indicator { + background-color: #02adba; +} + +[type="checkbox"].filled-in:checked + span:not(.lever):after { + border: 2px solid #02adba; + background-color: #02adba; +} +[type="checkbox"].filled-in:not(:checked) + span:not(.lever):after { + background-color: transparent; + border: 2px solid #02adba; +} + +.distinguished { + border: 2px solid #02adba; + border-radius: 4px; + padding: 5px 5px 3px 5px; + margin-left: -5px; +} + +.checkbox-label { + color: black; + font-size: 14px; +} + +.column-label { + position: relative; + margin-top: 2rem; + margin-bottom: 1rem; +} diff --git a/app/public/segment.css b/app/public/segment.css index 313de5d..6ca1768 100644 --- a/app/public/segment.css +++ b/app/public/segment.css @@ -25,6 +25,12 @@ border-right: 1px solid #02adba; } +.segmented.small [type="radio"]:not(:checked) + span, +.segmented.small [type="radio"]:checked + span { + padding-left: 7px; + padding-right: 7px; +} + .segmented :last-child .label { border-right: none; } diff --git a/app/views/advancedFilters.ejs b/app/views/advancedFilters.ejs new file mode 100644 index 0000000..23e50f7 --- /dev/null +++ b/app/views/advancedFilters.ejs @@ -0,0 +1,69 @@ +
+ +<% for (const filter of advancedBooleanFilterObjects){ %> +

+ +

+<% } %> + +
+<% for (const filter of advancedRangeFilterObjects){ %> +
+

+ <%= filter.title %> +

+
+ " + > + +
+
+ " + > + +
+
+<% } %> + +
+<% for (const filter of advancedSegmentSelectFilterObjects){ %> +
+

+ + <% for (const segmentObject of filter.values) { %> + + <% } %> + +
+<% } %> + +
+

+ +

diff --git a/app/views/realEstateFilters.ejs b/app/views/realEstateFilters.ejs index 1b3eb01..caf3c23 100644 --- a/app/views/realEstateFilters.ejs +++ b/app/views/realEstateFilters.ejs @@ -1,63 +1,20 @@ +
-
- -
-
Cijena
-

-
-
- -
-
- - + -
- +
+ <%- include("./standardFilters.ejs") %> +
+
+ <%- include("./advancedFilters.ejs") %>
-
- -
-
Površina
-

-
-
- -
-
-
- - -
-
- -
-
- -
- - <% if(hasGardenSize) { %> -
-
Površina okućnice
-

-
-
- -
-
-
- - -
-
- -
-
- <% } %> -
Dalje @@ -66,125 +23,7 @@ diff --git a/app/views/standardFilters.ejs b/app/views/standardFilters.ejs new file mode 100644 index 0000000..27a6bba --- /dev/null +++ b/app/views/standardFilters.ejs @@ -0,0 +1,195 @@ +
+ +
+
Cijena (KM)
+

+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+ +
+
Površina (m2)
+

+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+ +<% if(hasGardenSize) { %> +
+
Površina okućnice (m2)
+

+
+
+
+
+ +
+
+
+ +
+
+ +
+
+<% } %> + + diff --git a/package-lock.json b/package-lock.json index b18ae09..9661459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1052,6 +1052,14 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "eslint-plugin-prettier": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz", + "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==", + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1271,6 +1279,11 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -3179,6 +3192,19 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==" + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "requires": { + "fast-diff": "^1.1.2" + } + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", diff --git a/package.json b/package.json index 7b5441f..75a7cc4 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "compression": "^1.7.4", "dotenv": "^7.0.0", "ejs": "^2.6.1", + "eslint-plugin-prettier": "^3.1.2", "express": "^4.16.4", "express-ejs-layouts": "^2.5.0", "express-layout": "^0.1.0", @@ -44,6 +45,7 @@ "node-fetch": "^2.3.0", "node-schedule": "^1.3.2", "pg": "^7.10.0", + "prettier": "^1.19.1", "react-step-wizard": "^5.1.0", "sequelize": "^5.18.4", "sequelize-cli": "^5.5.0"