Compare commits
118 Commits
migrate-to
...
promise-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1776755d47 | ||
|
|
6eba5c2a97 | ||
|
|
2f474619ca | ||
|
|
80ff9bcb6b | ||
|
|
3c59292f23 | ||
|
|
1bcc5e8e5d | ||
|
|
c8ee848f0e | ||
|
|
0f630e9ea4 | ||
|
|
9a8a27d1d9 | ||
|
|
b17b6862ba | ||
|
|
6aaaea1612 | ||
|
|
fdd0124924 | ||
|
|
c15f45e8f4 | ||
|
|
371eac900e | ||
|
|
5d6e7f3938 | ||
|
|
efda7fdccd | ||
|
|
8bb0908c45 | ||
|
|
5c75d690b0 | ||
|
|
f0e8a72756 | ||
|
|
62bf3380cd | ||
|
|
caa1871939 | ||
|
|
506ac67956 | ||
|
|
8f9e3ae46a | ||
|
|
d6e999fcf1 | ||
|
|
08a94ca4f8 | ||
|
|
a0f2b044b2 | ||
|
|
7db74acad7 | ||
|
|
56865b4670 | ||
|
|
1a8ac3fba4 | ||
|
|
de3c76315e | ||
|
|
e969a8dc8b | ||
|
|
f4baec23cf | ||
|
|
5bf95e0594 | ||
|
|
6fbacb326f | ||
|
|
be416ffc0c | ||
|
|
a3d9a82fee | ||
|
|
6772f8a953 | ||
|
|
89a3c9e355 | ||
|
|
dd38602c5a | ||
|
|
a3f76d20fe | ||
|
|
fc1275566e | ||
|
|
c64ee42914 | ||
|
|
aa3c965d5c | ||
|
|
126da48852 | ||
|
|
58ae430564 | ||
|
|
315a29749c | ||
|
|
02bee9cf2c | ||
|
|
1c2847509a | ||
|
|
87dc742e41 | ||
|
|
70ddc1f734 | ||
|
|
2c415bbd79 | ||
|
|
b07eb5bbeb | ||
|
|
53585d3ae1 | ||
|
|
c652a306db | ||
|
|
b15295bfe6 | ||
|
|
7ad1117cae | ||
|
|
393f6731e6 | ||
|
|
93faa7c9e3 | ||
|
|
93f5d8071e | ||
|
|
7192c28c07 | ||
|
|
76f9457d4f | ||
|
|
68172951ed | ||
|
|
dbf40b199e | ||
|
|
4309bc709d | ||
|
|
4323017d02 | ||
|
|
42505a7089 | ||
|
|
1542310a81 | ||
|
|
c505062770 | ||
|
|
ab681e5eeb | ||
|
|
616eddbb19 | ||
|
|
e5eb6b99a2 | ||
|
|
27fa721627 | ||
|
|
9fdfce49ed | ||
|
|
59723410b6 | ||
|
|
51ed3551c7 | ||
|
|
58177a8cce | ||
|
|
864b917b4f | ||
|
|
a2f6f033bf | ||
|
|
64f2cb82a8 | ||
|
|
17492eb52c | ||
|
|
298c901759 | ||
|
|
c534c1ee34 | ||
|
|
2380c85122 | ||
|
|
0f7e9f9285 | ||
|
|
dee4df9bd8 | ||
|
|
4248e6304a | ||
|
|
9fd9fe8b82 | ||
|
|
467d551857 | ||
|
|
28f95b9c05 | ||
|
|
9aba66c273 | ||
|
|
d03e85a0dc | ||
|
|
9d49e72bb4 | ||
|
|
efe2dd66a3 | ||
|
|
5f2fee504a | ||
|
|
add905c793 | ||
|
|
5b25068009 | ||
|
|
262f71164c | ||
|
|
a6fdf259a0 | ||
|
|
da241c8200 | ||
|
|
8c7f26b099 | ||
|
|
0d7c154958 | ||
|
|
2ceb6805ce | ||
|
|
d240fcbcda | ||
|
|
f53d60ab0f | ||
|
|
6821054494 | ||
|
|
cb34890583 | ||
|
|
2669aaa9eb | ||
|
|
13a9886292 | ||
|
|
a6df827cd1 | ||
|
|
b67b15c4b9 | ||
|
|
a4ed76e29b | ||
|
|
7fc24add1f | ||
|
|
b1a08a7a57 | ||
|
|
1f7063f94e | ||
|
|
8a1e406f43 | ||
|
|
01b864d75b | ||
|
|
b7cb61b53b | ||
|
|
a957293029 |
1
backend/.gitignore → .gitignore
vendored
1
backend/.gitignore → .gitignore
vendored
@@ -1 +1,2 @@
|
||||
node_modules/
|
||||
.env
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM postgres:11.3
|
||||
|
||||
ENV POSTGIS_MAJOR 2.4
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get --assume-yes install software-properties-common postgis\
|
||||
&& rm -rf /var/lib/apt/lists/
|
||||
|
||||
RUN mkdir -p /docker-entrypoint-initdb.d
|
||||
|
||||
CMD ["postgres"]
|
||||
36
README.md
36
README.md
@@ -1,4 +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
|
||||
|
||||
## Setup
|
||||
|
||||
### Setup with npm commands
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
### 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
|
||||
|
||||
15
app/config/config.json
Normal file
15
app/config/config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"development": {
|
||||
"username": "docker",
|
||||
"password": "docker",
|
||||
"database": "marketalerts",
|
||||
"port": "5432",
|
||||
"dialect": "postgres"
|
||||
},
|
||||
"test": {
|
||||
"use_env_variable": "DATABASE_URL"
|
||||
},
|
||||
"production": {
|
||||
"use_env_variable": "DATABASE_URL"
|
||||
}
|
||||
}
|
||||
43
app/controllers/gardenSizes.js
Normal file
43
app/controllers/gardenSizes.js
Normal 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
|
||||
};
|
||||
7
app/controllers/goAgain.js
Normal file
7
app/controllers/goAgain.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const getGoAgain = async (req,res) => {
|
||||
res.render('goAgain');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getGoAgain
|
||||
};
|
||||
26
app/controllers/municipalities.js
Normal file
26
app/controllers/municipalities.js
Normal 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
|
||||
};
|
||||
37
app/controllers/neighborhoodMap.js
Normal file
37
app/controllers/neighborhoodMap.js
Normal 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
40
app/controllers/prices.js
Normal 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
|
||||
};
|
||||
85
app/controllers/queryReview.js
Normal file
85
app/controllers/queryReview.js
Normal 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
|
||||
};
|
||||
51
app/controllers/querySubmit.js
Normal file
51
app/controllers/querySubmit.js
Normal 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
|
||||
};
|
||||
44
app/controllers/realEstateTypes.js
Normal file
44
app/controllers/realEstateTypes.js
Normal 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
|
||||
};
|
||||
27
app/controllers/regions.js
Normal file
27
app/controllers/regions.js
Normal 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
42
app/controllers/sizes.js
Normal 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
|
||||
};
|
||||
15
app/controllers/unsubscribe.js
Normal file
15
app/controllers/unsubscribe.js
Normal 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
|
||||
};
|
||||
7
app/controllers/welcome.js
Normal file
7
app/controllers/welcome.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const getWelcome = (req,res) => {
|
||||
res.render('welcome', { nextStep: '/vrstanekretnine' } );
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getWelcome
|
||||
};
|
||||
96
app/helpers/awsEmail.js
Normal file
96
app/helpers/awsEmail.js
Normal 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
928
app/helpers/codes.js
Normal 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
|
||||
};
|
||||
407
app/helpers/crawlers/olxClawler.js
Normal file
407
app/helpers/crawlers/olxClawler.js
Normal 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));
|
||||
}
|
||||
}
|
||||
0
app/helpers/crawlers/olxPagerIndex.js
Normal file
0
app/helpers/crawlers/olxPagerIndex.js
Normal file
10
app/helpers/db/dbHelper.js
Normal file
10
app/helpers/db/dbHelper.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
// const db = require('../../models/index');
|
||||
|
||||
|
||||
// const bulkInsert = async (reuslts) => {
|
||||
// db.MarketAlert.bulkCreate({
|
||||
|
||||
// })
|
||||
|
||||
// }
|
||||
9
app/helpers/email.js
Normal file
9
app/helpers/email.js
Normal 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
57
app/helpers/enums.js
Normal 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,
|
||||
};
|
||||
23
app/helpers/url.js
Normal file
23
app/helpers/url.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const db = require('../models/index');
|
||||
|
||||
const currentRERequest = async (req) => {
|
||||
const uniqueId = req.params['request_id'];
|
||||
if(!uniqueId) return null;
|
||||
|
||||
const request = await db.RealEstateRequest.findOne({ where: {uniqueId} });
|
||||
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 = {
|
||||
currentRERequest,
|
||||
allRERequest,
|
||||
findPointInsideBoundingBox
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
const convertToDate = require("./convertToDate");
|
||||
|
||||
function areThereAnyNewItems(lastItemDate, controlDate) {
|
||||
if (!lastItemDate) {
|
||||
return true;
|
||||
}
|
||||
return new Date(controlDate) < convertToDate(lastItemDate);
|
||||
}
|
||||
|
||||
module.exports = areThereAnyNewItems;
|
||||
@@ -1,6 +1,6 @@
|
||||
let fetch = require("node-fetch");
|
||||
let cheerio = require("cheerio");
|
||||
const areThereAnyNewItems = require("./arethereanynewitems");
|
||||
const areThereAnyNewItems = require("./areThereAnyNewItems");
|
||||
|
||||
async function scrapTheItems(url, controlDate, noNewItems = false) {
|
||||
let items = [];
|
||||
58
app/lib/sendNotification.js
Normal file
58
app/lib/sendNotification.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const scrapTheItems = require("./scrapTheItems");
|
||||
const convertToDate = require("./convertToDate");
|
||||
const AWS = require('aws-sdk');
|
||||
// AWS.config.update({region: 'eu-central-1'});
|
||||
|
||||
|
||||
async function sendNotification(marketAlert) {
|
||||
const { id, email, olx_url } = marketAlert;
|
||||
let url =
|
||||
"https://www.olx.ba/pretraga?" + olx_url + "&sort_order=desc&sort_po=datum";
|
||||
let newItems = await scrapTheItems(url);
|
||||
let lastDate = newItems.length && newItems[0].date;
|
||||
let message =
|
||||
newItems.length &&
|
||||
newItems.reduce(
|
||||
(mes, item) => mes + `<strong>${item.url} i ${item.price}</strong>`,
|
||||
""
|
||||
);
|
||||
|
||||
// Create sendEmail params
|
||||
const params = {
|
||||
Destination: { /* required */
|
||||
CcAddresses: [
|
||||
],
|
||||
ToAddresses: [
|
||||
email
|
||||
]
|
||||
},
|
||||
Message: { /* required */
|
||||
Body: { /* required */
|
||||
Html: {
|
||||
Charset: "UTF-8",
|
||||
Data: message
|
||||
},
|
||||
Text: {
|
||||
Charset: "UTF-8",
|
||||
Data: message // TODO: convert to text
|
||||
}
|
||||
},
|
||||
Subject: {
|
||||
Charset: 'UTF-8',
|
||||
Data: 'Javimi alert'
|
||||
}
|
||||
},
|
||||
Source: 'info@saburly.com', /* required */
|
||||
ReplyToAddresses: [
|
||||
'info@saburly.com',
|
||||
],
|
||||
};
|
||||
|
||||
if (message) {
|
||||
const sendPromise = new AWS.SES({apiVersion: '2010-12-01'}).sendEmail(params).promise();
|
||||
await sendPromise;
|
||||
return { id, date: String(convertToDate(lastDate)) };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = sendNotification;
|
||||
34
app/migrations/20190417035319-create-market-alert.js
Normal file
34
app/migrations/20190417035319-create-market-alert.js
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
return queryInterface.createTable('MarketAlerts', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
olxUrl: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
lastDate: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
});
|
||||
},
|
||||
down: (queryInterface, Sequelize) => {
|
||||
return queryInterface.dropTable('MarketAlerts');
|
||||
}
|
||||
};
|
||||
33
app/migrations/20190417035707-create-real-estate-request.js
Normal file
33
app/migrations/20190417035707-create-real-estate-request.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
return queryInterface.createTable('RealEstateRequests', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
uniqueId: {
|
||||
type: Sequelize.UUID
|
||||
},
|
||||
realEstateType: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
});
|
||||
},
|
||||
down: (queryInterface, Sequelize) => {
|
||||
return queryInterface.dropTable('RealEstateRequests');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
return queryInterface.addColumn(
|
||||
'RealEstateRequests',
|
||||
'city',
|
||||
Sequelize.STRING
|
||||
);
|
||||
},
|
||||
|
||||
down: (queryInterface, Sequelize) => {
|
||||
return queryInterface.removeColumn(
|
||||
'RealEstateRequests',
|
||||
'city'
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
19
app/migrations/20190516180226-rename-place-column.js
Normal file
19
app/migrations/20190516180226-rename-place-column.js
Normal 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'
|
||||
);
|
||||
}
|
||||
};
|
||||
19
app/migrations/20190516222240-rename-city-column.js
Normal file
19
app/migrations/20190516222240-rename-city-column.js
Normal 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'
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
};
|
||||
15
app/migrations/20190523144812-activate-postgis.js
Normal file
15
app/migrations/20190523144812-activate-postgis.js
Normal 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
|
||||
})
|
||||
}
|
||||
};
|
||||
17
app/migrations/20190523151420-add-bounding-box-column.js
Normal file
17
app/migrations/20190523151420-add-bounding-box-column.js
Normal 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
|
||||
})
|
||||
}
|
||||
};
|
||||
27
app/migrations/20190529093410-slider-fields.js
Normal file
27
app/migrations/20190529093410-slider-fields.js
Normal 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 })
|
||||
])
|
||||
})
|
||||
}
|
||||
};
|
||||
63
app/migrations/20190530101945-range-fields.js
Normal file
63
app/migrations/20190530101945-range-fields.js
Normal 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 })
|
||||
])
|
||||
})
|
||||
}
|
||||
};
|
||||
18
app/migrations/20190531111232-subscribed-boolean.js
Normal file
18
app/migrations/20190531111232-subscribed-boolean.js
Normal 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'
|
||||
);
|
||||
}
|
||||
};
|
||||
37
app/migrations/20190618103020-expand-maketalert.js
Normal file
37
app/migrations/20190618103020-expand-maketalert.js
Normal 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 })
|
||||
])
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -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 })
|
||||
])
|
||||
})
|
||||
}
|
||||
};
|
||||
20
app/migrations/20190621162321-add-category-to-marketalert.js
Normal file
20
app/migrations/20190621162321-add-category-to-marketalert.js
Normal 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'
|
||||
);
|
||||
}
|
||||
};
|
||||
37
app/models/index.js
Normal file
37
app/models/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const basename = path.basename(__filename);
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const config = require(__dirname + '/../config/config.json')[env];
|
||||
const db = {};
|
||||
|
||||
let sequelize;
|
||||
if (config.use_env_variable) {
|
||||
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||
} else {
|
||||
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||
}
|
||||
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
|
||||
})
|
||||
.forEach(file => {
|
||||
const model = sequelize['import'](path.join(__dirname, file));
|
||||
db[model.name] = model;
|
||||
});
|
||||
|
||||
Object.keys(db).forEach(modelName => {
|
||||
if (db[modelName].associate) {
|
||||
db[modelName].associate(db);
|
||||
}
|
||||
});
|
||||
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
24
app/models/marketalert.js
Normal file
24
app/models/marketalert.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const MarketAlert = sequelize.define('MarketAlert', {
|
||||
url: DataTypes.STRING,
|
||||
realestateOrigin: DataTypes.STRING,
|
||||
originId: DataTypes.STRING,
|
||||
lastDate: DataTypes.STRING,
|
||||
size : DataTypes.INTEGER,
|
||||
gardenSize : DataTypes.INTEGER,
|
||||
price : DataTypes.INTEGER,
|
||||
municipality : DataTypes.STRING,
|
||||
region : DataTypes.STRING,
|
||||
realEstateType : DataTypes.STRING,
|
||||
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNul: false
|
||||
}
|
||||
}, {});
|
||||
MarketAlert.associate = function(models) {
|
||||
// associations can be defined here
|
||||
};
|
||||
return MarketAlert;
|
||||
};
|
||||
28
app/models/realestaterequest.js
Normal file
28
app/models/realestaterequest.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
|
||||
const RealEstateRequest = sequelize.define('RealEstateRequest', {
|
||||
uniqueId: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
allowNull: false
|
||||
},
|
||||
realEstateType: DataTypes.STRING,
|
||||
email: DataTypes.STRING,
|
||||
region: DataTypes.STRING,
|
||||
municipality: DataTypes.STRING,
|
||||
sizeMin: DataTypes.INTEGER,
|
||||
sizeMax: DataTypes.INTEGER,
|
||||
gardenSizeMin: DataTypes.INTEGER,
|
||||
gardenSizeMax: DataTypes.INTEGER,
|
||||
priceMin: DataTypes.INTEGER,
|
||||
priceMax: DataTypes.INTEGER,
|
||||
bounding_box: DataTypes.GEOMETRY('POINT', 4326),
|
||||
subscribed: DataTypes.BOOLEAN
|
||||
}, {});
|
||||
RealEstateRequest.associate = function(models) {
|
||||
// associations can be defined here
|
||||
};
|
||||
return RealEstateRequest;
|
||||
};
|
||||
BIN
app/public/images/logo.png
Normal file
BIN
app/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
39
app/public/main.css
Normal file
39
app/public/main.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.welcome-center-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.welcome-big-logo {
|
||||
font-size: 200pt;
|
||||
background-image: url(./images/logo.png);
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
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;
|
||||
}
|
||||
58
app/services/crawlerService.js
Normal file
58
app/services/crawlerService.js
Normal 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
6
app/views/gardenSize.ejs
Normal 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
25
app/views/goAgain.ejs
Normal 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>
|
||||
|
||||
22
app/views/layout.ejs
Normal file
22
app/views/layout.ejs
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype>
|
||||
<html>
|
||||
<head>
|
||||
<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/noUiSlider/13.1.5/nouislider.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="/assets/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<%-body%>
|
||||
</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/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>
|
||||
</html>
|
||||
28
app/views/municipality.ejs
Normal file
28
app/views/municipality.ejs
Normal 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>
|
||||
129
app/views/neighborhoodMap.ejs
Normal file
129
app/views/neighborhoodMap.ejs
Normal 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>
|
||||
53
app/views/partials/range.ejs
Normal file
53
app/views/partials/range.ejs
Normal 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
6
app/views/price.ejs
Normal 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
36
app/views/queryReview.ejs
Normal 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
68
app/views/querySubmit.ejs
Normal 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>
|
||||
33
app/views/realEstateType.ejs
Normal file
33
app/views/realEstateType.ejs
Normal 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
29
app/views/region.ejs
Normal 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
6
app/views/size.ejs
Normal 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
14
app/views/unsubscribe.ejs
Normal 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>
|
||||
17
app/views/welcome.ejs
Normal file
17
app/views/welcome.ejs
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
<!-- -->
|
||||
<div class="row center-align">
|
||||
<span class="welcome-big-logo">🤙</span>
|
||||
</div>
|
||||
<div class="row center-align">
|
||||
<div>Sve nekretnine dostupne u oglasima.</div>
|
||||
<div> Na vaš email. </div>
|
||||
<div> BESPLATNO </div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s6 push-s3">
|
||||
<a href="<%= nextStep %>" class="welcome-center-button waves-effect waves-light btn">
|
||||
Javi mi
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
19
backend/.env
19
backend/.env
@@ -1,19 +0,0 @@
|
||||
AWS_ENVIRONMENT=development
|
||||
AWS_ACCESS_KEY_ID=your_key
|
||||
AWS_SECRET_ACCESS_KEY=your_secret
|
||||
AWS_PROFILE=
|
||||
AWS_SESSION_TOKEN=
|
||||
AWS_ROLE_ARN=your_amazon_role
|
||||
AWS_REGION=us-east-1
|
||||
AWS_FUNCTION_NAME=
|
||||
AWS_HANDLER=index.handler
|
||||
AWS_MEMORY_SIZE=128
|
||||
AWS_TIMEOUT=3
|
||||
AWS_DESCRIPTION=
|
||||
AWS_RUNTIME=nodejs6.10
|
||||
AWS_VPC_SUBNETS=
|
||||
AWS_VPC_SECURITY_GROUPS=
|
||||
AWS_TRACING_CONFIG=
|
||||
AWS_LOGS_RETENTION_IN_DAYS=
|
||||
EXCLUDE_GLOBS="event.json"
|
||||
PACKAGE_DIRECTORY=build
|
||||
@@ -1,17 +0,0 @@
|
||||
const Sequelize = require("sequelize");
|
||||
const sequelize = require("./db.js");
|
||||
const MarketAlert = sequelize.define("market_alert", {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
olx_url: Sequelize.STRING,
|
||||
last_date: Sequelize.STRING,
|
||||
email: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MarketAlert;
|
||||
@@ -1,4 +0,0 @@
|
||||
# How to deploy automatically:
|
||||
|
||||
1. set up aws cli with aws configure
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,8 +0,0 @@
|
||||
const Sequelize = require("sequelize");
|
||||
const sequelize = new Sequelize("sql7276322", "sql7276322", "RS53ihYlg9", {
|
||||
host: "sql7.freemysqlhosting.net",
|
||||
dialect: "mysql",
|
||||
operatorsAliases: false
|
||||
});
|
||||
|
||||
module.exports = sequelize;
|
||||
@@ -1 +0,0 @@
|
||||
SECRET_VARIABLE=mysecretval
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"key": "value",
|
||||
"key2": "value2",
|
||||
"other_key": "other_value"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"EventSourceMappings": [
|
||||
{
|
||||
"EventSourceArn": "your event source arn",
|
||||
"StartingPosition": "LATEST",
|
||||
"BatchSize": 100,
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"ScheduleEvents": [
|
||||
{
|
||||
"ScheduleName": "node-lambda-test-schedule",
|
||||
"ScheduleState": "ENABLED",
|
||||
"ScheduleExpression": "rate(1 hour)",
|
||||
"Input":
|
||||
{
|
||||
"key1": "value",
|
||||
"key2": "value"
|
||||
}
|
||||
}
|
||||
],
|
||||
"S3Events": [{
|
||||
"Bucket": "BUCKET_NAME",
|
||||
"Events": [
|
||||
"s3:ObjectCreated:*"
|
||||
],
|
||||
"Filter": {
|
||||
"Key": {
|
||||
"FilterRules": [{
|
||||
"Name": "prefix",
|
||||
"Value": "STRING_VALUE"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
let express = require("express");
|
||||
const path = require("path");
|
||||
const bodyParser = require("body-parser");
|
||||
const MarketAlert = require("./MarketAlert");
|
||||
const sendNotification = require("./utils/sendnotification");
|
||||
const scrapTheItems = require("./utils/scraptheitems");
|
||||
const sequelize = require("./db.js");
|
||||
const Twocheckout = require("2checkout-node");
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
const port = process.env.PORT || 5000;
|
||||
|
||||
app.get("/sendnotifications", async function(req, res) {
|
||||
let marketAlerts = await MarketAlert.findAll();
|
||||
|
||||
let lastDateUpdate = await Promise.all(
|
||||
marketAlerts
|
||||
.map(marketAlert => {
|
||||
const { id, email, olx_url, last_date } = marketAlert.dataValues;
|
||||
return { id, email, olx_url, last_date };
|
||||
})
|
||||
.map(sendNotification)
|
||||
);
|
||||
lastDateUpdate = lastDateUpdate.filter(Boolean(dateUpdate));
|
||||
lastDateUpdate.length &&
|
||||
lastDateUpdate.forEach(dateUpdate =>
|
||||
MarketAlert.update(
|
||||
{ last_date: dateUpdate.date },
|
||||
{ where: { id: dateUpdate.id } }
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/items/:url", async (req, res) => {
|
||||
let url =
|
||||
"https://www.olx.ba/pretraga?" +
|
||||
req.params.url +
|
||||
"&sort_order=desc&sort_po=datum";
|
||||
let appts = await scrapTheItems(url);
|
||||
res.json({
|
||||
last_date: appts[0] && appts[0].date,
|
||||
items: appts
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/marketalerts", function(req, res) {
|
||||
const { email, last_date, olx_url } = req.body;
|
||||
console.log(email, last_date, olx_url);
|
||||
res.json({ message: "Market Alert Created!" });
|
||||
// sequelize.sync().then(() =>
|
||||
// MarketAlert.create({
|
||||
// olx_url,
|
||||
// last_date,
|
||||
// email
|
||||
// })
|
||||
// );
|
||||
// res.json({ message: "Market Alert Created!" });
|
||||
});
|
||||
|
||||
app.post("/payforalert", function(request, response) {
|
||||
let tco = new Twocheckout({
|
||||
sellerId: "901402692",
|
||||
privateKey: "A28DCE5F-9292-405C-8161-F84D8BB83AFC",
|
||||
sandbox: true
|
||||
});
|
||||
|
||||
let params = {
|
||||
merchantOrderId: "123",
|
||||
token: request.body.token,
|
||||
currency: "USD",
|
||||
total: "2.00",
|
||||
billingAddr: {
|
||||
name: "Testing Tester",
|
||||
addrLine1: "123 Test St",
|
||||
city: "Sarajevo",
|
||||
state: "BiH",
|
||||
zipCode: "71000",
|
||||
country: "BiH",
|
||||
email: request.body.email,
|
||||
phoneNumber: "5555555555"
|
||||
}
|
||||
};
|
||||
|
||||
tco.checkout.authorize(params, function(error, data) {
|
||||
if (error) {
|
||||
response.send(error.message);
|
||||
} else {
|
||||
response.send(data.response.responseMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
|
||||
1168
backend/package-lock.json
generated
1168
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Ehvan Gradanin",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"2checkout-node": "0.0.1",
|
||||
"@sendgrid/mail": "^6.3.1",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"express": "^4.16.4",
|
||||
"mysql2": "^1.6.4",
|
||||
"node-fetch": "^2.3.0",
|
||||
"sequelize": "^4.42.0"
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
const scrapTheItems = require("./scraptheitems");
|
||||
const convertToDate = require("./convertToDate");
|
||||
const sgMail = require("@sendgrid/mail");
|
||||
// should be process.env.SENDGRID_API_KEY
|
||||
sgMail.setApiKey(
|
||||
"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"
|
||||
);
|
||||
|
||||
async function sendNotification(marketAlert) {
|
||||
const { id, email, olx_url, last_date } = marketAlert;
|
||||
let url =
|
||||
"https://www.olx.ba/pretraga?" + olx_url + "&sort_order=desc&sort_po=datum";
|
||||
let newItems = await scrapTheItems(url);
|
||||
let lastDate = newItems.length && newItems[0].date;
|
||||
let message =
|
||||
newItems.length &&
|
||||
newItems.reduce(
|
||||
(mes, item) => mes + `<strong>${item.url} i ${item.price}</strong>`,
|
||||
""
|
||||
);
|
||||
const msg = {
|
||||
to: email,
|
||||
from: "test@example.com",
|
||||
subject: "Market Alert",
|
||||
text: "New items on olx",
|
||||
html: message
|
||||
};
|
||||
if (message) {
|
||||
await sgMail.send(msg);
|
||||
return { id, date: String(convertToDate(lastDate)) };
|
||||
}
|
||||
}
|
||||
module.exports = sendNotification;
|
||||
5
development.env
Normal file
5
development.env
Normal 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
|
||||
23
frontend-react/.gitignore
vendored
23
frontend-react/.gitignore
vendored
@@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -1,68 +0,0 @@
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br>
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br>
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
|
||||
17025
frontend-react/package-lock.json
generated
17025
frontend-react/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "frontend-react",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^3.9.0",
|
||||
"@material-ui/icons": "^3.0.2",
|
||||
"axios": "^0.18.0",
|
||||
"history": "^4.7.2",
|
||||
"prettier": "^1.15.3",
|
||||
"rc-slider": "^8.6.4",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-redux": "^6.0.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-scripts": "^2.1.5",
|
||||
"react-select": "^2.2.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-devtools-extension": "^2.13.7",
|
||||
"redux-logger": "^3.0.6"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "NODE_PATH=src/ react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"proxy": "http://localhost:5000/",
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,52 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Roboto+Slab:400,700|Material+Icons"
|
||||
/>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://www.2checkout.com/checkout/api/2co.min.js"
|
||||
></script>
|
||||
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
html {
|
||||
font-family: Roboto, sans-serif;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
* {
|
||||
font-family: Roboto, serif;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
.radio-style {
|
||||
color: #e91e63 !important;
|
||||
}
|
||||
|
||||
.checkbox-style {
|
||||
color: #e91e63 !important;
|
||||
}
|
||||
.label-style {
|
||||
color: white;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { drawerWidth, transition } from "./globalStyle";
|
||||
|
||||
const appStyle = theme => ({
|
||||
wrapper: {
|
||||
top: "0",
|
||||
display: "grid",
|
||||
minHeight: "750px",
|
||||
"grid-template-columns": `${drawerWidth}px 1fr`,
|
||||
"grid-template-areas": `"sidebar content"`,
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
"grid-template-areas": `"sidebar" "content"`,
|
||||
"grid-template-columns": "1fr",
|
||||
"grid-template-rows": "auto 1fr"
|
||||
}
|
||||
},
|
||||
mainPanel: {
|
||||
...transition,
|
||||
overflowScrolling: "touch",
|
||||
"grid-area": "content",
|
||||
zIndex: "1",
|
||||
backgroundColor: "#272727",
|
||||
backgroundImage: "linear-gradient(180deg,#272727, #21525f)"
|
||||
}
|
||||
});
|
||||
|
||||
export default appStyle;
|
||||
@@ -1,42 +0,0 @@
|
||||
const drawerWidth = 360;
|
||||
|
||||
const transition = {
|
||||
transition: "all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1)"
|
||||
};
|
||||
|
||||
const boxShadow = {
|
||||
boxShadow:
|
||||
"0 10px 30px -12px rgba(0, 0, 0, 0.42), 0 4px 25px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2)"
|
||||
};
|
||||
|
||||
const defaultFont = {
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontWeight: "300",
|
||||
lineHeight: "1.5em"
|
||||
};
|
||||
|
||||
const primaryColor = "#e91e63";
|
||||
|
||||
const primaryBoxShadow = {
|
||||
boxShadow:
|
||||
"0 4px 20px 0px rgba(0, 0, 0, 0.14), 0 7px 10px -5px rgba(233, 30, 99, 0.4)"
|
||||
};
|
||||
|
||||
const defaultBoxShadow = {
|
||||
border: "0",
|
||||
borderRadius: "3px",
|
||||
boxShadow:
|
||||
"0 10px 20px -12px rgba(0, 0, 0, 0.42), 0 3px 20px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2)",
|
||||
padding: "10px 0",
|
||||
transition: "all 150ms ease 0s"
|
||||
};
|
||||
|
||||
export {
|
||||
drawerWidth,
|
||||
transition,
|
||||
boxShadow,
|
||||
defaultFont,
|
||||
primaryColor,
|
||||
primaryBoxShadow,
|
||||
defaultBoxShadow
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 272 KiB |
@@ -1,115 +0,0 @@
|
||||
const modalStyle = {
|
||||
modal: {
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "#010000"
|
||||
},
|
||||
modalHeader: {
|
||||
borderBottom: "none",
|
||||
paddingTop: "24px",
|
||||
paddingRight: "24px",
|
||||
paddingBottom: "0",
|
||||
paddingLeft: "24px",
|
||||
minHeight: "16.43px",
|
||||
color: "white"
|
||||
},
|
||||
modalTitle: {
|
||||
margin: "0",
|
||||
lineHeight: "1.42857143"
|
||||
},
|
||||
modalCloseButton: {
|
||||
color: "white",
|
||||
marginTop: "-12px",
|
||||
WebkitAppearance: "none",
|
||||
padding: "0",
|
||||
cursor: "pointer",
|
||||
background: "0 0",
|
||||
border: "0",
|
||||
fontSize: "inherit",
|
||||
opacity: ".9",
|
||||
textShadow: "none",
|
||||
fontWeight: "700",
|
||||
lineHeight: "1",
|
||||
float: "right"
|
||||
},
|
||||
modalClose: {
|
||||
width: "16px",
|
||||
height: "16px"
|
||||
},
|
||||
modalBody: {
|
||||
paddingTop: "24px",
|
||||
paddingRight: "24px",
|
||||
paddingBottom: "16px",
|
||||
paddingLeft: "24px",
|
||||
position: "relative"
|
||||
},
|
||||
modalFooter: {
|
||||
padding: "15px",
|
||||
textAlign: "right",
|
||||
paddingTop: "0",
|
||||
margin: "0"
|
||||
},
|
||||
modalFooterCenter: {
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto"
|
||||
},
|
||||
whiteText: {
|
||||
color: "white"
|
||||
},
|
||||
inputStyle: {
|
||||
color: "white",
|
||||
"&:after": {
|
||||
borderBottom: "1px solid #e91e63"
|
||||
}
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: "#e91e63 !important",
|
||||
color: "white !important"
|
||||
},
|
||||
closeButton: {
|
||||
backgroundColor: "white",
|
||||
color: "#e91e63"
|
||||
},
|
||||
checkBoxStyle: {
|
||||
color: "#e91e63 !important"
|
||||
},
|
||||
tooltip: {
|
||||
padding: "10px 15px",
|
||||
minWidth: "130px",
|
||||
color: "#e91e63",
|
||||
lineHeight: "1.7em",
|
||||
background: "white",
|
||||
border: "none",
|
||||
borderRadius: "3px",
|
||||
boxShadow:
|
||||
"0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2)",
|
||||
maxWidth: "200px",
|
||||
textAlign: "center",
|
||||
fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif',
|
||||
fontSize: "0.875em",
|
||||
fontStyle: "normal",
|
||||
fontWeight: "400",
|
||||
textShadow: "none",
|
||||
textTransform: "none",
|
||||
letterSpacing: "normal",
|
||||
wordBreak: "normal",
|
||||
wordSpacing: "normal",
|
||||
wordWrap: "normal",
|
||||
whiteSpace: "normal",
|
||||
lineBreak: "auto",
|
||||
zIndex: 5000
|
||||
},
|
||||
buttonStyle: {
|
||||
width: "150px",
|
||||
margin: "0 auto",
|
||||
position: "relative",
|
||||
borderRadius: "8px",
|
||||
display: "block",
|
||||
padding: "10px 10px",
|
||||
textAlign: "center",
|
||||
marginBottom: "20px",
|
||||
color: "white",
|
||||
backgroundColor: "#e91e63",
|
||||
border: "1px solid #e91e63"
|
||||
}
|
||||
};
|
||||
export default modalStyle;
|
||||
@@ -1,28 +0,0 @@
|
||||
.rc-slider-handle {
|
||||
border: solid 2px #e91e63 !important;
|
||||
}
|
||||
.rc-slider-handle:hover {
|
||||
border: solid 2px #e91e63 !important;
|
||||
}
|
||||
.rc-slider-rail {
|
||||
background-color: #e91e63 !important;
|
||||
width: 20px;
|
||||
}
|
||||
.rc-slider-track {
|
||||
background-color: grey !important;
|
||||
}
|
||||
.input-style:after {
|
||||
border-bottom: 1px solid #e91e63 !important;
|
||||
}
|
||||
.input-style {
|
||||
color: white !important;
|
||||
width: 80% !important;
|
||||
margin: 0 auto !important;
|
||||
position: relative !important;
|
||||
display: block !important;
|
||||
margin: 10px auto !important;
|
||||
}
|
||||
.rc-slider {
|
||||
width: 80% !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
import {
|
||||
boxShadow,
|
||||
defaultFont,
|
||||
primaryColor,
|
||||
primaryBoxShadow
|
||||
} from "./globalStyle";
|
||||
|
||||
const sidebarStyle = theme => ({
|
||||
drawerPaper: {
|
||||
border: "none",
|
||||
position: "relative",
|
||||
"overflow-y": "auto",
|
||||
"overflow-x": "none",
|
||||
zIndex: "1",
|
||||
...boxShadow,
|
||||
"grid-area": "sidebar",
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
position: "relative"
|
||||
}
|
||||
},
|
||||
logo: {
|
||||
position: "relative",
|
||||
padding: "15px 15px",
|
||||
zIndex: "4",
|
||||
"&:after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: "0",
|
||||
|
||||
height: "1px",
|
||||
right: "15px",
|
||||
width: "calc(100% - 30px)",
|
||||
backgroundColor: "rgba(180, 180, 180, 0.3)"
|
||||
}
|
||||
},
|
||||
logoLink: {
|
||||
...defaultFont,
|
||||
textTransform: "uppercase",
|
||||
padding: "5px 0",
|
||||
display: "block",
|
||||
fontSize: "18px",
|
||||
textAlign: "left",
|
||||
fontWeight: "400",
|
||||
lineHeight: "30px",
|
||||
textDecoration: "none",
|
||||
backgroundColor: "transparent",
|
||||
"&,&:hover": {
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
},
|
||||
logoImage: {
|
||||
width: "30px",
|
||||
display: "inline-block",
|
||||
maxHeight: "30px",
|
||||
marginLeft: "10px",
|
||||
marginRight: "15px"
|
||||
},
|
||||
img: {
|
||||
width: "35px",
|
||||
top: "22px",
|
||||
position: "absolute",
|
||||
verticalAlign: "middle",
|
||||
border: "0"
|
||||
},
|
||||
background: {
|
||||
position: "absolute",
|
||||
zIndex: "1",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "block",
|
||||
top: "0",
|
||||
left: "0",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center center",
|
||||
"&:after": {
|
||||
position: "absolute",
|
||||
zIndex: "3",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
content: '""',
|
||||
display: "block",
|
||||
background: "#000",
|
||||
opacity: ".8"
|
||||
}
|
||||
},
|
||||
list: {
|
||||
marginTop: "20px",
|
||||
paddingLeft: "0",
|
||||
paddingTop: "0",
|
||||
paddingBottom: "0",
|
||||
marginBottom: "0",
|
||||
listStyle: "none",
|
||||
position: "unset"
|
||||
},
|
||||
item: {
|
||||
position: "relative",
|
||||
display: "block",
|
||||
textDecoration: "none",
|
||||
"&:hover,&:focus,&:visited,&": {
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
},
|
||||
itemLink: {
|
||||
width: "auto",
|
||||
transition: "all 300ms linear",
|
||||
margin: "10px 15px 0",
|
||||
borderRadius: "3px",
|
||||
position: "relative",
|
||||
display: "block",
|
||||
padding: "10px 15px",
|
||||
backgroundColor: "transparent",
|
||||
...defaultFont
|
||||
},
|
||||
itemIcon: {
|
||||
width: "24px",
|
||||
height: "30px",
|
||||
fontSize: "24px",
|
||||
lineHeight: "30px",
|
||||
float: "left",
|
||||
marginRight: "15px",
|
||||
textAlign: "center",
|
||||
verticalAlign: "middle",
|
||||
color: "rgba(255, 255, 255, 0.8)"
|
||||
},
|
||||
itemText: {
|
||||
...defaultFont,
|
||||
margin: "0",
|
||||
lineHeight: "30px",
|
||||
fontSize: "14px",
|
||||
color: "#FFFFFF"
|
||||
},
|
||||
whiteFont: {
|
||||
color: "#FFFFFF"
|
||||
},
|
||||
|
||||
sidebarWrapper: {
|
||||
width: "360px",
|
||||
position: "relative",
|
||||
overflow: "auto",
|
||||
zIndex: "4",
|
||||
overflowScrolling: "touch"
|
||||
},
|
||||
whiteText: {
|
||||
zIndex: "5000",
|
||||
color: "white"
|
||||
},
|
||||
collapsedItemStyle: {
|
||||
zIndex: "5000",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: primaryColor,
|
||||
color: "#FFFFFF",
|
||||
...primaryBoxShadow
|
||||
},
|
||||
cursor: "pointer"
|
||||
},
|
||||
collapse: {
|
||||
width: "80%",
|
||||
margin: "0 auto"
|
||||
},
|
||||
checkedItem: {
|
||||
backgroundColor: primaryColor,
|
||||
color: "#FFFFFF",
|
||||
...primaryBoxShadow
|
||||
},
|
||||
subOptionIndent: {
|
||||
paddingLeft: "20px"
|
||||
}
|
||||
});
|
||||
|
||||
export default sidebarStyle;
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import withStyles from "@material-ui/core/styles/withStyles";
|
||||
import { connect } from "react-redux";
|
||||
import { ITEMS_CHANGED, USER_DATA_CHANGED } from "constants/actionTypes";
|
||||
import { areObjectEqual } from "utils/helpers";
|
||||
import { createOlxLink } from "utils/createOlxLink";
|
||||
import axios from "axios";
|
||||
|
||||
import image from "assets/img/sidebar-1.jpg";
|
||||
import logo from "assets/img/reactlogo.png";
|
||||
|
||||
import Sidebar from "components/Sidebar.js";
|
||||
import ItemsContainer from "./items/itemscontainer/ItemsContainer";
|
||||
import NotificationModal from "./NotificationModal";
|
||||
|
||||
import dashboardStyle from "assets/dashboardStyle.js";
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
category: state.category,
|
||||
options: state.options,
|
||||
subcategory: state.subcategory,
|
||||
items: state.items,
|
||||
userdata: state.userdata
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onItemsChanged: items => dispatch({ type: ITEMS_CHANGED, items }),
|
||||
onUserDataChange: change => dispatch({ type: USER_DATA_CHANGED, ...change })
|
||||
});
|
||||
|
||||
let lastUpdateTime = null;
|
||||
let interval = null;
|
||||
class App extends React.Component {
|
||||
componentDidMount() {
|
||||
interval = setInterval(() => {
|
||||
if (lastUpdateTime && Date.now() - lastUpdateTime > 2000) {
|
||||
const {
|
||||
category,
|
||||
options,
|
||||
subcategory,
|
||||
onItemsChanged,
|
||||
onUserDataChange
|
||||
} = this.props;
|
||||
let url = createOlxLink(category, subcategory, options);
|
||||
url = encodeURI(url);
|
||||
onUserDataChange({ info: "olx_url", value: url });
|
||||
if (url) {
|
||||
axios
|
||||
.get(`/items/${url}`)
|
||||
.then(response => {
|
||||
onItemsChanged(response.data.items);
|
||||
onUserDataChange({
|
||||
info: "last_date",
|
||||
value: response.data.last_date
|
||||
});
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
lastUpdateTime = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(interval);
|
||||
}
|
||||
componentWillReceiveProps(newProps) {
|
||||
const { subcategory, category, options } = this.props;
|
||||
if (
|
||||
newProps.subcategory !== subcategory ||
|
||||
newProps.category !== category ||
|
||||
!areObjectEqual(newProps.options, options)
|
||||
) {
|
||||
lastUpdateTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, classes } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<Sidebar logoText={"Market Alarm"} logo={logo} image={image} />
|
||||
|
||||
<div className={classes.mainPanel}>
|
||||
<ItemsContainer />
|
||||
{items.length ? <NotificationModal /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
App.propTypes = {
|
||||
classes: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(dashboardStyle)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App)
|
||||
);
|
||||
@@ -1,278 +0,0 @@
|
||||
import React from "react";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import DialogActions from "@material-ui/core/DialogActions";
|
||||
import Slide from "@material-ui/core/Slide";
|
||||
import withStyles from "@material-ui/core/styles/withStyles";
|
||||
import { notificationmodalwrapper } from "utils/notificationmodalwrapper";
|
||||
import modalStyle from "assets/modalStyle.js";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import Close from "@material-ui/icons/Close";
|
||||
import Input from "@material-ui/core/Input";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Tooltip from "@material-ui/core/Tooltip";
|
||||
import axios from "axios";
|
||||
|
||||
function Transition(props) {
|
||||
return <Slide direction="down" {...props} />;
|
||||
}
|
||||
|
||||
let token = "";
|
||||
class NotificationModal extends React.Component {
|
||||
componentDidMount() {
|
||||
console.log("load sandbox");
|
||||
window.TCO.loadPubKey("sandbox");
|
||||
}
|
||||
handleOpen = () => {
|
||||
this.props.onModalOpen();
|
||||
};
|
||||
handleClose = () => {
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
checkEmail = email =>
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
email
|
||||
);
|
||||
|
||||
handleEmail = e => {
|
||||
this.props.onUserDataChange({ info: "email", value: e.target.value });
|
||||
this.props.onUserDataChange({
|
||||
info: "validEmail",
|
||||
value: this.checkEmail(e.target.value)
|
||||
});
|
||||
};
|
||||
|
||||
handleInput = (e, infoName) => {
|
||||
this.props.onUserDataChange({ info: infoName, value: e.target.value });
|
||||
};
|
||||
|
||||
isChecked = optionName => Boolean(this.props.userdata[optionName]);
|
||||
|
||||
optionChange = optionName => {
|
||||
const { userdata } = this.props;
|
||||
this.props.onUserDataChange({
|
||||
info: optionName,
|
||||
value: !userdata[optionName]
|
||||
});
|
||||
};
|
||||
|
||||
successCallback = data => {
|
||||
token = data.response.token.token;
|
||||
const {
|
||||
userdata: { email, last_date, olx_url }
|
||||
} = this.props;
|
||||
|
||||
axios
|
||||
.post("/marketalerts", {
|
||||
email,
|
||||
last_date,
|
||||
olx_url
|
||||
})
|
||||
.then(response =>
|
||||
axios
|
||||
.post("/payforalert", {
|
||||
email,
|
||||
token
|
||||
})
|
||||
.then(response => {
|
||||
this.handleClose();
|
||||
alert("Market Alert Created");
|
||||
})
|
||||
.catch(error => console.log(error))
|
||||
)
|
||||
.catch(error => console.log(error));
|
||||
};
|
||||
|
||||
errorCallback = data => {
|
||||
if (data.errorCode === 200) {
|
||||
this.tokenRequest();
|
||||
} else {
|
||||
alert(data.errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
tokenRequest = () => {
|
||||
const {
|
||||
userdata: { ccNo, expYear, expMonth, cvv }
|
||||
} = this.props;
|
||||
const sellerId = "901402692";
|
||||
const publishableKey = "93546B8D-B726-4376-A6DF-F698FD8893CA";
|
||||
var args = {
|
||||
sellerId,
|
||||
publishableKey,
|
||||
ccNo,
|
||||
cvv,
|
||||
expMonth,
|
||||
expYear
|
||||
};
|
||||
window.TCO.requestToken(this.successCallback, this.errorCallback, args);
|
||||
};
|
||||
|
||||
handleSaveMarketAlert = () => {
|
||||
this.tokenRequest();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
modal,
|
||||
classes,
|
||||
userdata: { validEmail }
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className={classes.buttonStyle} onClick={this.handleOpen}>
|
||||
Napravi notifikaciju
|
||||
</button>
|
||||
<Dialog
|
||||
classes={{
|
||||
root: classes.center,
|
||||
paper: classes.modal
|
||||
}}
|
||||
open={modal}
|
||||
TransitionComponent={Transition}
|
||||
keepMounted
|
||||
onClose={() => this.handleClose()}
|
||||
aria-labelledby="classic-modal-slide-title"
|
||||
aria-describedby="classic-modal-slide-description"
|
||||
>
|
||||
<DialogTitle
|
||||
id="classic-modal-slide-title"
|
||||
disableTypography
|
||||
className={classes.modalHeader}
|
||||
>
|
||||
<IconButton
|
||||
className={classes.modalCloseButton}
|
||||
key="close"
|
||||
aria-label="Close"
|
||||
color="inherit"
|
||||
onClick={() => this.handleClose()}
|
||||
>
|
||||
<Close className={classes.modalClose} />
|
||||
</IconButton>
|
||||
<h4 className={classes.modalTitle}>Save Market Alert</h4>
|
||||
</DialogTitle>
|
||||
<DialogContent
|
||||
id="classic-modal-slide-description"
|
||||
className={classes.modalBody}
|
||||
>
|
||||
<FormControlLabel
|
||||
className={classes.whiteText}
|
||||
control={
|
||||
<Checkbox
|
||||
className={classes.checkBoxStyle}
|
||||
checked={this.isChecked("emailChecked")}
|
||||
type={"checkbox"}
|
||||
value={""}
|
||||
onChange={() => this.optionChange("emailChecked")}
|
||||
/>
|
||||
}
|
||||
label={<Typography style={{ color: "white" }}>Email</Typography>}
|
||||
/>
|
||||
|
||||
{this.isChecked("emailChecked") ? (
|
||||
<div>
|
||||
<Input
|
||||
className={classes.inputStyle}
|
||||
placeholder="Email"
|
||||
inputProps={{
|
||||
"aria-label": "Email"
|
||||
}}
|
||||
type="email"
|
||||
onChange={this.handleEmail}
|
||||
/>
|
||||
<Input
|
||||
className={classes.inputStyle}
|
||||
type="hidden"
|
||||
value={token}
|
||||
/>
|
||||
<Input
|
||||
className={classes.inputStyle}
|
||||
placeholder="Card Number"
|
||||
inputProps={{
|
||||
"aria-label": "Card Number"
|
||||
}}
|
||||
required
|
||||
autoComplete="off"
|
||||
type="number"
|
||||
onChange={e => this.handleInput(e, "ccNo")}
|
||||
/>
|
||||
<Input
|
||||
className={classes.inputStyle}
|
||||
placeholder="Expiration Year"
|
||||
inputProps={{
|
||||
"aria-label": "Expiration Year"
|
||||
}}
|
||||
required
|
||||
autoComplete="off"
|
||||
type="number"
|
||||
onChange={e => this.handleInput(e, "expYear")}
|
||||
/>
|
||||
<Input
|
||||
className={classes.inputStyle}
|
||||
placeholder="Expiration Month"
|
||||
inputProps={{
|
||||
"aria-label": "Expiration Month"
|
||||
}}
|
||||
required
|
||||
autoComplete="off"
|
||||
type="number"
|
||||
onChange={e => this.handleInput(e, "expMonth")}
|
||||
/>
|
||||
<Input
|
||||
className={classes.inputStyle}
|
||||
placeholder="CVV"
|
||||
inputProps={{
|
||||
"aria-label": "CVV"
|
||||
}}
|
||||
required
|
||||
autoComplete="off"
|
||||
type="number"
|
||||
onChange={e => this.handleInput(e, "cvv")}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions className={classes.modalFooter}>
|
||||
{validEmail ? (
|
||||
<Button
|
||||
onClick={this.handleSaveMarketAlert}
|
||||
className={classes.saveButton}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip
|
||||
title="Provide a valid email"
|
||||
placement="top"
|
||||
classes={{ tooltip: classes.tooltip }}
|
||||
>
|
||||
<div>
|
||||
<Button disabled className={classes.saveButton}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className={classes.closeButton}
|
||||
onClick={() => this.handleClose()}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(modalStyle)(
|
||||
notificationmodalwrapper(NotificationModal)
|
||||
);
|
||||
@@ -1,117 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import withStyles from "@material-ui/core/styles/withStyles";
|
||||
import List from "@material-ui/core/List";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
import Dashboard from "@material-ui/icons/Dashboard";
|
||||
import ExpandLess from "@material-ui/icons/ExpandLess";
|
||||
import ExpandMore from "@material-ui/icons/ExpandMore";
|
||||
import Collapse from "@material-ui/core/Collapse";
|
||||
import StarBorder from "@material-ui/icons/StarBorder";
|
||||
|
||||
import sidebarStyle from "assets/sidebarStyle.js";
|
||||
import CollapseWrapperStyled from "components/widgets/CollapseWrapperStyled";
|
||||
import DeepCategoryWrapper from "./widgets/DeepCategoryWrapper";
|
||||
import { hoc } from "utils/helpers";
|
||||
|
||||
import * as Vozila from "./categories/Vozila";
|
||||
import * as Nekretnine from "./categories/Nekretnine";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { CATEGORY_SELECT } from "constants/actionTypes";
|
||||
|
||||
const options = [
|
||||
{ value: "Nekretnine", label: "Nekretnine" }
|
||||
];
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
category: state.category
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onCategoryChanged: option => dispatch({ type: CATEGORY_SELECT, option })
|
||||
});
|
||||
|
||||
class Sidebar extends React.Component {
|
||||
handleCategoryChange = selectedOption => {
|
||||
this.props.onCategoryChanged(selectedOption);
|
||||
};
|
||||
render() {
|
||||
const { classes, logo, image, logoText, category } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.drawerPaper}>
|
||||
<div className={classes.logo}>
|
||||
<a href="https://www.creative-tim.com" className={classes.logoLink}>
|
||||
<div className={classes.logoImage}>
|
||||
<img src={logo} alt="logo" className={classes.img} />
|
||||
</div>
|
||||
{logoText}
|
||||
</a>
|
||||
</div>
|
||||
<div className={classes.sidebarWrapper}>
|
||||
<List role="menu">
|
||||
<Collapse in={true} timeout="auto" unmountOnExit>
|
||||
<List disablePadding>
|
||||
{options.map(({ label, value }, index) => (
|
||||
<ListItem
|
||||
onClick={() => this.handleCategoryChange({ label, value })}
|
||||
className={
|
||||
classes.nested +
|
||||
" " +
|
||||
classes.collapsedItemStyle +
|
||||
" " +
|
||||
(category && category.value === value
|
||||
? classes.checkedItem
|
||||
: "")
|
||||
}
|
||||
key={index}
|
||||
>
|
||||
<ListItemIcon className={classes.whiteText}>
|
||||
<StarBorder />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
className={classes.whiteText}
|
||||
primary={label}
|
||||
disableTypography={true}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
|
||||
{hoc(category && category.value, {
|
||||
Nekretnine: (
|
||||
<CollapseWrapperStyled componentName="Podkategorija">
|
||||
<DeepCategoryWrapper {...Nekretnine.properties} />
|
||||
</CollapseWrapperStyled>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
{image !== undefined ? (
|
||||
<div
|
||||
className={classes.background}
|
||||
style={{ backgroundImage: "url(" + image + ")" }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Sidebar.propTypes = {
|
||||
classes: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(sidebarStyle)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Sidebar)
|
||||
);
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import Stanovi from "../subcategories/nekretnine/Stanovi";
|
||||
import Kuce from "../subcategories/nekretnine/Kuce";
|
||||
|
||||
const options = [{ value: 23, label: "Stanovi" }, { value: 24, label: "Kuce" }];
|
||||
const depth = 0;
|
||||
const childrenComponents = {
|
||||
23: <Stanovi />,
|
||||
24: <Kuce />
|
||||
};
|
||||
|
||||
export const properties = {
|
||||
options,
|
||||
depth,
|
||||
childrenComponents
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import Automobili from "../subcategories/vozila/Automobili";
|
||||
import Motocikli from "../subcategories/vozila/Motocikli";
|
||||
|
||||
const options = [
|
||||
{ value: 18, label: "Automobili" },
|
||||
{ value: 21, label: "Motocikli" }
|
||||
];
|
||||
const depth = 0;
|
||||
const childrenComponents = {
|
||||
18: <Automobili />,
|
||||
21: <Motocikli />
|
||||
};
|
||||
|
||||
export const properties = {
|
||||
options,
|
||||
depth,
|
||||
childrenComponents
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user