Merge branch 'project-fixes' into 'master'

Project fixes

See merge request saburly/marketalarm/web!26
This commit was merged in pull request #26.
This commit is contained in:
Bilal Catic
2019-09-09 10:49:56 +00:00
50 changed files with 2552 additions and 2408 deletions

9
app/config/appConfig.js Normal file
View File

@@ -0,0 +1,9 @@
const APP_PORT = process.env.APP_PORT || 5000;
const APP_BASE_URL = process.env.APP_BASE_URL || "http://localhost";
const APP_URL = `${APP_BASE_URL}:${APP_PORT}`;
module.exports = {
APP_PORT,
APP_URL
};

View File

@@ -1,32 +1,31 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const { getRealEstateTypeEnum } = require('../helpers/enums'); const { getRealEstateTypeEnum } = require("../helpers/enums");
const getGardenSize = (req,res) => { const getGardenSize = (req, res) => {
const title = "Koliko okućnice tražite ?";
const title = "Koliko okućnice tražite ?" const unit = " m2";
const unit = " m2"
const rangeFrom = { const rangeFrom = {
min : 10, min: 10,
max : 3000, max: 3000,
value : 0, value: 0,
step : 10 step: 10
} };
const rangeTo = { const rangeTo = {
min : 10, min: 10,
max : 3000, max: 3000,
value : 100, value: 100,
step : 10 step: 10
} };
res.render('gardenSize', { rangeFrom, rangeTo, unit, title }); res.render("gardenSize", { rangeFrom, rangeTo, unit, title });
}; };
const postGardenSize = async (req, res) => { const postGardenSize = async (req, res) => {
const request = await currentRERequest(req); const request = await currentRERequest(req);
const nextStepPage = req.query.nextStep || 'cijena'; const nextStepPage = req.query.nextStep || "cijena";
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`; const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
const realEstateType = getRealEstateTypeEnum(request.realEstateType); const realEstateType = getRealEstateTypeEnum(request.realEstateType);

View File

@@ -1,6 +1,6 @@
const getGoAgain = async (req,res) => { const getGoAgain = async (req, res) => {
const title = "Želite li pretražiti još jednu nekretninu ?"; const title = "Želite li pretražiti još jednu nekretninu ?";
res.render('goAgain', {title}); res.render("goAgain", { title });
}; };
module.exports = { module.exports = {

View File

@@ -1,20 +1,26 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const { getMunicipalitiesForRegion, getMunicipalityName } = require('../helpers/codes'); const {
getMunicipalitiesForRegion,
getMunicipalityName
} = require("../helpers/codes");
const getMunicipality = async (req, res) => { const getMunicipality = async (req, res) => {
const title = "U kojem mjestu tražite nekretninu?";
const title = "U kojem mjestu tražite nekretninu?"
let request = await currentRERequest(req); let request = await currentRERequest(req);
const municipalities = getMunicipalitiesForRegion(request.region); const municipalities = getMunicipalitiesForRegion(request.region);
res.render('municipality', { municipalities, title }); res.render("municipality", { municipalities, title });
}; };
const postMunicipality = async (req, res) => { const postMunicipality = async (req, res) => {
const request = await currentRERequest(req); const request = await currentRERequest(req);
const nextStepParam = req.query.nextStep ? "?nextStep=" + req.query.nextStep : ""; const nextStepParam = req.query.nextStep
const nextStepUrl = `/${'naselje'}/${request.uniqueId}/${getMunicipalityName(request.region, req.body.municipality)}${nextStepParam}`; ? "?nextStep=" + req.query.nextStep
: "";
const nextStepUrl = `/${"naselje"}/${request.uniqueId}/${getMunicipalityName(
request.region,
req.body.municipality
)}${nextStepParam}`;
request.municipality = req.body.municipality; request.municipality = req.body.municipality;
await request.save(); await request.save();

View File

@@ -1,41 +1,37 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const getNeighborhood = async (req, res) => { const getNeighborhood = async (req, res) => {
const title = "U kojem naselju tražite nekretninu?";
const municipality = req.params.municipality;
const nextStep = req.query.nextStep || "/";
const title = "U kojem naselju tražite nekretninu?" res.render("neighborhoodMap", {
const municipality = req.params.municipality nextStep,
const nextStep = req.query.nextStep || '/'; municipality,
title
res.render('neighborhoodMap', { });
nextStep,
municipality,
title
});
}; };
const postNeighborhood = async (req, res) => { const postNeighborhood = async (req, res) => {
let request = await currentRERequest(req); let request = await currentRERequest(req);
const northWest = [req.body.west, req.body.north]; const northWest = [req.body.west, req.body.north];
const northEast = [req.body.east, req.body.north]; const northEast = [req.body.east, req.body.north];
const southEast = [req.body.east, req.body.south]; const southEast = [req.body.east, req.body.south];
const southWest = [req.body.west, req.body.south]; const southWest = [req.body.west, req.body.south];
request.bounding_box = { request.bounding_box = {
type: 'Polygon', coordinates: [ type: "Polygon",
[northWest, northEast, southEast, coordinates: [[northWest, northEast, southEast, southWest, northWest]]
southWest, northWest] };
] await request.save();
};
await request.save();
const nextStepPage = req.query.nextStep || 'povrsina'; const nextStepPage = req.query.nextStep || "povrsina";
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`; const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
res.redirect(nextStepUrl); res.redirect(nextStepUrl);
}; };
module.exports = { module.exports = {
getNeighborhood, getNeighborhood,
postNeighborhood postNeighborhood
}; };

View File

@@ -1,32 +1,30 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const getPrice = (req,res) => { const getPrice = (req, res) => {
const title = "Koja Vam okvirna cijena odgovara ?";
const title = "Koja Vam okvirna cijena odgovara ?" const unit = " KM";
const unit = " KM"
const rangeFrom = { const rangeFrom = {
min : 1000, min: 1000,
max : 250000, max: 250000,
value : 0, value: 0,
step : 1000 step: 1000
} };
const rangeTo = { const rangeTo = {
min : 1000, min: 1000,
max : 250000, max: 250000,
value : 50000, value: 50000,
step : 1000 step: 1000
} };
res.render("price", { rangeFrom, rangeTo, unit, title });
res.render('price', {rangeFrom, rangeTo, unit, title });
}; };
const postPrice = async (req, res) => { const postPrice = async (req, res) => {
const request = await currentRERequest(req); const request = await currentRERequest(req);
const nextStepPage = req.query.nextStep || 'pregled'; const nextStepPage = req.query.nextStep || "pregled";
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`; const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
request.priceMin = req.body.from; request.priceMin = req.body.from;

View File

@@ -1,10 +1,16 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const { getRegionName, getMunicipalityName } = require('../helpers/codes'); const { getRegionName, getMunicipalityName } = require("../helpers/codes");
const { realEstateTypes, sizes, gardenSizes, prices, getEnumTypeTitle, getRealEstateTypeEnum } = require('../helpers/enums'); const {
realEstateTypes,
sizes,
gardenSizes,
prices,
getEnumTypeTitle,
getRealEstateTypeEnum
} = require("../helpers/enums");
const getQueryReview = async (req,res) => { const getQueryReview = async (req, res) => {
const title = "Da li je ovo to što ste tražili ?";
const title = "Da li je ovo to što ste tražili ?"
const request = await currentRERequest(req); const request = await currentRERequest(req);
const nextStep = req.query.nextStep; const nextStep = req.query.nextStep;
@@ -12,63 +18,73 @@ const getQueryReview = async (req,res) => {
return null; return null;
} }
const { const {
realEstateType, realEstateType,
region, region,
municipality, municipality,
sizeMin, sizeMin,
sizeMax, sizeMax,
gardenSizeMin, gardenSizeMin,
gardenSizeMax, gardenSizeMax,
priceMin, priceMin,
priceMax } = request.dataValues; priceMax
} = request.dataValues;
const realEstateTypeObject = getRealEstateTypeEnum(realEstateType); const realEstateTypeObject = getRealEstateTypeEnum(realEstateType);
const enableGardenSizeEdit = realEstateTypeObject ? realEstateTypeObject.hasGardenSize : false; const enableGardenSizeEdit = realEstateTypeObject
? realEstateTypeObject.hasGardenSize
: false;
const realEstateTypeTitle = realEstateType ? getEnumTypeTitle(realEstateTypes, realEstateType) : null; const realEstateTypeTitle = realEstateType
? getEnumTypeTitle(realEstateTypes, realEstateType)
: null;
const regionName = region ? getRegionName(region) : null; const regionName = region ? getRegionName(region) : null;
const municipalityName = (region && municipality) ? getMunicipalityName(region, municipality) : null; const municipalityName =
region && municipality ? getMunicipalityName(region, municipality) : null;
const sizeTitle = sizeMin ? sizeMin + "-" + sizeMax + " m2" : null; const sizeTitle = sizeMin ? sizeMin + "-" + sizeMax + " m2" : null;
const gardenSizeTitle = gardenSizeMin ? gardenSizeMin + "-" + gardenSizeMax + " m2" : null; const gardenSizeTitle = gardenSizeMin
const priceTitle = priceMin ? priceMin + "-" + priceMax + " KM" : null; ? gardenSizeMin + "-" + gardenSizeMax + " m2"
: null;
const priceTitle = priceMin ? priceMin + "-" + priceMax + " KM" : null;
const uniqueId = request.dataValues.uniqueId ? request.dataValues.uniqueId : ''; const uniqueId = request.dataValues.uniqueId
? request.dataValues.uniqueId
: "";
const queryData = [ const queryData = [
{ {
id: 'realEstateType', id: "realEstateType",
title: realEstateTypeTitle, title: realEstateTypeTitle,
url: `/vrstanekretnine/${uniqueId}?nextStep=pregled`, url: `/vrstanekretnine/${uniqueId}?nextStep=pregled`
}, },
{ {
id: 'region', id: "region",
title: regionName, title: regionName,
url: `/grad/${uniqueId}?nextStep=mjesto`, url: `/grad/${uniqueId}?nextStep=mjesto`
}, },
{ {
id: 'municipality', id: "municipality",
title: municipalityName, title: municipalityName,
url: `/mjesto/${uniqueId}?nextStep=pregled`, url: `/mjesto/${uniqueId}?nextStep=pregled`
}, },
{ {
id: 'size', id: "size",
title: sizeTitle, title: sizeTitle,
url: `/povrsina/${uniqueId}?nextStep=pregled`, url: `/povrsina/${uniqueId}?nextStep=pregled`
}, },
{ {
id: 'gardenSize', id: "gardenSize",
title: gardenSizeTitle, title: gardenSizeTitle,
url: enableGardenSizeEdit ? `/okucnica/${uniqueId}?nextStep=pregled` : '', url: enableGardenSizeEdit ? `/okucnica/${uniqueId}?nextStep=pregled` : ""
}, },
{ {
id: 'price', id: "price",
title: priceTitle, title: priceTitle,
url: `/cijena/${uniqueId}?nextStep=pregled` url: `/cijena/${uniqueId}?nextStep=pregled`
} }
]; ];
res.render('queryReview', { res.render("queryReview", {
nextStep, nextStep,
queryData, queryData,
title title

View File

@@ -1,14 +1,13 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const { isValidEmail } = require('../helpers/email'); const { isValidEmail } = require("../helpers/email");
const { sendTemplatedEmail} = require('../helpers/awsEmail'); const { sendTemplatedEmail } = require("../helpers/awsEmail");
const getQuerySubmit = async (req, res) => { const getQuerySubmit = async (req, res) => {
const title = "Upišite vaš e-mail";
const title = "Upišite vaš e-mail"
const nextStep = req.query.nextStep; const nextStep = req.query.nextStep;
const error = req.query.error; const error = req.query.error;
res.render('querySubmit', { res.render("querySubmit", {
nextStep, nextStep,
error, error,
title title
@@ -17,25 +16,23 @@ const getQuerySubmit = async (req, res) => {
const postQuerySubmit = async (req, res) => { const postQuerySubmit = async (req, res) => {
const request = await currentRERequest(req); const request = await currentRERequest(req);
const nextStep = req.query.nextStep || '/ponovo'; const nextStep = req.query.nextStep || "/ponovo";
const emailInput = req.body.email; const emailInput = req.body.email;
const emailConfirmInput = req.body.confirm; const emailConfirmInput = req.body.confirm;
let error = "Greška ! Unesite validan email"; let error = "Greška ! Unesite validan email";
if (!isValidEmail(emailInput) || !isValidEmail(emailConfirmInput)) { if (!isValidEmail(emailInput) || !isValidEmail(emailConfirmInput)) {
error = "Greška ! Unesite validan email"; error = "Greška ! Unesite validan email";
res.render('querySubmit', { res.render("querySubmit", {
error error
}); });
return; return;
} }
if (emailInput !== emailConfirmInput) { if (emailInput !== emailConfirmInput) {
error = "Greška ! Unešeni emailovi nisu isti"; error = "Greška ! Unešeni emailovi nisu isti";
res.render('querySubmit', { res.render("querySubmit", {
error error
}); });
return; return;
@@ -45,7 +42,7 @@ const postQuerySubmit = async (req, res) => {
request.subscribed = true; request.subscribed = true;
await request.save(); await request.save();
sendTemplatedEmail(req.body.email, request); sendTemplatedEmail(req.body.email, request);
res.redirect(nextStep); res.redirect(nextStep);
}; };
module.exports = { module.exports = {

View File

@@ -1,42 +1,39 @@
const db = require('../models/index'); const db = require("../models/index");
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const { realEstateTypes, getRealEstateTypeEnum } = require('../helpers/enums'); const { realEstateTypes, getRealEstateTypeEnum } = require("../helpers/enums");
const getRealEstateTypes = (req, res) => {
const getRealEstateTypes = (req,res) => { const title = "Koju nekretninu tražite?";
const title = "Koju nekretninu tražite?" res.render("realEstateType", { realEstateTypes, title });
res.render('realEstateType', { realEstateTypes, title });
}; };
const postRealEstateTypes = async (req, res) => { const postRealEstateTypes = async (req, res) => {
const request = await currentRERequest(req); const request = await currentRERequest(req);
const nextStepPage = req.query.nextStep || 'grad'; const nextStepPage = req.query.nextStep || "grad";
if (request && request.uniqueId) { if (request && request.uniqueId) {
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`; const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
request.realEstateType = req.body.realestatetype; request.realEstateType = req.body.realestatetype;
if (!getRealEstateTypeEnum(request.realEstateType).hasGardenSize){ if (!getRealEstateTypeEnum(request.realEstateType).hasGardenSize) {
request.gardenSize = null; request.gardenSize = null;
} }
await request.save(); await request.save();
res.redirect(nextStepUrl) res.redirect(nextStepUrl);
} else { } else {
db.RealEstateRequest.create({ db.RealEstateRequest.create({
realEstateType: req.body.realestatetype realEstateType: req.body.realestatetype
}).then( (result) => { })
const nextStepUrl = `/${nextStepPage}/${result.uniqueId}`; .then(result => {
res.redirect(nextStepUrl); const nextStepUrl = `/${nextStepPage}/${result.uniqueId}`;
}).catch( (e) => { res.redirect(nextStepUrl);
res.send(e); })
}); .catch(e => {
res.send(e);
});
} }
}; };
module.exports = { module.exports = {

View File

@@ -1,18 +1,13 @@
const { allMarketAlertsByRequest } = require("../helpers/db/dbHelper");
const {allMarketAlertsByRequest} = require('../helpers/db/dbHelper'); const getRealEstates = async (req, res) => {
const request = req.params["request_id"];
const realEstates = await allMarketAlertsByRequest(request);
const getRealEstates = async (req,res) => { const title = "Ovo su nekretnine koje smo pronašli za vas";
console.log("Enter get realestates"); res.render("realEstates", { realEstates, title });
const request = req.params['request_id']; };
console.log(req.params['request_id']);
const realEstates = await allMarketAlertsByRequest(request);
console.log(realEstates);
const title = "Ovo su nekretnine koje smo pronašli za vas" module.exports = {
res.render('realEstates', {realEstates, title } ); getRealEstates
}; };
module.exports = {
getRealEstates
};

View File

@@ -1,25 +1,25 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const { getRegions } = require('../helpers/codes'); const { getRegions } = require("../helpers/codes");
const regions = getRegions(); const regions = getRegions();
const getRegion = (req,res) => { const getRegion = (req, res) => {
const title = "U kojoj regiji tražite nekretninu?" const title = "U kojoj regiji tražite nekretninu?";
res.render('region', { regions, title }); res.render("region", { regions, title });
}; };
const postRegion = async (req, res) => { const postRegion = async (req, res) => {
const request = await currentRERequest(req); const request = await currentRERequest(req);
const nextStepQueryParam = req.query.nextStep ? '?nextStep=pregled' : ''; const nextStepQueryParam = req.query.nextStep ? "?nextStep=pregled" : "";
const nextStepPage = req.query.nextStep || 'mjesto'; const nextStepPage = req.query.nextStep || "mjesto";
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}${nextStepQueryParam}`; const nextStepUrl = `/${nextStepPage}/${request.uniqueId}${nextStepQueryParam}`;
request.region = req.body.region; request.region = req.body.region;
request.municipality = null; request.municipality = null;
await request.save(); await request.save();
res.redirect(nextStepUrl) res.redirect(nextStepUrl);
}; };
module.exports = { module.exports = {

View File

@@ -1,25 +1,24 @@
const { currentRERequest } = require('../helpers/url'); const { currentRERequest } = require("../helpers/url");
const { sizes, getRealEstateTypeEnum } = require('../helpers/enums'); const { sizes, getRealEstateTypeEnum } = require("../helpers/enums");
const getSize = (req,res) => { const getSize = (req, res) => {
const title = "Od koliko kvadrata tražite nekretninu ?";
const title = "Od koliko kvadrata tražite nekretninu ?" const unit = " m2";
const unit = " m2"
const rangeFrom = { const rangeFrom = {
min : 10, min: 10,
max : 250, max: 250,
value : 0, value: 0,
step : 10 step: 10
} };
const rangeTo = { const rangeTo = {
min : 10, min: 10,
max : 250, max: 250,
value : 50, value: 50,
step : 10 step: 10
} };
res.render('size', { rangeFrom, rangeTo, unit, title }); res.render("size", { rangeFrom, rangeTo, unit, title });
}; };
const postSize = async (req, res) => { const postSize = async (req, res) => {
@@ -27,7 +26,8 @@ const postSize = async (req, res) => {
const realEstateType = getRealEstateTypeEnum(request.realEstateType); const realEstateType = getRealEstateTypeEnum(request.realEstateType);
const nextStep = realEstateType && realEstateType.hasGardenSize ? 'okucnica' : 'cijena'; const nextStep =
realEstateType && realEstateType.hasGardenSize ? "okucnica" : "cijena";
const nextStepPage = req.query.nextStep || nextStep; const nextStepPage = req.query.nextStep || nextStep;
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`; const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
request.sizeMin = req.body.from; request.sizeMin = req.body.from;

View File

@@ -1,17 +1,14 @@
const { currentRERequest } = require("../helpers/url");
const { currentRERequest } = require('../helpers/url');
const getUnsubscribe = async (req, res) => { const getUnsubscribe = async (req, res) => {
const title = "Uspješno ste se odjavili";
const request = await currentRERequest(req);
request.subscribed = false;
await request.save();
const title = "Uspješno ste se odjavili" res.render("unsubscribe", { nextStep: "/vrstanekretnine", title });
const request = await currentRERequest(req);
request.subscribed = false;
await request.save();
res.render('unsubscribe', { nextStep: '/vrstanekretnine', title });
}; };
module.exports = { module.exports = {
getUnsubscribe getUnsubscribe
}; };

View File

@@ -1,6 +1,6 @@
const getWelcome = (req,res) => { const getWelcome = (req, res) => {
const title = "Koju nekretninu tražite?" const title = "Koju nekretninu tražite?";
res.render('welcome', { nextStep: '/vrstanekretnine', title } ); res.render("welcome", { nextStep: "/vrstanekretnine", title });
}; };
module.exports = { module.exports = {

View File

@@ -1,4 +1,4 @@
const dotenv = require("dotenv").config(); const { APP_URL } = require("../config/appConfig");
const { getRealEstateTypeEnum } = require("./enums"); const { getRealEstateTypeEnum } = require("./enums");
const { getRegionName, getMunicipalityName } = require("./codes"); const { getRegionName, getMunicipalityName } = require("./codes");
const { allRERequestByUiid } = require("./db/dbHelper"); const { allRERequestByUiid } = require("./db/dbHelper");
@@ -57,9 +57,7 @@ const getGreetingsEmailHTML = realestateRequest => {
realestateRequest.realEstateType realestateRequest.realEstateType
); );
const gardenSize = realEstateType.hasGardenSize const gardenSize = realEstateType.hasGardenSize
? `<div><strong>Kvadratura okućnice: Od ${ ? `<div><strong>Kvadratura okućnice: Od ${realestateRequest.gardenSizeMin} do ${realestateRequest.gardenSizeMax} m2 </strong></div>`
realestateRequest.gardenSizeMin
} do ${realestateRequest.gardenSizeMax} m2 </strong></div>`
: ``; : ``;
return `<h1> Zdravo, return `<h1> Zdravo,
@@ -85,15 +83,15 @@ const getGreetingsEmailHTML = realestateRequest => {
<div> <div>
</div> </div>
<div><strong> Ako želis prestati dobijati obavještenja za ovu pretragu klikni ${ <div><strong> Ako želis prestati dobijati obavještenja za ovu pretragu klikni ${APP_URL}/odjava/${
process.env.APP_URL realestateRequest.uniqueId
}/odjava/${realestateRequest.uniqueId} </strong></div> } </strong></div>
<div><strong>Ako želiš promijeniti uslove pretrage klikni ${ <div><strong>Ako želiš promijeniti uslove pretrage klikni ${APP_URL}/pregled/${
process.env.APP_URL realestateRequest.uniqueId
}/pregled/${realestateRequest.uniqueId} </strong></div> } </strong></div>
<h4> Tvoj, <h4> Tvoj,
Javimi tim. Javimi tim.
</h4>`; </h4>`;
}; };
const getGreetingsEmaiTextVersion = realestateRequest => { const getGreetingsEmaiTextVersion = realestateRequest => {
@@ -101,40 +99,27 @@ const getGreetingsEmaiTextVersion = realestateRequest => {
realestateRequest.realEstateType realestateRequest.realEstateType
); );
const gardenSize = realEstateType.hasGardenSize const gardenSize = realEstateType.hasGardenSize
? "Kvadratura okućnice od " + ? `Kvadratura okućnice od ${realestateRequest.gardenSizeMin} do ${realestateRequest.gardenSizeMax}`
realestateRequest.gardenSizeMin +
" do " +
realestateRequest.gardenSizeMax
: ""; : "";
const text = const text = `Zdravo, \n Naručio/la si da ti javimo ako se nekretnina pojavi u oglasima
"Zdravo, \n Naručio/la si da ti javimo ako se nekretnina pojavi u oglasima \n Ovo je tražena nekretnina: \n , Tip nekretnine: " + \n Ovo je tražena nekretnina: \n , Tip nekretnine: ${
realestateRequest.realEstateType + realestateRequest.realEstateType
"\n Područje" + } \n Područje ${getRegionName(
getRegionName(realestateRequest.region) + realestateRequest.region
"\n Mjesto " + )} \n Mjesto ${getMunicipalityName(
getMunicipalityName( realestateRequest.region,
realestateRequest.region, realestateRequest.municipality
realestateRequest.municipality )}
) + \n Kvadratura nekretnine Od ${realestateRequest.sizeMin} do ${
"\n Kvadratura nekretnine Od " + realestateRequest.sizeMaX
realestateRequest.sizeMin + } ${gardenSize} \n Cijena od ${realestateRequest.priceMin} do ${
" do " + realestateRequest.priceMax
realestateRequest.sizeMaX + } \n Ako želis prestati dobijati obavještenja za ovu pretragu klikni
+gardenSize; ${APP_URL}/odjava/${
"\n Cijena od " + realestateRequest.uniqueId
realestateRequest.priceMin + }\n Ako želiš promijeniti uslove pretrage klikni
" do " + ${APP_URL}/odpregled/${realestateRequest.uniqueId}\n Tvoj,\n Javimi tim`;
realestateRequest.priceMax +
"\n Ako želis prestati dobijati obavještenja za ovu pretragu klikni" +
process.env.APP_URL +
"/odjava/" +
realestateRequest.uniqueId +
"\n Ako želiš promijeniti uslove pretrage klikni " +
process.env.APP_URL +
"/odpregled/" +
realestateRequest.uniqueId +
"\n Tvoj,\n Javimi tim";
return text; return text;
}; };
@@ -165,7 +150,7 @@ const sendBulkEmail = async marketAlerts => {
realEstateType: RERequest.realEstateType, realEstateType: RERequest.realEstateType,
region: RERequest.region, region: RERequest.region,
municipality: RERequest.municipality, municipality: RERequest.municipality,
requestUrl: `${process.env.APP_URL}/nekretnine/${RERequest.uniqueId}` requestUrl: `${APP_URL}/nekretnine/${RERequest.uniqueId}`
}; };
}); });
@@ -213,8 +198,6 @@ const sendBulkEmail = async marketAlerts => {
ReplacementTemplateData: repData ReplacementTemplateData: repData
}); });
} }
console.log("AWS EMAIL : Bulk email replacement data:");
console.log(destinations);
var params = { var params = {
Destinations: destinations, Destinations: destinations,
@@ -229,15 +212,12 @@ const sendBulkEmail = async marketAlerts => {
.sendBulkTemplatedEmail(params) .sendBulkTemplatedEmail(params)
.promise(); .promise();
const awsResult = await sendPromise; const awsResult = await sendPromise;
console.log("AWS SES bulk email response");
console.log(awsResult);
} catch (e) { } catch (e) {
console.log("Could not send bulk email", e); console.log("Could not send bulk email", e);
} }
}; };
const redirectUrl = marketAlertId => const redirectUrl = marketAlertId => `${APP_URL}/redirect/${marketAlertId}`;
`${process.env.APP_URL}/redirect/${marketAlertId}`;
const toAWSArray = urlArray => { const toAWSArray = urlArray => {
let arrayString = ""; let arrayString = "";
urlArray.forEach(element => { urlArray.forEach(element => {
@@ -250,11 +230,11 @@ const toAWSArray = urlArray => {
}; };
const getNotificationEmailHtml = () => { const getNotificationEmailHtml = () => {
return `<h2> Zdravo, return `<h2> Zdravo,
Pronašli smo nekretninu koju ste tražili. </h2> Pronašli smo nekretninu koju ste tražili. </h2>
<h3> Ovo su tražene nekretnine: </h3> <h3> Ovo su tražene nekretnine: </h3>
<div> <div>
<div>{{#each marketAlertUrl}}<li><a href="{{url}}">{{title}}</a></li><br />{{/each}}<div/> <div>{{#each marketAlertUrl}}<li><a href="{{url}}">{{title}}</a></li><br />{{/each}}<div/>
<div/> <div/>
<div>Kompletan spisak nekretnina možete pegledati ovdije: <a href="{{requestUrl}}">Nekretnine</a> <div> <div>Kompletan spisak nekretnina možete pegledati ovdije: <a href="{{requestUrl}}">Nekretnine</a> <div>
</div>`; </div>`;

File diff suppressed because it is too large Load Diff

View File

@@ -1,375 +1,364 @@
const fetch = require('node-fetch'); const fetch = require("node-fetch");
const cheerio = require('cheerio'); const cheerio = require("cheerio");
const { allRERequest, findPointInsideBoundingBox } = require('../db/dbHelper'); const { allRERequest, findPointInsideBoundingBox } = require("../db/dbHelper");
const { getRealEstateTypeEnum } = require('../enums'); const { getRealEstateTypeEnum } = require("../enums");
const { getRegion, getMunicipality } = require('../codes') const { getRegion, getMunicipality } = require("../codes");
const Promise = require("bluebird"); const Promise = require("bluebird");
module.exports = class OlxCrawler { module.exports = class OlxCrawler {
//TODO figure best way to handle paging //TODO figure best way to handle paging
constructor(hrefs = []) { constructor(hrefs = []) {
this.hrefs = hrefs; this.hrefs = hrefs;
} }
async indexPages(urls) { async indexPages(urls) {
const indexers = []; const indexers = [];
urls.forEach(url => { urls.forEach(url => {
indexers.push(new Indexer(url)); indexers.push(new Indexer(url));
}); });
return Promise.map(indexers, function (indexer) { return Promise.map(indexers, function(indexer) {
return indexer.indexWithPagination(); return indexer.indexWithPagination();
}).then(async (results) => { }).then(async results => {
return results return results;
}) });
} }
async crawl() { async crawl() {
console.log("OLX CRAWLER: start crawl"); const filteredResults = [];
const realestateRequests = await allRERequest();
const urls = this.createRequestUrls(realestateRequests);
let results = await this.indexPages(
urls,
this.fromPage,
this.toPage,
this.maxResults
);
const flatResults = results.flat();
if (flatResults) {
for (const finalResult of flatResults) {
if (null !== finalResult) {
if (
finalResult.lat !== undefined &&
finalResult.lat !== null &&
finalResult.lat !== ""
) {
const pointInsideBoundingBox = await findPointInsideBoundingBox(
[finalResult.lng, finalResult.lat],
finalResult.email,
finalResult.uuid
);
const filteredResults = []; if (pointInsideBoundingBox[0].length !== 0) {
const realestateRequests = await allRERequest(); finalResult.hasLocation = true;
console.log("OLX CRAWLER: found " + realestateRequests.length + "subscribed RealEstateRequests"); filteredResults.push(finalResult);
const urls = this.createRequestUrls(realestateRequests); } else {
let results = await this.indexPages(urls, this.fromPage, this.toPage, this.maxResults); finalResult.hasLocation = false;
console.log("Final crawler results"); filteredResults.push(finalResult);
const flatResults = results.flat();
console.log(flatResults);
if (flatResults) {
console.log(flatResults.length);
for (const finalResult of flatResults) {
if (null !== finalResult) {
if (finalResult.lat !== undefined && finalResult.lat !== null && finalResult.lat !== "") {
const pointInsideBoundingBox = await findPointInsideBoundingBox([finalResult.lng, finalResult.lat], finalResult.email, finalResult.uuid);
if (pointInsideBoundingBox[0].length !== 0) {
finalResult.hasLocation = true
filteredResults.push(finalResult);
} else {
finalResult.hasLocation = false
filteredResults.push(finalResult);
}
}
}
} }
}
console.log("OLX CRAWLER: number of olx crawler results, after geo location filtering: " + filteredResults.length);
return filteredResults;
} }
return [] }
return filteredResults;
}
return [];
}
createRequestUrls(realestateRequests) {
const urls = [];
for (const request of realestateRequests) {
const realsestateType =
"kategorija=" +
getRealEstateTypeEnum(request.realEstateType).olxCategory;
const region = "kanton=" + getRegion(request.region).olxid;
const municipality =
"grad%5B%5D=" +
getMunicipality(request.region, request.municipality).olxid;
const sizeMin = "kvadrata_min=" + request.sizeMin;
const sizeMax = "kvadrata_max=" + request.sizeMax;
const priceMin = "od=" + request.priceMin;
const priceMax = "do=" + request.priceMax;
const olxUrl = {
url: `https://www.olx.ba/pretraga?${realsestateType}&id=2&stanje=0&vrstapregleda=tabela&sort_order=desc&${region}&${municipality}&${priceMin}&${priceMax}&vrsta=samoprodaja&${sizeMin}&${sizeMax}&stranica=`,
email: request.email,
uuid: request.uniqueId,
hrefs: this.hrefs
};
urls.push(olxUrl);
} }
createRequestUrls(realestateRequests) { return urls;
const urls = [] }
for (const request of realestateRequests) {
const realsestateType = "kategorija=" + getRealEstateTypeEnum(request.realEstateType).olxCategory;
const region = "kanton=" + getRegion(request.region).olxid;
const municipality = "grad%5B%5D=" + getMunicipality(request.region, request.municipality).olxid;
const sizeMin = "kvadrata_min=" + request.sizeMin;
const sizeMax = "kvadrata_max=" + request.sizeMax;
const priceMin = "od=" + request.priceMin;
const priceMax = "do=" + request.priceMax;
const olxUrl = {
url: `https://www.olx.ba/pretraga?${realsestateType}&id=2&stanje=0&vrstapregleda=tabela&sort_order=desc&${region}&${municipality}&${priceMin}&${priceMax}&vrsta=samoprodaja&${sizeMin}&${sizeMax}&stranica=`,
email: request.email,
uuid: request.uniqueId,
hrefs: this.hrefs
}
console.log(olxUrl.url);
urls.push(olxUrl);
}
return urls;
}
}; };
class Indexer { class Indexer {
/**
*
* @param {String|Array} olxUrl single or array of objects containing url email and uuid
* @param {Array} hrefResutls array contaning urls from crawler results
*/
/** constructor(olxUrl, hrefResutls) {
* this.olxUrl = olxUrl;
* @param {String|Array} olxUrl single or array of objects containing url email and uuid this.hrefResutls = hrefResutls;
* @param {Array} hrefResutls array contaning urls from crawler results }
*/
constructor(olxUrl, hrefResutls) { async indexWithPagination(pageNumber = 1) {
this.olxUrl = olxUrl; const pageNr = this.olxUrl.url.match(/\d+$/);
this.hrefResutls = hrefResutls; const indexers = this.prepareIndexers(pageNumber ? [pageNumber] : pageNr);
}
async indexWithPagination(pageNumber = 1) { try {
return Promise.map(indexers.indexers, function(indexer) {
return indexer.indexPage(pageNumber);
}).then(async results => {
let hasResults = false;
console.log("This is olxUrl:" + this.olxUrl.url); results.forEach(result => {
const pageNr = this.olxUrl.url.match(/\d+$/); if (!hasResults) {
const indexers = this.prepareIndexers(pageNumber ? [pageNumber] : pageNr); hasResults = result.hasResults;
}
});
try { if (!hasResults) {
const singlePageIndexers = this.prepareHrefIndexers(results);
if (singlePageIndexers.length === 0) {
return [];
}
return Promise.map(indexers.indexers, function (indexer) { return Promise.map(singlePageIndexers, function(indexer) {
return indexer.indexPage(pageNumber); return indexer.indexSingle();
}).then(async (results) => { }).then(async results => {
let hasResults = false; return results;
});
results.forEach(result => {
if (!hasResults) {
console.log("No results detected")
hasResults = result.hasResults
}
});
if (!hasResults) {
console.log("HAS NO MORE RESULTS, stop the paging, there are some results and they should contain only HREFS");
console.log(results.length);
const singlePageIndexers = this.prepareHrefIndexers(results);
if (singlePageIndexers.length === 0) {
console.log("THERE IS NOT EVEN SINGLE RESULT");
return []
}
return Promise.map(singlePageIndexers, function (indexer) {
return indexer.indexSingle();
}).then(async (results) => {
console.log("SinglePageMethod in HAS NO RESULTS, MarketAralms");
console.log(results.length);
return results;
});
} else {
console.log("HAS MORE RESULTS, should only contain HREFS");
console.log(results.length);
const newResults = await this.indexWithPagination(results[0].pageNumber + 5);
const singlePageIndexers = this.prepareHrefIndexers(results);
const newerResults = await Promise.map(singlePageIndexers, function (indexer) {
return indexer.indexSingle();
}).then(async (results) => {
console.log("SinglePageMethod HAS RESULTS, should contain MarketAlerts only");
console.log(results.length);
return results;
});
Array.prototype.push.apply(newResults, newerResults);
return newResults;
}
});
} catch (e) {
console.error("Error has accured", e);
}
}
prepareIndexers(pageNr) {
console.log("Entering prepareIndexers : page nr - " + pageNr);
const indexers = [];
let lastPageNumber;
if (pageNr) {
for (let index = Number(pageNr[0]); index <= Number(pageNr[0]) + 5; index++) {
lastPageNumber = index;
const newOlxUrl = {
url: this.olxUrl.url.replace(/\d+$/, "") + index,
email: this.olxUrl.email,
uuid: this.olxUrl.uuid,
hrefs: this.olxUrl.hrefs
}
indexers.push(new Indexer(newOlxUrl));
}
} else { } else {
for (let index = 1; index <= 5; index++) { const newResults = await this.indexWithPagination(
lastPageNumber = index; results[0].pageNumber + 5
const newOlxUrl = { );
url: this.olxUrl.url + index, const singlePageIndexers = this.prepareHrefIndexers(results);
email: this.olxUrl.email,
uuid: this.olxUrl.uuid, const newerResults = await Promise.map(singlePageIndexers, function(
hrefs: this.olxUrl.hrefs indexer
} ) {
indexers.push(new Indexer(newOlxUrl)); return indexer.indexSingle();
} }).then(async results => {
return results;
});
Array.prototype.push.apply(newResults, newerResults);
return newResults;
} }
return { });
indexers: indexers, } catch (e) {
lastPageNumber: lastPageNumber console.error("Error has accured", e);
}
}
prepareIndexers(pageNr) {
const indexers = [];
let lastPageNumber;
if (pageNr) {
for (
let index = Number(pageNr[0]);
index <= Number(pageNr[0]) + 5;
index++
) {
lastPageNumber = index;
const newOlxUrl = {
url: this.olxUrl.url.replace(/\d+$/, "") + index,
email: this.olxUrl.email,
uuid: this.olxUrl.uuid,
hrefs: this.olxUrl.hrefs
}; };
indexers.push(new Indexer(newOlxUrl));
}
} else {
for (let index = 1; index <= 5; index++) {
lastPageNumber = index;
const newOlxUrl = {
url: this.olxUrl.url + index,
email: this.olxUrl.email,
uuid: this.olxUrl.uuid,
hrefs: this.olxUrl.hrefs
};
indexers.push(new Indexer(newOlxUrl));
}
} }
return {
indexers: indexers,
lastPageNumber: lastPageNumber
};
}
prepareHrefIndexers(results) { prepareHrefIndexers(results) {
const indexers = [] const indexers = [];
if (!Array.isArray(results)) { if (!Array.isArray(results)) {
results.hrefs.forEach(href => { results.hrefs.forEach(href => {
const newOlxUrl = { const newOlxUrl = {
url: href, url: href,
email: results.olxUrl.email, email: results.olxUrl.email,
uuid: results.olxUrl.uuid, uuid: results.olxUrl.uuid,
hrefs: this.olxUrl.hrefs hrefs: this.olxUrl.hrefs
} };
indexers.push(new Indexer(newOlxUrl)); indexers.push(new Indexer(newOlxUrl));
}); });
} else {
} else { results.forEach(result => {
if (result !== null && result.hasOwnProperty("hrefs")) {
result.hrefs.forEach(href => {
results.forEach(result => { const newOlxUrl = {
url: href,
if (result !== null && result.hasOwnProperty('hrefs')) { email: result.olxUrl.email,
result.hrefs.forEach(href => { uuid: result.olxUrl.uuid,
const newOlxUrl = { hrefs: this.olxUrl.hrefs
url: href,
email: result.olxUrl.email,
uuid: result.olxUrl.uuid,
hrefs: this.olxUrl.hrefs
}
indexers.push(new Indexer(newOlxUrl));
})
}
});
}
return indexers;
}
async indexPage(pageNumber) {
console.log("Page number in index page, max page number :")
console.log(pageNumber);
try {
console.log("Indexing page: " + this.olxUrl.url);
const res = await fetch(this.olxUrl.url);
const body = await res.text();
const $ = cheerio.load(body);
const hrefs = [];
let hasResults = false
$('#rezultatipretrage').find('.listitem').each((i, elem) => {
hasResults = true
const href = $(elem).find('a').first().attr('href');
hrefs.push(href);
});
console.log("this is hrefs for olxUrl" + this.olxUrl.url);
console.log("NUMBER OF HREFS " + hrefs.length);
return {
hrefs: hrefs,
hasResults: hasResults,
pageNumber: pageNumber,
olxUrl: this.olxUrl
}
} catch (e) {
console.error('Exception caught:' + e);
}
}
async indexSingle() {
try {
console.log("Index single");
console.log(this.olxUrl.url);
if (this.olxUrl.url === undefined) {
return {}
}
// if (global.hrefs) {
if (this.olxUrl.hrefs[this.olxUrl.uuid] && this.olxUrl.hrefs[this.olxUrl.uuid].includes(this.olxUrl.url)) {
console.log("We found duplicate URL");
return null
}
// }
const res = await fetch(this.olxUrl.url);
const body = await res.text();
const $ = cheerio.load(body);
const title = $('#naslovartikla').text().trim();
const realEstateType = $('#artikal_glavni_div > div.artikal_lijevo > div:nth-child(3) > div > span:nth-child(3) > a > span').text();
const price = $('#pc > p:nth-child(2)').text();
const size = $('#dodatnapolja1 > div:nth-child(1) > div.df2').text();
const rooms = $('#dodatnapolja1 > div:nth-child(2) > div.df2').text();
const address = $('#dodatnapolja1 > div:nth-child(5) > div.df2').text();
const gardenSize = $('#dodatnapolja1 > div:nth-child(6) > div.df2').text();
const location = $('#artikal_glavni_div > div.artikal_lijevo > div.op.pop.mobile-lokacija').attr('data-content');
const time = $('time').attr('datetime');
const olxId = $('#artikal_glavni_div > div.artikal_lijevo > div:nth-child(15) > div:nth-child(4) > div.df2').text();
const descriptions = $('.artikal_detaljniopis_tekst');
const latLngRe = /LatLng\(([0-9]+\.[0-9]+)\,\s+([0-9]+\.[0-9]+)\)/g;
const imgRe = /href":("[^"]*")/g;
const matches = latLngRe.exec(body);
let lng = '',
lat = '';
const parsePrice = (price) => parseFloat(price.replace(".", ""))
if (matches && matches.length >= 3) {
lat = matches[1];
lng = matches[2];
}
const parsedPrice = parsePrice(price);
const locationArray = location.split(",");
const region = locationArray[0];
const municipality = locationArray[1];
const data = {
realEstateType: this.getCategoryId(realEstateType),
email: this.olxUrl.email,
uuid: this.olxUrl.uuid,
olxId: olxId,
url: this.olxUrl.url,
title,
price: isNaN(parsedPrice) ? 0 : parsedPrice,
size: parseFloat(size),
gardenSize: isNaN(parseFloat(gardenSize)) ? 0 : parseFloat(gardenSize),
address,
region,
municipality,
time,
shortDescription: descriptions.first().text(),
longDescription: descriptions.last().text(),
lat,
lng,
loc: [parseFloat(lat), parseFloat(lng)],
}; };
return data; indexers.push(new Indexer(newOlxUrl));
} catch (e) { });
console.error('Exception caught: ' + e.message);
} }
});
}
return indexers;
}
async indexPage(pageNumber) {
try {
const res = await fetch(this.olxUrl.url);
const body = await res.text();
const $ = cheerio.load(body);
const hrefs = [];
let hasResults = false;
$("#rezultatipretrage")
.find(".listitem")
.each((i, elem) => {
hasResults = true;
const href = $(elem)
.find("a")
.first()
.attr("href");
hrefs.push(href);
});
return {
hrefs: hrefs,
hasResults: hasResults,
pageNumber: pageNumber,
olxUrl: this.olxUrl
};
} catch (e) {
console.error("Exception caught:" + e);
}
}
async indexSingle() {
try {
if (this.olxUrl.url === undefined) {
return {};
}
// if (global.hrefs) {
if (
this.olxUrl.hrefs[this.olxUrl.uuid] &&
this.olxUrl.hrefs[this.olxUrl.uuid].includes(this.olxUrl.url)
) {
return null; return null;
}
// }
const res = await fetch(this.olxUrl.url);
const body = await res.text();
const $ = cheerio.load(body);
const title = $("#naslovartikla")
.text()
.trim();
const realEstateType = $(
"#artikal_glavni_div > div.artikal_lijevo > div:nth-child(3) > div > span:nth-child(3) > a > span"
).text();
const price = $("#pc > p:nth-child(2)").text();
const size = $("#dodatnapolja1 > div:nth-child(1) > div.df2").text();
const rooms = $("#dodatnapolja1 > div:nth-child(2) > div.df2").text();
const address = $("#dodatnapolja1 > div:nth-child(5) > div.df2").text();
const gardenSize = $(
"#dodatnapolja1 > div:nth-child(6) > div.df2"
).text();
const location = $(
"#artikal_glavni_div > div.artikal_lijevo > div.op.pop.mobile-lokacija"
).attr("data-content");
const time = $("time").attr("datetime");
const olxId = $(
"#artikal_glavni_div > div.artikal_lijevo > div:nth-child(15) > div:nth-child(4) > div.df2"
).text();
const descriptions = $(".artikal_detaljniopis_tekst");
const latLngRe = /LatLng\(([0-9]+\.[0-9]+)\,\s+([0-9]+\.[0-9]+)\)/g;
const imgRe = /href":("[^"]*")/g;
const matches = latLngRe.exec(body);
let lng = "",
lat = "";
const parsePrice = price => parseFloat(price.replace(".", ""));
if (matches && matches.length >= 3) {
lat = matches[1];
lng = matches[2];
}
const parsedPrice = parsePrice(price);
const locationArray = location.split(",");
const region = locationArray[0];
const municipality = locationArray[1];
const data = {
realEstateType: this.getCategoryId(realEstateType),
email: this.olxUrl.email,
uuid: this.olxUrl.uuid,
olxId: olxId,
url: this.olxUrl.url,
title,
price: isNaN(parsedPrice) ? 0 : parsedPrice,
size: parseFloat(size),
gardenSize: isNaN(parseFloat(gardenSize)) ? 0 : parseFloat(gardenSize),
address,
region,
municipality,
time,
shortDescription: descriptions.first().text(),
longDescription: descriptions.last().text(),
lat,
lng,
loc: [parseFloat(lat), parseFloat(lng)]
};
return data;
} catch (e) {
console.error("Exception caught: " + e.message);
} }
getCategoryId(category) { return null;
}
switch (category) { getCategoryId(category) {
case 'Stanovi': switch (category) {
return 'stan'; case "Stanovi":
return "stan";
case 'Vikendice': case "Vikendice":
return 'vikendica' return "vikendica";
case 'Kuće': case "Kuće":
return 'kuca'; return "kuca";
default: default:
return ''; return "";
}
} }
}
} }

View File

@@ -1,7 +1,6 @@
const isValidEmail = email => {
const isValidEmail = (email) => {
const simpleEmailRegex = /^.+@.+\..+$/; const simpleEmailRegex = /^.+@.+\..+$/;
return (email && email.length < 250 && simpleEmailRegex.test(email)); return email && email.length < 250 && simpleEmailRegex.test(email);
}; };
module.exports = { module.exports = {

View File

@@ -1,57 +1,57 @@
const realEstateTypes = [ const realEstateTypes = [
{ title: "Kuća", id: "kuca", hasGardenSize: true, olxCategory: 24 }, { title: "Kuća", id: "kuca", hasGardenSize: true, olxCategory: 24 },
{ title: "Stan", id: "stan", hasGardenSize: false, olxCategory: 23}, { title: "Stan", id: "stan", hasGardenSize: false, olxCategory: 23 },
{ title: "Vikendica", id: "vikendica", hasGardenSize: true, olxCategory: 26 } { title: "Vikendica", id: "vikendica", hasGardenSize: true, olxCategory: 26 }
]; ];
const sizes = [ const sizes = [
{ title: "do 50 m2", id: "50m2" }, { title: "do 50 m2", id: "50m2" },
{ title: "do 75 m2", id: "75m2" }, { title: "do 75 m2", id: "75m2" },
{ title: "do 100 m2", id: "100m2" }, { title: "do 100 m2", id: "100m2" },
{ title: "do 150 m2", id: "150m2" }, { title: "do 150 m2", id: "150m2" },
{ title: "do 200 m2", id: "200m2" }, { title: "do 200 m2", id: "200m2" },
{ title: "preko 200 m2", id: "moreThan200m2" } { title: "preko 200 m2", id: "moreThan200m2" }
]; ];
const gardenSizes = [ const gardenSizes = [
{ title: "do 100 m2", id: "100m2" }, { title: "do 100 m2", id: "100m2" },
{ title: "do 500 m2", id: "500m2" }, { title: "do 500 m2", id: "500m2" },
{ title: "do 1 dunum", id: "1000m2" }, { title: "do 1 dunum", id: "1000m2" },
{ title: "do 2 dunuma", id: "2000m2" }, { title: "do 2 dunuma", id: "2000m2" },
{ title: "do 3 dunuma", id: "3000m2" }, { title: "do 3 dunuma", id: "3000m2" },
{ title: "preko 3 dunuma", id: "moreThan3000m2" } { title: "preko 3 dunuma", id: "moreThan3000m2" }
]; ];
const prices = [ const prices = [
{ title: "do 50 000 KM", id: "50kKM" }, { title: "do 50 000 KM", id: "50kKM" },
{ title: "do 100 000 KM", id: "100kKM" }, { title: "do 100 000 KM", id: "100kKM" },
{ title: "do 150 000 KM", id: "150kKM" }, { title: "do 150 000 KM", id: "150kKM" },
{ title: "do 200 000 KM", id: "200kKM" }, { title: "do 200 000 KM", id: "200kKM" },
{ title: "do 250 000 KM", id: "250kKM" }, { title: "do 250 000 KM", id: "250kKM" },
{ title: "preko 250 000 KM", id: "moreThan250kKM" } { title: "preko 250 000 KM", id: "moreThan250kKM" }
]; ];
const getEnumObject = (enumType, enumId) => { const getEnumObject = (enumType, enumId) => {
return enumType.find(enumValue => enumValue.id === enumId); return enumType.find(enumValue => enumValue.id === enumId);
}; };
const getRealEstateTypeEnum = (enumId) => { const getRealEstateTypeEnum = enumId => {
return getEnumObject(realEstateTypes, enumId) || null; return getEnumObject(realEstateTypes, enumId) || null;
} };
const getEnumTypeTitle = (enumType, enumId) => { const getEnumTypeTitle = (enumType, enumId) => {
const enumObject = getEnumObject(enumType, enumId); const enumObject = getEnumObject(enumType, enumId);
if (!enumObject){ if (!enumObject) {
return null; return null;
} }
return enumObject.title; return enumObject.title;
}; };
module.exports = { module.exports = {
realEstateTypes, realEstateTypes,
sizes, sizes,
gardenSizes, gardenSizes,
prices, prices,
getRealEstateTypeEnum, getRealEstateTypeEnum,
getEnumTypeTitle, getEnumTypeTitle
}; };

View File

@@ -1,12 +1,12 @@
const db = require('../models/index'); const db = require("../models/index");
const currentRERequest = async (req) => { const currentRERequest = async req => {
const uniqueId = req.params['request_id']; const uniqueId = req.params["request_id"];
if(!uniqueId) return null; if (!uniqueId) return null;
const request = await db.RealEstateRequest.findOne({ where: {uniqueId} }); const request = await db.RealEstateRequest.findOne({ where: { uniqueId } });
return request; return request;
}; };
module.exports = { module.exports = {
currentRERequest, currentRERequest
}; };

View File

@@ -1,9 +1,8 @@
const scrapTheItems = require("./scrapTheItems"); const scrapTheItems = require("./scrapTheItems");
const convertToDate = require("./convertToDate"); const convertToDate = require("./convertToDate");
const AWS = require('aws-sdk'); const AWS = require("aws-sdk");
// AWS.config.update({region: 'eu-central-1'}); // AWS.config.update({region: 'eu-central-1'});
async function sendNotification(marketAlert) { async function sendNotification(marketAlert) {
const { id, email, olx_url } = marketAlert; const { id, email, olx_url } = marketAlert;
let url = let url =
@@ -19,37 +18,37 @@ async function sendNotification(marketAlert) {
// Create sendEmail params // Create sendEmail params
const params = { const params = {
Destination: { /* required */ Destination: {
CcAddresses: [ /* required */
], CcAddresses: [],
ToAddresses: [ ToAddresses: [email]
email
]
}, },
Message: { /* required */ Message: {
Body: { /* required */ /* required */
Body: {
/* required */
Html: { Html: {
Charset: "UTF-8", Charset: "UTF-8",
Data: message Data: message
}, },
Text: { Text: {
Charset: "UTF-8", Charset: "UTF-8",
Data: message // TODO: convert to text Data: message // TODO: convert to text
} }
}, },
Subject: { Subject: {
Charset: 'UTF-8', Charset: "UTF-8",
Data: 'Javimi alert' Data: "Javimi alert"
} }
}, },
Source: 'info@saburly.com', /* required */ Source: "info@saburly.com" /* required */,
ReplyToAddresses: [ ReplyToAddresses: ["info@saburly.com"]
'info@saburly.com',
],
}; };
if (message) { if (message) {
const sendPromise = new AWS.SES({apiVersion: '2010-12-01'}).sendEmail(params).promise(); const sendPromise = new AWS.SES({ apiVersion: "2010-12-01" })
.sendEmail(params)
.promise();
await sendPromise; await sendPromise;
return { id, date: String(convertToDate(lastDate)) }; return { id, date: String(convertToDate(lastDate)) };
} }

View File

@@ -1,7 +1,7 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.createTable('MarketAlerts', { return queryInterface.createTable("MarketAlerts", {
id: { id: {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
@@ -29,6 +29,6 @@ module.exports = {
}); });
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('MarketAlerts'); return queryInterface.dropTable("MarketAlerts");
} }
}; };

View File

@@ -1,7 +1,7 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.createTable('RealEstateRequests', { return queryInterface.createTable("RealEstateRequests", {
id: { id: {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
@@ -28,6 +28,6 @@ module.exports = {
}); });
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('RealEstateRequests'); return queryInterface.dropTable("RealEstateRequests");
} }
}; };

View File

@@ -1,18 +1,15 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn(
'RealEstateRequests', "RealEstateRequests",
'city', "city",
Sequelize.STRING Sequelize.STRING
); );
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("RealEstateRequests", "city");
'RealEstateRequests',
'city'
);
} }
}; };

View File

@@ -1,18 +1,15 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn(
'RealEstateRequests', "RealEstateRequests",
'place', "place",
Sequelize.STRING Sequelize.STRING
); );
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("RealEstateRequests", "place");
'RealEstateRequests',
'place'
);
} }
}; };

View File

@@ -1,19 +1,19 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.renameColumn( return queryInterface.renameColumn(
'RealEstateRequests', "RealEstateRequests",
'place', "place",
'municipality' "municipality"
); );
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.renameColumn( return queryInterface.renameColumn(
'RealEstateRequests', "RealEstateRequests",
'municipality', "municipality",
'place' "place"
); );
} }
}; };

View File

@@ -1,19 +1,11 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.renameColumn( return queryInterface.renameColumn("RealEstateRequests", "city", "region");
'RealEstateRequests',
'city',
'region'
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.renameColumn( return queryInterface.renameColumn("RealEstateRequests", "region", "city");
'RealEstateRequests',
'region',
'city'
);
} }
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("RealEstateRequests", "size", {
'RealEstateRequests', type: Sequelize.STRING
'size', });
{
type: Sequelize.STRING
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("RealEstateRequests", "size");
'RealEstateRequests',
'size'
);
} }
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("RealEstateRequests", "gardenSize", {
'RealEstateRequests', type: Sequelize.STRING
'gardenSize', });
{
type: Sequelize.STRING
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("RealEstateRequests", "gardenSize");
'RealEstateRequests',
'gardenSize'
);
} }
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("RealEstateRequests", "price", {
'RealEstateRequests', type: Sequelize.STRING
'price', });
{
type: Sequelize.STRING
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("RealEstateRequests", "price");
'RealEstateRequests',
'price'
);
} }
}; };

View File

@@ -1,15 +1,19 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.query("CREATE EXTENSION postgis").then(([results, metadata]) => { return queryInterface.sequelize
/// No result .query("CREATE EXTENSION postgis")
}) .then(([results, metadata]) => {
/// No result
});
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.query("DROP EXTENSION IF EXISTS postgis").then(([results, metadata]) => { return queryInterface.sequelize
.query("DROP EXTENSION IF EXISTS postgis")
.then(([results, metadata]) => {
/// No result /// No result
}) });
} }
}; };

View File

@@ -1,17 +1,21 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.sequelize
return queryInterface.sequelize.query("ALTER TABLE \"RealEstateRequests\" ADD COLUMN bounding_box geometry(Polygon);").then(([results, metadata]) => { .query(
/// No result 'ALTER TABLE "RealEstateRequests" ADD COLUMN bounding_box geometry(Polygon);'
}) )
.then(([results, metadata]) => {
/// No result
});
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.query("ALTER TABLE \"RealEstateRequests\" DROP COLUMN bounding_box").then(([results, metadata]) => { return queryInterface.sequelize
/// No result .query('ALTER TABLE "RealEstateRequests" DROP COLUMN bounding_box')
}) .then(([results, metadata]) => {
/// No result
});
} }
}; };

View File

@@ -1,27 +1,48 @@
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.addColumn('RealEstateRequests', 'sizeRange', { queryInterface.addColumn(
type: Sequelize.STRING "RealEstateRequests",
}, { transaction: t }), "sizeRange",
queryInterface.addColumn('RealEstateRequests', 'gardenSizeRange', { {
type: Sequelize.STRING, type: Sequelize.STRING
}, { transaction: t }), },
queryInterface.addColumn('RealEstateRequests', 'priceRange', { { transaction: t }
type: Sequelize.STRING, ),
}, { transaction: t }) queryInterface.addColumn(
]) "RealEstateRequests",
}) "gardenSizeRange",
}, {
type: Sequelize.STRING
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"priceRange",
{
type: Sequelize.STRING
},
{ transaction: t }
)
]);
});
},
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.removeColumn('RealEstateRequests', 'sizeRange', { transaction: t }), queryInterface.removeColumn("RealEstateRequests", "sizeRange", {
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeRange', { transaction: t }), transaction: t
queryInterface.removeColumn('RealEstateRequests', 'priceRange', { transaction: t }) }),
]) queryInterface.removeColumn("RealEstateRequests", "gardenSizeRange", {
transaction: t
}),
queryInterface.removeColumn("RealEstateRequests", "priceRange", {
transaction: t
}) })
} ]);
}; });
}
};

View File

@@ -1,63 +1,147 @@
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.removeColumn('RealEstateRequests', 'sizeRange', { transaction: t }), queryInterface.removeColumn("RealEstateRequests", "sizeRange", {
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeRange', { transaction: t }), transaction: t
queryInterface.removeColumn('RealEstateRequests', 'priceRange', { transaction: t }), }),
queryInterface.removeColumn('RealEstateRequests', 'size', { transaction: t }), queryInterface.removeColumn("RealEstateRequests", "gardenSizeRange", {
queryInterface.removeColumn('RealEstateRequests', 'gardenSize', { transaction: t }), transaction: t
queryInterface.removeColumn('RealEstateRequests', 'price', { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'gardenSizeMin', { queryInterface.removeColumn("RealEstateRequests", "priceRange", {
type: Sequelize.INTEGER, transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'gardenSizeMax', { queryInterface.removeColumn("RealEstateRequests", "size", {
type: Sequelize.INTEGER, transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'sizeMin', { queryInterface.removeColumn("RealEstateRequests", "gardenSize", {
type: Sequelize.INTEGER transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'sizeMax', { queryInterface.removeColumn("RealEstateRequests", "price", {
type: Sequelize.INTEGER, transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'priceMin', { queryInterface.addColumn(
type: Sequelize.INTEGER, "RealEstateRequests",
}, { transaction: t }), "gardenSizeMin",
queryInterface.addColumn('RealEstateRequests', 'priceMax', { {
type: Sequelize.INTEGER type: Sequelize.INTEGER
}, { transaction: t }) },
]) { transaction: t }
}) ),
}, queryInterface.addColumn(
"RealEstateRequests",
"gardenSizeMax",
{
type: Sequelize.INTEGER
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"sizeMin",
{
type: Sequelize.INTEGER
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"sizeMax",
{
type: Sequelize.INTEGER
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"priceMin",
{
type: Sequelize.INTEGER
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"priceMax",
{
type: Sequelize.INTEGER
},
{ transaction: t }
)
]);
});
},
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeMin', { transaction: t }), queryInterface.removeColumn("RealEstateRequests", "gardenSizeMin", {
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeMax', { transaction: t }), transaction: t
queryInterface.removeColumn('RealEstateRequests', 'sizeMin', { transaction: t }), }),
queryInterface.removeColumn('RealEstateRequests', 'sizeMax', { transaction: t }), queryInterface.removeColumn("RealEstateRequests", "gardenSizeMax", {
queryInterface.removeColumn('RealEstateRequests', 'priceMin', { transaction: t }), transaction: t
queryInterface.removeColumn('RealEstateRequests', 'priceMin', { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'priceMax', { queryInterface.removeColumn("RealEstateRequests", "sizeMin", {
type: Sequelize.STRING transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'gardenSizeRange', { queryInterface.removeColumn("RealEstateRequests", "sizeMax", {
type: Sequelize.STRING, transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'priceRange', { queryInterface.removeColumn("RealEstateRequests", "priceMin", {
type: Sequelize.STRING, transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'size', { queryInterface.removeColumn("RealEstateRequests", "priceMin", {
type: Sequelize.STRING transaction: t
}, { transaction: t }), }),
queryInterface.addColumn('RealEstateRequests', 'gardenSize', { queryInterface.addColumn(
type: Sequelize.STRING, "RealEstateRequests",
}, { transaction: t }), "priceMax",
queryInterface.addColumn('RealEstateRequests', 'price', { {
type: Sequelize.STRING, type: Sequelize.STRING
}, { transaction: t }) },
]) { transaction: t }
}) ),
} queryInterface.addColumn(
}; "RealEstateRequests",
"gardenSizeRange",
{
type: Sequelize.STRING
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"priceRange",
{
type: Sequelize.STRING
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"size",
{
type: Sequelize.STRING
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"gardenSize",
{
type: Sequelize.STRING
},
{ transaction: t }
),
queryInterface.addColumn(
"RealEstateRequests",
"price",
{
type: Sequelize.STRING
},
{ transaction: t }
)
]);
});
}
};

View File

@@ -1,18 +1,15 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn(
'RealEstateRequests', "RealEstateRequests",
'subscribed', "subscribed",
Sequelize.BOOLEAN Sequelize.BOOLEAN
); );
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("RealEstateRequests", "subscribed");
'RealEstateRequests',
'subscribed'
);
} }
}; };

View File

@@ -1,37 +1,70 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.addColumn('MarketAlerts', 'size', { queryInterface.addColumn(
type: Sequelize.INTEGER, "MarketAlerts",
}, { transaction: t }), "size",
queryInterface.addColumn('MarketAlerts', 'gardenSize', { {
type: Sequelize.INTEGER, type: Sequelize.INTEGER
}, { transaction: t }), },
queryInterface.addColumn('MarketAlerts', 'price', { { transaction: t }
type: Sequelize.INTEGER, ),
}, { transaction: t }), queryInterface.addColumn(
queryInterface.addColumn('MarketAlerts', 'municipality', { "MarketAlerts",
type: Sequelize.STRING, "gardenSize",
}, { transaction: t }), {
queryInterface.addColumn('MarketAlerts', 'region', { type: Sequelize.INTEGER
type: Sequelize.STRING, },
}, { transaction: t }) { transaction: t }
]) ),
}) queryInterface.addColumn(
}, "MarketAlerts",
"price",
{
type: Sequelize.INTEGER
},
{ transaction: t }
),
queryInterface.addColumn(
"MarketAlerts",
"municipality",
{
type: Sequelize.STRING
},
{ transaction: t }
),
queryInterface.addColumn(
"MarketAlerts",
"region",
{
type: Sequelize.STRING
},
{ transaction: t }
)
]);
});
},
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.removeColumn('MarketAlerts', 'size', { transaction: t }), queryInterface.removeColumn("MarketAlerts", "size", { transaction: t }),
queryInterface.removeColumn('MarketAlerts', 'gardenSize', { transaction: t }), queryInterface.removeColumn("MarketAlerts", "gardenSize", {
queryInterface.removeColumn('MarketAlerts', 'price', { transaction: t }), transaction: t
queryInterface.removeColumn('MarketAlerts', 'municipality', { transaction: t }), }),
queryInterface.removeColumn('MarketAlerts', 'region', { transaction: t }) queryInterface.removeColumn("MarketAlerts", "price", {
]) transaction: t
}),
queryInterface.removeColumn("MarketAlerts", "municipality", {
transaction: t
}),
queryInterface.removeColumn("MarketAlerts", "region", {
transaction: t
}) })
} ]);
});
}
}; };

View File

@@ -1,33 +1,59 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.removeColumn('MarketAlerts', 'olxUrl', { transaction: t }), queryInterface.removeColumn("MarketAlerts", "olxUrl", {
queryInterface.addColumn('MarketAlerts', 'url', { transaction: t
type: Sequelize.STRING, }),
}, { transaction: t }), queryInterface.addColumn(
queryInterface.addColumn('MarketAlerts', 'realestateOrigin', { "MarketAlerts",
type: Sequelize.STRING, "url",
}, { transaction: t }), {
queryInterface.addColumn('MarketAlerts', 'originId', { type: Sequelize.STRING
type: Sequelize.STRING, },
}, { transaction: t }) { transaction: t }
]) ),
}) queryInterface.addColumn(
}, "MarketAlerts",
"realestateOrigin",
{
type: Sequelize.STRING
},
{ transaction: t }
),
queryInterface.addColumn(
"MarketAlerts",
"originId",
{
type: Sequelize.STRING
},
{ transaction: t }
)
]);
});
},
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => { return queryInterface.sequelize.transaction(t => {
return Promise.all([ return Promise.all([
queryInterface.removeColumn('MarketAlerts', 'url', { transaction: t }), queryInterface.removeColumn("MarketAlerts", "url", { transaction: t }),
queryInterface.removeColumn('MarketAlerts', 'realestateOrigin', { transaction: t }), queryInterface.removeColumn("MarketAlerts", "realestateOrigin", {
queryInterface.removeColumn('MarketAlerts', 'originId', { transaction: t }), transaction: t
queryInterface.addColumn('MarketAlerts', 'olxUrl', { }),
type: Sequelize.STRING queryInterface.removeColumn("MarketAlerts", "originId", {
}, { transaction: t }) transaction: t
]) }),
}) queryInterface.addColumn(
} "MarketAlerts",
"olxUrl",
{
type: Sequelize.STRING
},
{ transaction: t }
)
]);
});
}
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("MarketAlerts", "realEstateType", {
'MarketAlerts', type: Sequelize.STRING
'realEstateType', });
{
type: Sequelize.STRING
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("MarketAlerts", "realEstateType");
'MarketAlerts',
'realEstateType'
);
} }
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("MarketAlerts", "notified", {
'MarketAlerts', type: Sequelize.BOOLEAN
'notified', });
{
type: Sequelize.BOOLEAN
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("MarketAlerts", "notified");
'MarketAlerts',
'notified'
);
} }
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("MarketAlerts", "title", {
'MarketAlerts', type: Sequelize.STRING
'title', });
{
type: Sequelize.STRING
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("MarketAlerts", "title");
'MarketAlerts',
'title'
);
} }
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("MarketAlerts", "request", {
'MarketAlerts', type: Sequelize.STRING
'request', });
{
type: Sequelize.STRING
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("MarketAlerts", "request");
'MarketAlerts',
'request'
);
} }
}; };

View File

@@ -1,20 +1,13 @@
'use strict'; "use strict";
module.exports = { module.exports = {
up: (queryInterface, Sequelize) => { up: (queryInterface, Sequelize) => {
return queryInterface.addColumn( return queryInterface.addColumn("MarketAlerts", "hasLocation", {
'MarketAlerts', type: Sequelize.BOOLEAN
'hasLocation', });
{
type: Sequelize.BOOLEAN
}
);
}, },
down: (queryInterface, Sequelize) => { down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn( return queryInterface.removeColumn("MarketAlerts", "hasLocation");
'MarketAlerts',
'hasLocation'
);
} }
}; };

View File

@@ -1,27 +1,39 @@
'use strict'; "use strict";
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const Sequelize = require('sequelize'); const Sequelize = require("sequelize");
const basename = path.basename(__filename); const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development'; const env = process.env.NODE_ENV || "development";
const config = require(__dirname + '/../config/config.json')[env]; const config = require(__dirname + "/../config/config.json")[env];
const db = {}; const db = {};
config.username = process.env.DB_USERNAME || config.username;
config.password = process.env.DB_PASSWORD || config.password;
config.database = process.env.DB_NAME || config.database;
config.port = process.env.DB_PORT || config.port;
config.logging = parseInt(process.env.SEQUELIZE_LOGGING) ? console.log : false;
let sequelize; let sequelize;
if (config.use_env_variable) { if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config); sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else { } else {
sequelize = new Sequelize(config.database, config.username, config.password, config); sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
} }
fs fs.readdirSync(__dirname)
.readdirSync(__dirname)
.filter(file => { .filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); return (
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
);
}) })
.forEach(file => { .forEach(file => {
const model = sequelize['import'](path.join(__dirname, file)); const model = sequelize["import"](path.join(__dirname, file));
db[model.name] = model; db[model.name] = model;
}); });

View File

@@ -1,26 +1,30 @@
'use strict'; "use strict";
module.exports = (sequelize, DataTypes) => { module.exports = (sequelize, DataTypes) => {
const MarketAlert = sequelize.define('MarketAlert', { const MarketAlert = sequelize.define(
url: DataTypes.STRING, "MarketAlert",
realestateOrigin: DataTypes.STRING, {
originId: DataTypes.STRING, url: DataTypes.STRING,
lastDate: DataTypes.STRING, realestateOrigin: DataTypes.STRING,
size : DataTypes.INTEGER, originId: DataTypes.STRING,
gardenSize : DataTypes.INTEGER, lastDate: DataTypes.STRING,
price : DataTypes.INTEGER, size: DataTypes.INTEGER,
municipality : DataTypes.STRING, gardenSize: DataTypes.INTEGER,
region : DataTypes.STRING, price: DataTypes.INTEGER,
realEstateType : DataTypes.STRING, municipality: DataTypes.STRING,
notified : DataTypes.BOOLEAN, region: DataTypes.STRING,
title : DataTypes.STRING, realEstateType: DataTypes.STRING,
request: DataTypes.STRING, notified: DataTypes.BOOLEAN,
hasLocation: DataTypes.BOOLEAN, title: DataTypes.STRING,
request: DataTypes.STRING,
email: { hasLocation: DataTypes.BOOLEAN,
type: DataTypes.STRING,
allowNul: false email: {
} type: DataTypes.STRING,
}, {}); allowNul: false
}
},
{}
);
MarketAlert.associate = function(models) { MarketAlert.associate = function(models) {
// associations can be defined here // associations can be defined here
}; };

View File

@@ -1,26 +1,29 @@
'use strict'; "use strict";
module.exports = (sequelize, DataTypes) => { module.exports = (sequelize, DataTypes) => {
const RealEstateRequest = sequelize.define(
const RealEstateRequest = sequelize.define('RealEstateRequest', { "RealEstateRequest",
uniqueId: { {
type: DataTypes.UUID, uniqueId: {
defaultValue: DataTypes.UUIDV4, type: DataTypes.UUID,
allowNull: false defaultValue: DataTypes.UUIDV4,
allowNull: false
},
realEstateType: DataTypes.STRING,
email: DataTypes.STRING,
region: DataTypes.STRING,
municipality: DataTypes.STRING,
sizeMin: DataTypes.INTEGER,
sizeMax: DataTypes.INTEGER,
gardenSizeMin: DataTypes.INTEGER,
gardenSizeMax: DataTypes.INTEGER,
priceMin: DataTypes.INTEGER,
priceMax: DataTypes.INTEGER,
bounding_box: DataTypes.GEOMETRY("POINT", 4326),
subscribed: DataTypes.BOOLEAN
}, },
realEstateType: DataTypes.STRING, {}
email: DataTypes.STRING, );
region: DataTypes.STRING,
municipality: DataTypes.STRING,
sizeMin: DataTypes.INTEGER,
sizeMax: DataTypes.INTEGER,
gardenSizeMin: DataTypes.INTEGER,
gardenSizeMax: DataTypes.INTEGER,
priceMin: DataTypes.INTEGER,
priceMax: DataTypes.INTEGER,
bounding_box: DataTypes.GEOMETRY('POINT', 4326),
subscribed: DataTypes.BOOLEAN
}, {});
RealEstateRequest.associate = function(models) { RealEstateRequest.associate = function(models) {
// associations can be defined here // associations can be defined here
}; };

View File

@@ -1,85 +1,77 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const OlxCrawler = require("../helpers/crawlers/olxClawler"); const OlxCrawler = require("../helpers/crawlers/olxClawler");
const db = require("../models/index"); const db = require("../models/index");
const { allMarketAlerts } = require('../helpers/db/dbHelper'); const { allMarketAlerts } = require("../helpers/db/dbHelper");
async function crawlAll() { async function crawlAll() {
console.log("CRAWLER SERVICE: crawlAll"); try {
const marketAlertsFromDb = await allMarketAlerts(true);
const hrefs = [];
try { marketAlertsFromDb.map(marketAlert => {
const marketAlertsFromDb = await allMarketAlerts(true); if (hrefs[marketAlert.request] === undefined) {
const hrefs = []; hrefs[marketAlert.request] = [];
}
marketAlertsFromDb.map(marketAlert => { hrefs[marketAlert.request].push(marketAlert.url);
if (hrefs[marketAlert.request] === undefined) { });
hrefs[marketAlert.request] = []
}
hrefs[marketAlert.request].push(marketAlert.url); const olxCrawler = new OlxCrawler(hrefs);
})
console.log("CRAWLER SERVICE: GLOBAL HREFS"); const crawlers = [olxCrawler];
console.log(hrefs);
const olxCrawler = new OlxCrawler(hrefs);
const crawlers = [ return Promise.map(crawlers, function(crawler) {
olxCrawler, return crawler.crawl();
]; }).then(async results => {
try {
return Promise.map(crawlers, function (crawler) { const marketAlertsFromDb = await allMarketAlerts(false, true);
return crawler.crawl();
}).then(async (results) => {
try { const marketAlerts = [];
const mergedResults = [].concat.apply([], results);
const marketAlertsFromDb = await allMarketAlerts(false, true); for (const result of mergedResults) {
marketAlerts.push({
url: result.url,
realestateOrigin: "OLX",
originId: 1,
size: result.size,
price: result.price,
email: result.email,
request: result.uuid,
municipality: result.municipality,
region: result.region,
gardenSize: isNaN(result.gardenSize) ? 0 : result.gardenSize,
realEstateType: result.realEstateType,
title: result.title,
notified: false,
hasLocation: result.hasLocation
});
}
console.log("CRAWLER SERVICE: number of existing MarketAlerts from db: " + marketAlertsFromDb.length); try {
const filteredMarketAlerts = marketAlerts.filter(
elem =>
!marketAlertsFromDb.find(({ url, request }) => {
return elem.url === url && elem.request === request;
})
);
const marketAlerts = []; await db.MarketAlert.bulkCreate(filteredMarketAlerts);
const mergedResults = [].concat.apply([], results); } catch (e) {
console.log(
for (const result of mergedResults) { "CRAWLER SERVICE: Could not bulkCreate marketalers reason: ",
marketAlerts.push({ e
url: result.url, );
realestateOrigin: "OLX", }
originId: 1, } catch (e) {
size: result.size, console.log(
price: result.price, "CRAWLER SERVICE: Error crawling. Trying next crawler! ",
email: result.email, e
request: result.uuid, );
municipality: result.municipality, }
region: result.region, });
gardenSize: isNaN(result.gardenSize) ? 0 : result.gardenSize, } catch (e) {
realEstateType: result.realEstateType, console.error("CRAWLER SERVICE:could not fetch marketalerts ", e);
title: result.title, }
notified: false, }
hasLocation: result.hasLocation
})
}
console.log("CRAWLER SERVICE: Number of crawler results: " + marketAlerts.length);
try {
const filteredMarketAlerts = marketAlerts.filter((elem) => !marketAlertsFromDb.find(({ url, request }) => {
return (elem.url === url && elem.request === request)
}));
console.log("CRAWLER SERVICE: Number of new crawler results: " + filteredMarketAlerts.length);
await db.MarketAlert.bulkCreate(filteredMarketAlerts);
} catch (e) {
console.log("CRAWLER SERVICE: Could not bulkCreate marketalers reason: ", e);
}
} catch (e) {
console.log("CRAWLER SERVICE: Error crawling. Trying next crawler! ", e);
}
})
} catch (e) {
console.error("CRAWLER SERVICE:could not fetch marketalerts ", e);
}
};
module.exports = crawlAll; module.exports = crawlAll;

View File

@@ -1,29 +1,28 @@
const db = require("../models/index"); const db = require("../models/index");
const { allMarketAlerts } = require('../helpers/db/dbHelper'); const { allMarketAlerts } = require("../helpers/db/dbHelper");
const { createMarketAlertEmailTemplate, sendBulkEmail } = require('../helpers/awsEmail'); const {
createMarketAlertEmailTemplate,
sendBulkEmail
} = require("../helpers/awsEmail");
async function processNotifications() { async function processNotifications() {
try {
try { const marketAlerts = await allMarketAlerts(false, false);
const marketAlerts = await allMarketAlerts(false, false); await createMarketAlertEmailTemplate();
console.log(marketAlerts.length) if (marketAlerts.length > 0) {
await createMarketAlertEmailTemplate(); await sendBulkEmail(marketAlerts);
if (marketAlerts.length > 0) {
console.log("NOTIFICATION SERVICE: Number of new alerts: " + marketAlerts.length)
await sendBulkEmail(marketAlerts);
} else {
console.log("NOTIFICATION SERVICE: No new alerts");
}
await db.MarketAlert.update(
{ notified: true }, /* set attributes' value */
{ where: { notified: false } } /* where criteria */
);
} catch (e) {
console.log("NOTIFICATION SERVICE: could not send notifications reason: ", e);
} }
await db.MarketAlert.update(
{ notified: true } /* set attributes' value */,
{ where: { notified: false } } /* where criteria */
);
} catch (e) {
console.log(
"NOTIFICATION SERVICE: could not send notifications reason: ",
e
);
}
} }
module.exports = processNotifications; module.exports = processNotifications;

View File

@@ -1,5 +1,15 @@
AMAZON_ACCES_KEY_ID=(your-key-here) DB_USERNAME=Username for the database
AMAZON_SECRET_ACCESS_KEY=(your-key-here) DB_PASSWORD=Password for the database
AMAZON_REGION=eu-west-1 DB_NAME=Database name
APP_URL=http://localhost:3001 DB_PORT=Database port
SOURCE_EMAIL=info@saburly.com
SEQUELIZE_LOGGING=0- no sequelize logging, 1- log to the console
APP_PORT=Port for the app, defaults to 5000
APP_BASE_URL=base url for the app
AMAZON_ACCES_KEY_ID=(your-key-here)
AMAZON_SECRET_ACCESS_KEY=(your-key-here)
AMAZON_REGION=eu-west-1
APP_URL=http://localhost:3001
SOURCE_EMAIL=info@saburly.com

View File

@@ -1,3 +1,6 @@
require("dotenv").config();
const { APP_PORT } = require("./app/config/appConfig");
const welcome = require("./app/controllers/welcome").getWelcome; const welcome = require("./app/controllers/welcome").getWelcome;
const { const {
getRealEstateTypes, getRealEstateTypes,
@@ -48,8 +51,6 @@ const app = express();
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
const port = process.env.PORT || 5000;
app.set("views", path.join(__dirname, "/app/views")); app.set("views", path.join(__dirname, "/app/views"));
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.use(layout()); app.use(layout());
@@ -180,10 +181,13 @@ app.get("/redirect/:id", redirect);
app.use("/assets", express.static("./app/public")); app.use("/assets", express.static("./app/public"));
app.listen(port, () => console.log(`Example app listening on port ${port}!`)); app.listen(APP_PORT, () =>
console.log(`Example app listening on port ${APP_PORT}!`)
);
var rule = new schedule.RecurrenceRule(); //TODO: based on node-schedule package author, setInterval is better suited for this kind of the job
rule.seccond = 1; const rule = new schedule.RecurrenceRule();
rule.second = 1;
schedule.scheduleJob(rule, async function() { schedule.scheduleJob(rule, async function() {
console.log(new Date(), "Crawler service started"); console.log(new Date(), "Crawler service started");
await crawlAll(); await crawlAll();

View File

@@ -1,389 +1,430 @@
<!DOCTYPE> <!DOCTYPE >
<html> <html>
<head> <head>
<script src="https://code.jquery.com/jquery-3.4.0.js"></script> <script src="https://code.jquery.com/jquery-3.4.0.js"></script>
</head> </head>
<body> <body>
<select name="kanton" id="kanton" class="drop-select" onchange="pronadji_gradove()"> <select
<option value="">Iz svih lokacija</option> name="kanton"
id="kanton"
<option value="" disabled="">Federacija BiH</option> class="drop-select"
<option value="9">&nbsp;Sarajevo</option> onchange="pronadji_gradove()"
<option value="3">&nbsp;Tuzlanski</option> >
<option value="4">&nbsp;Zeničko-Dobojski</option> <option value="">Iz svih lokacija</option>
<option value="1">&nbsp;Unsko-Sanski</option>
<option value="2">&nbsp;Posavski</option> <option value="" disabled="">Federacija BiH</option>
<option value="5">&nbsp;Bosansko-podrinjski</option> <option value="9">&nbsp;Sarajevo</option>
<option value="6">&nbsp;Srednjobosanski</option> <option value="3">&nbsp;Tuzlanski</option>
<option value="7">&nbsp;Hercegovačko-Neretvanski</option> <option value="4">&nbsp;Zeničko-Dobojski</option>
<option value="8">&nbsp;Zapadno-Hercegovački</option> <option value="1">&nbsp;Unsko-Sanski</option>
<option value="10">&nbsp;Livanjski</option> <option value="2">&nbsp;Posavski</option>
<option value="5">&nbsp;Bosansko-podrinjski</option>
<option value="" disabled="">Republika Srpska</option> <option value="6">&nbsp;Srednjobosanski</option>
<option value="14">&nbsp;Banjalučka</option> <option value="7">&nbsp;Hercegovačko-Neretvanski</option>
<option value="15">&nbsp;Dobojsko-Bijeljinska</option> <option value="8">&nbsp;Zapadno-Hercegovački</option>
<option value="16">&nbsp;Sarajevsko-Zvornička</option> <option value="10">&nbsp;Livanjski</option>
<option value="17">&nbsp;Trebinjsko-Fočanska</option>
<option value="" disabled="">Republika Srpska</option>
<option value="12">Disktrikt Brčko</option> <option value="14">&nbsp;Banjalučka</option>
</select> <option value="15">&nbsp;Dobojsko-Bijeljinska</option>
<option value="16">&nbsp;Sarajevsko-Zvornička</option>
<option value="17">&nbsp;Trebinjsko-Fočanska</option>
<div style="height:40px;">
<option value="12">Disktrikt Brčko</option>
<span class="drop-container" style="width: 168px;"> </select>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<select name="unskosanski" id="unskosanski" class="drop-select" onchange="stavi_grad()"> <div style="height:40px;">
<option value="0">Mjesto</option> <span class="drop-container" style="width: 168px;">
<option value="75">Bihać</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="373">Bosanska Krupa</option> <select
<option value="504">Bosanski Petrovac</option> name="unskosanski"
<option value="374">Bužim</option> id="unskosanski"
<option value="857">Cazin</option> class="drop-select"
<option value="2362">Ključ</option> onchange="stavi_grad()"
<option value="3738">Sanski Most</option> >
<option value="5122">Velika Kladuša</option> <option value="0">Mjesto</option>
</select> <option value="75">Bihać</option>
</span> <option value="373">Bosanska Krupa</option>
</div> <option value="504">Bosanski Petrovac</option>
<option value="374">Bužim</option>
<option value="857">Cazin</option>
<div style="height:40px;"> <option value="2362">Ključ</option>
<option value="3738">Sanski Most</option>
<span class="drop-container" style="width: 168px;"> <option value="5122">Velika Kladuša</option>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> </select>
<select name="posavski" id="posavski" class="drop-select" onchange="stavi_grad()"> </span>
<option value="0">Mjesto</option> </div>
<option value="6144">Domaljevac</option>
<option value="424">Odžak</option> <div style="height:40px;">
<option value="3252">Orašje</option> <span class="drop-container" style="width: 168px;">
<option value="540">Šamac</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
</select> <select
</span> name="posavski"
</div> id="posavski"
class="drop-select"
onchange="stavi_grad()"
<div style="height:40px;"> >
<option value="0">Mjesto</option>
<span class="drop-container" style="width: 168px;"> <option value="6144">Domaljevac</option>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <option value="424">Odžak</option>
<select name="tuzlanski" id="tuzlanski" class="drop-select" onchange="stavi_grad()"> <option value="3252">Orašje</option>
<option value="0">Mjesto</option> <option value="540">Šamac</option>
<option value="2">Banovići</option> </select>
<option value="1090">Doboj-Istok</option> </span>
<option value="1854">Gradačac</option> </div>
<option value="1826">Gračanica</option>
<option value="2129">Kalesija</option> <div style="height:40px;">
<option value="2319">Kladanj</option> <span class="drop-container" style="width: 168px;">
<option value="2840">Lukavac</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="5699">Sapna</option> <select
<option value="4391">Srebrenik</option> name="tuzlanski"
<option value="5010">Teočak</option> id="tuzlanski"
<option value="4944">Tuzla</option> class="drop-select"
<option value="2801">Čelić</option> onchange="stavi_grad()"
<option value="5774">Živinice</option> >
</select> <option value="0">Mjesto</option>
</span> <option value="2">Banovići</option>
</div> <option value="1090">Doboj-Istok</option>
<option value="1854">Gradačac</option>
<option value="1826">Gračanica</option>
<div style="height:40px;"> <option value="2129">Kalesija</option>
<option value="2319">Kladanj</option>
<span class="drop-container" style="width: 168px;"> <option value="2840">Lukavac</option>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <option value="5699">Sapna</option>
<select name="zenickodobojski" id="zenickodobojski" class="drop-select" onchange="stavi_grad()"> <option value="4391">Srebrenik</option>
<option value="0">Mjesto</option> <option value="5010">Teočak</option>
<option value="704">Breza</option> <option value="4944">Tuzla</option>
<option value="1122">Doboj-Jug</option> <option value="2801">Čelić</option>
<option value="2022">Kakanj</option> <option value="5774">Živinice</option>
<option value="2941">Maglaj</option> </select>
<option value="1925">Olovo</option> </span>
<option value="4594">Tešanj</option> </div>
<option value="1087">Usora</option>
<option value="5037">Vareš</option> <div style="height:40px;">
<option value="5171">Visoko</option> <span class="drop-container" style="width: 168px;">
<option value="5548">Zavidovići</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="4571">Zenica</option> <select
<option value="2940">Žepče</option> name="zenickodobojski"
</select> id="zenickodobojski"
</span> class="drop-select"
</div> onchange="stavi_grad()"
>
<option value="0">Mjesto</option>
<div style="height:40px;"> <option value="704">Breza</option>
<option value="1122">Doboj-Jug</option>
<span class="drop-container" style="width: 168px;"> <option value="2022">Kakanj</option>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <option value="2941">Maglaj</option>
<select name="" id="bosanskopodrinjski" class="drop-select" onchange="stavi_grad()"> <option value="1925">Olovo</option>
<option value="0">Mjesto</option> <option value="4594">Tešanj</option>
<option value="1289">Foča</option> <option value="1087">Usora</option>
<option value="1588">Goražde</option> <option value="5037">Vareš</option>
<option value="3546">Pale</option> <option value="5171">Visoko</option>
</select> <option value="5548">Zavidovići</option>
</span> <option value="4571">Zenica</option>
</div> <option value="2940">Žepče</option>
</select>
</span>
<div style="height:40px;"> </div>
<span class="drop-container" style="width: 168px;"> <div style="height:40px;">
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <span class="drop-container" style="width: 168px;">
<select name="" id="srednjobosanski" class="drop-select" onchange="stavi_grad()"> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="0">Mjesto</option> <select
<option value="732">Bugojno</option> name=""
<option value="810">Busovača</option> id="bosanskopodrinjski"
<option value="4151">Dobretići</option> class="drop-select"
<option value="1160">Donji Vakuf</option> onchange="stavi_grad()"
<option value="1407">Fojnica</option> >
<option value="1775">Gornji Vakuf - Uskoplje</option> <option value="0">Mjesto</option>
<option value="1960">Jajce</option> <option value="1289">Foča</option>
<option value="2237">Kiseljak</option> <option value="1588">Goražde</option>
<option value="2608">Kreševo</option> <option value="3546">Pale</option>
<option value="3477">Novi Travnik</option> </select>
<option value="4678">Travnik</option> </span>
<option value="5422">Vitez</option> </div>
</select>
</span> <div style="height:40px;">
</div> <span class="drop-container" style="width: 168px;">
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<select
<div style="height:40px;"> name=""
id="srednjobosanski"
<span class="drop-container" style="width: 168px;"> class="drop-select"
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> onchange="stavi_grad()"
<select name="" id="hercegovackoneretvanski" class="drop-select" onchange="stavi_grad()"> >
<option value="0">Mjesto</option> <option value="0">Mjesto</option>
<option value="3017">Grad Mostar</option> <option value="732">Bugojno</option>
<option value="1930">Jablanica</option> <option value="810">Busovača</option>
<option value="2169">Konjic</option> <option value="4151">Dobretići</option>
<option value="3111">Neum</option> <option value="1160">Donji Vakuf</option>
<option value="3421">Prozor</option> <option value="1407">Fojnica</option>
<option value="4769">Ravno</option> <option value="1775">Gornji Vakuf - Uskoplje</option>
<option value="4439">Stolac</option> <option value="1960">Jajce</option>
<option value="947">Čapljina</option> <option value="2237">Kiseljak</option>
<option value="1009">Čitluk</option> <option value="2608">Kreševo</option>
</select> <option value="3477">Novi Travnik</option>
</span> <option value="4678">Travnik</option>
</div> <option value="5422">Vitez</option>
</select>
</span>
<div style="height:40px;"> </div>
<span class="drop-container" style="width: 168px;"> <div style="height:40px;">
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <span class="drop-container" style="width: 168px;">
<select name="" id="zapadnohercegovacki" class="drop-select" onchange="stavi_grad()"> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="0">Mjesto</option> <select
<option value="1892">Grude</option> name=""
<option value="2905">Ljubuški</option> id="hercegovackoneretvanski"
<option value="3268">Posušje</option> class="drop-select"
<option value="2708">Široki Brijeg</option> onchange="stavi_grad()"
</select> >
</span> <option value="0">Mjesto</option>
</div> <option value="3017">Grad Mostar</option>
<option value="1930">Jablanica</option>
<option value="2169">Konjic</option>
<div style="height:40px;"> <option value="3111">Neum</option>
<option value="3421">Prozor</option>
<span class="drop-container" style="width: 168px;"> <option value="4769">Ravno</option>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <option value="4439">Stolac</option>
<select name="" id="sarajevo" class="drop-select" onchange="stavi_grad()"> <option value="947">Čapljina</option>
<option value="0">Mjesto</option> <option value="1009">Čitluk</option>
<option value="3817">Hadžići</option> </select>
<option value="3879">Ilidža</option> </span>
<option value="3892">Ilijaš</option> </div>
<option value="3812">Sarajevo - Centar</option>
<option value="3969">Sarajevo-Novi Grad</option> <div style="height:40px;">
<option value="5896">Sarajevo-Novo Sarajevo</option> <span class="drop-container" style="width: 168px;">
<option value="4048">Sarajevo-Stari Grad</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="4063">Trnovo</option> <select
<option value="4126">Vogošća</option> name=""
</select> id="zapadnohercegovacki"
</span> class="drop-select"
</div> onchange="stavi_grad()"
>
<option value="0">Mjesto</option>
<div style="height:40px;"> <option value="1892">Grude</option>
<option value="2905">Ljubuški</option>
<span class="drop-container" style="width: 168px;"> <option value="3268">Posušje</option>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <option value="2708">Široki Brijeg</option>
<select name="" id="livanjski" class="drop-select" onchange="stavi_grad()"> </select>
<option value="0">Mjesto</option> </span>
<option value="560">Bosansko Grahovo</option> </div>
<option value="4640">Drvar</option>
<option value="1533">Glamoč</option> <div style="height:40px;">
<option value="2635">Kupres</option> <span class="drop-container" style="width: 168px;">
<option value="2741">Livno</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="1228">Tomislavgrad</option> <select
</select> name=""
</span> id="sarajevo"
</div> class="drop-select"
onchange="stavi_grad()"
>
<div style="height:40px;"> <option value="0">Mjesto</option>
<option value="3817">Hadžići</option>
<span class="drop-container" style="width: 168px;"> <option value="3879">Ilidža</option>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <option value="3892">Ilijaš</option>
<select name="" id="grad11" class="drop-select" onchange="stavi_grad()"> <option value="3812">Sarajevo - Centar</option>
<option value="0">Mjesto</option> <option value="3969">Sarajevo-Novi Grad</option>
<option value="645">distriktbrcko</option> <option value="5896">Sarajevo-Novo Sarajevo</option>
</select> <option value="4048">Sarajevo-Stari Grad</option>
</span> <option value="4063">Trnovo</option>
</div> <option value="4126">Vogošća</option>
</select>
</span>
<div style="height:40px;"> </div>
<span class="drop-container" style="width: 168px;"> <div style="height:40px;">
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <span class="drop-container" style="width: 168px;">
<select name="" id="banjalučka" class="drop-select" onchange="stavi_grad()"> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="0">Mjesto</option> <select
<option value="21">Banja Luka</option> name=""
<option value="305">Gradiška</option> id="livanjski"
<option value="4662">Istočni Drvar</option> class="drop-select"
<option value="1965">Jezero</option> onchange="stavi_grad()"
<option value="4147">Kneževo</option> >
<option value="6142">Kostajnica</option> <option value="0">Mjesto</option>
<option value="2574">Kotor Varoš</option> <option value="560">Bosansko Grahovo</option>
<option value="244">Kozarska Dubica</option> <option value="4640">Drvar</option>
<option value="382">Krupa na uni</option> <option value="1533">Glamoč</option>
<option value="2654">Kupres </option> <option value="2635">Kupres</option>
<option value="2671">Laktaši</option> <option value="2741">Livno</option>
<option value="3073">Mrkonjić Grad</option> <option value="1228">Tomislavgrad</option>
<option value="444">Novi Grad</option> </select>
<option value="3737">Oštra Luka</option> </span>
<option value="515">Petrovac</option> </div>
<option value="3287">Prijedor</option>
<option value="3358">Prnjavor</option> <div style="height:40px;">
<option value="2365">Ribnik</option> <span class="drop-container" style="width: 168px;">
<option value="4271">Srbac</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="979">Čelinac</option> <select name="" id="grad11" class="drop-select" onchange="stavi_grad()">
<option value="4509">Šipovo</option> <option value="0">Mjesto</option>
</select> <option value="645">distriktbrcko</option>
</span> </select>
</div> </span>
</div>
<div style="height:40px;"> <div style="height:40px;">
<span class="drop-container" style="width: 168px;">
<span class="drop-container" style="width: 168px;"> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> <select
<select name="" id="dobojskobijeljinska" class="drop-select" onchange="stavi_grad()"> name=""
<option value="0">Mjesto</option> id="banjalučka"
<option value="123">Bijeljina</option> class="drop-select"
<option value="421">Bosanski Brod</option> onchange="stavi_grad()"
<option value="1030">Derventa</option> >
<option value="1088">Doboj</option> <option value="0">Mjesto</option>
<option value="3254">Donji Žabar</option> <option value="21">Banja Luka</option>
<option value="2800">Lopare</option> <option value="305">Gradiška</option>
<option value="6029">Lukavac</option> <option value="4662">Istočni Drvar</option>
<option value="2996">Modriča</option> <option value="1965">Jezero</option>
<option value="1856">Pelagićevo</option> <option value="4147">Kneževo</option>
<option value="1827">Petrovo</option> <option value="6142">Kostajnica</option>
<option value="1148">Stanari</option> <option value="2574">Kotor Varoš</option>
<option value="4549">Teslić</option> <option value="244">Kozarska Dubica</option>
<option value="4636">Tešanj</option> <option value="382">Krupa na uni</option>
<option value="4692">Travnik</option> <option value="2654">Kupres </option>
<option value="4966">Tuzla</option> <option value="2671">Laktaši</option>
<option value="5009">Ugljevik</option> <option value="3073">Mrkonjić Grad</option>
<option value="3197">Vukosavlje</option> <option value="444">Novi Grad</option>
<option value="539">Šamac</option> <option value="3737">Oštra Luka</option>
</select> <option value="515">Petrovac</option>
</span> <option value="3287">Prijedor</option>
</div> <option value="3358">Prnjavor</option>
<option value="2365">Ribnik</option>
<option value="4271">Srbac</option>
<div style="height:40px;"> <option value="979">Čelinac</option>
<option value="4509">Šipovo</option>
<span class="drop-container" style="width: 168px;"> </select>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span> </span>
<select name="" id="sarajevskozvornicka" class="drop-select" onchange="stavi_grad()"> </div>
<option value="0">Mjesto</option>
<option value="595">Bratunac</option> <div style="height:40px;">
<option value="1904">Han Pijesak</option> <span class="drop-container" style="width: 168px;">
<option value="3947">Ilijaš</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="4049">Istočni Stari Grad</option> <select
<option value="3880">Kasindo</option> name=""
<option value="2325">Kladanj</option> id="dobojskobijeljinska"
<option value="3971">Lukavica</option> class="drop-select"
<option value="6143">Milići</option> onchange="stavi_grad()"
<option value="3221">Olovo</option> >
<option value="2128">Osmaci</option> <option value="0">Mjesto</option>
<option value="3978">Pale</option> <option value="123">Bijeljina</option>
<option value="3529">Rogatica</option> <option value="421">Bosanski Brod</option>
<option value="3648">Rudo</option> <option value="1030">Derventa</option>
<option value="6069">Sarajevo-Novi Grad</option> <option value="1088">Doboj</option>
<option value="4183">Sokolac</option> <option value="3254">Donji Žabar</option>
<option value="4310">Srebrenica</option> <option value="2800">Lopare</option>
<option value="4067">Trnovo</option> <option value="6029">Lukavac</option>
<option value="1593">Ustiprača</option> <option value="2996">Modriča</option>
<option value="5259">Višegrad</option> <option value="1856">Pelagićevo</option>
<option value="5456">Vlasenica</option> <option value="1827">Petrovo</option>
<option value="5684">Zvornik</option> <option value="1148">Stanari</option>
<option value="4475">Šekovi</option> <option value="4549">Tesl</option>
<option value="1906">Žepa</option> <option value="4636">Tešanj</option>
</select> <option value="4692">Travnik</option>
</span> <option value="4966">Tuzla</option>
</div> <option value="5009">Ugljevik</option>
<option value="3197">Vukosavlje</option>
<option value="539">Šamac</option>
<div style="height:40px;"> </select>
</span>
<span class="drop-container" style="width: 168px;"> </div>
<span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<select name="" id="trebinjskofocanska" class="drop-select" onchange="stavi_grad()"> <div style="height:40px;">
<option value="0">Mjesto</option> <span class="drop-container" style="width: 168px;">
<option value="4441">Berkovići</option> <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
<option value="183">Bileća</option> <select
<option value="1287">Foča</option> name=""
<option value="1462">Gacko</option> id="sarajevskozvornicka"
<option value="3038">Istočni Mostar</option> class="drop-select"
<option value="2164">Kalinovik</option> onchange="stavi_grad()"
<option value="2884">Ljubinje</option> >
<option value="3138">Nevesinje</option> <option value="0">Mjesto</option>
<option value="4766">Trebinje</option> <option value="595">Bratunac</option>
<option value="911">Čajniče</option> <option value="1904">Han Pijesak</option>
</select> <option value="3947">Ilijaš</option>
</span> <option value="4049">Istočni Stari Grad</option>
</div> <option value="3880">Kasindo</option>
<option value="2325">Kladanj</option>
<script> <option value="3971">Lukavica</option>
<option value="6143">Milići</option>
const stavi_grad = () => {}; <option value="3221">Olovo</option>
const gradovi = [ <option value="2128">Osmaci</option>
{"ime":" Sarajevo","id":"sarajevo"}, <option value="3978">Pale</option>
{"ime":" Unsko-sanski","id":"unskosanski"}, <option value="3529">Rogatica</option>
{"ime":" Posavski","id":"posavski"}, <option value="3648">Rudo</option>
{"ime":" Tuzlanski","id":"tuzlanski"}, <option value="6069">Sarajevo-Novi Grad</option>
{"ime":" Zeničko-dobojski","id":"zenickodobojski"}, <option value="4183">Sokolac</option>
{"ime":" Bosansko-podrinjski","id":"bosanskopodrinjski"}, <option value="4310">Srebrenica</option>
{"ime":" Srednjobosanski","id":"srednjobosanski"}, <option value="4067">Trnovo</option>
{"ime":" Hercegovačko-neretvanski","id":"hercegovackoneretvanski"}, <option value="1593">Ustiprača</option>
{"ime":" Zapadno-hercegovački","id":"zapadnohercegovacki"}, <option value="5259">Višegrad</option>
{"ime":" Livanjski","id":"livanjski"}, <option value="5456">Vlasenica</option>
{"ime":" Banjalučka","id":"banjalučka"}, <option value="5684">Zvornik</option>
{"ime":" Dobojsko-Bijeljinska","id":"dobojskobijeljinska"}, <option value="4475">Šekovići</option>
{"ime":" Sarajevsko-Zvornička","id":"sarajevskozvornicka"}, <option value="1906">Žepa</option>
{"ime":" Trebinjsko-Fočanska","id":"trebinjskofocanska"}, </select>
{"ime":"Distrikt Brčko","id":"distriktbrcko"}, </span>
]; </div>
for (let grad of gradovi) { <div style="height:40px;">
const mjesta = []; <span class="drop-container" style="width: 168px;">
$('#' + grad.id).children().each( function() { <span class="drop-label">Mjesto</span><span class="drop-arrow"></span>
const mjesto = $(this).text(); <select
mjesta.push( { name=""
ime: mjesto, id="trebinjskofocanska"
id: mjesto.toLowerCase().replace(/[^a-z]/g,''), class="drop-select"
olxid: $(this).val() onchange="stavi_grad()"
}); >
}); <option value="0">Mjesto</option>
grad.mjesta = mjesta; <option value="4441">Berkovići</option>
} <option value="183">Bileća</option>
<option value="1287">Foča</option>
console.log(JSON.stringify(gradovi)); <option value="1462">Gacko</option>
<option value="3038">Istočni Mostar</option>
</script> <option value="2164">Kalinovik</option>
<option value="2884">Ljubinje</option>
</body> <option value="3138">Nevesinje</option>
</html> <option value="4766">Trebinje</option>
<option value="911">Čajniče</option>
</select>
</span>
</div>
<script>
const stavi_grad = () => {};
const gradovi = [
{ ime: " Sarajevo", id: "sarajevo" },
{ ime: " Unsko-sanski", id: "unskosanski" },
{ ime: " Posavski", id: "posavski" },
{ ime: " Tuzlanski", id: "tuzlanski" },
{ ime: " Zeničko-dobojski", id: "zenickodobojski" },
{ ime: " Bosansko-podrinjski", id: "bosanskopodrinjski" },
{ ime: " Srednjobosanski", id: "srednjobosanski" },
{ ime: " Hercegovačko-neretvanski", id: "hercegovackoneretvanski" },
{ ime: " Zapadno-hercegovački", id: "zapadnohercegovacki" },
{ ime: " Livanjski", id: "livanjski" },
{ ime: " Banjalučka", id: "banjalučka" },
{ ime: " Dobojsko-Bijeljinska", id: "dobojskobijeljinska" },
{ ime: " Sarajevsko-Zvornička", id: "sarajevskozvornicka" },
{ ime: " Trebinjsko-Fočanska", id: "trebinjskofocanska" },
{ ime: "Distrikt Brčko", id: "distriktbrcko" }
];
for (let grad of gradovi) {
const mjesta = [];
$("#" + grad.id)
.children()
.each(function() {
const mjesto = $(this).text();
mjesta.push({
ime: mjesto,
id: mjesto.toLowerCase().replace(/[^a-z]/g, ""),
olxid: $(this).val()
});
});
grad.mjesta = mjesta;
}
</script>
</body>
</html>