Compare commits

...

135 Commits

Author SHA1 Message Date
Nedim Uka
a457226207 Fixed margin for navbar 2019-07-29 14:39:31 +02:00
Nedim Uka
4f35305b0a Show only 10 realestates in notificatio email 2019-07-29 13:04:25 +02:00
Nedim Uka
09792db21c Merge branch 'marketalerts-page' into 'master'
Real Estate Page

See merge request saburly/marketalarm/web!24
2019-07-15 09:48:08 +00:00
Nedim Uka
1999d45cb2 Changed template name 2019-07-15 11:47:41 +02:00
Nedim Uka
778b5ff411 Fixed bug, for duplicate results for 2 similar re reqests of one user 2019-07-15 11:40:28 +02:00
Nedim Uka
81c30c36ec Added realestate link to bulk email 2019-07-12 18:00:02 +02:00
Nedim Uka
753a09aa36 Fixed crawler not reading and comparing all RERequest results 2019-07-12 16:13:03 +02:00
Bilal Catic
4517624fa8 Merge branch 'nav-bar' into 'master'
Handle nav-bar

See merge request saburly/marketalarm/web!23
2019-07-12 13:22:16 +00:00
Nedim Uka
f9abf48f61 Removed unecessary comments 2019-07-12 10:53:23 +02:00
Nedim Uka
afeffe8c71 Added roboto font 2019-07-11 14:33:59 +02:00
Nedim Uka
a6bd63b7b8 Handle nav-bar 2019-07-11 14:25:38 +02:00
Bilal Catic
e305c547e1 Merge branch 'fetch-optimisation' into 'master'
Fetch optimization

See merge request saburly/marketalarm/web!22
2019-07-10 14:35:37 +00:00
Nedim Uka
33f9e37d93 Filter data by geolocation now sets hasLocation boolean instead of excluding results 2019-07-10 15:21:46 +02:00
Nedim Uka
5829de64e0 Added hrefs to global varialbe 2019-07-10 12:39:32 +02:00
Bilal Catic
efea857889 Merge branch 'services-scheduler' into 'master'
Added node schedule to run crawler and notification service

See merge request saburly/marketalarm/web!21
2019-07-09 21:51:15 +00:00
Nedim Uka
a43723485c Added node schedule to run crawler and notification service 2019-07-09 16:33:00 +02:00
Nedim Uka
1b098f181c Reduced pager to 5 pages at a time 2019-07-08 13:02:28 +02:00
Bilal Catic
2dd1eaa5fd Merge branch 'crawler-optimisation' into 'master'
Crawler optimisation

See merge request saburly/marketalarm/web!20
2019-07-08 08:01:48 +00:00
Nedim Uka
039b1a6376 Optimiset crawlers , and pagingation 2019-07-05 17:18:47 +02:00
Nedim Uka
222a134bbf Optimised crawler speed by using promises 2019-07-04 17:28:09 +02:00
Nedim Uka
0672f3c019 Changed template name 2019-07-04 09:51:04 +02:00
Bilal Catic
e4b3e3961d Merge branch 'notification-email-subject' into 'master'
Notification email subject

See merge request saburly/marketalarm/web!19
2019-07-04 07:44:49 +00:00
Nedim Uka
a807cb5bf2 Bulk emali subject 2019-07-03 16:01:55 +02:00
Nedim Uka
b79a274f96 Added formated subject to bulk email 2019-07-02 21:49:56 +02:00
Bilal Catic
7f0b2d299e Merge branch 'send-notification' into 'master'
Send notification

See merge request saburly/marketalarm/web!18
2019-07-02 10:32:11 +00:00
Nedim Uka
8b20f0e170 Formated title 2019-07-02 12:25:22 +02:00
Nedim Uka
93c147e73b Looged amazon send bulk email response, fixed some emails not sent bug 2019-07-02 11:54:33 +02:00
Nedim Uka
96e9da1fb1 Send templated bulk email, and remember notifed marketalerts 2019-06-28 18:06:19 +02:00
Nedim Uka
b3baffe174 Send notification email 2019-06-27 17:29:57 +02:00
Nedim Uka
208faa08df Added send notification service, and queried unsent marketalerts, fixed some issues with crawler, and added proper logging 2019-06-25 17:07:02 +02:00
Bilal Catic
5ffdaef1bf Merge branch 'crawler-service' into 'master'
Crawler service

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

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

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

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

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

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

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

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

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

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

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

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

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

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

See merge request saburly/marketalarm/web!2
2019-04-30 12:07:09 +00:00
MirnaM
59723410b6 Update README 2019-04-30 09:47:50 +02:00
MirnaM
51ed3551c7 Use default postgres port 2019-04-30 09:19:52 +02:00
MirnaM
58177a8cce Add dockerfile 2019-04-30 09:06:46 +02:00
Senad Uka
864b917b4f Make place selection possible 2019-04-30 06:48:41 +02:00
Senad Uka
a2f6f033bf Places almost finished 2019-04-28 11:13:46 +02:00
Senad Uka
64f2cb82a8 Scrape kantoni into html file 2019-04-28 09:02:46 +02:00
Senad Uka
17492eb52c City is now saved 2019-04-27 07:08:36 +02:00
Senad Uka
298c901759 Added migrations and saving real estate type correctly 2019-04-20 05:26:14 +02:00
Senad Uka
c534c1ee34 Added a new model - does not work yet 2019-04-16 06:27:11 +02:00
Senad Uka
2380c85122 Now posting the type of real estate 2019-04-15 06:56:03 +02:00
Senad Uka
0f7e9f9285 Type of real estate 2019-04-14 06:01:37 +02:00
Senad Uka
dee4df9bd8 Added compression 2019-04-13 10:38:25 +02:00
Senad Uka
4248e6304a Poruka 2019-04-13 10:27:35 +02:00
Senad Uka
9fd9fe8b82 Fixed css 2019-04-12 06:47:51 +02:00
Senad Uka
467d551857 Logo set up 2019-04-12 05:40:52 +02:00
Senad Uka
28f95b9c05 Logo and button - logo unfinished 2019-04-11 05:27:55 +02:00
Senad Uka
9aba66c273 Logo and button - logo unfinished 2019-04-11 05:27:34 +02:00
Senad Uka
d03e85a0dc Switched to materializecss + jquery 2019-04-10 05:17:39 +02:00
Senad Uka
9d49e72bb4 Fix mixed content 2019-04-10 05:01:19 +02:00
Senad Uka
efe2dd66a3 Heroku fix fix 2019-04-09 06:11:41 +02:00
Senad Uka
5f2fee504a Heroku postbuild fix 2019-04-09 06:08:22 +02:00
Senad Uka
add905c793 Saved what was unsaved 2019-04-09 06:04:17 +02:00
Senad Uka
5b25068009 Add first two pages (placeholders), and navigation mechanism 2019-04-09 06:01:21 +02:00
Senad Uka
262f71164c Removed react version 2019-03-26 05:10:21 +01:00
Senad Uka
a6fdf259a0 Hello world is now in a view 2019-03-26 05:09:53 +01:00
Senad Uka
da241c8200 Index now works 2019-03-26 05:06:15 +01:00
Senad Uka
8c7f26b099 Refactored backend 2019-03-25 05:16:58 +01:00
Senad Uka
0d7c154958 Express / EJS based app 2019-03-23 05:08:21 +01:00
Senad Uka
2ceb6805ce Configured wizard 2019-03-22 06:34:15 +01:00
Senad Uka
d240fcbcda Added the wizard component 2019-03-22 06:22:50 +01:00
142 changed files with 7264 additions and 19766 deletions

1
.gitignore vendored
View File

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

11
Dockerfile Normal file
View 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"]

View File

@@ -1,4 +1,43 @@
# 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
### AWS SES
- AWS SES credentials are handled with env vratiables
- Notification emails are sent in batches of 50, by using SES templates
- Make sure that you are using different templates for different envirorments

15
app/config/config.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,45 @@
const { currentRERequest } = require('../helpers/url');
const { getRealEstateTypeEnum } = require('../helpers/enums');
const getGardenSize = (req,res) => {
const title = "Koliko okućnice tražite ?"
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, title });
};
const postGardenSize = async (req, res) => {
const request = await currentRERequest(req);
const nextStepPage = req.query.nextStep || 'cijena';
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
const realEstateType = getRealEstateTypeEnum(request.realEstateType);
if (realEstateType && realEstateType.hasGardenSize) {
request.gardenSizeMin = req.body.from;
request.gardenSizeMax = req.body.to;
await request.save();
}
res.redirect(nextStepUrl);
};
module.exports = {
getGardenSize,
postGardenSize
};

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
const { currentRERequest } = require('../helpers/url');
const getNeighborhood = async (req, res) => {
const title = "U kojem naselju tražite nekretninu?"
const municipality = req.params.municipality
const nextStep = req.query.nextStep || '/';
res.render('neighborhoodMap', {
nextStep,
municipality,
title
});
};
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
};

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

@@ -0,0 +1,42 @@
const { currentRERequest } = require('../helpers/url');
const getPrice = (req,res) => {
const title = "Koja Vam okvirna cijena odgovara ?"
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, title });
};
const postPrice = async (req, res) => {
const request = await currentRERequest(req);
const nextStepPage = req.query.nextStep || 'pregled';
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
request.priceMin = req.body.from;
request.priceMax = req.body.to;
await request.save();
res.redirect(nextStepUrl);
};
module.exports = {
getPrice,
postPrice
};

View File

@@ -0,0 +1,88 @@
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 title = "Da li je ovo to što ste tražili ?"
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,
title
});
};
const postQueryReview = async (req, res) => {
const request = await currentRERequest(req);
const nextStep = req.query.nextStep || `/posalji/${request.uniqueId}`;
res.redirect(nextStep);
};
module.exports = {
getQueryReview,
postQueryReview
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
const { currentRERequest } = require('../helpers/url');
const { getRegions } = require('../helpers/codes');
const regions = getRegions();
const getRegion = (req,res) => {
const title = "U kojoj regiji tražite nekretninu?"
res.render('region', { regions, title });
};
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
};

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

@@ -0,0 +1,43 @@
const { currentRERequest } = require('../helpers/url');
const { sizes, getRealEstateTypeEnum } = require('../helpers/enums');
const getSize = (req,res) => {
const title = "Od koliko kvadrata tražite nekretninu ?"
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, title });
};
const postSize = async (req, res) => {
const request = await currentRERequest(req);
const realEstateType = getRealEstateTypeEnum(request.realEstateType);
const nextStep = realEstateType && realEstateType.hasGardenSize ? 'okucnica' : 'cijena';
const nextStepPage = req.query.nextStep || nextStep;
const nextStepUrl = `/${nextStepPage}/${request.uniqueId}`;
request.sizeMin = req.body.from;
request.sizeMax = req.body.to;
await request.save();
res.redirect(nextStepUrl);
};
module.exports = {
getSize,
postSize
};

View File

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

View File

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

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

@@ -0,0 +1,253 @@
const dotenv = require('dotenv').config();
const { getRealEstateTypeEnum } = require('./enums');
const { getRegionName, getMunicipalityName } = require('./codes');
const { allRERequestByUiid } = require('./db/dbHelper');
var AWS = require('aws-sdk');
const TEMPLATE_NAME = process.env.AMAZON_NOTIFICATION_EMAIL_TEMPLATE;
const NUMBER_OF_RESULTS_TO_SHOW=10;
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: getGreetingsEmailHTML(request)
},
Text: {
Charset: "UTF-8",
Data: getGreetingsEmaiTextVersion(request)
}
},
Subject: {
Charset: 'UTF-8',
Data: `Javimi Potvrda: ${getSubject(request.realEstateType, 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 getGreetingsEmailHTML = (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 getGreetingsEmaiTextVersion = (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;
}
const sendBulkEmail = async (marketAlerts) => {
try {
destinations = []
groupedRERequests = [];
const RERequestUuidsMaped = marketAlerts.map(marketAlert => marketAlert.request);
const RERequestUuidsArray = Array.from(new Set(RERequestUuidsMaped));
const RERequestUuids = RERequestUuidsArray.map(marketAlert => {
return { uniqueId: marketAlert }
});
const RERequests = await allRERequestByUiid(RERequestUuids);
const requestDataValues = [];
RERequests.forEach(RERequest => {
var formatedRequest = {};
formatedRequest[RERequest.uniqueId] =
requestDataValues[RERequest.uniqueId] = {
realEstateType: RERequest.realEstateType,
region: RERequest.region,
municipality: RERequest.municipality,
requestUrl : `${process.env.APP_URL}/nekretnine/${RERequest.uniqueId}`
};
});
marketAlerts.forEach(marketAlert => {
const requestObject = {
email: marketAlert.email,
realEstateType: requestDataValues[marketAlert.request].realEstateType,
municipality: requestDataValues[marketAlert.request].municipality,
region: requestDataValues[marketAlert.request].region,
requestUrl: requestDataValues[marketAlert.request].requestUrl
}
if (!groupedRERequests[marketAlert.request]) {
groupedRERequests[marketAlert.request] = {
requestObject: requestObject,
marketAlertArray: []
};
}
groupedRERequests[marketAlert.request].marketAlertArray.push({
url: marketAlert.url,
title: marketAlert.title,
});
});
for (request in groupedRERequests) {
const marketAlert = groupedRERequests[request];
let extractedData = toAWSArray(marketAlert.marketAlertArray);
const realEstateType = getRealEstateTypeEnum(marketAlert.requestObject.realEstateType).title;
const region = getRegionName(marketAlert.requestObject.region);
const municipality = getMunicipalityName(marketAlert.requestObject.region, marketAlert.requestObject.municipality);
const requestUrl = marketAlert.requestObject.requestUrl
let repData = `{ "marketAlertUrl":[${extractedData}], "realestateType":"${realEstateType}", "region":"${region}", "municipality":"${municipality}", "requestUrl":"${requestUrl}" }`
destinations.push({
Destination: {
ToAddresses: [
marketAlert.requestObject.email
]
},
ReplacementTemplateData: repData
})
}
console.log("AWS EMAIL : Bulk email replacement data:");
console.log(destinations);
var params = {
Destinations:
destinations,
Source: process.env.SOURCE_EMAIL, /* required */
Template: TEMPLATE_NAME, /* required */
DefaultTemplateData: '{ \"REPLACEMENT_TAG_NAME\":\"REPLACEMENT_VALUE\" }',
ReplyToAddresses: [
process.env.SOURCE_EMAIL,
]
};
// Create the promise and SES service object
const sendPromise = new AWS.SES({ apiVersion: '2010-12-01' }).sendBulkTemplatedEmail(params).promise();
const awsResult = await sendPromise;
console.log("AWS SES bulk email response");
console.log(awsResult);
} catch (e) {
console.log("Could not send bulk email", e)
}
}
const toAWSArray = (urlArray) => {
let arrayString = ""
i = 0
urlArray.forEach(element => {
const formatetdTitle = element.title.replace(/"/g, "");
arrayString = i <= NUMBER_OF_RESULTS_TO_SHOW ? arrayString + `{"url":"${element.url.trim()}" , "title":"${formatetdTitle}"},` : arrayString;
console.log(i);
console.log(arrayString);
i++
});
return arrayString.slice(0, -1);
}
const getNotificationEmailHtml = () => {
return `<h2> Zdravo,
Pronašli smo nekretninu koju ste tražili. </h2>
<h3> Ovo su tražene nekretnine: </h3>
<div>
<div>{{#each marketAlertUrl}}<li><a href="{{url}}">{{title}}</a></li><br />{{/each}}<div/>
<div/>
<div>Kompletan spisak nekretnina možete pegledati ovdije: <a href="{{requestUrl}}">Nekretnine</a> <div>
</div>`
}
const getNotificationEmailText = () => {
return ` Zdravo,
Pronašli smo nekretninu koju ste tražili. Ovo su tražene nekretnine: {{#each marketAlertUrl}} {{url}} {{title}} {{/each}} , Kompletan spisan nekretnina mozete pegledati ovdije: {{requestUrl}}`
}
const createMarketAlertEmailTemplate = async () => {
const marketAlertTemplate = {
Template: {
TemplateName: TEMPLATE_NAME,
SubjectPart: "Javi mi obavijest: {{realestateType}}, {{region}}, {{municipality}}",
TextPart: getNotificationEmailText(),
HtmlPart: getNotificationEmailHtml()
}
}
try {
const templatePromise = new AWS.SES({ apiVersion: '2010-12-01' }).updateTemplate(marketAlertTemplate).promise();
await templatePromise
} catch (e) {
console.log("Could not create MarketAlertEmailTemplate", e);
}
}
const getSubject = (realEstateType, region, municipality) => {
return `${getRealEstateTypeEnum(realEstateType).title} ${getRegionName(region)}, ${getMunicipalityName(region, municipality)}`
}
module.exports = {
sendTemplatedEmail,
sendBulkEmail,
createMarketAlertEmailTemplate
};

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

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

View File

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

View File

@@ -0,0 +1,90 @@
const db = require('../../models/index');
/**
* Find all subscribed RealEstateRequests
*/
const allRERequest = async () => {
return await db.RealEstateRequest.findAll({
where: {
subscribed: true
}
});
}
/**
* Find all subscribed RealEstateRequests by UUID
*/
const allRERequestByUiid = async (requestArray) => {
const Op = db.Sequelize.Op;
return await db.RealEstateRequest.findAll({
where: {
subscribed: true,
[Op.or]: requestArray
}
});
}
/**
* Find all , or all depending on notified bolean marketalerts, that the hasLocation is true, and order them by email
*
* @param fechAll bolean
* @param notified bolean
*
* @returns array of MarketAlerts
*/
const allMarketAlerts = async (fetchAll, notified) => {
let queryObject = {
order: [
['email', 'DESC'],
]
}
if (!fetchAll){
queryObject.where = {
notified: notified,
hasLocation: true
}
}
return await db.MarketAlert.findAll(queryObject);
}
/**
* Find all , MarketAlerts depending on request
*
* @param request string
*
* @returns array of MarketAlerts
*/
const allMarketAlertsByRequest = async (request) => {
let queryObject = {
where : {
request: request
}
}
return await db.MarketAlert.findAll(queryObject);
}
/**
* Find all unnotified marketalerts
* @param latLng array
* @param email string
*
* @returns array of MarketAlerts
*/
const findPointInsideBoundingBox = async (latLng, email, uniqueId) => {
return await db.sequelize.query(`SELECT * FROM "RealEstateRequests" WHERE email = '${email}' AND "uniqueId" = '${uniqueId}' AND subscribed = true AND ST_Contains("RealEstateRequests".bounding_box, ST_GEOMFROMTEXT('POINT (${latLng[0]} ${latLng[1]})'))`);
}
module.exports = {
allRERequest,
allMarketAlerts,
allRERequestByUiid,
findPointInsideBoundingBox,
allMarketAlertsByRequest
};

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

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

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

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

12
app/helpers/url.js Normal file
View File

@@ -0,0 +1,12 @@
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;
};
module.exports = {
currentRERequest,
};

View File

@@ -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;

View File

@@ -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 = [];

View File

@@ -1,11 +1,11 @@
const scrapTheItems = require("./scraptheitems");
const scrapTheItems = require("./scrapTheItems");
const convertToDate = require("./convertToDate");
const AWS = require('aws-sdk');
AWS.config.update({region: 'eu-central-1'});
// AWS.config.update({region: 'eu-central-1'});
async function sendNotification(marketAlert) {
const { id, email, olx_url, last_date } = 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);
@@ -17,8 +17,8 @@ async function sendNotification(marketAlert) {
""
);
// Create sendEmail params
var params = {
// Create sendEmail params
const params = {
Destination: { /* required */
CcAddresses: [
],
@@ -50,7 +50,7 @@ async function sendNotification(marketAlert) {
if (message) {
const sendPromise = new AWS.SES({apiVersion: '2010-12-01'}).sendEmail(params).promise();
await sendPromise;
await sendPromise;
return { id, date: String(convertToDate(lastDate)) };
}
}

View 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');
}
};

View 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');
}
};

View File

@@ -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'
);
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

37
app/models/index.js Normal file
View 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;

28
app/models/marketalert.js Normal file
View File

@@ -0,0 +1,28 @@
'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,
notified : DataTypes.BOOLEAN,
title : DataTypes.STRING,
request: DataTypes.STRING,
hasLocation: DataTypes.BOOLEAN,
email: {
type: DataTypes.STRING,
allowNul: false
}
}, {});
MarketAlert.associate = function(models) {
// associations can be defined here
};
return MarketAlert;
};

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

52
app/public/main.css Normal file
View File

@@ -0,0 +1,52 @@
.welcome-center-button {
width: 100%;
}
.next-center-button {
width: 50%;
left: 25%;
}
.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%
}
.centered-element {
margin-top: 200px;
}
.centered-element-small {
margin-top: 100px;
}
#map {
height: 50%;
}
html,
body {
height: 100%;
}
#floating-panel {
top: 10px;
left: 25%;
z-index: 5;
background-color: #fff;
padding: 5px;
border: 1px solid #999;
text-align: center;
font-family: 'Roboto', 'sans-serif';
line-height: 30px;
padding-left: 10px;
}

View File

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

View File

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

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

@@ -0,0 +1,2 @@
<!--suppress HtmlUnknownAnchorTarget -->
<% include partials/range %>

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

@@ -0,0 +1,17 @@
<!--suppress HtmlUnknownAnchorTarget -->
<form method="POST" id="form-goagain">
<div class="row centered-element">
<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>

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

@@ -0,0 +1,25 @@
<!doctype>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/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>
<% include partials/navBar %>
<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>

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
<div class="row center-align">
<nav style="background-color: #26a69a; margin: auto;">
<div class="row center-align">
<h6 style="padding-top: 20px;"><%= title %></h5>
</div>
</nav>
</div>

View File

@@ -0,0 +1,53 @@
<form method="POST" id="form-range">
<div class="row center-align no-ui-slider centered-element-small" id="slider"></div>
<div class="col s6 push-s3 centered-element-small">
<a id="btnsubmit" href="#" class="next-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>

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

@@ -0,0 +1,2 @@
<!--suppress HtmlUnknownAnchorTarget -->
<% include partials/range %>

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

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

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

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

View File

@@ -0,0 +1,29 @@
<!--suppress HtmlUnknownAnchorTarget -->
<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>

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

@@ -0,0 +1,14 @@
<!--suppress HtmlUnknownAnchorTarget -->
<div class="row center-align">
<ul class="collection with-header">
<% for(const realEstate of realEstates) { %>
<li class="collection-item">
<div><%= realEstate.title %>
<a href="<%= realEstate.url %>" class="secondary-content">
<i class="material-icons">send</i>
</a>
</div>
</li>
<% } %>
</ul>
</div>

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

@@ -0,0 +1,25 @@
<!--suppress HtmlUnknownAnchorTarget -->
<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>

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

@@ -0,0 +1,2 @@
<!--suppress HtmlUnknownAnchorTarget -->
<% include partials/range %>

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

@@ -0,0 +1,11 @@
<div class="row center-align">
<span class="welcome-big-logo">🤙</span>
</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
View 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>

View File

@@ -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
backend/.gitignore vendored
View File

@@ -1 +0,0 @@
node_modules/

View File

@@ -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;

View File

@@ -1,4 +0,0 @@
# How to deploy automatically:
1. set up aws cli with aws configure

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,4 +0,0 @@
const Sequelize = require("sequelize");
const sequelize = new Sequelize(process.env.JAWSDB_URL);
module.exports = sequelize;

View File

@@ -1 +0,0 @@
SECRET_VARIABLE=mysecretval

View File

@@ -1,5 +0,0 @@
{
"key": "value",
"key2": "value2",
"other_key": "other_value"
}

View File

@@ -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"
}]
}
}
}]
}

6
development.env Normal file
View File

@@ -0,0 +1,6 @@
AMAZON_ACCES_KEY_ID=(your-key-here)
AMAZON_SECRET_ACCESS_KEY=(your-key-here)
AMAZON_REGION=eu-west-1
AMAZON_NOTIFICATION_EMAIL_TEMPLATE=MarketAlertTemplateDevelopment
APP_URL=http://localhost:3001
SOURCE_EMAIL=info@saburly.com

View File

@@ -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*

View File

@@ -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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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>

View File

@@ -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"
}

View File

@@ -1,8 +0,0 @@
html {
font-family: Roboto, sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
* {
font-family: Roboto, serif;
}

View File

@@ -1,10 +0,0 @@
.radio-style {
color: #e91e63 !important;
}
.checkbox-style {
color: #e91e63 !important;
}
.label-style {
color: white;
}

View File

@@ -1,30 +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)"
},
itemsCountTitle: {
textAlign: 'center',
color: 'white'
}
});
export default appStyle;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,104 +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}>
{items.length && <h3 className={classes.itemsCountTitle}>Pronađeno {items.length} nekretnina. Napravite notifikaciju i primite vise detalja na vas emailu adresu.</h3>}
{items.length ? <NotificationModal /> : null}
</div>
</div>
);
}
}
App.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(dashboardStyle)(
connect(
mapStateToProps,
mapDispatchToProps
)(App)
);

View File

@@ -1,211 +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 => {
this.handleClose();
alert("Market Alert Created");
})
.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.successCallback();
//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}
>
<div>
<Input
className={classes.inputStyle}
placeholder="Email"
inputProps={{
"aria-label": "Email"
}}
type="email"
onChange={this.handleEmail}
/>
<Input
className={classes.inputStyle}
type="hidden"
value={token}
/>
</div>
</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)
);

Some files were not shown because too many files have changed in this diff Show More