Compare commits

...

76 Commits

Author SHA1 Message Date
Nedim Uka
1776755d47 experiment 2019-07-04 10:57:56 +02:00
Nedim Uka
6eba5c2a97 gardenSize nan 2019-06-24 11:49:13 +02:00
Nedim Uka
2f474619ca Compare crawler results with db, and only save new if necessary 2019-06-21 16:48:19 +02:00
Nedim Uka
80ff9bcb6b saving additional fields, improved async functions with promises 2019-06-21 15:14:43 +02:00
Nedim Uka
3c59292f23 refactoring 2019-06-20 21:27:51 +02:00
Nedim Uka
1bcc5e8e5d Preparing to save results to db 2019-06-20 14:51:14 +02:00
Nedim Uka
c8ee848f0e Improved results filtering by lat lng 2019-06-20 10:57:37 +02:00
Nedim Uka
0f630e9ea4 Olix crawling, filter crawling result by lat, lng 2019-06-19 17:12:22 +02:00
Nedim Uka
9a8a27d1d9 Scheduler 2019-06-18 15:05:40 +02:00
Nedim Uka
b17b6862ba Added migrations, expanded maketalert table 2019-06-18 14:01:09 +02:00
Nedim Uka
6aaaea1612 working on crawler 2019-06-13 15:49:31 +02:00
Nedim Uka
fdd0124924 Added crawler service 2019-06-13 13:31:35 +02:00
Nedim Uka
c15f45e8f4 Fixed map not loding bug 2019-06-12 15:20:58 +02:00
Nedim Uka
371eac900e Merge branch 'range-slider' into 'master'
Range slider

See merge request saburly/marketalarm/web!16
2019-06-12 11:37:15 +00:00
Nedim Uka
5d6e7f3938 fixed slider css overlaping 2019-06-12 13:36:49 +02:00
Nedim Uka
efda7fdccd Added nouiRange slider 2019-06-12 13:32:28 +02:00
Nedim Uka
8bb0908c45 Slider thumb fix 2019-06-11 16:22:17 +02:00
Bilal Catic
5c75d690b0 Merge branch 'slider-bug' into 'master'
Sliders bug

See merge request saburly/marketalarm/web!15
2019-06-11 11:21:33 +00:00
Nedim Uka
f0e8a72756 Sliders now returning to correct range if they go beyond allowed value 2019-06-11 11:44:59 +02:00
Bilal Catic
62bf3380cd Merge branch 'confirmation-email' into 'master'
Geocoding restricttions, added values for range finders, added confirmation email, and .env file

See merge request saburly/marketalarm/web!14
2019-06-11 08:42:06 +00:00
Nedim Uka
caa1871939 deleted env file 2019-06-11 10:34:48 +02:00
Nedim Uka
506ac67956 Fixed garden size email issues 2019-06-11 10:26:48 +02:00
Nedim Uka
8f9e3ae46a Geocoding restricttions, added values for range finders, added confirmation email, and .env file 2019-06-10 17:29:31 +02:00
Bilal Catic
d6e999fcf1 Merge branch 'double-email' into 'master'
Double email

See merge request saburly/marketalarm/web!13
2019-05-30 12:08:25 +00:00
Nedim Uka
08a94ca4f8 Fixed google maps bug, changed size, gardenSize, and price colum names, fixed bug with query review not showing default values 2019-05-30 10:43:47 +02:00
Nedim Uka
a0f2b044b2 Added validation to email confirmation 2019-05-29 17:10:41 +02:00
Nedim Uka
7db74acad7 Set range fileds to be integer instead of strings 2019-05-29 17:10:41 +02:00
Bilal Catic
56865b4670 Merge branch 'realestate-size-slider' into 'master'
Real Estate Slider

See merge request saburly/marketalarm/web!12
2019-05-29 10:18:17 +00:00
Nedim Uka
1a8ac3fba4 Added range slider to gardensize and price 2019-05-29 11:03:01 +02:00
Nedim Uka
de3c76315e Real Estate Slider 2019-05-28 16:46:38 +02:00
Bilal Catic
e969a8dc8b Merge branch 'edit-bug-fix' into 'master'
Fixed bug related to map region edit

See merge request saburly/marketalarm/web!11
2019-05-27 15:52:11 +00:00
Nedim Uka
f4baec23cf Fixed bug related to map region edit 2019-05-27 15:17:41 +02:00
Bilal Catic
5bf95e0594 Merge branch 'google-maps' into 'master'
Added google maps step

See merge request saburly/marketalarm/web!10
2019-05-27 08:35:04 +00:00
Nedim Uka
6fbacb326f Fixed bounding box bug, and removed unecesary params 2019-05-27 09:18:54 +02:00
Nedim Uka
be416ffc0c Added google maps step 2019-05-24 16:16:47 +02:00
Nedim Uka
a3d9a82fee Merge branch 'fix-minor-bugs' into 'master'
Fix minor bugs

See merge request saburly/marketalarm/web!9
2019-05-24 13:54:40 +00:00
Nedim Uka
6772f8a953 Merge branch 'enable-return-to-query-review-directly' into 'master'
Enable return to query review directly

See merge request saburly/marketalarm/web!8
2019-05-24 13:53:27 +00:00
Nedim Uka
89a3c9e355 Merge branch 'change-migrations-use-string-instead-of-enums' into 'master'
Change migrations - use string instead of enum

See merge request saburly/marketalarm/web!7
2019-05-24 13:50:55 +00:00
Bilal Catic
dd38602c5a add simple email validation 2019-05-22 16:57:08 +02:00
Bilal Catic
a3f76d20fe fix URL on send icon 2019-05-22 15:58:42 +02:00
Bilal Catic
fc1275566e handle undefined realEstateType 2019-05-22 13:19:27 +02:00
Bilal Catic
c64ee42914 Skip and prevent saving garden size if not needed 2019-05-22 11:36:01 +02:00
Bilal Catic
aa3c965d5c skip to query review directly when editing data 2019-05-21 15:32:47 +02:00
Bilal Catic
126da48852 modify realEstateRequest model to use String instead of enum 2019-05-21 15:26:53 +02:00
Bilal Catic
58ae430564 modify migrations - use string instead of enum 2019-05-21 15:26:53 +02:00
Senad Uka
315a29749c Update database configuration 2019-05-21 12:11:30 +02:00
Nedim Uka
02bee9cf2c Merge branch 'add-steps-to-wizard' into 'master'
Add steps to wizard

See merge request saburly/marketalarm/web!6
2019-05-21 09:39:43 +00:00
Bilal Catic
1c2847509a add final page 2019-05-19 19:45:19 +02:00
Bilal Catic
87dc742e41 add query submit page 2019-05-19 13:34:44 +02:00
Bilal Catic
70ddc1f734 add query review page 2019-05-19 12:29:55 +02:00
Bilal Catic
2c415bbd79 use enums from enum file 2019-05-19 10:03:52 +02:00
Bilal Catic
b07eb5bbeb fix available value input for size 2019-05-19 10:03:36 +02:00
Bilal Catic
53585d3ae1 use enums from enum file 2019-05-19 02:14:20 +02:00
Bilal Catic
c652a306db add price screen 2019-05-17 11:32:41 +02:00
Bilal Catic
b15295bfe6 add garden size screen 2019-05-17 11:12:24 +02:00
Bilal Catic
7ad1117cae add size screen 2019-05-17 11:06:32 +02:00
Nedim Uka
393f6731e6 Merge branch 'refactor' into 'master'
Refactor

See merge request saburly/marketalarm/web!5
2019-05-17 07:46:44 +00:00
Bilal Catic
93faa7c9e3 update readme 2019-05-17 09:14:16 +02:00
Bilal Catic
93f5d8071e add npm commands for docker and setup 2019-05-17 09:10:03 +02:00
Bilal Catic
7192c28c07 update readme 2019-05-17 08:55:36 +02:00
Bilal Catic
76f9457d4f add nodemon and migrate scripts 2019-05-17 08:49:01 +02:00
Bilal Catic
68172951ed change region and municipality property names to english 2019-05-17 00:52:43 +02:00
Bilal Catic
dbf40b199e Merge branch 'refactor' of https://gitlab.com/saburly/marketalarm/web into refactor 2019-05-17 00:34:13 +02:00
Bilal Catic
4309bc709d change column name from 'city' to 'region' 2019-05-17 00:33:10 +02:00
Bilal Catic
4323017d02 fix Readme 2019-05-16 21:34:32 +00:00
Bilal Catic
42505a7089 change column name from 'place' to 'municipality' 2019-05-16 23:32:18 +02:00
Bilal Catic
1542310a81 clean code 2019-05-16 19:58:48 +02:00
Bilal Catic
c505062770 change controller file name to plural 2019-05-16 19:42:15 +02:00
Bilal Catic
ab681e5eeb change file names to CamelCase 2019-05-16 19:40:26 +02:00
Bilal Catic
616eddbb19 improve Readme 2019-05-16 17:12:17 +02:00
Bilal Catic
e5eb6b99a2 Merge branch 'rename' into 'master'
Refactoring

See merge request saburly/marketalarm/web!4
2019-05-16 12:49:52 +00:00
Nedim Uka
27fa721627 Renaming to english 2019-05-16 13:00:08 +02:00
Senad Uka
9fdfce49ed Merge branch 'dockerize-database' into 'master'
Dockerize database

See merge request saburly/marketalarm/web!2
2019-04-30 12:07:09 +00:00
Senad Uka
864b917b4f Make place selection possible 2019-04-30 06:48:41 +02:00
Senad Uka
a2f6f033bf Places almost finished 2019-04-28 11:13:46 +02:00
Senad Uka
64f2cb82a8 Scrape kantoni into html file 2019-04-28 09:02:46 +02:00
70 changed files with 5596 additions and 237 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules/ node_modules/
.env

View File

@@ -1,48 +1,11 @@
# FROM postgres:11.3
# example Dockerfile for https://docs.docker.com/engine/examples/postgresql_service/
#
FROM ubuntu:16.04 ENV POSTGIS_MAJOR 2.4
# Add the PostgreSQL PGP key to verify their Debian packages. RUN apt-get update \
# It should be the same key as https://www.postgresql.org/media/keys/ACCC4CF8.asc && apt-get --assume-yes install software-properties-common postgis\
RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 && rm -rf /var/lib/apt/lists/
# Add PostgreSQL's repository. It contains the most recent stable release RUN mkdir -p /docker-entrypoint-initdb.d
# of PostgreSQL, ``9.3``.
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" > /etc/apt/sources.list.d/pgdg.list
# Install ``python-software-properties``, ``software-properties-common`` and PostgreSQL 9.3 CMD ["postgres"]
# There are some warnings (in red) that show up during the build. You can hide
# them by prefixing each apt-get statement with DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y python-software-properties software-properties-common postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3
# Note: The official Debian and Ubuntu images automatically ``apt-get clean``
# after each ``apt-get``
# Run the rest of the commands as the ``postgres`` user created by the ``postgres-9.3`` package when it was ``apt-get installed``
USER postgres
# Create a PostgreSQL role named ``docker`` with ``docker`` as the password and
# then create a database `docker` owned by the ``docker`` role.
# Note: here we use ``&&\`` to run commands one after the other - the ``\``
# allows the RUN command to span multiple lines.
RUN /etc/init.d/postgresql start &&\
psql --command "CREATE USER docker WITH SUPERUSER PASSWORD 'docker';" &&\
createdb -O docker marketalerts
# Adjust PostgreSQL configuration so that remote connections to the
# database are possible.
RUN echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.3/main/pg_hba.conf
# And add ``listen_addresses`` to ``/etc/postgresql/9.3/main/postgresql.conf``
RUN echo "listen_addresses='*'" >> /etc/postgresql/9.3/main/postgresql.conf
# Expose the PostgreSQL port
EXPOSE 5432
# Add VOLUMEs to allow backup of config, logs and databases
VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"]
# Set the default command to run when starting the container
CMD ["/usr/lib/postgresql/9.3/bin/postgres", "-D", "/var/lib/postgresql/9.3/main", "-c", "config_file=/etc/postgresql/9.3/main/postgresql.conf"]

View File

@@ -1,10 +1,36 @@
# web # MarketAlert
The purpose of this project is to build a web application that enables subscribing to notifications when new products are published on various ad based marketplaces. The MVP will be only based on OLX.ba The purpose of this project is to build a web application that enables subscribing to notifications when new products are published on various ad based marketplaces. The MVP will be only based on OLX.ba
Run postgres image with: ## Setup
docker run --name pg_test -d -p 5432:5432 marketalerts
Run with: ### Setup with npm commands
$ npm start
1. Install packages
`npm install`
2. Run setup script
`npm run setup`
this will create and run postgres image and then execute migrations
3. Run app
`npm start` to run app without restart on changes or
`npm run start-mon` to run app with automatic restart on code change
### Manual setup
1. Create postgres docker image
`docker build -t marketalerts .`
2. Run postgres image with
`docker run --name pg_marketalerts -d -p 5432:5432 marketalerts`
3. Install packages
`npm install`
4. Run migrations from `app` folder
`npm run migrate` or `npx sequelize db:migrate`
5. Run app
`npm start` or `npm run start-mon` to run app with automatic restart on code change

View File

@@ -7,9 +7,9 @@
"dialect": "postgres" "dialect": "postgres"
}, },
"test": { "test": {
"use_env_variable": "JAWSDB_URL" "use_env_variable": "DATABASE_URL"
}, },
"production": { "production": {
"use_env_variable": "JAWSDB_URL" "use_env_variable": "DATABASE_URL"
} }
} }

View File

@@ -1,7 +0,0 @@
const getDobrodosli = (req,res) => {
res.render('dobrodosli', { nextStep: '/vrstanekretnine' } );
}
module.exports = {
getDobrodosli
};

View File

@@ -0,0 +1,43 @@
const { currentRERequest } = require('../helpers/url');
const { getRealEstateTypeEnum } = require('../helpers/enums');
const getGardenSize = (req,res) => {
const unit = " m2"
const rangeFrom = {
min : 10,
max : 3000,
value : 0,
step : 10
}
const rangeTo = {
min : 10,
max : 3000,
value : 100,
step : 10
}
res.render('gardenSize', { rangeFrom, rangeTo, unit });
};
const postGardenSize = async (req, res) => {
const request = await currentRERequest(req);
const nextStepPage = req.query.nextStep || 'cijena';
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
const realEstateType = getRealEstateTypeEnum(request.realEstateType);
if (realEstateType && realEstateType.hasGardenSize) {
request.gardenSizeMin = req.body.from;
request.gardenSizeMax = req.body.to;
await request.save();
}
res.redirect(nextStepUrl);
};
module.exports = {
getGardenSize,
postGardenSize
};

View File

@@ -0,0 +1,7 @@
const getGoAgain = async (req,res) => {
res.render('goAgain');
};
module.exports = {
getGoAgain
};

View File

@@ -1,41 +0,0 @@
const db = require('../models/index');
const { currentRERequest } = require('../helpers/url');
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"},
];
const getGrad = (req,res) => {
const nextStep = req.query.nextStep || '/';
res.render('grad', {
nextStep,
gradovi
});
}
const postGrad = async (req, res) => {
let request = await currentRERequest(req);
request.city = req.body.grad;
await request.save();
res.send("Result is " + JSON.stringify(request));
}
module.exports = {
getGrad,
postGrad
};

View File

@@ -0,0 +1,26 @@
const { currentRERequest } = require('../helpers/url');
const { getMunicipalitiesForRegion, getMunicipalityName } = require('../helpers/codes');
const getMunicipality = async (req, res) => {
let request = await currentRERequest(req);
const municipalities = getMunicipalitiesForRegion(request.region);
res.render('municipality', { municipalities });
};
const postMunicipality = async (req, res) => {
const request = await currentRERequest(req);
const nextStepParam = req.query.nextStep ? "?nextStep=" + req.query.nextStep : "";
const nextStepUrl = `/${'naselje'}/${request.uniqueId}/${getMunicipalityName(request.region, req.body.municipality)}${nextStepParam}`;
request.municipality = req.body.municipality;
await request.save();
res.redirect(nextStepUrl);
};
module.exports = {
getMunicipality,
postMunicipality
};

View File

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

40
app/controllers/prices.js Normal file
View File

@@ -0,0 +1,40 @@
const { currentRERequest } = require('../helpers/url');
const getPrice = (req,res) => {
const unit = " KM"
const rangeFrom = {
min : 1000,
max : 250000,
value : 0,
step : 1000
}
const rangeTo = {
min : 1000,
max : 250000,
value : 50000,
step : 1000
}
res.render('price', {rangeFrom, rangeTo, unit });
};
const postPrice = async (req, res) => {
const request = await currentRERequest(req);
const nextStepPage = req.query.nextStep || 'pregled';
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
request.priceMin = req.body.from;
request.priceMax = req.body.to;
await request.save();
res.redirect(nextStepUrl);
};
module.exports = {
getPrice,
postPrice
};

View File

@@ -0,0 +1,85 @@
const { currentRERequest } = require('../helpers/url');
const { getRegionName, getMunicipalityName } = require('../helpers/codes');
const { realEstateTypes, sizes, gardenSizes, prices, getEnumTypeTitle, getRealEstateTypeEnum } = require('../helpers/enums');
const getQueryReview = async (req,res) => {
const request = await currentRERequest(req);
const nextStep = req.query.nextStep;
if (!request || !request.dataValues) {
return null;
}
const {
realEstateType,
region,
municipality,
sizeMin,
sizeMax,
gardenSizeMin,
gardenSizeMax,
priceMin,
priceMax } = request.dataValues;
const realEstateTypeObject = getRealEstateTypeEnum(realEstateType);
const enableGardenSizeEdit = realEstateTypeObject ? realEstateTypeObject.hasGardenSize : false;
const realEstateTypeTitle = realEstateType ? getEnumTypeTitle(realEstateTypes, realEstateType) : null;
const regionName = region ? getRegionName(region) : null;
const municipalityName = (region && municipality) ? getMunicipalityName(region, municipality) : null;
const sizeTitle = sizeMin ? sizeMin + "-" + sizeMax + " m2" : null;
const gardenSizeTitle = gardenSizeMin ? gardenSizeMin + "-" + gardenSizeMax + " m2" : null;
const priceTitle = priceMin ? priceMin + "-" + priceMax + " KM" : null;
const uniqueId = request.dataValues.uniqueId ? request.dataValues.uniqueId : '';
const queryData = [
{
id: 'realEstateType',
title: realEstateTypeTitle,
url: `/vrstanekretnine/${uniqueId}?nextStep=pregled`,
},
{
id: 'region',
title: regionName,
url: `/grad/${uniqueId}?nextStep=mjesto`,
},
{
id: 'municipality',
title: municipalityName,
url: `/mjesto/${uniqueId}?nextStep=pregled`,
},
{
id: 'size',
title: sizeTitle,
url: `/povrsina/${uniqueId}?nextStep=pregled`,
},
{
id: 'gardenSize',
title: gardenSizeTitle,
url: enableGardenSizeEdit ? `/okucnica/${uniqueId}?nextStep=pregled` : '',
},
{
id: 'price',
title: priceTitle,
url: `/cijena/${uniqueId}?nextStep=pregled`
}
];
res.render('queryReview', {
nextStep,
queryData,
});
};
const postQueryReview = async (req, res) => {
const request = await currentRERequest(req);
const nextStep = req.query.nextStep || `/posalji/${request.uniqueId}`;
res.redirect(nextStep);
};
module.exports = {
getQueryReview,
postQueryReview
};

View File

@@ -0,0 +1,51 @@
const { currentRERequest } = require('../helpers/url');
const { isValidEmail } = require('../helpers/email');
const { sendTemplatedEmail} = require('../helpers/awsEmail');
const getQuerySubmit = async (req, res) => {
const nextStep = req.query.nextStep;
const error = req.query.error;
res.render('querySubmit', {
nextStep,
error
});
};
const postQuerySubmit = async (req, res) => {
const request = await currentRERequest(req);
const nextStep = req.query.nextStep || '/ponovo';
const emailInput = req.body.email;
const emailConfirmInput = req.body.confirm;
let error = "Greška ! Unesite validan email";
if (!isValidEmail(emailInput) || !isValidEmail(emailConfirmInput)) {
error = "Greška ! Unesite validan email";
res.render('querySubmit', {
error
});
return;
}
if (emailInput !== emailConfirmInput) {
error = "Greška ! Unešeni emailovi nisu isti";
res.render('querySubmit', {
error
});
return;
}
request.email = req.body.email;
request.subscribed = true;
await request.save();
sendTemplatedEmail(req.body.email, request);
res.redirect(nextStep);
};
module.exports = {
getQuerySubmit,
postQuerySubmit
};

View File

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

View File

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

42
app/controllers/sizes.js Normal file
View File

@@ -0,0 +1,42 @@
const { currentRERequest } = require('../helpers/url');
const { sizes, getRealEstateTypeEnum } = require('../helpers/enums');
const getSize = (req,res) => {
const unit = " m2"
const rangeFrom = {
min : 10,
max : 250,
value : 0,
step : 10
}
const rangeTo = {
min : 10,
max : 250,
value : 50,
step : 10
}
res.render('size', { rangeFrom, rangeTo, unit });
};
const postSize = async (req, res) => {
const request = await currentRERequest(req);
const realEstateType = getRealEstateTypeEnum(request.realEstateType);
const nextStep = realEstateType && realEstateType.hasGardenSize ? 'okucnica' : 'cijena';
const nextStepPage = req.query.nextStep || nextStep;
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
request.sizeMin = req.body.from;
request.sizeMax = req.body.to;
await request.save();
res.redirect(nextStepUrl);
};
module.exports = {
getSize,
postSize
};

View File

@@ -0,0 +1,15 @@
const { currentRERequest } = require('../helpers/url');
const getUnsubscribe = async (req, res) => {
const request = await currentRERequest(req);
request.subscribed = false;
await request.save();
res.render('unsubscribe', { nextStep: '/vrstanekretnine' });
};
module.exports = {
getUnsubscribe
};

View File

@@ -1,35 +0,0 @@
const db = require('../models/index');
const vrsteNekretnina = [
{ ime: "Kuća", id: "kuca" },
{ ime: "Stan", id: "stan" },
{ ime: "Vikendica", id: "vikendica" }
];
const getVrstaNekretnine = (req,res) => {
const nextStep = req.query.nextStep;
res.render('vrsta_nekretnine', {
nextStep,
vrste: vrsteNekretnina
});
}
const postVrstaNekretnine = (req, res) => {
let nextStep = req.query.nextStep;
db.RealEstateRequest.create({
realEstateType: req.body.vrsta
}).then( (result) => {
nextStep = nextStep || `/grad/${result.uniqueId}`;
res.redirect(nextStep);
}).catch( (e) => {
res.send(e);
});
}
module.exports = {
getVrstaNekretnine,
postVrstaNekretnine
};

View File

@@ -0,0 +1,7 @@
const getWelcome = (req,res) => {
res.render('welcome', { nextStep: '/vrstanekretnine' } );
};
module.exports = {
getWelcome
};

96
app/helpers/awsEmail.js Normal file
View File

@@ -0,0 +1,96 @@
const dotenv = require('dotenv');
dotenv.config();
const { getRealEstateTypeEnum } = require('./enums');
const { getRegionName, getMunicipalityName } = require('./codes');
const AWS = require('aws-sdk');
AWS.config.update({
region: process.env.AMAZON_REGION,
credentials:
{
accessKeyId: process.env.AMAZON_ACCES_KEY_ID,
secretAccessKey: process.env.AMAZON_SECRET_ACCESS_KEY
}
});
const sendTemplatedEmail = async (email, request) => {
const params = {
Destination: { /* required */
CcAddresses: [
],
ToAddresses: [
email
]
},
Message: { /* required */
Body: { /* required */
Html: {
Charset: "UTF-8",
Data: getEmailHTML(request)
},
Text: {
Charset: "UTF-8",
Data: getEmaiTextVersion(request)
}
},
Subject: {
Charset: 'UTF-8',
Data: `Javimi Potvrda: ${request.realEstateType} ${getRegionName(request.region)}, ${getMunicipalityName(request.region, request.municipality)}`
}
},
Source: process.env.SOURCE_EMAIL, /* required */
ReplyToAddresses: [
process.env.SOURCE_EMAIL,
],
};
const sendEmailPromise = new AWS.SES({ apiVersion: '2010-12-01' }).sendEmail(params).promise();
await sendEmailPromise;
}
const getEmailHTML = (realestateRequest) => {
const realEstateType = getRealEstateTypeEnum(realestateRequest.realEstateType);
const gardenSize = realEstateType.hasGardenSize ? `<div><strong>Kvadratura okućnice: Od ${realestateRequest.gardenSizeMin} do ${realestateRequest.gardenSizeMax} m2 </strong></div>` : ``
return `<h1> Zdravo,
Naručio/la si da ti javimo ako se nekretnina pojavi u oglasima. </h1>
<h2> Ovo je tražena nekretnina: </h2>
<div>
<div> <strong>Tip nekretnine: ${realEstateType.title} </strong></div>
<div><strong>Područje: ${getRegionName(realestateRequest.region)} </strong></div>
<div><strong>Mjesto: ${getMunicipalityName(realestateRequest.region, realestateRequest.municipality)} </strong></div>
<div><strong>Kvadratura nekretnine: Od ${realestateRequest.sizeMin} do ${realestateRequest.sizeMax} m2 </strong></div>
${gardenSize}
<div><strong>Cijena: ${realestateRequest.priceMin} do ${realestateRequest.priceMax} KM </strong></div>
</div>
<div>
</div>
<div><strong> Ako želis prestati dobijati obavještenja za ovu pretragu klikni ${process.env.APP_URL}/odjava/${realestateRequest.uniqueId} </strong></div>
<div><strong>Ako želiš promijeniti uslove pretrage klikni ${process.env.APP_URL}/pregled/${realestateRequest.uniqueId} </strong></div>
<h4> Tvoj,
Javimi tim.
</h4>`
}
const getEmaiTextVersion = (realestateRequest) => {
const realEstateType = getRealEstateTypeEnum(realestateRequest.realEstateType);
const gardenSize = realEstateType.hasGardenSize ? "Kvadratura okućnice od " + realestateRequest.gardenSizeMin + " do " + realestateRequest.gardenSizeMax : ""
const text = "Zdravo, \n Naručio/la si da ti javimo ako se nekretnina pojavi u oglasima \n Ovo je tražena nekretnina: \n , Tip nekretnine: "
+ realestateRequest.realEstateType + "\n Područje" + getRegionName(realestateRequest.region) + "\n Mjesto " + getMunicipalityName(realestateRequest.region, realestateRequest.municipality)
+ "\n Kvadratura nekretnine Od " + realestateRequest.sizeMin + " do " + realestateRequest.sizeMaX +
+ gardenSize
"\n Cijena od " + realestateRequest.priceMin + " do " + 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;
}
module.exports = {
sendTemplatedEmail
};

928
app/helpers/codes.js Normal file
View File

@@ -0,0 +1,928 @@
const regions = [
{
"name": " Sarajevo",
"id": "sarajevo",
"olxid": "9",
"municipalities": [
{
"name": "Hadžići",
"id": "hadii",
"olxid": "3817"
},
{
"name": "Ilidža",
"id": "ilida",
"olxid": "3879"
},
{
"name": "Ilijaš",
"id": "ilija",
"olxid": "3892"
},
{
"name": "Sarajevo - Centar",
"id": "sarajevocentar",
"olxid": "3812"
},
{
"name": "Sarajevo-Novi Grad",
"id": "sarajevonovigrad",
"olxid": "3969"
},
{
"name": "Sarajevo-Novo Sarajevo",
"id": "sarajevonovosarajevo",
"olxid": "5896"
},
{
"name": "Sarajevo-Stari Grad",
"id": "sarajevostarigrad",
"olxid": "4048"
},
{
"name": "Trnovo",
"id": "trnovo",
"olxid": "4063"
},
{
"name": "Vogošća",
"id": "vogoa",
"olxid": "4126"
}
]
},
{
"name": " Unsko-sanski",
"id": "unskosanski",
"olxid": "9",
"municipalities": [
{
"name": "Bihać",
"id": "biha",
"olxid": "75"
},
{
"name": "Bosanska Krupa",
"id": "bosanskakrupa",
"olxid": "373"
},
{
"name": "Bosanski Petrovac",
"id": "bosanskipetrovac",
"olxid": "504"
},
{
"name": "Bužim",
"id": "buim",
"olxid": "374"
},
{
"name": "Cazin",
"id": "cazin",
"olxid": "857"
},
{
"name": "Ključ",
"id": "klju",
"olxid": "2362"
},
{
"name": "Sanski Most",
"id": "sanskimost",
"olxid": "3738"
},
{
"name": "Velika Kladuša",
"id": "velikakladua",
"olxid": "5122"
}
]
},
{
"name": " Posavski",
"id": "posavski",
"olxid": "15",
"municipalities": [
{
"name": "Domaljevac",
"id": "domaljevac",
"olxid": "6144"
},
{
"name": "Odžak",
"id": "odak",
"olxid": "424"
},
{
"name": "Orašje",
"id": "oraje",
"olxid": "3252"
},
{
"name": "Šamac",
"id": "amac",
"olxid": "540"
}
]
},
{
"name": " Tuzlanski",
"id": "tuzlanski",
"olxid": "15",
"municipalities": [
{
"name": "Banovići",
"id": "banovii",
"olxid": "2"
},
{
"name": "Doboj-Istok",
"id": "dobojistok",
"olxid": "1090"
},
{
"name": "Gradačac",
"id": "gradaac",
"olxid": "1854"
},
{
"name": "Gračanica",
"id": "graanica",
"olxid": "1826"
},
{
"name": "Kalesija",
"id": "kalesija",
"olxid": "2129"
},
{
"name": "Kladanj",
"id": "kladanj",
"olxid": "2319"
},
{
"name": "Lukavac",
"id": "lukavac",
"olxid": "2840"
},
{
"name": "Sapna",
"id": "sapna",
"olxid": "5699"
},
{
"name": "Srebrenik",
"id": "srebrenik",
"olxid": "4391"
},
{
"name": "Teočak",
"id": "teoak",
"olxid": "5010"
},
{
"name": "Tuzla",
"id": "tuzla",
"olxid": "4944"
},
{
"name": "Čelić",
"id": "eli",
"olxid": "2801"
},
{
"name": "Živinice",
"id": "ivinice",
"olxid": "5774"
}
]
},
{
"name": " Zeničko-dobojski",
"id": "zenickodobojski",
"olxid": "15",
"municipalities": [
{
"name": "Breza",
"id": "breza",
"olxid": "704"
},
{
"name": "Doboj-Jug",
"id": "dobojjug",
"olxid": "1122"
},
{
"name": "Kakanj",
"id": "kakanj",
"olxid": "2022"
},
{
"name": "Maglaj",
"id": "maglaj",
"olxid": "2941"
},
{
"name": "Olovo",
"id": "olovo",
"olxid": "1925"
},
{
"name": "Tešanj",
"id": "teanj",
"olxid": "4594"
},
{
"name": "Usora",
"id": "usora",
"olxid": "1087"
},
{
"name": "Vareš",
"id": "vare",
"olxid": "5037"
},
{
"name": "Visoko",
"id": "visoko",
"olxid": "5171"
},
{
"name": "Zavidovići",
"id": "zavidovii",
"olxid": "5548"
},
{
"name": "Zenica",
"id": "zenica",
"olxid": "4571"
},
{
"name": "Žepče",
"id": "epe",
"olxid": "2940"
}
]
},
{
"name": " Bosansko-podrinjski",
"id": "bosanskopodrinjski",
"olxid": "15",
"municipalities": [
{
"name": "Foča",
"id": "foa",
"olxid": "1289"
},
{
"name": "Goražde",
"id": "gorade",
"olxid": "1588"
},
{
"name": "Pale",
"id": "pale",
"olxid": "3546"
}
]
},
{
"name": " Srednjobosanski",
"id": "srednjobosanski",
"olxid": "6",
"municipalities": [
{
"name": "Bugojno",
"id": "bugojno",
"olxid": "732"
},
{
"name": "Busovača",
"id": "busovaa",
"olxid": "810"
},
{
"name": "Dobretići",
"id": "dobretii",
"olxid": "4151"
},
{
"name": "Donji Vakuf",
"id": "donjivakuf",
"olxid": "1160"
},
{
"name": "Fojnica",
"id": "fojnica",
"olxid": "1407"
},
{
"name": "Gornji Vakuf - Uskoplje",
"id": "gornjivakufuskoplje",
"olxid": "1775"
},
{
"name": "Jajce",
"id": "jajce",
"olxid": "1960"
},
{
"name": "Kiseljak",
"id": "kiseljak",
"olxid": "2237"
},
{
"name": "Kreševo",
"id": "kreevo",
"olxid": "2608"
},
{
"name": "Novi Travnik",
"id": "novitravnik",
"olxid": "3477"
},
{
"name": "Travnik",
"id": "travnik",
"olxid": "4678"
},
{
"name": "Vitez",
"id": "vitez",
"olxid": "5422"
}
]
},
{
"name": " Hercegovačko-neretvanski",
"id": "hercegovackoneretvanski",
"olxid": "7",
"municipalities": [
{
"name": "Grad Mostar",
"id": "gradmostar",
"olxid": "3017"
},
{
"name": "Jablanica",
"id": "jablanica",
"olxid": "1930"
},
{
"name": "Konjic",
"id": "konjic",
"olxid": "2169"
},
{
"name": "Neum",
"id": "neum",
"olxid": "3111"
},
{
"name": "Prozor",
"id": "prozor",
"olxid": "3421"
},
{
"name": "Ravno",
"id": "ravno",
"olxid": "4769"
},
{
"name": "Stolac",
"id": "stolac",
"olxid": "4439"
},
{
"name": "Čapljina",
"id": "apljina",
"olxid": "947"
},
{
"name": "Čitluk",
"id": "itluk",
"olxid": "1009"
}
]
},
{
"name": " Zapadno-hercegovački",
"id": "zapadnohercegovacki",
"olxid": "8",
"municipalities": [
{
"name": "Grude",
"id": "grude",
"olxid": "1892"
},
{
"name": "Ljubuški",
"id": "ljubuki",
"olxid": "2905"
},
{
"name": "Posušje",
"id": "posuje",
"olxid": "3268"
},
{
"name": "Široki Brijeg",
"id": "irokibrijeg",
"olxid": "2708"
}
]
},
{
"name": " Livanjski",
"id": "livanjski",
"olxid": "10",
"municipalities": [
{
"name": "Bosansko Grahovo",
"id": "bosanskograhovo",
"olxid": "560"
},
{
"name": "Drvar",
"id": "drvar",
"olxid": "4640"
},
{
"name": "Glamoč",
"id": "glamo",
"olxid": "1533"
},
{
"name": "Kupres",
"id": "kupres",
"olxid": "2635"
},
{
"name": "Livno",
"id": "livno",
"olxid": "2741"
},
{
"name": "Tomislavgrad",
"id": "tomislavgrad",
"olxid": "1228"
}
]
},
{
"name": " Banjalučka",
"id": "banjalučka",
"olxid": "14",
"municipalities": [
{
"name": "Banja Luka",
"id": "banjaluka",
"olxid": "21"
},
{
"name": "Gradiška",
"id": "gradika",
"olxid": "305"
},
{
"name": "Istočni Drvar",
"id": "istonidrvar",
"olxid": "4662"
},
{
"name": "Jezero",
"id": "jezero",
"olxid": "1965"
},
{
"name": "Kneževo",
"id": "kneevo",
"olxid": "4147"
},
{
"name": "Kostajnica",
"id": "kostajnica",
"olxid": "6142"
},
{
"name": "Kotor Varoš",
"id": "kotorvaro",
"olxid": "2574"
},
{
"name": "Kozarska Dubica",
"id": "kozarskadubica",
"olxid": "244"
},
{
"name": "Krupa na uni",
"id": "krupanauni",
"olxid": "382"
},
{
"name": "Kupres ",
"id": "kupres",
"olxid": "2654"
},
{
"name": "Laktaši",
"id": "laktai",
"olxid": "2671"
},
{
"name": "Mrkonjić Grad",
"id": "mrkonjigrad",
"olxid": "3073"
},
{
"name": "Novi Grad",
"id": "novigrad",
"olxid": "444"
},
{
"name": "Oštra Luka",
"id": "otraluka",
"olxid": "3737"
},
{
"name": "Petrovac",
"id": "petrovac",
"olxid": "515"
},
{
"name": "Prijedor",
"id": "prijedor",
"olxid": "3287"
},
{
"name": "Prnjavor",
"id": "prnjavor",
"olxid": "3358"
},
{
"name": "Ribnik",
"id": "ribnik",
"olxid": "2365"
},
{
"name": "Srbac",
"id": "srbac",
"olxid": "4271"
},
{
"name": "Čelinac",
"id": "elinac",
"olxid": "979"
},
{
"name": "Šipovo",
"id": "ipovo",
"olxid": "4509"
}
]
},
{
"name": " Dobojsko-Bijeljinska",
"id": "dobojskobijeljinska",
"olxid": "15",
"municipalities": [
{
"name": "Bijeljina",
"id": "bijeljina",
"olxid": "123"
},
{
"name": "Bosanski Brod",
"id": "bosanskibrod",
"olxid": "421"
},
{
"name": "Derventa",
"id": "derventa",
"olxid": "1030"
},
{
"name": "Doboj",
"id": "doboj",
"olxid": "1088"
},
{
"name": "Donji Žabar",
"id": "donjiabar",
"olxid": "3254"
},
{
"name": "Lopare",
"id": "lopare",
"olxid": "2800"
},
{
"name": "Lukavac",
"id": "lukavac",
"olxid": "6029"
},
{
"name": "Modriča",
"id": "modria",
"olxid": "2996"
},
{
"name": "Pelagićevo",
"id": "pelagievo",
"olxid": "1856"
},
{
"name": "Petrovo",
"id": "petrovo",
"olxid": "1827"
},
{
"name": "Stanari",
"id": "stanari",
"olxid": "1148"
},
{
"name": "Teslić",
"id": "tesli",
"olxid": "4549"
},
{
"name": "Tešanj",
"id": "teanj",
"olxid": "4636"
},
{
"name": "Travnik",
"id": "travnik",
"olxid": "4692"
},
{
"name": "Tuzla",
"id": "tuzla",
"olxid": "4966"
},
{
"name": "Ugljevik",
"id": "ugljevik",
"olxid": "5009"
},
{
"name": "Vukosavlje",
"id": "vukosavlje",
"olxid": "3197"
},
{
"name": "Šamac",
"id": "amac",
"olxid": "539"
}
]
},
{
"name": " Sarajevsko-Zvornička",
"id": "sarajevskozvornicka",
"olxid": "16",
"municipalities": [
{
"name": "Bratunac",
"id": "bratunac",
"olxid": "595"
},
{
"name": "Han Pijesak",
"id": "hanpijesak",
"olxid": "1904"
},
{
"name": "Ilijaš",
"id": "ilija",
"olxid": "3947"
},
{
"name": "Istočni Stari Grad",
"id": "istonistarigrad",
"olxid": "4049"
},
{
"name": "Kasindo",
"id": "kasindo",
"olxid": "3880"
},
{
"name": "Kladanj",
"id": "kladanj",
"olxid": "2325"
},
{
"name": "Lukavica",
"id": "lukavica",
"olxid": "3971"
},
{
"name": "Milići",
"id": "milii",
"olxid": "6143"
},
{
"name": "Olovo",
"id": "olovo",
"olxid": "3221"
},
{
"name": "Osmaci",
"id": "osmaci",
"olxid": "2128"
},
{
"name": "Pale",
"id": "pale",
"olxid": "3978"
},
{
"name": "Rogatica",
"id": "rogatica",
"olxid": "3529"
},
{
"name": "Rudo",
"id": "rudo",
"olxid": "3648"
},
{
"name": "Sarajevo-Novi Grad",
"id": "sarajevonovigrad",
"olxid": "6069"
},
{
"name": "Sokolac",
"id": "sokolac",
"olxid": "4183"
},
{
"name": "Srebrenica",
"id": "srebrenica",
"olxid": "4310"
},
{
"name": "Trnovo",
"id": "trnovo",
"olxid": "4067"
},
{
"name": "Ustiprača",
"id": "ustipraa",
"olxid": "1593"
},
{
"name": "Višegrad",
"id": "viegrad",
"olxid": "5259"
},
{
"name": "Vlasenica",
"id": "vlasenica",
"olxid": "5456"
},
{
"name": "Zvornik",
"id": "zvornik",
"olxid": "5684"
},
{
"name": "Šekovići",
"id": "ekovii",
"olxid": "4475"
},
{
"name": "Žepa",
"id": "epa",
"olxid": "1906"
}
]
},
{
"name": " Trebinjsko-Fočanska",
"id": "trebinjskofocanska",
"olxid": "17",
"municipalities": [
{
"name": "Berkovići",
"id": "berkovii",
"olxid": "4441"
},
{
"name": "Bileća",
"id": "bilea",
"olxid": "183"
},
{
"name": "Foča",
"id": "foa",
"olxid": "1287"
},
{
"name": "Gacko",
"id": "gacko",
"olxid": "1462"
},
{
"name": "Istočni Mostar",
"id": "istonimostar",
"olxid": "3038"
},
{
"name": "Kalinovik",
"id": "kalinovik",
"olxid": "2164"
},
{
"name": "Ljubinje",
"id": "ljubinje",
"olxid": "2884"
},
{
"name": "Nevesinje",
"id": "nevesinje",
"olxid": "3138"
},
{
"name": "Trebinje",
"id": "trebinje",
"olxid": "4766"
},
{
"name": "Čajniče",
"id": "ajnie",
"olxid": "911"
}
]
},
{
"name": "Distrikt Brčko",
"id": "distriktbrcko",
"olxid": "12",
"municipalities": [
{
"name": "Brčko",
"id": "brko",
"olxid": "12"
}
]
}
];
const getRegions = () => {
return regions.map((g) => ({ name: g.name, id: g.id, olxid: g.olxid }));
};
const getRegion = (regionId) => {
return regions.find(region => region.id === regionId);
};
const getRegionName = (regionId) => {
const region = getRegion(regionId);
return (region && region.name) ? region.name : null;
};
const getMunicipalitiesForRegion = (regionId) => {
const region = getRegion(regionId);
return (region && region.municipalities) ? region.municipalities : null;
};
const getMunicipality = (regionId, municipalityId) => {
const region = getRegion(regionId);
if (!region) {
return null;
}
const municipality = region.municipalities.find(municipality => municipality.id === municipalityId);
if (!municipality) {
return null;
}
return municipality;
};
const getMunicipalityName = (regionId, municipalityId) => {
const region = getRegion(regionId);
if (!region) {
return null;
}
const municipality = region.municipalities.find(municipality => municipality.id === municipalityId);
if (!municipality) {
return null;
}
return municipality.name;
};
module.exports = {
getRegion,
getRegions,
getRegionName,
getMunicipalitiesForRegion,
getMunicipalityName,
getMunicipality
};

View File

@@ -0,0 +1,407 @@
const fetch = require('node-fetch');
const cheerio = require('cheerio');
const { allRERequest, findPointInsideBoundingBox } = require('../url');
const { getRealEstateTypeEnum } = require('../enums');
const { getRegion, getMunicipality } = require('../codes')
const Promise = require("bluebird");
module.exports = class OlxCrawler {
//TODO figure best way to handle paging
constructor(fromPage = 0, toPage = 10, maxResults = 1000) {
this.fromPage = fromPage;
this.toPage = toPage;
this.maxResults = maxResults;
}
async indexSingle(url, email) {
try {
const res = await fetch(url);
const body = await res.text();
const $ = cheerio.load(body);
//TODO figure out what to do with username
const username = $('#lg > div.desno2.profil > div:nth-child(2) > div.vrsta1.vrsta_desno > a > div.username > span').text();
// if (IGNORED_USERNAMES.includes((username || '').toLowerCase())) {
// return null;
// }
//TODO remove properties that are not needed, and add some if they are missing
const title = $('#naslovartikla').text();
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 adType = $('#artikal_glavni_div > div.artikal_lijevo > div:nth-child(15) > div:nth-child(2) > div.df2').text();
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 floor = $('#dodatnapolja1').find(':contains(Sprat)').last().nextAll().text();
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 parseRooms = (rooms) => parseInt([...rooms].filter(c => !isNaN(c)).filter(c => c.trim()).join())
const parsePrice = (price) => parseFloat(price.replace(".", ""))
// TODO we dont save images ??
// const images = [];
// const imgMatches = body.match(imgRe);
// for (let i = 0; imgMatches && i < imgMatches.length; i++) {
// let img = imgMatches[i].replace("href\":", "")
// img = img.replace("\"", "");
// img = img.replace("\"", "");
// images.push(img);
// }
// const uploadPromises = images.map(img => {
// const imgFixed = eval(`'${img}'`);
// return cloudinary.uploader.upload(eval(`'${img}'`));
// });
// const uploadResults = await Promise.all(uploadPromises);
// const cloudinaryImages = uploadResults.map(ur => ur.url);
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 : email,
olxId: olxId,
// category: category,
url,
title,
price: isNaN(parsedPrice) ? price : parsedPrice,
size: parseFloat(size),
gardenSize: parseFloat(gardenSize),
address,
region,
municipality,
// adType: AD_TYPE_SALE,
time,
shortDescription: descriptions.first().text(),
longDescription: descriptions.last().text(),
lat,
lng,
loc: [parseFloat(lat), parseFloat(lng)],
// images: cloudinaryImages
};
return data;
} catch (e) {
console.error('Exception caught: ' + e.message);
}
return null;
}
async indexPage(olxUrl, maxResults = 1000) {
try {
// console.log('Starting to index page: ' + pageNr);
// const url = `http://www.olx.ba/pretraga?vrsta=samoprodaja&sort_order=desc&kategorija=23&sort_po=datum&kanton=9&stranica=${pageNr}`;
const res = await fetch(olxUrl.url);
const body = await res.text();
const $ = cheerio.load(body);
const hrefs = [];
const results = [];
$('#rezultatipretrage').find('.listitem').each((i, elem) => {
const href = $(elem).find('a').first().attr('href');
hrefs.push(href);
});
let actualNoOfResults = (hrefs.length <= maxResults) ? hrefs.length : maxResults;
for (let i = 0; i < hrefs.length; i++) {
console.log(`indexing: ${hrefs[i]}`);
const singleData = await this.indexSingle(hrefs[i], olxUrl.email);
if (singleData) {
results.push(singleData);
}
// await this.sleep(500);
}
return results;
} catch (e) {
console.error('Exception caught:' + e);
}
}
getCategoryId (category) {
if (category === 'Stanovi') {
return 'stan';
} else if (category === 'Vikendice') {
return 'vikendica';
} else if (category === 'Kuće') {
return 'kuca';
}
}
async indexPages(urls, start, end, maxResults = 1000) {
// let results = {};
// for (let i = start; i <= end; i++) {
// let result = await this.indexPage(i, maxResults);
// Object.assign(results, result)
// await this.sleep(5000);
// }
// return results;
let results = [];
const indexers= [];
let it = 3
for (let url of urls) {
// let result = await this.indexPage(url, maxResults);
// results.push(result);
it++
indexers.push(new Indexer(it * 2000));
}
return Promise.map(indexers, function (indexer) {
return indexer.indexPage();
}).then(async (results) => {
return results
})
}
async crawl() {
const filteredResults = [];
const realestateRequests = await allRERequest()
const urls = this.createRequestUrls(realestateRequests);
let results = await this.indexPages(urls, this.fromPage, this.toPage, this.maxResults);
for (const result of results) {
for (const re1 of result) {
if (re1.lat !== undefined && re1.lat !== null && re1.lat !== "") {
const pointInsideBoundingBox = await findPointInsideBoundingBox([re1.lng, re1.lat]);
if (pointInsideBoundingBox[0].length !== 0) {
filteredResults.push(re1);
}
}
}
}
// await this.sleep(10000);
console.log(filteredResults);
return filteredResults;
}
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
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,
email: request.email
}
console.log(olxUrl.url);
urls.push(olxUrl);
}
return urls;
}
};
class Indexer {
constructor(olxUrl, email, uuid) {
this.olxUrl = olxUrl
this.email = email
this.uuid = uuid
}
async indexPage() {
try {
// console.log('Starting to index page: ' + pageNr);
// const url = `http://www.olx.ba/pretraga?vrsta=samoprodaja&sort_order=desc&kategorija=23&sort_po=datum&kanton=9&stranica=${pageNr}`;
// const res = await fetch(this.olxUrl.url);
// const body = await res.text();
// const $ = cheerio.load(body);
// const hrefs = [];
// const results = [];
// $('#rezultatipretrage').find('.listitem').each((i, elem) => {
// const href = $(elem).find('a').first().attr('href');
// hrefs.push(href);
// });
// let actualNoOfResults = (hrefs.length <= maxResults) ? hrefs.length : maxResults;
// for (let i = 0; i < hrefs.length; i++) {
// console.log(`indexing: ${hrefs[i]}`);
// const singleData = await this.indexSingle(hrefs[i], this.olxUrl.email);
// if (singleData) {
// results.push(singleData);
// }
// // await this.sleep(500);
// }
await this.sleep(this.olxUrl);
console.log('Finished indexing PAGE');
const singleIndex = [new Indexer(this.olxUrl), new Indexer(this.olxUrl), new Indexer(this.olxUrl), new Indexer(this.olxUrl), new Indexer(this.olxUrl)]
return Promise.map(singleIndex, function (indexer) {
return indexer.indexSingle();
}).then(async (results) => {
return results
})
// return results;
} catch (e) {
console.error('Exception caught:' + e);
}
}
async indexSingle() {
// try {
// const res = await fetch(url);
// const body = await res.text();
// const $ = cheerio.load(body);
// //TODO figure out what to do with username
// const username = $('#lg > div.desno2.profil > div:nth-child(2) > div.vrsta1.vrsta_desno > a > div.username > span').text();
// // if (IGNORED_USERNAMES.includes((username || '').toLowerCase())) {
// // return null;
// // }
// //TODO remove properties that are not needed, and add some if they are missing
// const title = $('#naslovartikla').text();
// 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 adType = $('#artikal_glavni_div > div.artikal_lijevo > div:nth-child(15) > div:nth-child(2) > div.df2').text();
// 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 floor = $('#dodatnapolja1').find(':contains(Sprat)').last().nextAll().text();
// 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 parseRooms = (rooms) => parseInt([...rooms].filter(c => !isNaN(c)).filter(c => c.trim()).join())
// const parsePrice = (price) => parseFloat(price.replace(".", ""))
// // TODO we dont save images ??
// // const images = [];
// // const imgMatches = body.match(imgRe);
// // for (let i = 0; imgMatches && i < imgMatches.length; i++) {
// // let img = imgMatches[i].replace("href\":", "")
// // img = img.replace("\"", "");
// // img = img.replace("\"", "");
// // images.push(img);
// // }
// // const uploadPromises = images.map(img => {
// // const imgFixed = eval(`'${img}'`);
// // return cloudinary.uploader.upload(eval(`'${img}'`));
// // });
// // const uploadResults = await Promise.all(uploadPromises);
// // const cloudinaryImages = uploadResults.map(ur => ur.url);
// 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 : email,
// olxId: olxId,
// // category: category,
// url,
// title,
// price: isNaN(parsedPrice) ? price : parsedPrice,
// size: parseFloat(size),
// gardenSize: parseFloat(gardenSize),
// address,
// region,
// municipality,
// // adType: AD_TYPE_SALE,
// time,
// shortDescription: descriptions.first().text(),
// longDescription: descriptions.last().text(),
// lat,
// lng,
// loc: [parseFloat(lat), parseFloat(lng)],
// // images: cloudinaryImages
// };
// return data;
// } catch (e) {
// console.error('Exception caught: ' + e.message);
// }
// return null;
await this.sleep(this.olxUrl);
console.log("Finished indexing single page");
return {};
}
async sleep(ms) {
console.log(ms);
return new Promise(resolve => setTimeout(resolve, ms));
}
}

View File

View File

@@ -0,0 +1,10 @@
// const db = require('../../models/index');
// const bulkInsert = async (reuslts) => {
// db.MarketAlert.bulkCreate({
// })
// }

9
app/helpers/email.js Normal file
View File

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

57
app/helpers/enums.js Normal file
View File

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

View File

@@ -5,10 +5,19 @@ const currentRERequest = async (req) => {
if(!uniqueId) return null; if(!uniqueId) return null;
const request = await db.RealEstateRequest.findOne({ where: {uniqueId} }); const request = await db.RealEstateRequest.findOne({ where: {uniqueId} });
console.log("Request ", request);
return request; return request;
};
// TODO Fetch only subscribed realestate requests
const allRERequest = async () => {
return await db.RealEstateRequest.findAll();
}
const findPointInsideBoundingBox = async (latLng) => {
return await db.sequelize.query("SELECT * FROM \"RealEstateRequests\" WHERE ST_Contains(\"RealEstateRequests\".bounding_box, ST_GEOMFROMTEXT(\'POINT (" + latLng[0] + " " + latLng[1]+ ")\'))");
} }
module.exports = { module.exports = {
currentRERequest currentRERequest,
} allRERequest,
findPointInsideBoundingBox
};

View File

@@ -1,8 +1,10 @@
const convertToDate = require("./convertToDate"); const convertToDate = require("./convertToDate");
function areThereAnyNewItems(lastItemDate, controlDate) { function areThereAnyNewItems(lastItemDate, controlDate) {
if (!lastItemDate) { if (!lastItemDate) {
return true; return true;
} }
return new Date(controlDate) < convertToDate(lastItemDate); return new Date(controlDate) < convertToDate(lastItemDate);
} }
module.exports = areThereAnyNewItems; module.exports = areThereAnyNewItems;

View File

@@ -1,6 +1,6 @@
let fetch = require("node-fetch"); let fetch = require("node-fetch");
let cheerio = require("cheerio"); let cheerio = require("cheerio");
const areThereAnyNewItems = require("./arethereanynewitems"); const areThereAnyNewItems = require("./areThereAnyNewItems");
async function scrapTheItems(url, controlDate, noNewItems = false) { async function scrapTheItems(url, controlDate, noNewItems = false) {
let items = []; let items = [];

View File

@@ -1,11 +1,11 @@
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, last_date } = marketAlert; const { id, email, olx_url } = marketAlert;
let url = let url =
"https://www.olx.ba/pretraga?" + olx_url + "&sort_order=desc&sort_po=datum"; "https://www.olx.ba/pretraga?" + olx_url + "&sort_order=desc&sort_po=datum";
let newItems = await scrapTheItems(url); let newItems = await scrapTheItems(url);
@@ -17,8 +17,8 @@ async function sendNotification(marketAlert) {
"" ""
); );
// Create sendEmail params // Create sendEmail params
var params = { const params = {
Destination: { /* required */ Destination: { /* required */
CcAddresses: [ CcAddresses: [
], ],
@@ -50,7 +50,7 @@ async function sendNotification(marketAlert) {
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

@@ -12,8 +12,7 @@ module.exports = {
type: Sequelize.UUID type: Sequelize.UUID
}, },
realEstateType: { realEstateType: {
type: Sequelize.ENUM, type: Sequelize.STRING
values: ['kuca','stan','vikendica','plac','poslovni_prostor','apartman','garaza']
}, },
email: { email: {
type: Sequelize.STRING type: Sequelize.STRING

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.addColumn('RealEstateRequests', 'sizeRange', {
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) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.removeColumn('RealEstateRequests', 'sizeRange', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeRange', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'priceRange', { transaction: t })
])
})
}
};

View File

@@ -0,0 +1,63 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.removeColumn('RealEstateRequests', 'sizeRange', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeRange', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'priceRange', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'size', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'gardenSize', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'price', { transaction: t }),
queryInterface.addColumn('RealEstateRequests', 'gardenSizeMin', {
type: Sequelize.INTEGER,
}, { 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) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeMin', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'gardenSizeMax', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'sizeMin', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'sizeMax', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'priceMin', { transaction: t }),
queryInterface.removeColumn('RealEstateRequests', 'priceMin', { transaction: t }),
queryInterface.addColumn('RealEstateRequests', 'priceMax', {
type: Sequelize.STRING
}, { 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

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

View File

@@ -0,0 +1,37 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.addColumn('MarketAlerts', 'size', {
type: Sequelize.INTEGER,
}, { transaction: t }),
queryInterface.addColumn('MarketAlerts', 'gardenSize', {
type: Sequelize.INTEGER,
}, { 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) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.removeColumn('MarketAlerts', 'size', { transaction: t }),
queryInterface.removeColumn('MarketAlerts', 'gardenSize', { transaction: t }),
queryInterface.removeColumn('MarketAlerts', 'price', { transaction: t }),
queryInterface.removeColumn('MarketAlerts', 'municipality', { transaction: t }),
queryInterface.removeColumn('MarketAlerts', 'region', { transaction: t })
])
})
}
};

View File

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

View File

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

View File

@@ -1,8 +1,17 @@
'use strict'; 'use strict';
module.exports = (sequelize, DataTypes) => { module.exports = (sequelize, DataTypes) => {
const MarketAlert = sequelize.define('MarketAlert', { const MarketAlert = sequelize.define('MarketAlert', {
olxUrl: DataTypes.STRING, url: DataTypes.STRING,
realestateOrigin: DataTypes.STRING,
originId: DataTypes.STRING,
lastDate: DataTypes.STRING, lastDate: DataTypes.STRING,
size : DataTypes.INTEGER,
gardenSize : DataTypes.INTEGER,
price : DataTypes.INTEGER,
municipality : DataTypes.STRING,
region : DataTypes.STRING,
realEstateType : DataTypes.STRING,
email: { email: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNul: false allowNul: false

View File

@@ -1,17 +1,25 @@
'use strict'; 'use strict';
module.exports = (sequelize, DataTypes) => { module.exports = (sequelize, DataTypes) => {
const RealEstateRequest = sequelize.define('RealEstateRequest', { const RealEstateRequest = sequelize.define('RealEstateRequest', {
uniqueId: { uniqueId: {
type: DataTypes.UUID, type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
allowNull: false allowNull: false
}, },
realEstateType: { realEstateType: DataTypes.STRING,
type: DataTypes.ENUM, email: DataTypes.STRING,
values: ['kuca','stan','vikendica','plac','poslovni_prostor','apartman','garaza'] region: DataTypes.STRING,
}, municipality: DataTypes.STRING,
email: DataTypes.STRING, sizeMin: DataTypes.INTEGER,
city: DataTypes.STRING, 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,11 +1,39 @@
.dobrodosli-center-button { .welcome-center-button {
width: 100%; width: 100%;
} }
.dobrodosli-big-logo { .welcome-big-logo {
font-size: 200pt; font-size: 200pt;
background-image: url(./images/logo.png); background-image: url(./images/logo.png);
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
color: rgba(0, 0, 0, 0); color: rgba(0, 0, 0, 0);
} }
.no-ui-slider {
width: 95%
}
#map {
height: 50%;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#floating-panel {
top: 10px;
left: 25%;
z-index: 5;
background-color: #fff;
padding: 5px;
border: 1px solid #999;
text-align: center;
font-family: 'Roboto', 'sans-serif';
line-height: 30px;
padding-left: 10px;
}

View File

@@ -0,0 +1,58 @@
const Promise = require("bluebird");
const OlxCrawler = require("../helpers/crawlers/olxClawler");
const db = require("../models/index");
const olxCrawler = new OlxCrawler(1, 2, 3);
const olxCrawler1 = new OlxCrawler(1, 2, 3);
const olxCrawler2 = new OlxCrawler(1, 2, 3);
const crawlers = [
olxCrawler,
olxCrawler1,
olxCrawler2,
];
async function crawlAll() {
Promise.map(crawlers, function (crawler) {
return crawler.crawl();
}).then(async (results) => {
try {
const marketAlertsFromDb = await db.MarketAlert.findAll();
const marketAlerts = [];
const mergedResults = [].concat.apply([], results);
for (const result of mergedResults) {
marketAlerts.push({
url: result.url,
realestateOrigin: "OLX",
originId: 1,
size: result.size,
price: result.price,
email: result.email,
// lastDate: DataTypes.STRING,
municipality: result.municipality,
region: result.region,
gardenSize: isNaN(result.gardenSize) ? 0 : result.gardenSize,
realEstateType: result.realEstateType
})
}
try {
const filteredMarketAlerts = marketAlerts.filter((elem) => !marketAlertsFromDb.find(({ url }) => elem.url === url));
await db.MarketAlert.bulkCreate(filteredMarketAlerts);
process.exit()
} catch (e) {
console.log("Could not bulkCreate marketalers reason: ", e);
}
} catch (e) {
console.log("Error crawling. Trying next crawler! ", e);
}
})
};
crawlAll();

6
app/views/gardenSize.ejs Normal file
View File

@@ -0,0 +1,6 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h2>Koliko okućnice tražite ?</h2>
</div>
<% include partials/range %>

25
app/views/goAgain.ejs Normal file
View File

@@ -0,0 +1,25 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h4>Provjerite Vaš email !</h4>
</div>
<div class="row center-align">
<h4>Želite li pretražiti još jednu nekretninu ?</h4>
</div>
<form method="POST" id="form-goagain">
<div class="row">
<div class="col s3 push-s3">
<a href="/" class="welcome-center-button waves-effect waves-light btn">
Da
</a>
</div>
<div class="col s3 push-s3">
<a href="/" class="welcome-center-button waves-effect waves-light btn">
Ne
</a>
</div>
</div>
</form>

View File

@@ -1,25 +0,0 @@
<div class="row center-align">
<h2>U kojoj regiji tražite nekretninu?</h2>
</div>
<form method="POST" id="form-grad">
<div class="row center-align">
<ul class="collection with-header">
<% for(const grad of gradovi) { %>
<li class="collection-item" > <div id="<%= grad.id %>" ><%= grad.ime %><a href="#!" class="secondary-content"><i class="material-icons">send</i></a></div></li>
<% } %>
</ul>
<input type="hidden" name="grad" id="grad" />
</div>
</form>
<script>
$(document).ready( () => {
$(".collection-item").click( (e) => {
const clickedId = $(e.target).attr("id");
$("#grad").val(clickedId);
$("#form-grad").submit();
});
});
</script>

View File

@@ -3,6 +3,7 @@
<head> <head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/13.1.5/nouislider.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script> <script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -13,5 +14,9 @@
<%-body%> <%-body%>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/13.1.5/nouislider.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/wnumb/1.1.0/wNumb.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,28 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h2>U kojem mjestu tražite nekretninu?</h2>
</div>
<form method="POST" id="form-municipality">
<div class="row center-align">
<ul class="collection with-header">
<% for(const municipality of municipalities) { %>
<li class="collection-item">
<div val="<%= municipality.name %>" id="<%= municipality.id %>" onclick="saveAndSubmit(this.id)"><%= municipality.name %>
<a href="#" class="secondary-content">
<i class="material-icons">send</i>
</a>
</div>
</li>
<% } %>
</ul>
<input type="hidden" name="municipality" id="municipality" />
</div>
</form>
<script>
function saveAndSubmit(id, name) {
$("#municipality").val(id);
$("#form-municipality").submit();
}
</script>

View File

@@ -0,0 +1,129 @@
<div class="row center-align">
<h2>U kojem naselju tražite nekretninu?</h2>
</div>
<div class="row center-align">
<div id="floating-panel">
<input id="address" type="textbox" value="">
<input id="submit" type="button" value="Trazi">
</div>
<div id="map"></div>
</div>
<form method="POST" id="form-map-output">
<div class="row center-align">
<div class="col s6 push-s3">
<a id="btnsubmit" href="#" class="welcome-center-button waves-effect waves-light btn">
Dalje
</a>
</div>
</div>
<input type="hidden" name="north" id=north />
<input type="hidden" name="south" id=south />
<input type="hidden" name="east" id=east />
<input type="hidden" name="west" id=west />
</form>
<script>
var map;
var municipality = "<%= municipality%>";
var defaultAddress = document.getElementById('address');
var latLngRestrictions = [];
var BOSNIA_BOUNDS = {
north: 45.70,
south: 41.69,
west: 15.55,
east: 20.77,
};
function initMap() {
var geocoder = new google.maps.Geocoder();
document.getElementById('submit').addEventListener('click', function () {
geocodeAddress(geocoder, map, false, latLngRestrictions);
});
defaultAddress.value = municipality;
geocodeAddress(geocoder, map, true);
$(document).ready(() => {
$("#btnsubmit").click(() => {
var bounds = map.getBounds();
$("#north").val(map.getBounds().getNorthEast().lat());
$("#south").val(map.getBounds().getSouthWest().lat());
$("#east").val(map.getBounds().getNorthEast().lng());
$("#west").val(map.getBounds().getSouthWest().lng());
$("#form-map-output").submit();
});
});
}
function geocodeAddress(geocoder, resultsMap, isInit, geocoderRestrictions) {
var address = document.getElementById('address').value;
let geocoderOptions = geocoderRestrictions
? { 'address': address, 'bounds': geocoderRestrictions }
: { 'address': address }
geocoder.geocode(geocoderOptions, function (results, status) {
if (status === 'OK') {
var bounds = results[0].geometry.bounds;
var resultBounds = new google.maps.LatLngBounds(
results[0].geometry.viewport.getSouthWest(),
results[0].geometry.viewport.getNorthEast()
);
if (isInit) {
map = new google.maps.Map(document.getElementById('map'), {
zoom: 11,
});
resultsMap = map
}
// map.fitBounds(resultBounds);
resultsMap.setCenter(results[0].geometry.location);
if (isInit) {
latLngRestrictions = new google.maps.LatLngBounds(
new google.maps.LatLng(bounds.getSouthWest().lat(), bounds.getSouthWest().lng()),
new google.maps.LatLng(bounds.getNorthEast().lng(), bounds.getNorthEast().lng()));
let latLngRestrictionsa = {
west: bounds.getSouthWest().lng(),
east: bounds.getNorthEast().lng(),
north: bounds.getNorthEast().lat(),
south: bounds.getSouthWest().lat()
}
map.setOptions({
restriction: {
latLngBounds: latLngRestrictionsa,
strictBounds: false,
}
})
} else {
resultsMap.setZoom(17);
}
} else {
alert('Geocode was not successful for the following reason: ' + status);
}
});
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAna8ohfV2HBMcxGk_29vqxU5Z_bDickqg&callback=initMap" async
defer></script>
</div>

View File

@@ -0,0 +1,53 @@
<form method="POST" id="form-range">
<div class="row center-align no-ui-slider" id="slider"></div>
<div class="col s6 push-s3">
<a id="btnsubmit" href="#" class="welcome-center-button waves-effect waves-light btn">
Dalje
</a>
</div>
<input type="hidden" name="from" id="from" />
<input type="hidden" name="to" id="to" />
</form>
<script>
$(document).ready(() => {
var slider = document.getElementById('slider');
const unitFormat = wNumb({
decimals: 3,
thousand: '.',
suffix: '<%= unit %>'
})
noUiSlider.create(slider, {
start: [<%= rangeFrom.value %>, <%= rangeTo.value %>],
connect: true,
tooltips: true,
step: <%= rangeFrom.step %>,
range: {
'min': <%= rangeFrom.min %>,
'max': <%= rangeTo.max %>
},
format: unitFormat
});
$("#btnsubmit").click(() => {
const sliderValues = slider.noUiSlider.get();
$("#from").val(unitFormat.from(sliderValues[0]));
$("#to").val(unitFormat.from(sliderValues[1]));
$("#form-range").submit();
// });
});
});
</script>

6
app/views/price.ejs Normal file
View File

@@ -0,0 +1,6 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h2>Koja Vam okvirna cijena odgovara ?</h2>
</div>
<% include partials/range %>

36
app/views/queryReview.ejs Normal file
View File

@@ -0,0 +1,36 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h2>Da li je ovo to što ste tražili ?</h2>
</div>
<form method="POST" id="form-queryreview">
<div class="row center-align">
<ul class="collection with-header">
<% for(const stepData of queryData) { %>
<li class="collection-item" >
<div id="<%= stepData.id %>" ><%= stepData.title || '-' %>
<a href="<%= stepData.url %>" class="secondary-content">
<i class="material-icons">edit</i>
</a>
</div>
</li>
<% } %>
</ul>
</div>
<div class="row">
<div class="col s6 push-s3">
<a id="submit" href="#" class="welcome-center-button waves-effect waves-light btn">
To je to
</a>
</div>
</div>
</form>
<script>
$(document).ready( () => {
$("#submit").click( () => {
$("#form-queryreview").submit();
});
});
</script>

68
app/views/querySubmit.ejs Normal file
View File

@@ -0,0 +1,68 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h4>Da Vam javimo kada se Vaša željena nekretnina pojavi u oglasima, upišite svoj e-mail</h4>
</div>
<form method="POST" id="form-submitquery">
<div class="row center-align">
<div class="col s6 push-s3">
<input id="email" name="email" type="email" placeholder="vas.email@mail.com" required size="250" />
</div>
</div>
<div class="row">
<div class="col s6 push-s3">
<h6 id="error-lable-email" style="color: red"><%= error %> </h6>
</div>
</div>
<div class="row center-align">
<div class="col s6 push-s3">
<input id="confirm" name="confirm" type="email" placeholder="potvrdite.email@mail.com" required size="250" />
</div>
</div>
<div class="row">
<div class="col s6 push-s3">
<h6 id="error-lable-email-confirm" style="color: red"></h6>
</div>
</div>
<div class="row">
<div class="col s6 push-s3">
<a id="submit" href="#" class="welcome-center-button waves-effect waves-light btn">
Javi mi
</a>
</div>
</div>
<div class="row">
<div class="col s6 push-s3">
<p>* U svakom trenutku možete prekinuti slanje objava kroz link u e-mailu</p>
</div>
</div>
</form>
<script>
$(document).ready(() => {
$("#submit").click(() => {
const emailField = document.getElementById('email');
const emailConfirmField = document.getElementById('confirm');
const errorMessage = "Greška ! Unedite validan email";
$("#error-lable-email").text("");
$("#error-lable-email-confirm").text("");
if (!emailField.validity.valid) {
$("#error-lable-email").text(errorMessage);
return
}
if (!emailConfirmField.validity.valid) {
$("#error-lable-email-confirm").text(errorMessage);
return
}
$("#form-submitquery").submit();
});
});
</script>

View File

@@ -0,0 +1,33 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h2>Koju nekretninu tražite?</h2>
</div>
<form method="POST" id="form-real-estate-type">
<div class="row center-align">
<ul class="collection with-header">
<% for(const realEstateType of realEstateTypes) { %>
<li class="collection-item">
<div id="<%= realEstateType.id %>" onclick="saveAndSubmit(this.id)"><%= realEstateType.title %>
<a href="#" class="secondary-content">
<i class="material-icons">send</i>
</a>
</div>
</li>
<% } %>
</ul>
<input type="hidden" name="realestatetype" id="realestatetype" />
</div>
</form>
<script>
function saveAndSubmit(id) {
$("#realestatetype").val(id);
$("#form-real-estate-type").submit();
}
</script>

29
app/views/region.ejs Normal file
View File

@@ -0,0 +1,29 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h2>U kojoj regiji tražite nekretninu?</h2>
</div>
<form method="POST" id="form-region">
<div class="row center-align">
<ul class="collection with-header">
<% for(const region of regions) { %>
<li class="collection-item">
<div id="<%= region.id %>" onclick="saveAndSubmit(this.id)"><%= region.name %>
<a href="#" class="secondary-content">
<i class="material-icons">send</i>
</a>
</div>
</li>
<% } %>
</ul>
<input type="hidden" name="region" id="region" />
</div>
</form>
<script>
function saveAndSubmit(id) {
$("#region").val(id);
$("#form-region").submit();
}
</script>

6
app/views/size.ejs Normal file
View File

@@ -0,0 +1,6 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<h2>Od koliko kvadrata tražite nekretninu ?</h2>
</div>
<% include partials/range %>

14
app/views/unsubscribe.ejs Normal file
View File

@@ -0,0 +1,14 @@
<div class="row center-align">
<span class="welcome-big-logo">🤙</span>
</div>
<div class="row center-align">
<div>Uspješno ste se odjavili</div>
</div>
<div class="row">
<div class="col s6 push-s3">
<a href="<%= nextStep %>" class="welcome-center-button waves-effect waves-light btn">
Nova pretraga
</a>
</div>
</div>

View File

@@ -1,29 +0,0 @@
<div class="row center-align">
<h2>Koju nekretninu tražite?</h2>
</div>
<form method="POST" id="form-vrsta">
<div class="row center-align">
<ul class="collection with-header">
<% for(let vrsta of vrste) { %>
<li class="collection-item" > <div id="<%= vrsta.id %>" ><%= vrsta.ime %><a href="#!" class="secondary-content"><i class="material-icons">send</i></a></div></li>
<% } %>
</ul>
<input type="hidden" name="vrsta" id="vrsta" />
</div>
</form>
<script>
$(document).ready( () => {
$(".collection-item").click( (e) => {
const clickedId = $(e.target).attr("id");
$("#vrsta").val(clickedId);
$("#form-vrsta").submit();
});
});
</script>

View File

@@ -1,7 +1,7 @@
<!-- --> <!-- -->
<div class="row center-align"> <div class="row center-align">
<span class="dobrodosli-big-logo">🤙</span> <span class="welcome-big-logo">🤙</span>
</div> </div>
<div class="row center-align"> <div class="row center-align">
<div>Sve nekretnine dostupne u oglasima.</div> <div>Sve nekretnine dostupne u oglasima.</div>
@@ -10,7 +10,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col s6 push-s3"> <div class="col s6 push-s3">
<a href="<%= nextStep %>" class="dobrodosli-center-button waves-effect waves-light btn"> <a href="<%= nextStep %>" class="welcome-center-button waves-effect waves-light btn">
Javi mi Javi mi
</a> </a>
</div> </div>

5
development.env Normal file
View File

@@ -0,0 +1,5 @@
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,13 +1,22 @@
const dobrodosli = require('./app/controllers/dobrodosli').getDobrodosli; const welcome = require('./app/controllers/welcome').getWelcome;
const { getVrstaNekretnine, postVrstaNekretnine} = require('./app/controllers/vrsta_nekretnine'); const { getRealEstateTypes, postRealEstateTypes } = require('./app/controllers/realEstateTypes');
const { getGrad, postGrad } = require('./app/controllers/grad'); const { getRegion, postRegion } = require('./app/controllers/regions');
const { getMunicipality, postMunicipality } = require('./app/controllers/municipalities');
const { getSize, postSize } = require('./app/controllers/sizes');
const { getGardenSize, postGardenSize } = require('./app/controllers/gardenSizes');
const { getPrice, postPrice } = require('./app/controllers/prices');
const { getQueryReview, postQueryReview } = require('./app/controllers/queryReview');
const { getQuerySubmit, postQuerySubmit } = require('./app/controllers/querySubmit');
const { getGoAgain } = require('./app/controllers/goAgain');
const { getNeighborhood, postNeighborhood } = require('./app/controllers/neighborhoodMap');
const { getUnsubscribe } = require('./app/controllers/unsubscribe');
let express = require("express"); let express = require("express");
const path = require("path"); const path = require("path");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
const MarketAlert = require("./app/models/marketalert"); const MarketAlert = require("./app/models/marketalert");
const sendNotification = require("./app/lib/sendnotification"); const sendNotification = require("./app/lib/sendNotification");
const scrapTheItems = require("./app/lib/scraptheitems"); const scrapTheItems = require("./app/lib/scrapTheItems");
const sequelize = require("./app/models/index").sequelize; const sequelize = require("./app/models/index").sequelize;
const Twocheckout = require("2checkout-node"); const Twocheckout = require("2checkout-node");
const layout = require('express-layout'); const layout = require('express-layout');
@@ -25,7 +34,7 @@ app.use(layout());
const compression = require('compression'); const compression = require('compression');
app.use(compression()); app.use(compression());
app.get("/api/sendnotifications", async function(req, res) { app.get("/api/sendnotifications", async (req, res) => {
let marketAlerts = await MarketAlert.findAll(); let marketAlerts = await MarketAlert.findAll();
let lastDateUpdate = await Promise.all( let lastDateUpdate = await Promise.all(
@@ -58,7 +67,7 @@ app.get("/api/items/:url", async (req, res) => {
}); });
}); });
app.post("/api/marketalerts", function(req, res) { app.post("/api/marketalerts", (req, res) => {
const { email, last_date, olx_url } = req.body; const { email, last_date, olx_url } = req.body;
console.log(email, last_date, olx_url); console.log(email, last_date, olx_url);
sequelize.sync().then(() => { sequelize.sync().then(() => {
@@ -74,7 +83,7 @@ app.post("/api/marketalerts", function(req, res) {
}); });
}); });
app.post("/api/payforalert", function(request, response) { app.post("/api/payforalert", (req, res) => {
let tco = new Twocheckout({ let tco = new Twocheckout({
sellerId: "901402692", sellerId: "901402692",
privateKey: "A28DCE5F-9292-405C-8161-F84D8BB83AFC", privateKey: "A28DCE5F-9292-405C-8161-F84D8BB83AFC",
@@ -83,7 +92,7 @@ app.post("/api/payforalert", function(request, response) {
let params = { let params = {
merchantOrderId: "123", merchantOrderId: "123",
token: request.body.token, token: req.body.token,
currency: "USD", currency: "USD",
total: "2.00", total: "2.00",
billingAddr: { billingAddr: {
@@ -93,30 +102,55 @@ app.post("/api/payforalert", function(request, response) {
state: "BiH", state: "BiH",
zipCode: "71000", zipCode: "71000",
country: "BiH", country: "BiH",
email: request.body.email, email: req.body.email,
phoneNumber: "5555555555" phoneNumber: "5555555555"
} }
}; };
tco.checkout.authorize(params, function(error, data) { tco.checkout.authorize(params, function (error, data) {
if (error) { if (error) {
response.send(error.message); res.send(error.message);
} else { } else {
response.send(data.response.responseMsg); res.send(data.response.responseMsg);
} }
}); });
}); });
app.get('/', dobrodosli); app.get('/', welcome);
app.get('/vrstanekretnine/:request_id', getVrstaNekretnine); app.get('/vrstanekretnine/:request_id', getRealEstateTypes);
app.get('/vrstanekretnine', getVrstaNekretnine); app.get('/vrstanekretnine', getRealEstateTypes);
app.post('/vrstanekretnine/:request_id', postVrstaNekretnine); app.post('/vrstanekretnine/:request_id', postRealEstateTypes);
app.post('/vrstanekretnine', postVrstaNekretnine); app.post('/vrstanekretnine', postRealEstateTypes);
app.get('/grad/:request_id', getGrad); app.get('/grad/:request_id', getRegion);
app.post('/grad/:request_id', postGrad); app.post('/grad/:request_id', postRegion);
app.use('/assets', express.static('./app/public')) app.get('/mjesto/:request_id', getMunicipality);
app.post('/mjesto/:request_id', postMunicipality);
app.get('/naselje/:request_id/:municipality', getNeighborhood);
app.post('/naselje/:request_id/:municipality', postNeighborhood);
app.get('/povrsina/:request_id', getSize);
app.post('/povrsina/:request_id', postSize);
app.get('/okucnica/:request_id', getGardenSize);
app.post('/okucnica/:request_id', postGardenSize);
app.get('/cijena/:request_id', getPrice);
app.post('/cijena/:request_id', postPrice);
app.get('/pregled/:request_id', getQueryReview);
app.post('/pregled/:request_id', postQueryReview);
app.get('/posalji/:request_id', getQuerySubmit);
app.post('/posalji/:request_id', postQuerySubmit);
app.get('/odjava/:request_id', getUnsubscribe);
app.get('/ponovo', getGoAgain);
app.use('/assets', express.static('./app/public'));
app.listen(port, () => console.log(`Example app listening on port ${port}!`)); app.listen(port, () => console.log(`Example app listening on port ${port}!`));

2246
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,13 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "node ./index.js" "start": "node ./index.js",
"start-mon": "nodemon ./index.js",
"scheduler": "node ./app/services/crawlerService.js",
"migrate": "cd app && npx sequelize db:migrate",
"setup": "docker build -t marketalerts . && docker run -e POSTGRES_USER=docker -e POSTGRES_PASSWORD=docker -e POSTGRES_DB=marketalerts --name pg_marketalerts -d -p 5432:5432 marketalerts && sleep 4 && npm run migrate",
"docker-start": "docker start pg_marketalerts",
"docker-stop": "docker stop pg_marketalerts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -20,6 +26,7 @@
"2checkout-node": "0.0.1", "2checkout-node": "0.0.1",
"@sendgrid/mail": "^6.3.1", "@sendgrid/mail": "^6.3.1",
"aws-sdk": "^2.422.0", "aws-sdk": "^2.422.0",
"bluebird": "^3.5.5",
"cheerio": "^1.0.0-rc.2", "cheerio": "^1.0.0-rc.2",
"compression": "^1.7.4", "compression": "^1.7.4",
"dotenv": "^7.0.0", "dotenv": "^7.0.0",
@@ -32,5 +39,8 @@
"react-step-wizard": "^5.1.0", "react-step-wizard": "^5.1.0",
"sequelize": "^4.43.2", "sequelize": "^4.43.2",
"sequelize-cli": "^5.4.0" "sequelize-cli": "^5.4.0"
},
"devDependencies": {
"nodemon": "^1.19.0"
} }
} }

389
tools/kantoni.html Normal file
View File

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