Compare commits
94 Commits
edit-locat
...
kivi-origi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f1fe3641d | ||
|
|
a7148ba6c3 | ||
|
|
5066c2fa70 | ||
|
|
5f674230e1 | ||
|
|
96bc66ef7b | ||
|
|
bbd9dab30d | ||
|
|
b13b4bc7c2 | ||
|
|
c11248e100 | ||
|
|
17f0e6443c | ||
|
|
57329b0311 | ||
|
|
57df42dd05 | ||
|
|
cbb3c1f954 | ||
|
|
ba07b9311f | ||
|
|
f24abf62b2 | ||
|
|
34744613a7 | ||
|
|
230ef60158 | ||
|
|
9bcadffe9c | ||
|
|
9c234a85fd | ||
|
|
edb22266bd | ||
|
|
22c1982ef6 | ||
|
|
86c7d23efd | ||
|
|
ab6812889a | ||
|
|
b82134e280 | ||
|
|
be378883c8 | ||
|
|
8a87b9e253 | ||
|
|
43bc23b164 | ||
|
|
fc6351af46 | ||
|
|
9e06731c84 | ||
|
|
7777081c99 | ||
|
|
6a957db183 | ||
|
|
f8349dae1f | ||
|
|
6267b2cab4 | ||
|
|
97724a47a1 | ||
|
|
91a1c6a91e | ||
|
|
05062201bf | ||
|
|
eb4ab2e341 | ||
|
|
2d0a00b967 | ||
|
|
74def9c059 | ||
|
|
d29b3eb1b3 | ||
|
|
41b59e8c7c | ||
|
|
b933fa96d4 | ||
|
|
6429bb30c2 | ||
|
|
7b97835e8b | ||
|
|
d45441f4be | ||
|
|
824db4fbc3 | ||
|
|
0f91841c43 | ||
|
|
12b4a8f6ec | ||
|
|
17621ad310 | ||
|
|
c461525959 | ||
|
|
aec9c1e1d5 | ||
|
|
2d672f4660 | ||
|
|
bc15cf65a5 | ||
|
|
ce11d57ab2 | ||
|
|
4a6bcf262e | ||
|
|
712cde1632 | ||
|
|
1ba7cf8531 | ||
|
|
7a7aecb3ee | ||
|
|
78c4054cde | ||
|
|
94ffc2d6d2 | ||
|
|
b11f18696f | ||
|
|
fa46f75dd3 | ||
|
|
470f53d29b | ||
|
|
40509d2836 | ||
|
|
b2c102bc1a | ||
|
|
98263364c7 | ||
|
|
5b3491fdba | ||
|
|
c6f0e039a5 | ||
|
|
8d3f001678 | ||
|
|
42eddb3aa5 | ||
|
|
0a181f742f | ||
|
|
d117383802 | ||
|
|
870b71a3c7 | ||
|
|
e6725355a0 | ||
|
|
4fd4018bf6 | ||
|
|
b9122f8f00 | ||
|
|
fc33c1210a | ||
|
|
511b290096 | ||
|
|
ba43fa0713 | ||
|
|
e70901d369 | ||
|
|
8505282670 | ||
|
|
64e4835899 | ||
|
|
1658325c4b | ||
|
|
49161c1b60 | ||
|
|
d23ddf849f | ||
|
|
38bd0343f5 | ||
|
|
259799144e | ||
|
|
bc73d4159d | ||
|
|
37ad32fe76 | ||
|
|
94875a0fa3 | ||
|
|
fa4e0d64de | ||
|
|
0c2d218d29 | ||
|
|
fed2dc00dc | ||
|
|
d5d3a1f306 | ||
|
|
42ff1f762f |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,4 +2,4 @@ node_modules/
|
||||
.env
|
||||
.idea/
|
||||
.eslintrc
|
||||
.vscode/
|
||||
.vscode/
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
*.ejs
|
||||
@@ -7,7 +7,42 @@ const PRICE_SLIDER_OPTIONS_SALE = {
|
||||
step: 1000,
|
||||
connect: true
|
||||
};
|
||||
|
||||
const FLAT_PRICE_SLIDER_OPTIONS_SALE = {
|
||||
start: [50000, 150000],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [800000]
|
||||
},
|
||||
step: 5000,
|
||||
connect: true
|
||||
};
|
||||
const HOUSE_PRICE_SLIDER_OPTIONS_SALE = {
|
||||
start: [50000, 150000],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [1500000]
|
||||
},
|
||||
step: 10000,
|
||||
connect: true
|
||||
};
|
||||
const OFFICE_PRICE_SLIDER_OPTIONS_SALE = {
|
||||
start: [15000, 50000],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [2000000]
|
||||
},
|
||||
step: 2000,
|
||||
connect: true
|
||||
};
|
||||
const LAND_PRICE_SLIDER_OPTIONS_SALE = {
|
||||
start: [40000, 80000],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [2000000]
|
||||
},
|
||||
step: 10000,
|
||||
connect: true
|
||||
};
|
||||
const PRICE_SLIDER_OPTIONS_RENT = {
|
||||
start: [300, 500],
|
||||
range: {
|
||||
@@ -17,18 +52,62 @@ const PRICE_SLIDER_OPTIONS_RENT = {
|
||||
step: 50,
|
||||
connect: true
|
||||
};
|
||||
|
||||
const FLAT_PRICE_SLIDER_OPTIONS_RENT = {
|
||||
start: [300, 600],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [4000]
|
||||
},
|
||||
step: 100,
|
||||
connect: true
|
||||
};
|
||||
const HOUSE_PRICE_SLIDER_OPTIONS_RENT = {
|
||||
start: [500, 1000],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [10000]
|
||||
},
|
||||
step: 100,
|
||||
connect: true
|
||||
};
|
||||
const OFFICE_PRICE_SLIDER_OPTIONS_RENT = {
|
||||
start: [200, 1000],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [20000]
|
||||
},
|
||||
step: 100,
|
||||
connect: true
|
||||
};
|
||||
const LAND_PRICE_SLIDER_OPTIONS_RENT = {
|
||||
start: [500, 1000],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [20000]
|
||||
},
|
||||
step: 100,
|
||||
connect: true
|
||||
};
|
||||
//This will be used for Flats, Apartments, Houses
|
||||
const HOME_SIZE_SLIDER_OPTIONS = {
|
||||
start: [30, 75],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [400]
|
||||
max: [500]
|
||||
},
|
||||
step: 5,
|
||||
connect: true
|
||||
};
|
||||
|
||||
const OFFICE_SIZE_SLIDER_OPTIONS = {
|
||||
start: [30, 150],
|
||||
range: {
|
||||
min: [0],
|
||||
max: [1200]
|
||||
},
|
||||
step: 10,
|
||||
connect: true
|
||||
};
|
||||
const GARDEN_SIZE_SLIDER_OPTIONS = {
|
||||
start: [100, 1000],
|
||||
range: {
|
||||
@@ -111,8 +190,8 @@ const AD_CATEGORY = {
|
||||
hasNumberOfRoom: true,
|
||||
hasNumberOfFloors: false,
|
||||
hasFloorProp: true,
|
||||
priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT,
|
||||
priceSliderOptionsSale: FLAT_PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: FLAT_PRICE_SLIDER_OPTIONS_RENT,
|
||||
sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS
|
||||
},
|
||||
HOUSE: {
|
||||
@@ -126,8 +205,8 @@ const AD_CATEGORY = {
|
||||
hasNumberOfRoom: true,
|
||||
hasNumberOfFloors: true,
|
||||
hasFloorProp: false,
|
||||
priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT,
|
||||
priceSliderOptionsSale: HOUSE_PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: HOUSE_PRICE_SLIDER_OPTIONS_RENT,
|
||||
sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS,
|
||||
gardenSizeSliderOptions: GARDEN_SIZE_SLIDER_OPTIONS
|
||||
},
|
||||
@@ -142,9 +221,9 @@ const AD_CATEGORY = {
|
||||
hasNumberOfRoom: true,
|
||||
hasNumberOfFloors: false,
|
||||
hasFloorProp: true,
|
||||
priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT,
|
||||
sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS
|
||||
priceSliderOptionsSale: OFFICE_PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: OFFICE_PRICE_SLIDER_OPTIONS_RENT,
|
||||
sizeSliderOptions: OFFICE_SIZE_SLIDER_OPTIONS
|
||||
},
|
||||
LAND: {
|
||||
id: "LAND",
|
||||
@@ -157,8 +236,8 @@ const AD_CATEGORY = {
|
||||
hasNumberOfRoom: false,
|
||||
hasNumberOfFloors: false,
|
||||
hasFloorProp: false,
|
||||
priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT,
|
||||
priceSliderOptionsSale: LAND_PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: LAND_PRICE_SLIDER_OPTIONS_RENT,
|
||||
sizeSliderOptions: LAND_SIZE_SLIDER_OPTIONS
|
||||
},
|
||||
APARTMENT: {
|
||||
@@ -172,8 +251,8 @@ const AD_CATEGORY = {
|
||||
hasNumberOfRoom: true,
|
||||
hasNumberOfFloors: false,
|
||||
hasFloorProp: true,
|
||||
priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT,
|
||||
priceSliderOptionsSale: FLAT_PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: FLAT_PRICE_SLIDER_OPTIONS_RENT,
|
||||
sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS
|
||||
},
|
||||
GARAGE: {
|
||||
@@ -202,8 +281,8 @@ const AD_CATEGORY = {
|
||||
hasNumberOfRoom: true,
|
||||
hasNumberOfFloors: true,
|
||||
hasFloorProp: false,
|
||||
priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT,
|
||||
priceSliderOptionsSale: HOUSE_PRICE_SLIDER_OPTIONS_SALE,
|
||||
priceSliderOptionsRent: HOUSE_PRICE_SLIDER_OPTIONS_RENT,
|
||||
sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS,
|
||||
gardenSizeSliderOptions: GARDEN_SIZE_SLIDER_OPTIONS
|
||||
}
|
||||
@@ -216,14 +295,17 @@ const AD_STATUS = {
|
||||
STATUS_DELETED: 4,
|
||||
STATUS_URGENT: 5,
|
||||
STATUS_DISCOUNTED: 6,
|
||||
STATUS_RENTED: 7
|
||||
STATUS_RENTED: 7,
|
||||
STATUS_VIP: 8
|
||||
};
|
||||
|
||||
const AD_AGENCY = {
|
||||
OLX: "OLX",
|
||||
RENTAL: "RENTAL",
|
||||
PROSTOR: "PROSTOR",
|
||||
AKTIDO: "AKTIDO"
|
||||
AKTIDO: "AKTIDO",
|
||||
KIVI: "KIVI",
|
||||
SALJIC: "SALJIC"
|
||||
};
|
||||
|
||||
const CRAWLER_AD_TYPE = {
|
||||
|
||||
492
app/common/publishEnums.js
Normal file
492
app/common/publishEnums.js
Normal file
@@ -0,0 +1,492 @@
|
||||
const {
|
||||
AD_CATEGORY,
|
||||
ACCESS_ROAD_TYPE,
|
||||
HEATING_TYPE,
|
||||
FURNISHING_TYPE
|
||||
} = require("./enums");
|
||||
|
||||
const BASIC_BOOLEAN_PUBLISH = [
|
||||
{
|
||||
dbField: "newBuilding",
|
||||
title: "Novogradnja",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "balcony",
|
||||
title: "Balkon",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
dbField: "elevator",
|
||||
title: "Lift",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "recentlyAdapted",
|
||||
title: "Nedavno adaptirano",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const BASIC_INPUT_PUBLISH = [
|
||||
{
|
||||
dbField: "title",
|
||||
title: "Naslov",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
],
|
||||
constraint: ["required"]
|
||||
},
|
||||
{
|
||||
dbField: "shortDescription",
|
||||
title: "Opis",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
],
|
||||
constraint: []
|
||||
},
|
||||
{
|
||||
dbField: "price",
|
||||
title: "Cijena (KM)",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
],
|
||||
constraint: ["numerical"]
|
||||
},
|
||||
{
|
||||
dbField: "area",
|
||||
title: "Površina (m\xB2)",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
],
|
||||
constraint: ["numerical"]
|
||||
},
|
||||
{
|
||||
dbField: "gardenSize",
|
||||
title: "Površina okućnice (m\xB2)",
|
||||
categoriesToShow: [AD_CATEGORY.HOUSE, AD_CATEGORY.COTTAGE],
|
||||
constraint: ["numerical"]
|
||||
},
|
||||
{
|
||||
dbField: "streetName",
|
||||
title: "Adresa",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
],
|
||||
constraint: []
|
||||
},
|
||||
{
|
||||
dbField: "numberOfRooms",
|
||||
title: "Broj soba",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
],
|
||||
constraint: ["integer"]
|
||||
},
|
||||
{
|
||||
dbField: "numberOfFloors",
|
||||
title: "Broj spratova",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE
|
||||
],
|
||||
constraint: ["integer"]
|
||||
},
|
||||
{
|
||||
dbField: "floor",
|
||||
title: "Sprat",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.OFFICE
|
||||
],
|
||||
constraint: ["integer"]
|
||||
}
|
||||
];
|
||||
|
||||
const BASIC_SEGMENT_PUBLISH = [
|
||||
{
|
||||
dbField: "furnishingType",
|
||||
title: "Namještaj",
|
||||
values: Object.keys(FURNISHING_TYPE).map(key => FURNISHING_TYPE[key]),
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const ADDITIONAL_BOOLEAN_PUBLISH = [
|
||||
{
|
||||
dbField: "water",
|
||||
title: "Voda",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "electricity",
|
||||
title: "Struja",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "drainageSystem",
|
||||
title: "Kanalizacija",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "registeredInZkBooks",
|
||||
title: "Uknjiženo",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
dbField: "parking",
|
||||
title: "Parking",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "garage",
|
||||
title: "Garaža",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "gas",
|
||||
title: "Plin",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "antiTheftDoor",
|
||||
title: "Blindirana vrata",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "airCondition",
|
||||
title: "Klimatizirano",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "phoneConnection",
|
||||
title: "Telefon",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "cableTV",
|
||||
title: "Kablovska",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "internet",
|
||||
title: "Internet",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "basementAttic",
|
||||
title: "Podrum-Tavan",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "storeRoom",
|
||||
title: "Ostava",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "videoSurveillance",
|
||||
title: "Video nadzor",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "alarm",
|
||||
title: "Alarm",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "suitableForStudents",
|
||||
title: "Za studente",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "includingBills",
|
||||
title: "Uključen trošak režija",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "animalsAllowed",
|
||||
title: "Kućni ljubimci dozvoljeni",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "pool",
|
||||
title: "Bazen",
|
||||
categoriesToShow: [AD_CATEGORY.HOUSE, AD_CATEGORY.COTTAGE]
|
||||
},
|
||||
{
|
||||
dbField: "exchange",
|
||||
title: "Zamjena",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "urbanPlanPermit",
|
||||
title: "Urbanistička dozvola",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "buildingPermit",
|
||||
title: "Građevinska dozvola",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const ADDITIONAL_INPUT_PUBLISH = [
|
||||
{
|
||||
dbField: "longDescription",
|
||||
title: "Detaljan opis",
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const ADDITIONAL_SEGMENT_PUBLISH = [
|
||||
{
|
||||
dbField: "accessRoadType",
|
||||
title: "Pristupni put",
|
||||
values: Object.keys(ACCESS_ROAD_TYPE).map(key => ACCESS_ROAD_TYPE[key]),
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE,
|
||||
AD_CATEGORY.LAND,
|
||||
AD_CATEGORY.GARAGE
|
||||
]
|
||||
},
|
||||
{
|
||||
dbField: "heatingType",
|
||||
title: "Grijanje",
|
||||
values: Object.keys(HEATING_TYPE).map(key => HEATING_TYPE[key]),
|
||||
categoriesToShow: [
|
||||
AD_CATEGORY.FLAT,
|
||||
AD_CATEGORY.HOUSE,
|
||||
AD_CATEGORY.APARTMENT,
|
||||
AD_CATEGORY.COTTAGE,
|
||||
AD_CATEGORY.OFFICE
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
BASIC_INPUT_PUBLISH,
|
||||
BASIC_SEGMENT_PUBLISH,
|
||||
BASIC_BOOLEAN_PUBLISH,
|
||||
ADDITIONAL_BOOLEAN_PUBLISH,
|
||||
ADDITIONAL_INPUT_PUBLISH,
|
||||
ADDITIONAL_SEGMENT_PUBLISH
|
||||
};
|
||||
@@ -9,11 +9,15 @@ const APP_URL =
|
||||
? process.env.APP_URL || "http://market-alarm"
|
||||
: process.env.APP_URL || `${APP_BASE_URL}:${APP_PORT}`;
|
||||
|
||||
const STAGING = process.env.SETTINGS !== "production";
|
||||
|
||||
const DEFAULT_TIMEZONE = "Europe/Sarajevo";
|
||||
|
||||
const CRAWLER_INTERVAL = parseInt(process.env.CRAWLER_INTERVAL) || 60;
|
||||
const STOP_CRAWLER = !!parseInt(process.env.STOP_CRAWLER);
|
||||
|
||||
const CHECK_UP_DAYS = parseInt(process.env.CHECK_UP_DAYS) || 10;
|
||||
|
||||
const AWS_EMAIL_CONFIG = {
|
||||
REGION: process.env.AWS_REGION || "",
|
||||
CREDENTIALS: {
|
||||
@@ -32,6 +36,11 @@ const PRINT_CRAWLER_DEBUG = process.env.PRINT_CRAWLER_DEBUG_INFO || 0;
|
||||
|
||||
const API_MAP_KEY = process.env.API_MAP_KEY || "";
|
||||
|
||||
const PROSTOR_LOGIN = {
|
||||
EMAIL: process.env.PROSTOR_LOGIN_EMAIL,
|
||||
PASSWORD: process.env.PROSTOR_LOGIN_PASS
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
APP_PORT,
|
||||
APP_URL,
|
||||
@@ -42,5 +51,9 @@ module.exports = {
|
||||
MAX_REAL_ESTATES_IN_EMAIL,
|
||||
MAX_REAL_ESTATES_IN_FIRST_EMAIL,
|
||||
PRINT_CRAWLER_DEBUG,
|
||||
API_MAP_KEY
|
||||
API_MAP_KEY,
|
||||
STAGING,
|
||||
CHECK_UP_DAYS,
|
||||
PROSTOR_LOGIN
|
||||
|
||||
};
|
||||
|
||||
319
app/controllers/publishRealEstate.js
Normal file
319
app/controllers/publishRealEstate.js
Normal file
@@ -0,0 +1,319 @@
|
||||
const { findRealEstateByAgencyId } = require("../helpers/db/realEstate");
|
||||
const { bulkUpsertKiviPhotos } = require("../helpers/db/kiviOriginalAdsPhotos");
|
||||
const { currentKiviRealEstate } = require("../helpers/url");
|
||||
|
||||
const validate = require("validate.js");
|
||||
|
||||
const {
|
||||
AD_CATEGORY,
|
||||
FURNISHING_TYPE,
|
||||
ACCESS_ROAD_TYPE,
|
||||
HEATING_TYPE
|
||||
} = require("../common/enums");
|
||||
const {
|
||||
BASIC_BOOLEAN_PUBLISH,
|
||||
BASIC_SEGMENT_PUBLISH,
|
||||
ADDITIONAL_BOOLEAN_PUBLISH,
|
||||
ADDITIONAL_SEGMENT_PUBLISH,
|
||||
BASIC_INPUT_PUBLISH,
|
||||
ADDITIONAL_INPUT_PUBLISH
|
||||
} = require("../common/publishEnums");
|
||||
|
||||
const getPublishInputs = async (req, res) => {
|
||||
const kiviOriginal = await currentKiviRealEstate(req);
|
||||
|
||||
const realEstate = await findRealEstateByAgencyId(kiviOriginal.kiviAdId);
|
||||
|
||||
if (!realEstate || !realEstate.dataValues) {
|
||||
res.render("notFound", { title: " " });
|
||||
return;
|
||||
}
|
||||
|
||||
const pageTitle = "Podaci o nekretnini";
|
||||
|
||||
const {
|
||||
price,
|
||||
area,
|
||||
adType,
|
||||
realEstateType,
|
||||
locationLat,
|
||||
locationLong,
|
||||
accessRoadType,
|
||||
heatingType,
|
||||
balcony,
|
||||
newBuilding,
|
||||
elevator,
|
||||
recentlyAdapted,
|
||||
gardenSize,
|
||||
numberOfRooms,
|
||||
numberOfFloors,
|
||||
floor,
|
||||
water,
|
||||
electricity,
|
||||
drainageSystem,
|
||||
registeredInZkBooks,
|
||||
parking,
|
||||
garage,
|
||||
gas,
|
||||
antiTheftDoor,
|
||||
airCondition,
|
||||
phoneConnection,
|
||||
cableTV,
|
||||
internet,
|
||||
basementAttic,
|
||||
storeRoom,
|
||||
videoSurveillance,
|
||||
alarm,
|
||||
suitableForStudents,
|
||||
includingBills,
|
||||
animalsAllowed,
|
||||
pool,
|
||||
exchange,
|
||||
urbanPlanPermit,
|
||||
buildingPermit,
|
||||
furnishingType,
|
||||
shortDescription,
|
||||
streetName,
|
||||
title,
|
||||
longDescription
|
||||
} = realEstate;
|
||||
const category = AD_CATEGORY[realEstateType] || AD_CATEGORY.FLAT;
|
||||
|
||||
// TODO: Maybe this is slow, pay attention to this
|
||||
const filterInputs = filterObject => {
|
||||
const filterCategories = filterObject.categoriesToShow;
|
||||
return filterCategories.indexOf(category) !== -1;
|
||||
};
|
||||
//Boolean inputs to be shown on Basic Data tab
|
||||
const basicBooleanPublishInputs = BASIC_BOOLEAN_PUBLISH.filter(filterInputs);
|
||||
const basicBooleanPublishValues = {
|
||||
balcony,
|
||||
elevator,
|
||||
newBuilding,
|
||||
recentlyAdapted
|
||||
};
|
||||
//Boolean inputs to be shown on Additional Data tab
|
||||
const additionalBooleanPublishInputs = ADDITIONAL_BOOLEAN_PUBLISH.filter(
|
||||
filterInputs
|
||||
);
|
||||
const additionalBooleanPublishValues = {
|
||||
water,
|
||||
electricity,
|
||||
drainageSystem,
|
||||
registeredInZkBooks,
|
||||
parking,
|
||||
garage,
|
||||
gas,
|
||||
antiTheftDoor,
|
||||
airCondition,
|
||||
phoneConnection,
|
||||
cableTV,
|
||||
internet,
|
||||
basementAttic,
|
||||
storeRoom,
|
||||
videoSurveillance,
|
||||
alarm,
|
||||
suitableForStudents,
|
||||
includingBills,
|
||||
animalsAllowed,
|
||||
pool,
|
||||
exchange,
|
||||
urbanPlanPermit,
|
||||
buildingPermit
|
||||
};
|
||||
//Segment select inputs to be shown on Basic Data tab
|
||||
const basicSegmentSelectInputs = BASIC_SEGMENT_PUBLISH.filter(filterInputs);
|
||||
const basicSegmentSelectValues = {
|
||||
furnishingType
|
||||
};
|
||||
//Segment select inputs to be shown on Additional Data tab
|
||||
const additionalSegmentSelectInputs = ADDITIONAL_SEGMENT_PUBLISH.filter(
|
||||
filterInputs
|
||||
);
|
||||
const additionalSegmentSelectValues = {
|
||||
accessRoadType,
|
||||
heatingType
|
||||
};
|
||||
//Input text type inputs to be shown on Basic Data tab
|
||||
const basicInputInputs = BASIC_INPUT_PUBLISH.filter(filterInputs);
|
||||
const basicInputValues = {
|
||||
price,
|
||||
area,
|
||||
gardenSize,
|
||||
numberOfRooms,
|
||||
numberOfFloors,
|
||||
floor,
|
||||
title,
|
||||
shortDescription,
|
||||
streetName
|
||||
};
|
||||
//Input type textare to be shown on Additional Data
|
||||
const additionalInputInputs = ADDITIONAL_INPUT_PUBLISH.filter(filterInputs);
|
||||
const additionalInputValues = {
|
||||
longDescription
|
||||
};
|
||||
|
||||
res.render("publishRealEstate", {
|
||||
title: pageTitle,
|
||||
basicBooleanPublishInputs,
|
||||
basicBooleanPublishValues,
|
||||
additionalBooleanPublishInputs,
|
||||
additionalBooleanPublishValues,
|
||||
basicSegmentSelectInputs,
|
||||
basicSegmentSelectValues,
|
||||
additionalSegmentSelectInputs,
|
||||
additionalSegmentSelectValues,
|
||||
basicInputInputs,
|
||||
basicInputValues,
|
||||
additionalInputInputs,
|
||||
additionalInputValues,
|
||||
validate: validate
|
||||
});
|
||||
};
|
||||
|
||||
const postPublishInputs = async (req, res) => {
|
||||
const kiviOriginal = await currentKiviRealEstate(req);
|
||||
|
||||
const realEstate = await findRealEstateByAgencyId(kiviOriginal.kiviAdId);
|
||||
|
||||
if (!realEstate || !realEstate.dataValues) {
|
||||
res.render("notFound", { title: " " });
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStepPage = req.query.nextStep || "/uspjesnaobjava";
|
||||
|
||||
//Request body
|
||||
//console.log("Body:", req.body);
|
||||
|
||||
const balcony = req.body.balcony === "on";
|
||||
const elevator = req.body.elevator === "on";
|
||||
const newBuilding = req.body.newBuilding === "on";
|
||||
const recentlyAdapted = req.body.recentlyAdapted === "on";
|
||||
const water = req.body.water === "on";
|
||||
const electricity = req.body.electricity === "on";
|
||||
const drainageSystem = req.body.drainageSystem === "on";
|
||||
const registeredInZkBooks = req.body.registeredInZkBooks === "on";
|
||||
const parking = req.body.parking === "on";
|
||||
const garage = req.body.garage === "on";
|
||||
const gas = req.body.gas === "on";
|
||||
const antiTheftDoor = req.body.antiTheftDoor === "on";
|
||||
const airCondition = req.body.airCondition === "on";
|
||||
const phoneConnection = req.body.phoneConnection === "on";
|
||||
const cableTV = req.body.cableTV === "on";
|
||||
const internet = req.body.internet === "on";
|
||||
const basementAttic = req.body.basementAttic === "on";
|
||||
const storeRoom = req.body.storeRoom === "on";
|
||||
const videoSurveillance = req.body.videoSurveillance === "on";
|
||||
const alarm = req.body.alarm === "on";
|
||||
const suitableForStudents = req.body.suitableForStudents === "on";
|
||||
const includingBills = req.body.includingBills === "on";
|
||||
const animalsAllowed = req.body.animalsAllowed === "on";
|
||||
const pool = req.body.pool === "on";
|
||||
const exchange = req.body.exchange === "on";
|
||||
const urbanPlanPermit = req.body.urbanPlanPermit === "on";
|
||||
const buildingPermit = req.body.buildingPermit === "on";
|
||||
|
||||
const furnishingType = req.body.furnishingType;
|
||||
//VALIDACIJA TAKO POTVRDITI DA JE ISPRAVNA VRIJEDNOST
|
||||
/* if (!FURNISHING_TYPE[furnishingType]) {
|
||||
res.render("notFound", { title: " Greška !" });
|
||||
return;
|
||||
} */
|
||||
const accessRoadType = req.body.accessRoadType;
|
||||
/*if (!ACCESS_ROAD_TYPE[accessRoadType]) {
|
||||
res.render("notFound", { title: " Greška !" });
|
||||
return;
|
||||
} */
|
||||
const heatingType = req.body.heatingType;
|
||||
/*if (!HEATING_TYPE[heatingType]) {
|
||||
res.render("notFound", { title: " Greška !" });
|
||||
return;
|
||||
}*/
|
||||
|
||||
const price = parseFloat(req.body.price) || null;
|
||||
const area = parseFloat(req.body.area) || null;
|
||||
const gardenSize = parseFloat(req.body.gardenSize) || null;
|
||||
const numberOfRooms = parseInt(req.body.numberOfRooms) || null;
|
||||
const numberOfFloors = parseInt(req.body.numberOfFloors) || null;
|
||||
const floor = parseInt(req.body.floor) || null;
|
||||
const title = req.body.title || "";
|
||||
const shortDescription = req.body.shortDescription || "";
|
||||
const streetName = req.body.streetName || "";
|
||||
const longDescription = req.body.longDescription || "";
|
||||
|
||||
const locationLat = req.body.lat || null;
|
||||
const locationLong = req.body.lng || null;
|
||||
//Contact email saved in other table
|
||||
const contactEmail = req.body.email || "";
|
||||
//Image urls are stored in new table
|
||||
const imageUrls =
|
||||
req.body.imageUrls.split("|").filter(url => url !== "") || [];
|
||||
const imageUrlsData = imageUrls.map(url => {
|
||||
return {
|
||||
kiviAdId: kiviOriginal.kiviAdId,
|
||||
photoUrl: url
|
||||
};
|
||||
});
|
||||
const savedImageUrls = await bulkUpsertKiviPhotos(imageUrlsData);
|
||||
|
||||
realEstate.balcony = balcony;
|
||||
realEstate.elevator = elevator;
|
||||
realEstate.newBuilding = newBuilding;
|
||||
realEstate.recentlyAdapted = recentlyAdapted;
|
||||
realEstate.water = water;
|
||||
realEstate.electricity = electricity;
|
||||
realEstate.drainageSystem = drainageSystem;
|
||||
realEstate.registeredInZkBooks = registeredInZkBooks;
|
||||
realEstate.parking = parking;
|
||||
realEstate.garage = garage;
|
||||
realEstate.gas = gas;
|
||||
realEstate.antiTheftDoor = antiTheftDoor;
|
||||
realEstate.airCondition = airCondition;
|
||||
realEstate.phoneConnection = phoneConnection;
|
||||
realEstate.cableTV = cableTV;
|
||||
realEstate.internet = internet;
|
||||
realEstate.basementAttic = basementAttic;
|
||||
realEstate.storeRoom = storeRoom;
|
||||
realEstate.videoSurveillance = videoSurveillance;
|
||||
realEstate.alarm = alarm;
|
||||
realEstate.suitableForStudents = suitableForStudents;
|
||||
realEstate.includingBills = includingBills;
|
||||
realEstate.animalsAllowed = animalsAllowed;
|
||||
realEstate.pool = pool;
|
||||
realEstate.exchange = exchange;
|
||||
realEstate.urbanPlanPermit = urbanPlanPermit;
|
||||
realEstate.buildingPermit = buildingPermit;
|
||||
|
||||
realEstate.furnishingType = furnishingType;
|
||||
realEstate.accessRoadType = accessRoadType;
|
||||
realEstate.heatingType = heatingType;
|
||||
|
||||
realEstate.price = price;
|
||||
realEstate.area = area;
|
||||
realEstate.gardenSize = gardenSize;
|
||||
realEstate.numberOfRooms = numberOfRooms;
|
||||
realEstate.numberOfFloors = numberOfFloors;
|
||||
realEstate.floor = floor;
|
||||
realEstate.title = title;
|
||||
realEstate.shortDescription = shortDescription;
|
||||
realEstate.streetName = streetName;
|
||||
|
||||
realEstate.longDescription = longDescription;
|
||||
|
||||
realEstate.locationLat = locationLat;
|
||||
realEstate.locationLong = locationLong;
|
||||
|
||||
kiviOriginal.email = contactEmail;
|
||||
|
||||
await realEstate.save();
|
||||
|
||||
await kiviOriginal.save();
|
||||
|
||||
res.redirect(nextStepPage);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPublishInputs,
|
||||
postPublishInputs
|
||||
};
|
||||
101
app/controllers/publishRealEstateTypes.js
Normal file
101
app/controllers/publishRealEstateTypes.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const { currentKiviRealEstate } = require("../helpers/url");
|
||||
const {
|
||||
createRealEstate,
|
||||
findRealEstateByAgencyId
|
||||
} = require("../helpers/db/realEstate");
|
||||
const { createKiviOriginal } = require("../helpers/db/kiviOriginal");
|
||||
|
||||
const { AD_CATEGORY, AD_TYPE, AD_AGENCY } = require("../common/enums");
|
||||
|
||||
const getPublishTypes = async (req, res) => {
|
||||
const kiviOriginal = await currentKiviRealEstate(req);
|
||||
|
||||
const realEstate = await findRealEstateByAgencyId(kiviOriginal.kiviAdId);
|
||||
|
||||
const title = "Koju nekretninu nudite?";
|
||||
let selectedAdType = AD_TYPE.AD_TYPE_SALE.id;
|
||||
const labelAdType = ["Prodaj", "Iznajmi"];
|
||||
|
||||
if (
|
||||
realEstate &&
|
||||
realEstate.adType &&
|
||||
realEstate.adType === AD_TYPE.AD_TYPE_RENT.stringId
|
||||
) {
|
||||
selectedAdType = AD_TYPE.AD_TYPE_RENT.id;
|
||||
}
|
||||
const realEstateTypes = Object.keys(AD_CATEGORY)
|
||||
.map(category => AD_CATEGORY[category])
|
||||
.filter(category => category.title);
|
||||
|
||||
res.render("realEstateType", {
|
||||
selectedAdType,
|
||||
labelAdType,
|
||||
realEstateTypes,
|
||||
title,
|
||||
AD_TYPE
|
||||
});
|
||||
};
|
||||
|
||||
const postPublishTypes = async (req, res) => {
|
||||
const kiviOriginal = await currentKiviRealEstate(req);
|
||||
|
||||
const realEstate = await findRealEstateByAgencyId(kiviOriginal.kiviAdId);
|
||||
|
||||
const adType = parseInt(req.body.adType);
|
||||
|
||||
const adTypeStringIds = {
|
||||
[AD_TYPE.AD_TYPE_SALE.id]: AD_TYPE.AD_TYPE_SALE.stringId,
|
||||
[AD_TYPE.AD_TYPE_RENT.id]: AD_TYPE.AD_TYPE_RENT.stringId
|
||||
};
|
||||
|
||||
const adTypeStringId =
|
||||
adTypeStringIds[adType] || AD_TYPE.AD_TYPE_SALE.stringId;
|
||||
|
||||
const validRealEstateTypes = Object.keys(AD_CATEGORY).filter(
|
||||
category => !!AD_CATEGORY[category].title
|
||||
);
|
||||
|
||||
const selectedRealEstateType = req.body.realEstateType || null;
|
||||
if (validRealEstateTypes.indexOf(selectedRealEstateType) === -1) {
|
||||
res.render("notFound", { title: " " });
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStepPage = req.query.nextStep || "podacionekretnini";
|
||||
|
||||
let nextStepUrl = "";
|
||||
if (kiviOriginal && kiviOriginal.kiviAdId && realEstate && realEstate.id) {
|
||||
//
|
||||
nextStepUrl = `/${nextStepPage}/${kiviOriginal.kiviAdId}`;
|
||||
|
||||
realEstate.adType = adTypeStringId;
|
||||
realEstate.realEstateType = selectedRealEstateType;
|
||||
|
||||
await realEstate.save();
|
||||
} else {
|
||||
try {
|
||||
const newKiviOriginal = await createKiviOriginal({
|
||||
email: ""
|
||||
});
|
||||
|
||||
const newRealEstate = await createRealEstate({
|
||||
adType: adTypeStringId,
|
||||
realEstateType: selectedRealEstateType,
|
||||
url: "http://localhost:5000/",
|
||||
originAgencyName: AD_AGENCY.KIVI,
|
||||
agencyObjectId: newKiviOriginal.kiviAdId
|
||||
});
|
||||
|
||||
nextStepUrl = `/${nextStepPage}/${newKiviOriginal.kiviAdId}`;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
nextStepUrl = `/`;
|
||||
}
|
||||
}
|
||||
res.redirect(nextStepUrl);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPublishTypes,
|
||||
postPublishTypes
|
||||
};
|
||||
8
app/controllers/publishSuccess.js
Normal file
8
app/controllers/publishSuccess.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const publishSuccess = async (req, res) => {
|
||||
const title = "Uspjeh!";
|
||||
res.render("publishSuccess", { title });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
publishSuccess
|
||||
};
|
||||
@@ -35,7 +35,8 @@ const getFilters = async (req, res) => {
|
||||
balcony,
|
||||
elevator,
|
||||
newBuilding,
|
||||
accessRoadType
|
||||
accessRoadType,
|
||||
includeWithoutPrice
|
||||
} = searchRequest;
|
||||
const category = AD_CATEGORY[realEstateType] || AD_CATEGORY.FLAT;
|
||||
|
||||
@@ -115,11 +116,14 @@ const getFilters = async (req, res) => {
|
||||
advancedSegmentSelectFilterValues,
|
||||
advancedRangeFilterObjects,
|
||||
advancedRangeFilterValues,
|
||||
includeIncompleteAds
|
||||
includeIncompleteAds,
|
||||
includeWithoutPrice
|
||||
});
|
||||
};
|
||||
|
||||
const postFilters = async (req, res) => {
|
||||
//
|
||||
console.log("postFilters");
|
||||
const searchRequest = await currentSearchRequest(req);
|
||||
|
||||
if (!searchRequest || !searchRequest.dataValues) {
|
||||
@@ -191,6 +195,7 @@ const postFilters = async (req, res) => {
|
||||
});
|
||||
|
||||
const includeIncompleteAds = req.body.includeIncompleteAds === "on";
|
||||
const includeWithoutPrice = req.body.includeWithoutPrice === "on";
|
||||
|
||||
const balcony = req.body.balcony === "on";
|
||||
const elevator = req.body.elevator === "on";
|
||||
@@ -217,6 +222,7 @@ const postFilters = async (req, res) => {
|
||||
searchRequest.newBuilding = newBuilding;
|
||||
|
||||
searchRequest.includeIncompleteAds = includeIncompleteAds;
|
||||
searchRequest.includeWithoutPrice = includeWithoutPrice;
|
||||
|
||||
searchRequest.accessRoadType = accessRoadType;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const getRealEstateTypes = async (req, res) => {
|
||||
|
||||
const title = "Koju nekretninu tražite?";
|
||||
let selectedAdType = AD_TYPE.AD_TYPE_SALE.id;
|
||||
const labelAdType = [AD_TYPE.AD_TYPE_SALE.title, AD_TYPE.AD_TYPE_RENT.title];
|
||||
if (
|
||||
searchRequest &&
|
||||
searchRequest.adType &&
|
||||
@@ -21,6 +22,7 @@ const getRealEstateTypes = async (req, res) => {
|
||||
|
||||
res.render("realEstateType", {
|
||||
selectedAdType,
|
||||
labelAdType,
|
||||
realEstateTypes,
|
||||
title,
|
||||
AD_TYPE
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
const {
|
||||
findRealEstatesForSearchRequest
|
||||
} = require("../helpers/db/searchRequestMatch");
|
||||
const { AD_STATUS } = require("../common/enums");
|
||||
|
||||
const getRealEstates = async (req, res) => {
|
||||
const searchRequestId = req.params["searchRequestId"] || "";
|
||||
const realEstates = await findRealEstatesForSearchRequest(searchRequestId);
|
||||
|
||||
const title = "Nekretnine koje odgovaraju Vašim uslovima pretrage";
|
||||
res.render("realEstates", { realEstates, title });
|
||||
res.render("realEstates", { realEstates, title, AD_STATUS });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const { getRealEstateById } = require("../helpers/db/realEstate");
|
||||
const { AD_STATUS } = require("../common/enums");
|
||||
|
||||
const getRedirect = async (req, res) => {
|
||||
const id = req.params.id || null;
|
||||
let error = false;
|
||||
let redirectUrl = undefined;
|
||||
let vipAd = undefined;
|
||||
if (!id) {
|
||||
error = true;
|
||||
} else {
|
||||
@@ -13,6 +15,7 @@ const getRedirect = async (req, res) => {
|
||||
error = true;
|
||||
} else {
|
||||
redirectUrl = realEstate.url;
|
||||
vipAd = realEstate.adStatus === AD_STATUS.STATUS_VIP;
|
||||
}
|
||||
} catch (e) {
|
||||
error = true;
|
||||
@@ -24,7 +27,7 @@ const getRedirect = async (req, res) => {
|
||||
res.render("notFound", { title });
|
||||
} else {
|
||||
const title = "Preusmjeravanje";
|
||||
res.render("redirect", { title, redirectUrl });
|
||||
res.render("redirect", { title, redirectUrl, vipAd });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { createSearchRequest } = require("../helpers/db/searchRequest");
|
||||
|
||||
const { AD_TYPE, AD_CATEGORY } = require("../common/enums");
|
||||
const { createRealEstate } = require("../helpers/db/realEstate");
|
||||
const { createKiviOriginal } = require("../helpers/db/kiviOriginal");
|
||||
const { AD_TYPE, AD_CATEGORY, AD_AGENCY } = require("../common/enums");
|
||||
|
||||
const getWelcome = (req, res) => {
|
||||
res.render("welcome", {
|
||||
@@ -11,7 +12,54 @@ const getWelcome = (req, res) => {
|
||||
|
||||
const postWelcome = async (req, res) => {
|
||||
const adType = parseInt(req.body.adType);
|
||||
const publishAdType = parseInt(req.body.publishAdType);
|
||||
|
||||
let nextStepUrl = "";
|
||||
|
||||
if (adType) {
|
||||
const adTypeStringId = getAdTypeString(adType);
|
||||
try {
|
||||
const newSearchRequest = await createSearchRequest({
|
||||
adType: adTypeStringId,
|
||||
realEstateType: AD_CATEGORY.FLAT.id
|
||||
});
|
||||
|
||||
nextStepUrl = `/vrstanekretnine/${newSearchRequest.id}`;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
nextStepUrl = `/`;
|
||||
}
|
||||
} else if (publishAdType) {
|
||||
const adTypeStringId = getAdTypeString(publishAdType);
|
||||
|
||||
try {
|
||||
//First we create new Kivi Ad Original object in db then new Real Estate
|
||||
//Problem with id-s
|
||||
const newKiviOriginal = await createKiviOriginal({
|
||||
email: ""
|
||||
});
|
||||
|
||||
const newRealEstate = await createRealEstate({
|
||||
adType: adTypeStringId,
|
||||
realEstateType: AD_CATEGORY.FLAT.id,
|
||||
//Temp variable because of the not null constraints
|
||||
url: "http://localhost:5000/",
|
||||
originAgencyName: AD_AGENCY.KIVI,
|
||||
agencyObjectId: newKiviOriginal.kiviAdId
|
||||
});
|
||||
|
||||
nextStepUrl = `/objavinekretninu/${newKiviOriginal.kiviAdId}`;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
nextStepUrl = `/`;
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect(nextStepUrl);
|
||||
};
|
||||
|
||||
//--- Helper function
|
||||
const getAdTypeString = adType => {
|
||||
const adTypeStringIds = {
|
||||
[AD_TYPE.AD_TYPE_SALE.id]: AD_TYPE.AD_TYPE_SALE.stringId,
|
||||
[AD_TYPE.AD_TYPE_RENT.id]: AD_TYPE.AD_TYPE_RENT.stringId
|
||||
@@ -20,20 +68,7 @@ const postWelcome = async (req, res) => {
|
||||
const adTypeStringId =
|
||||
adTypeStringIds[adType] || AD_TYPE.AD_TYPE_SALE.stringId;
|
||||
|
||||
let nextStepUrl = "";
|
||||
try {
|
||||
const newSearchRequest = await createSearchRequest({
|
||||
adType: adTypeStringId,
|
||||
realEstateType: AD_CATEGORY.FLAT.id
|
||||
});
|
||||
|
||||
nextStepUrl = `/vrstanekretnine/${newSearchRequest.id}`;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
nextStepUrl = `/`;
|
||||
}
|
||||
|
||||
res.redirect(nextStepUrl);
|
||||
return adTypeStringId;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -9,12 +9,14 @@ const OlxCrawler = require("./specificCrawlers/olx");
|
||||
const RentalCrawler = require("./specificCrawlers/rental");
|
||||
const ProstorCrawler = require("./specificCrawlers/prostor");
|
||||
const AktidoCrawler = require("./specificCrawlers/aktido");
|
||||
const SaljicCrawler = require("./specificCrawlers/saljic");
|
||||
|
||||
const {
|
||||
OLX_CONFIG,
|
||||
RENTAL_CONFIG,
|
||||
PROSTOR_CONFIG,
|
||||
AKTIDO_CONFIG
|
||||
AKTIDO_CONFIG,
|
||||
SALJIC_CONFIG
|
||||
} = require("./crawlerConfig");
|
||||
const PostgresSaver = require("./savers/postgres");
|
||||
|
||||
@@ -57,6 +59,15 @@ async function crawlAll() {
|
||||
AKTIDO_CONFIG.AKTIDO_MAX_RESULTS_PER_PAGE,
|
||||
AKTIDO_CONFIG.AKTIDO_IGNORED_USERNAMES,
|
||||
AKTIDO_CONFIG.AKTIDO_DELAY_BETWEEN_PAGES
|
||||
),
|
||||
new SaljicCrawler(
|
||||
[postgresSaver],
|
||||
SALJIC_CONFIG.SALJIC_CRAWLER_AD_TYPE,
|
||||
SALJIC_CONFIG.SALJIC_CRAWLER_AD_CATEGORIES,
|
||||
SALJIC_CONFIG.SALJIC_MAX_PAGES,
|
||||
SALJIC_CONFIG.SALJIC_MAX_RESULTS_PER_PAGE,
|
||||
SALJIC_CONFIG.SALJIC_IGNORED_USERNAMES,
|
||||
SALJIC_CONFIG.SALJIC_DELAY_BETWEEN_PAGES
|
||||
)
|
||||
];
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ const OLX_CONFIG = require("./specificConfigs/olx");
|
||||
const RENTAL_CONFIG = require("./specificConfigs/rental");
|
||||
const PROSTOR_CONFIG = require("./specificConfigs/prostor");
|
||||
const AKTIDO_CONFIG = require("./specificConfigs/aktido");
|
||||
const SALJIC_CONFIG = require("./specificConfigs/saljic");
|
||||
|
||||
module.exports = {
|
||||
OLX_CONFIG,
|
||||
RENTAL_CONFIG,
|
||||
PROSTOR_CONFIG,
|
||||
AKTIDO_CONFIG
|
||||
AKTIDO_CONFIG,
|
||||
SALJIC_CONFIG
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const moment = require("moment");
|
||||
|
||||
const { bulkUpsertRealEstates } = require("../../helpers/db/realEstate");
|
||||
const { bulkUpsertPriceHistory } = require("../../helpers/db/priceHistory");
|
||||
|
||||
class PostgresSaver {
|
||||
connect() {
|
||||
@@ -11,6 +12,21 @@ class PostgresSaver {
|
||||
|
||||
async save(results) {
|
||||
const savedRecords = await bulkUpsertRealEstates(results);
|
||||
//Extruding data for price history table
|
||||
const resultPrices = savedRecords.map(realEstate => {
|
||||
//Null values canot be recognized by ignore duplicates in sequalize
|
||||
//Value price = 0 indicates 'cijena na upit'
|
||||
const priceTmp =
|
||||
realEstate.dataValues.price === null ? 0 : realEstate.dataValues.price;
|
||||
|
||||
return {
|
||||
realEstateId: realEstate.dataValues.id,
|
||||
price: priceTmp,
|
||||
createdAt: realEstate.dataValues.createdAt,
|
||||
updatedAt: realEstate.dataValues.updatedAt
|
||||
};
|
||||
});
|
||||
const savedPrices = await bulkUpsertPriceHistory(resultPrices);
|
||||
|
||||
if (Array.isArray(savedRecords)) {
|
||||
const newRealEstates = [];
|
||||
|
||||
34
app/crawler/specificConfigs/saljic.js
Normal file
34
app/crawler/specificConfigs/saljic.js
Normal file
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
const { CRAWLER_AD_TYPE, AD_CATEGORY } = require("../../common/enums");
|
||||
|
||||
const saljicCrawlerAdType =
|
||||
process.env.SALJIC_CRAWLER_AD_TYPE !== undefined
|
||||
? CRAWLER_AD_TYPE[process.env.SALJIC_CRAWLER_AD_TYPE]
|
||||
: null;
|
||||
|
||||
const saljicParsedCrawlerAdCategories =
|
||||
process.env.SALJIC_CRAWLER_AD_CATEGORIES !== undefined
|
||||
? process.env.SALJIC_CRAWLER_AD_CATEGORIES.split(",").map(category =>
|
||||
category.trim()
|
||||
)
|
||||
: ["FLAT", "HOUSE"];
|
||||
|
||||
const saljicIgnoredUsernames = [];
|
||||
|
||||
const transformedSaljicCrawlerAdCategories = saljicParsedCrawlerAdCategories
|
||||
.map(categoryName =>
|
||||
AD_CATEGORY[categoryName] ? AD_CATEGORY[categoryName].id : undefined
|
||||
)
|
||||
.filter(category => !!category);
|
||||
|
||||
module.exports = {
|
||||
SALJIC_MAX_PAGES: parseInt(process.env.SALJIC_MAX_PAGES) || 100,
|
||||
SALJIC_MAX_RESULTS_PER_PAGE:
|
||||
parseInt(process.env.SALJIC_MAX_RESULTS_PER_PAGE) || 5000,
|
||||
SALJIC_CRAWLER_AD_TYPE: saljicCrawlerAdType || CRAWLER_AD_TYPE.NONE,
|
||||
SALJIC_CRAWLER_AD_CATEGORIES: transformedSaljicCrawlerAdCategories,
|
||||
SALJIC_IGNORED_USERNAMES: saljicIgnoredUsernames || [],
|
||||
SALJIC_DELAY_BETWEEN_PAGES:
|
||||
parseInt(process.env.SALJIC_DELAY_BETWEEN_PAGES) || 1000,
|
||||
SALJIC_FORCE_CRAWL: !!parseInt(process.env.SALJIC_FORCE_CRAWL)
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
const fetch = require("node-fetch");
|
||||
const cheerio = require("cheerio");
|
||||
const moment = require("moment-timezone");
|
||||
const FormData = require("form-data");
|
||||
|
||||
const {
|
||||
AD_TYPE,
|
||||
@@ -16,7 +17,8 @@ const {
|
||||
|
||||
const {
|
||||
PRINT_CRAWLER_DEBUG,
|
||||
DEFAULT_TIMEZONE
|
||||
DEFAULT_TIMEZONE,
|
||||
PROSTOR_LOGIN
|
||||
} = require("../../config/appConfig");
|
||||
const { PROSTOR_FORCE_CRAWL } = require("../specificConfigs/prostor");
|
||||
|
||||
@@ -60,13 +62,16 @@ class ProstorCrawler {
|
||||
|
||||
async crawl() {
|
||||
const crawlAdCategories = this.crawlerAdCategories;
|
||||
|
||||
//We need session cookie to use login privileges
|
||||
const prostorCookie = await this.getCookies();
|
||||
//New tag to check if crawler loged in
|
||||
const login = await this.loginForScraping(PROSTOR_LOGIN, prostorCookie);
|
||||
const newRealEstates = [];
|
||||
|
||||
if (crawlAdCategories) {
|
||||
//Crawl only if login was successful
|
||||
if (crawlAdCategories && login) {
|
||||
const indexGenerators = [];
|
||||
for (const adCategory of crawlAdCategories) {
|
||||
indexGenerators.push(this.categoryIndexer(adCategory));
|
||||
indexGenerators.push(this.categoryIndexer(adCategory, prostorCookie));
|
||||
}
|
||||
|
||||
let done = false;
|
||||
@@ -119,13 +124,14 @@ class ProstorCrawler {
|
||||
return newRealEstates;
|
||||
}
|
||||
|
||||
async *categoryIndexer(adCategory) {
|
||||
async *categoryIndexer(adCategory, prostorCookie) {
|
||||
const urlAdTypePart = PROSTOR_ENUMS.PROSTOR_AD_TYPE[this.crawlerAdTypes];
|
||||
const urlCategoryPart = PROSTOR_ENUMS.PROSTOR_AD_CATEGORY[adCategory];
|
||||
if (urlAdTypePart !== undefined && urlCategoryPart !== undefined) {
|
||||
const urlPageToCrawl = `${this.baseUrl}?remove_sold=0${urlAdTypePart}${urlCategoryPart}`;
|
||||
const listOfAllRealEstates = await this.extractRealEstates(
|
||||
urlPageToCrawl
|
||||
urlPageToCrawl,
|
||||
prostorCookie
|
||||
);
|
||||
|
||||
let elementToStartIndexFrom = 0;
|
||||
@@ -139,7 +145,8 @@ class ProstorCrawler {
|
||||
elementToStartIndexFrom += realEstatesForSinglePage.length;
|
||||
|
||||
const singlePageResults = await this.indexSinglePage(
|
||||
realEstatesForSinglePage
|
||||
realEstatesForSinglePage,
|
||||
prostorCookie
|
||||
);
|
||||
|
||||
const filteredSinglePageResults = singlePageResults.filter(
|
||||
@@ -163,10 +170,10 @@ class ProstorCrawler {
|
||||
}
|
||||
}
|
||||
|
||||
async indexSinglePage(realEstatesList) {
|
||||
async indexSinglePage(realEstatesList, prostorCookie) {
|
||||
const asyncActions = [];
|
||||
for (const realEstate of realEstatesList) {
|
||||
asyncActions.push(this.scrapeAd(realEstate));
|
||||
asyncActions.push(this.scrapeAd(realEstate, prostorCookie));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -180,12 +187,25 @@ class ProstorCrawler {
|
||||
}
|
||||
}
|
||||
|
||||
async scrapeAd(realEstate) {
|
||||
async scrapeAd(realEstate, prostorCookie) {
|
||||
const { lat, lng, property_name, price, size, link, status } = realEstate;
|
||||
|
||||
//Status information is given already in realestate list
|
||||
//For VIP Ads status ='' canot be used, but no VIP ads are crawled
|
||||
//We will make "fake" vip ad for RE that have size=55
|
||||
//It is weird because yesterday it said 'VIP ponuda' ???
|
||||
const adStatus =
|
||||
size === "55"
|
||||
? ProstorCrawler.getStatusId("VIP ponuda")
|
||||
: ProstorCrawler.getStatusId(status);
|
||||
|
||||
const url = `https://prostor.ba${link}`;
|
||||
|
||||
// console.log("[PROSTOR] Scraping : ", url);
|
||||
try {
|
||||
const adPageSource = await fetch(url);
|
||||
const adPageSource = await fetch(url, {
|
||||
headers: { Cookie: prostorCookie }
|
||||
});
|
||||
const body = await adPageSource.text();
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
@@ -330,7 +350,6 @@ class ProstorCrawler {
|
||||
furnishingType = FURNISHING_TYPE.NOT_FURNISHED.id;
|
||||
}
|
||||
|
||||
const adStatus = ProstorCrawler.getStatusId(status);
|
||||
const title = property_name;
|
||||
const parsedPrice = parseFloat(price.replace(/\./g, "")) || null;
|
||||
const parsedArea = parseFloat(size);
|
||||
@@ -408,13 +427,15 @@ class ProstorCrawler {
|
||||
}
|
||||
}
|
||||
|
||||
async extractRealEstates(url) {
|
||||
async extractRealEstates(url, prostorCookie) {
|
||||
if (PRINT_CRAWLER_DEBUG) {
|
||||
console.log("[PROSTOR] Index page : ", url);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const res = await fetch(url, {
|
||||
headers: { Cookie: prostorCookie }
|
||||
});
|
||||
const body = await res.text();
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
@@ -548,6 +569,8 @@ class ProstorCrawler {
|
||||
return AD_STATUS.STATUS_SOLD;
|
||||
case "Iznajmljeno":
|
||||
return AD_STATUS.STATUS_RENTED;
|
||||
case "VIP ponuda":
|
||||
return AD_STATUS.STATUS_VIP;
|
||||
default:
|
||||
console.log("[PROSTOR] Unknown AD_STATUS : [", statusText, "]");
|
||||
return AD_STATUS.STATUS_NORMAL;
|
||||
@@ -569,6 +592,51 @@ class ProstorCrawler {
|
||||
return savers[0].save(results);
|
||||
//so that we can use some sequelize options and information when data is inserted
|
||||
}
|
||||
async loginForScraping(PROSTOR_LOGIN, prostorCookie) {
|
||||
let formData = new FormData();
|
||||
formData.append("email", PROSTOR_LOGIN.EMAIL);
|
||||
formData.append("password", PROSTOR_LOGIN.PASSWORD);
|
||||
|
||||
return fetch("https://prostor.ba/moj-prostor/prijava", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: { Cookie: prostorCookie }
|
||||
})
|
||||
.then(page => {
|
||||
return page.text();
|
||||
})
|
||||
.then(resp => {
|
||||
const $ = cheerio.load(resp);
|
||||
if (
|
||||
$("h1")
|
||||
.text()
|
||||
.indexOf("Dobrodošli") !== -1
|
||||
) {
|
||||
console.log("[PROSTOR]: Crawler loged in!");
|
||||
return true;
|
||||
} else {
|
||||
console.log("[PROSTOR]: Crawler login failed - wrong credentials!");
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("[PROSTOR]: Crawler login error ", err);
|
||||
});
|
||||
}
|
||||
async getCookies() {
|
||||
const getResponse = await fetch("https://prostor.ba/moj-prostor/prijava", {
|
||||
headers: { Cookie: "" }
|
||||
});
|
||||
const raw = getResponse.headers.raw()["set-cookie"];
|
||||
const cookie = raw
|
||||
.map(datastring => {
|
||||
const data = datastring.split(";");
|
||||
const cookieData = data[0];
|
||||
return cookieData;
|
||||
})
|
||||
.join(";");
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProstorCrawler;
|
||||
|
||||
@@ -312,7 +312,7 @@ class RentalCrawler {
|
||||
|
||||
let numberOfRooms =
|
||||
parseInt(extractedData["re_realEstates_roomsNO"]) +
|
||||
parseInt(extractedData["re_realEstates_bedroomNO"]) || null,
|
||||
parseInt(extractedData["re_realEstates_bedNO"]) || null,
|
||||
numberOfFloors =
|
||||
parseInt(extractedData["re_realEstates_floorsNO"]) ||
|
||||
this.getNumberOfFloorsFromFloorId(extractedData["re_floorNO_id"]),
|
||||
@@ -352,7 +352,9 @@ class RentalCrawler {
|
||||
realEstatePropertiesFromInfrastructure.phoneConnection,
|
||||
cableTV = realEstatePropertiesFromInfrastructure.cableTV,
|
||||
internet = realEstatePropertiesFromInfrastructure.internet,
|
||||
basementAttic = realEstatePropertiesFromSpaces.basementAttic,
|
||||
basementAttic =
|
||||
realEstatePropertiesFromSpaces.basementAttic ||
|
||||
this.checkBasemAtticFromFloors(extractedData["re_floorNO_id"]),
|
||||
storeRoom = realEstatePropertiesFromSpaces.storeRoom,
|
||||
videoSurveillance =
|
||||
realEstatePropertiesFromDescriptions.videoSurveillance ||
|
||||
@@ -397,9 +399,7 @@ class RentalCrawler {
|
||||
);
|
||||
if (!publishedDateMoment.isValid()) {
|
||||
throw {
|
||||
message: `Invalid published date : ${
|
||||
extractedData["re_realEstates_inserted"]
|
||||
}`
|
||||
message: `Invalid published date : ${extractedData["re_realEstates_inserted"]}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -410,9 +410,7 @@ class RentalCrawler {
|
||||
);
|
||||
if (!renewedDateMoment.isValid()) {
|
||||
throw {
|
||||
message: `Invalid renewed date : ${
|
||||
extractedData["re_realEstates_edited"]
|
||||
}`
|
||||
message: `Invalid renewed date : ${extractedData["re_realEstates_edited"]}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -782,8 +780,42 @@ class RentalCrawler {
|
||||
if (floorIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let noOfFloors = floorIds.length;
|
||||
// Floors of 'suteren', 'podrum', 'tavan' and 'potkrovlje' are not counted
|
||||
floorIds.forEach(id => {
|
||||
if (
|
||||
parseInt(id) === 1 ||
|
||||
parseInt(id) === 2 ||
|
||||
parseInt(id) === 12 ||
|
||||
parseInt(id) === 14
|
||||
) {
|
||||
noOfFloors--;
|
||||
}
|
||||
});
|
||||
return noOfFloors;
|
||||
}
|
||||
|
||||
return floorIds.length;
|
||||
checkBasemAtticFromFloors(floorsIdText) {
|
||||
// floorIdText can be array of numbers, separated by comma or number
|
||||
const floorIds = floorsIdText.split(",");
|
||||
|
||||
let check = false;
|
||||
|
||||
if (floorIds.length === 0) {
|
||||
check = false;
|
||||
}
|
||||
//If floors 'suteren', 'podrum', 'tavan' and 'potkrovlje' exists then tag for basement-attic is true
|
||||
floorIds.forEach(id => {
|
||||
if (
|
||||
parseInt(id) === 1 ||
|
||||
parseInt(id) === 2 ||
|
||||
parseInt(id) === 12 ||
|
||||
parseInt(id) === 14
|
||||
) {
|
||||
check = true;
|
||||
}
|
||||
});
|
||||
return check;
|
||||
}
|
||||
|
||||
async sleep(ms) {
|
||||
|
||||
630
app/crawler/specificCrawlers/saljic.js
Normal file
630
app/crawler/specificCrawlers/saljic.js
Normal file
@@ -0,0 +1,630 @@
|
||||
"use strict";
|
||||
|
||||
const fetch = require("node-fetch");
|
||||
const cheerio = require("cheerio");
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
const {
|
||||
AD_TYPE,
|
||||
AD_CATEGORY,
|
||||
AD_AGENCY,
|
||||
AD_STATUS,
|
||||
CRAWLER_AD_TYPE,
|
||||
FURNISHING_TYPE,
|
||||
HEATING_TYPE
|
||||
} = require("../../common/enums");
|
||||
|
||||
const {
|
||||
PRINT_CRAWLER_DEBUG,
|
||||
DEFAULT_TIMEZONE
|
||||
} = require("../../config/appConfig");
|
||||
const { SALJIC_FORCE_CRAWL } = require("../specificConfigs/saljic");
|
||||
|
||||
const SALJIC_ENUMS = {
|
||||
SALJIC_AD_TYPE: {
|
||||
[CRAWLER_AD_TYPE.ALL]: "&input_vrsta=",
|
||||
[CRAWLER_AD_TYPE.ONLY_SELL]: "&input_vrsta=1",
|
||||
[CRAWLER_AD_TYPE.ONLY_RENT]: "&input_vrsta=2"
|
||||
},
|
||||
SALJIC_AD_CATEGORY: {
|
||||
[AD_CATEGORY.ALL.id]: "&input_kategorija=",
|
||||
[AD_CATEGORY.FLAT.id]: "&input_kategorija=15",
|
||||
[AD_CATEGORY.HOUSE.id]: "&input_kategorija=9",
|
||||
[AD_CATEGORY.LAND.id]: "&input_kategorija=5", //3 and 4 also gradjevinsko
|
||||
[AD_CATEGORY.OFFICE.id]: "&input_kategorija=8",
|
||||
[AD_CATEGORY.APARTMENT.id]: "&input_kategorija=1",
|
||||
[AD_CATEGORY.GARAGE.id]: "&input_kategorija=2"
|
||||
//[AD_CATEGORY.COTTAGE.id]: ""
|
||||
}
|
||||
};
|
||||
|
||||
class SaljicCrawler {
|
||||
constructor(
|
||||
savers = [],
|
||||
crawlerAdTypes = CRAWLER_AD_TYPE.ALL,
|
||||
crawlerAdCategories = [AD_CATEGORY.FLAT, AD_CATEGORY.HOUSE],
|
||||
maxPages = 5000,
|
||||
maxResultsPerPage = 5000,
|
||||
ignoredUsernames = [],
|
||||
delayBetweenPages = 1000
|
||||
) {
|
||||
this.savers = savers;
|
||||
this.baseUrl = "https://www.saljicnekretnine.ba/v2/nekretnine_search";
|
||||
this.crawlerAdTypes = crawlerAdTypes;
|
||||
this.crawlerAdCategories = crawlerAdCategories;
|
||||
this.maxResultsPerPage = maxResultsPerPage;
|
||||
this.delayBetweenPages = delayBetweenPages;
|
||||
}
|
||||
|
||||
async crawl() {
|
||||
const crawlAdCategories = this.crawlerAdCategories;
|
||||
|
||||
const newRealEstates = [];
|
||||
|
||||
if (crawlAdCategories) {
|
||||
const indexGenerators = [];
|
||||
for (const adCategory of crawlAdCategories) {
|
||||
indexGenerators.push(this.categoryIndexer(adCategory));
|
||||
}
|
||||
//
|
||||
//console.log(indexGenerators);
|
||||
//
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const categoryIndexerPromises = [];
|
||||
const generatorsToRemove = [];
|
||||
for (const indexGenerator of indexGenerators) {
|
||||
categoryIndexerPromises.push(indexGenerator.next());
|
||||
generatorsToRemove.push(false);
|
||||
}
|
||||
|
||||
const singlePageResults = await Promise.all(categoryIndexerPromises);
|
||||
const entries = singlePageResults.entries();
|
||||
|
||||
for (const [index, { value: singlePageResult }] of entries) {
|
||||
if (singlePageResult) {
|
||||
const saveResults = await this.saveCrawledResults(singlePageResult);
|
||||
const { newRecords } = saveResults;
|
||||
|
||||
newRealEstates.push(...newRecords);
|
||||
|
||||
if (
|
||||
Array.isArray(newRecords) &&
|
||||
newRecords.length === 0 &&
|
||||
!SALJIC_FORCE_CRAWL
|
||||
) {
|
||||
generatorsToRemove[index] = true;
|
||||
}
|
||||
} else {
|
||||
//Generator returned undefined, remove this generator from array
|
||||
generatorsToRemove[index] = true;
|
||||
// console.log("Generator ", index + 1, "has no more pages");
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("Generators state : ", generatorsToRemove);
|
||||
for (let i = generatorsToRemove.length - 1; i >= 0; i--) {
|
||||
if (generatorsToRemove[i]) {
|
||||
// console.log("\tRemove generator ", i + 1);
|
||||
indexGenerators.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (indexGenerators.length === 0) {
|
||||
done = true;
|
||||
}
|
||||
|
||||
await this.sleep(this.delayBetweenPages);
|
||||
}
|
||||
}
|
||||
return newRealEstates;
|
||||
}
|
||||
|
||||
async *categoryIndexer(adCategory) {
|
||||
let pageToIndex = 1;
|
||||
|
||||
const urlAdTypePart = SALJIC_ENUMS.SALJIC_AD_TYPE[this.crawlerAdTypes];
|
||||
const urlCategoryPart = SALJIC_ENUMS.SALJIC_AD_CATEGORY[adCategory];
|
||||
|
||||
if (urlAdTypePart !== undefined && urlCategoryPart !== undefined) {
|
||||
while (true) {
|
||||
const urlPagePart = pageToIndex === 1 ? "" : (pageToIndex - 1) * 2 * 11;
|
||||
const urlPageToCrawl = `${this.baseUrl}?order_by=${urlAdTypePart}${urlCategoryPart}&per_page=${urlPagePart}`;
|
||||
|
||||
const singlePageResults = await this.indexSinglePage(
|
||||
urlPageToCrawl,
|
||||
this.maxResultsPerPage
|
||||
);
|
||||
|
||||
if (Array.isArray(singlePageResults) && singlePageResults.length > 0) {
|
||||
yield singlePageResults;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
++pageToIndex;
|
||||
if (pageToIndex === this.maxPages) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async indexSinglePage(url, maxResultsPerPage) {
|
||||
if (PRINT_CRAWLER_DEBUG) {
|
||||
console.log("[SALJIC] Index page : ", url);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const body = await res.text();
|
||||
const $ = cheerio.load(body);
|
||||
let hrefs = [];
|
||||
|
||||
$("#shop")
|
||||
.find(".product")
|
||||
.each((i, elem) => {
|
||||
const href = $(elem)
|
||||
.find("a")
|
||||
.first()
|
||||
.attr("href");
|
||||
if (href) {
|
||||
hrefs.push(href);
|
||||
}
|
||||
});
|
||||
|
||||
let adTypesTmp = [];
|
||||
|
||||
$("#shop")
|
||||
.find(".product")
|
||||
.each((i, elem) => {
|
||||
const adType = $(elem)
|
||||
.find(".trakica-search-page")
|
||||
.text()
|
||||
.trim();
|
||||
if (adType) {
|
||||
adTypesTmp.push(adType);
|
||||
}
|
||||
});
|
||||
|
||||
//Converting to AD_TYPE
|
||||
const adTypes = adTypesTmp.map(adTypeText => {
|
||||
return this.getAdTypeId(adTypeText);
|
||||
});
|
||||
|
||||
//Converting to absolute URLs
|
||||
const hrefsAbs = hrefs.map(link => {
|
||||
return "https://www.saljicnekretnine.ba" + link;
|
||||
});
|
||||
|
||||
let actualNoOfResults =
|
||||
hrefsAbs.length <= maxResultsPerPage
|
||||
? hrefsAbs.length
|
||||
: maxResultsPerPage;
|
||||
|
||||
const asyncScraping = [];
|
||||
for (let i = 0; i < actualNoOfResults; i++) {
|
||||
asyncScraping.push(this.scrapeAd(hrefsAbs[i], adTypes[i]));
|
||||
}
|
||||
|
||||
const scrapedData = await Promise.all(asyncScraping);
|
||||
const filteredScrapedData = scrapedData.filter(adData => !!adData);
|
||||
return filteredScrapedData;
|
||||
} catch (e) {
|
||||
console.error("[SALJIC] Exception caught:" + e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async scrapeAd(url, adType) {
|
||||
// console.log("[SALJIC] Scraping : ", url);
|
||||
try {
|
||||
const adPageSource = await fetch(url);
|
||||
const body = await adPageSource.text();
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
// No information for status ex. PRODAN
|
||||
const status = AD_STATUS.STATUS_NORMAL;
|
||||
//Extracting agency ID from url
|
||||
const agencyObjectId = parseInt(url.substring(46, url.length));
|
||||
|
||||
//Extracting main properties
|
||||
const propertySelectors = {
|
||||
title:
|
||||
"div.content-wrap > div.container > div.col-md-8.nobottommargin > div.single-post > div.entry > div.entry-title > h2",
|
||||
price:
|
||||
"div.content-wrap > div.container > div.col-md-8.nobottommargin > div.single-post > div.entry > div.topmargin-sm.single-product > div.product > div.product-price > ins",
|
||||
streetName:
|
||||
"div.content-wrap > div.container > div.col-md-8.nobottommargin > div.single-post > div.entry > div.entry-content.topmargin > p",
|
||||
|
||||
descriptions:
|
||||
"div.content-wrap > div.container > div.col-md-8.nobottommargin > div.single-post > div.entry > div.entry-content.topmargin > div.toggle.toggle-bg > div.togglec >p:nth-child(1)",
|
||||
latAndLong:
|
||||
"div.content-wrap > div.container > div.col-md-8.nobottommargin > div.single-post > div.entry > div.entry-content.topmargin > div.gmap.bottommargin > iframe"
|
||||
};
|
||||
const title = $(propertySelectors.title)
|
||||
.text()
|
||||
.replace(/(\r\n|\n|\r)/gm, "")
|
||||
.replace(/ {1,}/g, " ")
|
||||
.trim();
|
||||
|
||||
const priceText = $(propertySelectors.price)
|
||||
.text()
|
||||
.replace(/(\r\n|\n|\r)/gm, "")
|
||||
.replace(/ {1,}/g, " ")
|
||||
.trim();
|
||||
const price =
|
||||
priceText === "CIJENA NA UPIT"
|
||||
? null
|
||||
: parseFloat(
|
||||
priceText.substring(8, priceText.length - 3).replace(",", "")
|
||||
);
|
||||
|
||||
const streetName = $(propertySelectors.streetName)
|
||||
.text()
|
||||
.replace(/(\r\n|\n|\r)/gm, "")
|
||||
.trim();
|
||||
|
||||
const descriptions = $(propertySelectors.descriptions)
|
||||
.text()
|
||||
.replace(/\"/g, "")
|
||||
.trim();
|
||||
|
||||
const latAndLongSrc = $(propertySelectors.latAndLong).attr("src");
|
||||
const latText = latAndLongSrc.substring(
|
||||
latAndLongSrc.indexOf("marker=") + 7,
|
||||
latAndLongSrc.indexOf("%2C", latAndLongSrc.indexOf("marker="))
|
||||
);
|
||||
const longText = latAndLongSrc.substring(
|
||||
latAndLongSrc.indexOf("%2C", latAndLongSrc.indexOf("marker=")) + 3,
|
||||
latAndLongSrc.length
|
||||
);
|
||||
const locationLat = parseFloat(latText) || null;
|
||||
const locationLong = parseFloat(longText) || null;
|
||||
|
||||
//====== DETAIL INFORMATION FIELDS ==========
|
||||
let area = null,
|
||||
gardenSize = null,
|
||||
numberOfRooms = null,
|
||||
numberOfFloors = null,
|
||||
floor = null,
|
||||
accessRoadType = null,
|
||||
heatingType = null,
|
||||
furnishingType = null,
|
||||
balcony = null,
|
||||
newBuilding = null,
|
||||
elevator = null,
|
||||
water = null,
|
||||
electricity = null,
|
||||
drainageSystem = null,
|
||||
registeredInZkBooks = null,
|
||||
recentlyAdapted = null,
|
||||
parking = null,
|
||||
garage = null,
|
||||
gas = null,
|
||||
antiTheftDoor = null,
|
||||
airCondition = null,
|
||||
phoneConnection = null,
|
||||
cableTV = null,
|
||||
internet = null,
|
||||
basementAttic = null,
|
||||
storeRoom = null,
|
||||
videoSurveillance = null,
|
||||
alarm = null,
|
||||
suitableForStudents = null,
|
||||
includingBills = null,
|
||||
animalsAllowed = null,
|
||||
pool = null,
|
||||
exchange = null,
|
||||
urbanPlanPermit = null,
|
||||
buildingPermit = null,
|
||||
utilityConnection = null,
|
||||
distanceToRiver = null;
|
||||
let publishedDate = null;
|
||||
let renewedDate = null;
|
||||
let realEstateType;
|
||||
let numberOfViewsAgency = null;
|
||||
let numberOfViewsKivi = null;
|
||||
let streetNumber = 0;
|
||||
let adStatus = status;
|
||||
let shortDescription = descriptions.substring(
|
||||
0,
|
||||
descriptions.indexOf(".")
|
||||
);
|
||||
let longDescription = descriptions;
|
||||
//Extracting data - Glavne karakteristike
|
||||
let mainFieldIndex = 1;
|
||||
do {
|
||||
const mainFieldSelector = `div.content-wrap > div.container > div.col-md-8.nobottommargin > div.single-post > div.entry > div.entry-content.topmargin > div.col-md-12.bottommargin > ul > li.list-group-item:nth-child(${mainFieldIndex})`;
|
||||
|
||||
const mainField = $(mainFieldSelector)
|
||||
.text()
|
||||
.replace(/[\n\r\t]/gm, "")
|
||||
.trim();
|
||||
|
||||
const mainFieldTitle = mainField.substring(0, mainField.indexOf(" "));
|
||||
const mainFieldValue = mainField
|
||||
.substring(mainField.indexOf(" "), mainField.length)
|
||||
.trim();
|
||||
|
||||
switch (mainFieldTitle) {
|
||||
case "Površina":
|
||||
area = parseFloat(
|
||||
mainFieldValue.substring(0, mainFieldValue.indexOf(" "))
|
||||
);
|
||||
break;
|
||||
case "Okućnica":
|
||||
gardenSize = parseFloat(
|
||||
mainFieldValue.substring(0, mainFieldValue.indexOf(" "))
|
||||
);
|
||||
break;
|
||||
case "Broj soba":
|
||||
numberOfRooms = parseInt(mainFieldValue);
|
||||
break;
|
||||
case "Broj spratova":
|
||||
numberOfFloors = parseInt(mainFieldValue);
|
||||
break;
|
||||
case "Sprat":
|
||||
floor = parseInt(mainFieldValue);
|
||||
break;
|
||||
case "Godina renoviranja":
|
||||
recentlyAdapted = true;
|
||||
break;
|
||||
case "Broj parking mjesta":
|
||||
parking = true;
|
||||
break;
|
||||
case "Dostupno od":
|
||||
const day = mainFieldValue.substring(0, 2);
|
||||
const month = mainFieldValue.substring(3, 5);
|
||||
const year = mainFieldValue.substring(6, mainFieldValue.length);
|
||||
publishedDate = new Date(`${month}/${day}/${year}`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (mainFieldTitle === "") {
|
||||
break;
|
||||
}
|
||||
mainFieldIndex++;
|
||||
} while (true);
|
||||
|
||||
//Extracting data - Sadrzaji
|
||||
let additionalFieldIndex = 1;
|
||||
do {
|
||||
const additionalFieldSelector = `div.content-wrap > div.container > div.col-md-8.nobottommargin > div.single-post > div.entry > div.entry-content.topmargin > div.col-md-12.bottommargin > ul > li.border-color.col-md-5.col-md-offset-1.col-md-pull-1.list-group-item-bottom:nth-child(${additionalFieldIndex})`;
|
||||
|
||||
const additionalField = $(additionalFieldSelector)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
if (additionalFieldIndex === 1) {
|
||||
//Extracting data of real estate type
|
||||
const categoryTmp = additionalField
|
||||
.replace(/[\n\r\t]/gm, "")
|
||||
.substring(
|
||||
additionalField.indexOf("Kategorija") + 10,
|
||||
additionalField.length
|
||||
)
|
||||
.trim();
|
||||
realEstateType = this.getAdCategoryId(categoryTmp);
|
||||
} else {
|
||||
switch (additionalField) {
|
||||
case "Internet":
|
||||
internet = true;
|
||||
break;
|
||||
case "Garaža":
|
||||
garage = true;
|
||||
break;
|
||||
case "Klima":
|
||||
airCondition = true;
|
||||
break;
|
||||
case "Balkon":
|
||||
balcony = true;
|
||||
break;
|
||||
case "Ostava":
|
||||
storeRoom = true;
|
||||
break;
|
||||
case "Podrum":
|
||||
basementAttic = true;
|
||||
break;
|
||||
case "Blindirana vrata":
|
||||
antiTheftDoor = true;
|
||||
break;
|
||||
case "Voda":
|
||||
water = true;
|
||||
break;
|
||||
case "Kablovska":
|
||||
cableTV = true;
|
||||
break;
|
||||
case "Uknjiženo":
|
||||
registeredInZkBooks = true;
|
||||
break;
|
||||
case "Grijanje - centralno":
|
||||
heatingType = HEATING_TYPE.CENTRAL_CITY.id;
|
||||
break;
|
||||
case "Grijanje - plin":
|
||||
heatingType = HEATING_TYPE.GAS.id;
|
||||
break;
|
||||
case "Grijanje - struja":
|
||||
heatingType = HEATING_TYPE.ELECTRICITY.id;
|
||||
break;
|
||||
case "Grijanje":
|
||||
heatingType = HEATING_TYPE.OTHER.id;
|
||||
break;
|
||||
case "Plin":
|
||||
gas = true;
|
||||
break;
|
||||
case "Namješten":
|
||||
furnishingType = FURNISHING_TYPE.FURNISHED.id;
|
||||
break;
|
||||
case "Alarm":
|
||||
alarm = true;
|
||||
break;
|
||||
case "Video nadzor":
|
||||
videoSurveillance = true;
|
||||
break;
|
||||
case "Lift":
|
||||
elevator = true;
|
||||
break;
|
||||
case "Novogradnja":
|
||||
newBuilding = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalField === "") {
|
||||
break;
|
||||
}
|
||||
additionalFieldIndex++;
|
||||
} while (true);
|
||||
|
||||
//If no published date it takes current date of crawling
|
||||
if (publishedDate) {
|
||||
renewedDate = new Date();
|
||||
} else {
|
||||
publishedDate = new Date();
|
||||
renewedDate = new Date();
|
||||
}
|
||||
|
||||
const originAgencyName = AD_AGENCY.SALJIC;
|
||||
const locality = "";
|
||||
const municipality = "";
|
||||
const city = "";
|
||||
const region = "";
|
||||
const entity = "";
|
||||
const country = "";
|
||||
|
||||
const data = {
|
||||
url,
|
||||
agencyObjectId,
|
||||
originAgencyName,
|
||||
realEstateType,
|
||||
adType,
|
||||
title,
|
||||
price,
|
||||
area,
|
||||
gardenSize,
|
||||
shortDescription,
|
||||
longDescription,
|
||||
streetNumber,
|
||||
streetName,
|
||||
locality,
|
||||
municipality,
|
||||
city,
|
||||
region,
|
||||
entity,
|
||||
country,
|
||||
locationLat,
|
||||
locationLong,
|
||||
adStatus,
|
||||
publishedDate,
|
||||
renewedDate,
|
||||
numberOfRooms,
|
||||
numberOfFloors,
|
||||
floor,
|
||||
accessRoadType,
|
||||
heatingType,
|
||||
furnishingType,
|
||||
balcony,
|
||||
newBuilding,
|
||||
elevator,
|
||||
water,
|
||||
electricity,
|
||||
drainageSystem,
|
||||
registeredInZkBooks,
|
||||
recentlyAdapted,
|
||||
parking,
|
||||
garage,
|
||||
gas,
|
||||
antiTheftDoor,
|
||||
airCondition,
|
||||
phoneConnection,
|
||||
cableTV,
|
||||
internet,
|
||||
basementAttic,
|
||||
storeRoom,
|
||||
videoSurveillance,
|
||||
alarm,
|
||||
suitableForStudents,
|
||||
includingBills,
|
||||
animalsAllowed,
|
||||
pool,
|
||||
exchange,
|
||||
urbanPlanPermit,
|
||||
buildingPermit,
|
||||
utilityConnection,
|
||||
distanceToRiver,
|
||||
numberOfViewsAgency,
|
||||
numberOfViewsKivi
|
||||
};
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error("Exception caught: " + e.message, "\r\nURL:", url);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//======= HELPER FUNCTIONS =============
|
||||
|
||||
getAdCategoryId(categoryText) {
|
||||
switch (categoryText) {
|
||||
case "Stan":
|
||||
return AD_CATEGORY.FLAT.id;
|
||||
case "Građevinsko zemljiste":
|
||||
return AD_CATEGORY.LAND.id;
|
||||
case "Industrijsko zemljiste":
|
||||
return AD_CATEGORY.LAND.id;
|
||||
case "Poljoprivredno zemljiste":
|
||||
return AD_CATEGORY.LAND.id;
|
||||
case "Kuća":
|
||||
return AD_CATEGORY.HOUSE.id;
|
||||
case "Poslovni prostor":
|
||||
return AD_CATEGORY.OFFICE.id;
|
||||
case "Kancelarije":
|
||||
return AD_CATEGORY.OFFICE.id;
|
||||
case "Apartmani":
|
||||
return AD_CATEGORY.APARTMENT.id;
|
||||
case "Garaža":
|
||||
return AD_CATEGORY.GARAGE.id;
|
||||
case "Vikendica":
|
||||
return AD_CATEGORY.COTTAGE.id;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getAdTypeId(adTypeText) {
|
||||
switch (adTypeText) {
|
||||
case "PRODAJA":
|
||||
return AD_TYPE.AD_TYPE_SALE.stringId;
|
||||
case "NAJAM":
|
||||
return AD_TYPE.AD_TYPE_RENT.stringId;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async saveCrawledResults(results) {
|
||||
const savers = this.savers;
|
||||
|
||||
// for (const saver of savers) {
|
||||
// await saver.save(results);
|
||||
// }
|
||||
|
||||
//For now, we use only Postgres saver, so ...
|
||||
return savers[0].save(results);
|
||||
//so that we can use some sequelize options and information when data is inserted
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SaljicCrawler;
|
||||
20
app/helpers/db/kiviOriginal.js
Normal file
20
app/helpers/db/kiviOriginal.js
Normal file
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
const db = require("../../models/index");
|
||||
const sequelize = require("sequelize");
|
||||
|
||||
const createKiviOriginal = async (kiviAdFields = {}) => {
|
||||
return await db.KiviOriginal.create(kiviAdFields);
|
||||
};
|
||||
|
||||
const getKiviOriginalById = async id => {
|
||||
try {
|
||||
return db.KiviOriginal.findByPk(id);
|
||||
} catch (error) {
|
||||
console.log("kiviOriginal.js", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
createKiviOriginal,
|
||||
getKiviOriginalById
|
||||
};
|
||||
17
app/helpers/db/kiviOriginalAdsPhotos.js
Normal file
17
app/helpers/db/kiviOriginalAdsPhotos.js
Normal file
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
const db = require("../../models/index");
|
||||
const sequelize = require("sequelize");
|
||||
|
||||
const bulkUpsertKiviPhotos = async kiviPhotosData => {
|
||||
try {
|
||||
return await db.KiviOriginalAdsPhotos.bulkCreate(kiviPhotosData, {
|
||||
ignoreDuplicates: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error bulk upserting kiviOriginalAdsPhotos : ", e);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
bulkUpsertKiviPhotos
|
||||
};
|
||||
20
app/helpers/db/priceHistory.js
Normal file
20
app/helpers/db/priceHistory.js
Normal file
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
const db = require("../../models/index");
|
||||
const sequelize = require("sequelize");
|
||||
|
||||
const bulkUpsertPriceHistory = async priceHistoryData => {
|
||||
try {
|
||||
const order = [["realEstateId", "desc"]];
|
||||
|
||||
return await db.PriceHistory.bulkCreate(priceHistoryData, {
|
||||
order,
|
||||
ignoreDuplicates: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error bulk upserting priceHistory : ", e);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
bulkUpsertPriceHistory
|
||||
};
|
||||
@@ -2,6 +2,8 @@
|
||||
const db = require("../../models/index");
|
||||
const sequelize = require("sequelize");
|
||||
const Op = sequelize.Op;
|
||||
const { AD_CATEGORY } = require("../../common/enums");
|
||||
|
||||
const bulkUpsertRealEstates = async realEstateData => {
|
||||
try {
|
||||
const fieldsToUpdateIfDuplicate = [
|
||||
@@ -75,9 +77,28 @@ const bulkUpsertRealEstates = async realEstateData => {
|
||||
};
|
||||
|
||||
const getRealEstateById = async id => {
|
||||
return db.RealEstate.findByPk(id);
|
||||
try {
|
||||
return db.RealEstate.findByPk(id);
|
||||
} catch (error) {
|
||||
console.log("realEstate.js", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const createRealEstate = async (realEstateFields = {}) => {
|
||||
return await db.RealEstate.create(realEstateFields);
|
||||
};
|
||||
|
||||
const findRealEstateByAgencyId = async kiviId => {
|
||||
try {
|
||||
return db.RealEstate.findOne({
|
||||
where: { agencyObjectId: kiviId }
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("realEstate.js", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
const {
|
||||
priceMin,
|
||||
@@ -96,12 +117,16 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
floorMin,
|
||||
floorMax,
|
||||
includeIncompleteAds,
|
||||
includeWithoutPrice,
|
||||
balcony,
|
||||
elevator,
|
||||
newBuilding,
|
||||
accessRoadType
|
||||
} = searchRequest;
|
||||
|
||||
//Needed for defining which attribute should exist or not
|
||||
const realEstateTypeObject = AD_CATEGORY[realEstateType];
|
||||
|
||||
const longitudeColumn = sequelize.col("locationLong");
|
||||
const latitudeColumn = sequelize.col("locationLat");
|
||||
|
||||
@@ -134,15 +159,6 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
const query = {
|
||||
adType,
|
||||
realEstateType,
|
||||
price: {
|
||||
[Op.or]: {
|
||||
[Op.and]: {
|
||||
[Op.lte]: priceMax,
|
||||
[Op.gte]: priceMin
|
||||
},
|
||||
[Op.is]: null
|
||||
}
|
||||
},
|
||||
area: {
|
||||
[Op.lte]: sizeMax,
|
||||
[Op.gte]: sizeMin
|
||||
@@ -154,15 +170,6 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
const queryIncludeIncomplete = {
|
||||
adType,
|
||||
realEstateType,
|
||||
price: {
|
||||
[Op.or]: {
|
||||
[Op.and]: {
|
||||
[Op.lte]: priceMax,
|
||||
[Op.gte]: priceMin
|
||||
},
|
||||
[Op.is]: null
|
||||
}
|
||||
},
|
||||
area: {
|
||||
[Op.or]: {
|
||||
[Op.and]: {
|
||||
@@ -175,8 +182,49 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
[Op.and]: geoSearchQueryPart
|
||||
};
|
||||
|
||||
//Every other attribute is checked separately and included in query only if it is defined
|
||||
if (gardenSizeMax && gardenSizeMin) {
|
||||
//Is user unchecked includeWithoutPrice FALSE then it shouldn't return null values of price
|
||||
//If not then null values are accepted (this is DEFAULT)
|
||||
//includeIncpompleteAds does not have effect on price query
|
||||
if (includeWithoutPrice) {
|
||||
query.price = {
|
||||
[Op.or]: {
|
||||
[Op.and]: {
|
||||
[Op.lte]: priceMax,
|
||||
[Op.gte]: priceMin
|
||||
},
|
||||
[Op.is]: null
|
||||
}
|
||||
};
|
||||
queryIncludeIncomplete.price = {
|
||||
[Op.or]: {
|
||||
[Op.and]: {
|
||||
[Op.lte]: priceMax,
|
||||
[Op.gte]: priceMin
|
||||
},
|
||||
[Op.is]: null
|
||||
}
|
||||
};
|
||||
} else {
|
||||
query.price = {
|
||||
[Op.and]: {
|
||||
[Op.lte]: priceMax,
|
||||
[Op.gte]: priceMin
|
||||
}
|
||||
};
|
||||
queryIncludeIncomplete.price = {
|
||||
[Op.and]: {
|
||||
[Op.lte]: priceMax,
|
||||
[Op.gte]: priceMin
|
||||
}
|
||||
};
|
||||
}
|
||||
//Every other attribute is checked separately and included in query only if it is defined for real estate type
|
||||
|
||||
if (
|
||||
realEstateTypeObject.hasGardenSize &&
|
||||
gardenSizeMax != null &&
|
||||
gardenSizeMin != null
|
||||
) {
|
||||
query.gardenSize = {
|
||||
[Op.lte]: gardenSizeMax,
|
||||
[Op.gte]: gardenSizeMin
|
||||
@@ -192,7 +240,11 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (numberOfRoomsMin && numberOfRoomsMax) {
|
||||
if (
|
||||
realEstateTypeObject.hasNumberOfRoom &&
|
||||
numberOfRoomsMin != null &&
|
||||
numberOfRoomsMax != null
|
||||
) {
|
||||
query.numberOfRooms = {
|
||||
[Op.lte]: numberOfRoomsMax,
|
||||
[Op.gte]: numberOfRoomsMin
|
||||
@@ -208,7 +260,11 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (numberOfFloorsMin && numberOfFloorsMax) {
|
||||
if (
|
||||
realEstateTypeObject.hasNumberOfFloors &&
|
||||
numberOfFloorsMin != null &&
|
||||
numberOfFloorsMax != null
|
||||
) {
|
||||
query.numberOfFloors = {
|
||||
[Op.lte]: numberOfFloorsMax,
|
||||
[Op.gte]: numberOfFloorsMin
|
||||
@@ -224,7 +280,11 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (floorMin && floorMax) {
|
||||
if (
|
||||
realEstateTypeObject.hasFloorProp &&
|
||||
floorMin != null &&
|
||||
floorMax != null
|
||||
) {
|
||||
query.floor = {
|
||||
[Op.lte]: floorMax,
|
||||
[Op.gte]: floorMin
|
||||
@@ -239,8 +299,10 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (balcony) {
|
||||
//Logic for balcony, newBuilding and elevator from users side
|
||||
//If true is checked, then I want characteristic to be true but,
|
||||
//if it is not checked, then I dont care - it can be null or false or true
|
||||
if (realEstateTypeObject.hasBalconyProp && balcony === true) {
|
||||
query.balcony = {
|
||||
[Op.eq]: balcony
|
||||
};
|
||||
@@ -252,7 +314,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (newBuilding) {
|
||||
if (realEstateTypeObject.hasNewBuildingProp && newBuilding === true) {
|
||||
query.newBuilding = {
|
||||
[Op.eq]: newBuilding
|
||||
};
|
||||
@@ -264,7 +326,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (elevator) {
|
||||
if (realEstateTypeObject.hasElevatorProp && elevator === true) {
|
||||
query.elevator = {
|
||||
[Op.eq]: elevator
|
||||
};
|
||||
@@ -275,7 +337,8 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//If user wants 'ANY' road type acces then it is not included in query -
|
||||
//returns every road type and null values
|
||||
if (accessRoadType !== "ANY") {
|
||||
query.accessRoadType = {
|
||||
[Op.eq]: accessRoadType
|
||||
@@ -300,5 +363,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
|
||||
module.exports = {
|
||||
bulkUpsertRealEstates,
|
||||
getRealEstateById,
|
||||
findRealEstatesForSearchRequest
|
||||
createRealEstate,
|
||||
findRealEstatesForSearchRequest,
|
||||
findRealEstateByAgencyId
|
||||
};
|
||||
|
||||
@@ -49,128 +49,390 @@ const findSearchRequestsForRealEstate = async realEstate => {
|
||||
|
||||
const geoSearchQueryPart = sequelize.where(contains, true);
|
||||
|
||||
//General query contains only attributes that are defined for every RealEstate - not null
|
||||
const query = {
|
||||
adType,
|
||||
realEstateType,
|
||||
subscribed: true,
|
||||
[Op.and]: geoSearchQueryPart
|
||||
};
|
||||
//Needed for defining which attribute should exist or not
|
||||
const realEstateTypeObject = AD_CATEGORY[realEstateType];
|
||||
//Needed to decide on including incomplete RealEstates data
|
||||
|
||||
// ?? Needed to decide on including incomplete RealEstates data
|
||||
let checkForIncompleteWanted = false;
|
||||
|
||||
//Attributes are checked separately and included in query only if defined
|
||||
//Price and area should be defined for every property
|
||||
//Attributes are checked separately to make different query parts
|
||||
|
||||
if (price) {
|
||||
query.priceMin = {
|
||||
[Op.lte]: price
|
||||
//If real estate price is number then it searches for req that have priceMin and priceMax
|
||||
//If real estate price is null it searches for req that accept ads without price
|
||||
//User always defines price and area (sliders) - not null in search req
|
||||
let priceQuery = {};
|
||||
if (price != null) {
|
||||
priceQuery = {
|
||||
[Op.and]: [
|
||||
{
|
||||
priceMin: {
|
||||
[Op.lte]: price
|
||||
}
|
||||
},
|
||||
{
|
||||
priceMax: {
|
||||
[Op.gte]: price
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
query.priceMax = {
|
||||
[Op.gte]: price
|
||||
} else {
|
||||
priceQuery = {
|
||||
includeWithoutPrice: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (area) {
|
||||
query.sizeMin = {
|
||||
[Op.lte]: area
|
||||
};
|
||||
query.sizeMax = {
|
||||
[Op.gte]: area
|
||||
let areaQuery = {};
|
||||
if (area != null) {
|
||||
areaQuery = {
|
||||
[Op.and]: [
|
||||
{
|
||||
sizeMin: {
|
||||
[Op.lte]: area
|
||||
}
|
||||
},
|
||||
{
|
||||
sizeMax: {
|
||||
[Op.gte]: area
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
checkForIncompleteWanted = true;
|
||||
}
|
||||
|
||||
//Other attributes can be defined or not depending on RealEstate type
|
||||
if (gardenSize) {
|
||||
query.gardenSizeMin = {
|
||||
[Op.lte]: gardenSize
|
||||
};
|
||||
query.gardenSizeMax = {
|
||||
[Op.gte]: gardenSize
|
||||
};
|
||||
} else if (realEstateTypeObject.hasGardenSize) {
|
||||
checkForIncompleteWanted = true;
|
||||
//we check what to include in query based on real estate type object
|
||||
let gardenSizeQuery = {};
|
||||
if (realEstateTypeObject.hasGardenSize) {
|
||||
if (gardenSize != null) {
|
||||
gardenSizeQuery = {
|
||||
[Op.and]: [
|
||||
{
|
||||
gardenSizeMin: {
|
||||
[Op.lte]: gardenSize
|
||||
}
|
||||
},
|
||||
{
|
||||
gardenSizeMax: {
|
||||
[Op.gte]: gardenSize
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
checkForIncompleteWanted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (numberOfRooms) {
|
||||
query.numberOfRoomsMin = {
|
||||
[Op.lte]: numberOfRooms
|
||||
};
|
||||
query.numberOfRoomsMax = {
|
||||
[Op.gte]: numberOfRooms
|
||||
};
|
||||
} else if (realEstateTypeObject.hasNumberOfRoom) {
|
||||
checkForIncompleteWanted = true;
|
||||
let numberOfRoomsQuery = {};
|
||||
if (realEstateTypeObject.hasNumberOfRoom) {
|
||||
if (numberOfRooms != null) {
|
||||
//If real estate has defined number of rooms ex. 3 it returns req
|
||||
// that accepts 3 rooms or ones that don't have defined number - null
|
||||
//Ex. they didnt choose advanced filters at all
|
||||
numberOfRoomsQuery = {
|
||||
[Op.and]: [
|
||||
{
|
||||
numberOfRoomsMin: {
|
||||
[Op.or]: {
|
||||
[Op.lte]: numberOfRooms,
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
numberOfRoomsMax: {
|
||||
[Op.or]: {
|
||||
[Op.gte]: numberOfRooms,
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
// If real estate dont have defined number of rooms ex. null
|
||||
//It returns requests that didn't choose number of rooms - also null
|
||||
//Or ones that picked some values but also picked to includeIncomplete ads
|
||||
numberOfRoomsQuery = {
|
||||
[Op.or]: [
|
||||
{
|
||||
[Op.and]: [
|
||||
{
|
||||
numberOfRoomsMin: {
|
||||
[Op.is]: null
|
||||
}
|
||||
},
|
||||
{
|
||||
numberOfRoomsMax: {
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
includeIncompleteAds: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
//Same logic for number of Floors and floors
|
||||
let numberOfFloorsQuery = {};
|
||||
if (realEstateTypeObject.hasNumberOfFloors) {
|
||||
if (numberOfFloors != null) {
|
||||
numberOfFloorsQuery = {
|
||||
[Op.and]: [
|
||||
{
|
||||
numberOfFloorsMin: {
|
||||
[Op.or]: {
|
||||
[Op.lte]: numberOfFloors,
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
numberOfFloorsMax: {
|
||||
[Op.or]: {
|
||||
[Op.gte]: numberOfFloors,
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
numberOfFloorsQuery = {
|
||||
[Op.or]: [
|
||||
{
|
||||
[Op.and]: [
|
||||
{
|
||||
numberOfFloorsMin: {
|
||||
[Op.is]: null
|
||||
}
|
||||
},
|
||||
{
|
||||
numberOfFloorsMax: {
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
includeIncompleteAds: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
let floorQuery = {};
|
||||
if (realEstateTypeObject.hasFloorProp) {
|
||||
if (floor != null) {
|
||||
floorQuery = {
|
||||
[Op.and]: [
|
||||
{
|
||||
floorMin: {
|
||||
[Op.or]: {
|
||||
[Op.lte]: floor,
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
floorMax: {
|
||||
[Op.or]: {
|
||||
[Op.gte]: floor,
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
floorQuery = {
|
||||
[Op.or]: [
|
||||
{
|
||||
[Op.and]: [
|
||||
{
|
||||
floorMin: {
|
||||
[Op.is]: null
|
||||
}
|
||||
},
|
||||
{
|
||||
floorMax: {
|
||||
[Op.is]: null
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
includeIncompleteAds: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (numberOfFloors) {
|
||||
query.numberOfFloorsMin = {
|
||||
[Op.lte]: numberOfFloors
|
||||
};
|
||||
query.numberOfFloorsMax = {
|
||||
[Op.gte]: numberOfFloors
|
||||
};
|
||||
} else if (realEstateTypeObject.hasNumberOfFloors) {
|
||||
checkForIncompleteWanted = true;
|
||||
//Logic for balcony, newBuilding and elevator
|
||||
//If user dont check checkbox for ex. elevator it does not mean he only wants no elevator
|
||||
//If real estate characteristic =true find all req, one that wants charachertistic or dont care - dont need query
|
||||
//If real estate characteristic = false, find all req exept for ones that wants characteristic to be true
|
||||
//If real estate characteristic = null, dont know if true or false, find req that dont care or want char and want incomplete ads
|
||||
let balconyQuery = {};
|
||||
if (realEstateTypeObject.hasBalconyProp && balcony !== true) {
|
||||
if (balcony === false) {
|
||||
balconyQuery = {
|
||||
balcony: {
|
||||
[Op.ne]: true
|
||||
}
|
||||
};
|
||||
} else if (balcony === null) {
|
||||
balconyQuery = {
|
||||
[Op.or]: [
|
||||
{
|
||||
balcony: {
|
||||
[Op.ne]: true
|
||||
}
|
||||
},
|
||||
{
|
||||
[Op.and]: [
|
||||
{
|
||||
balcony: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
},
|
||||
{
|
||||
includeIncompleteAds: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (floor) {
|
||||
query.floorMin = {
|
||||
[Op.lte]: floor
|
||||
};
|
||||
query.floorMax = {
|
||||
[Op.gte]: floor
|
||||
};
|
||||
} else if (realEstateTypeObject.hasFloorProp) {
|
||||
checkForIncompleteWanted = true;
|
||||
let newBuildingQuery = {};
|
||||
if (realEstateTypeObject.hasNewBuildingProp && newBuilding !== true) {
|
||||
if (newBuilding === false) {
|
||||
newBuildingQuery = {
|
||||
newBuilding: {
|
||||
[Op.ne]: true
|
||||
}
|
||||
};
|
||||
} else if (newBuilding === null) {
|
||||
newBuildingQuery = {
|
||||
[Op.or]: [
|
||||
{
|
||||
newBuilding: {
|
||||
[Op.ne]: true
|
||||
}
|
||||
},
|
||||
{
|
||||
[Op.and]: [
|
||||
{
|
||||
newBuilding: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
},
|
||||
{
|
||||
includeIncompleteAds: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
let elevatorQuery = {};
|
||||
if (realEstateTypeObject.hasElevatorProp && elevator !== true) {
|
||||
if (elevator === false) {
|
||||
elevatorQuery = {
|
||||
elevator: {
|
||||
[Op.ne]: true
|
||||
}
|
||||
};
|
||||
} else if (elevator === null) {
|
||||
elevatorQuery = {
|
||||
[Op.or]: [
|
||||
{
|
||||
elevator: {
|
||||
[Op.ne]: true
|
||||
}
|
||||
},
|
||||
{
|
||||
[Op.and]: [
|
||||
{
|
||||
elevator: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
},
|
||||
{
|
||||
includeIncompleteAds: {
|
||||
[Op.eq]: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
//General query consists of each individual query
|
||||
const query = {
|
||||
adType,
|
||||
realEstateType,
|
||||
subscribed: true,
|
||||
[Op.and]: [
|
||||
geoSearchQueryPart,
|
||||
priceQuery,
|
||||
areaQuery,
|
||||
gardenSizeQuery,
|
||||
numberOfRoomsQuery,
|
||||
numberOfFloorsQuery,
|
||||
floorQuery,
|
||||
balconyQuery,
|
||||
newBuildingQuery,
|
||||
elevatorQuery
|
||||
]
|
||||
};
|
||||
|
||||
if (accessRoadType) {
|
||||
//AccessRoadType is defined - should exists for each ad and estate type
|
||||
if (accessRoadType != null) {
|
||||
query.accessRoadType = {
|
||||
[Op.or]: {
|
||||
[Op.eq]: "ANY",
|
||||
[Op.like]: "ANY",
|
||||
[Op.eq]: accessRoadType
|
||||
}
|
||||
};
|
||||
} else if (realEstateTypeObject.hasAccesRoadType) {
|
||||
checkForIncompleteWanted = true;
|
||||
}
|
||||
|
||||
if (balcony) {
|
||||
query.balcony = {
|
||||
[Op.eq]: balcony
|
||||
} else {
|
||||
//Null values are returned for user request that wanted ANY acces road type
|
||||
query.accessRoadType = {
|
||||
[Op.eq]: "ANY"
|
||||
};
|
||||
} else if (realEstateTypeObject.hasBalconyProp) {
|
||||
checkForIncompleteWanted = true;
|
||||
}
|
||||
|
||||
if (newBuilding) {
|
||||
query.newBuilding = {
|
||||
[Op.eq]: newBuilding
|
||||
};
|
||||
} else if (realEstateTypeObject.hasNewBuildingProp) {
|
||||
checkForIncompleteWanted = true;
|
||||
}
|
||||
|
||||
if (elevator) {
|
||||
query.elevator = {
|
||||
[Op.eq]: elevator
|
||||
};
|
||||
} else if (realEstateTypeObject.hasElevatorProp) {
|
||||
checkForIncompleteWanted = true;
|
||||
}
|
||||
|
||||
//If one of the attributes that exists for property type is null
|
||||
//we include in query to check if incomplete real estates are accepted
|
||||
//Tag to check if incomplete ads are accepted in query
|
||||
if (checkForIncompleteWanted) {
|
||||
query.includeIncompleteAds = {
|
||||
[Op.eq]: true
|
||||
};
|
||||
}
|
||||
return await db.SearchRequest.findAll({ where: query });
|
||||
|
||||
return await db.SearchRequest.findAll({
|
||||
where: query
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use strict";
|
||||
const db = require("../../models/index");
|
||||
const sequelize = require("sequelize");
|
||||
const Op = sequelize.Op;
|
||||
const { CHECK_UP_DAYS } = require("../../config/appConfig");
|
||||
|
||||
const findRealEstatesForSearchRequest = async searchRequestId => {
|
||||
const query = {
|
||||
@@ -40,6 +43,42 @@ const findNotNotifiedMatches = async () => {
|
||||
|
||||
return matchingRecords;
|
||||
};
|
||||
const findAllRequestsForCheckUp = async () => {
|
||||
//First we find IDs of search request that don't need to be emailed for check up - to EXCLUDE
|
||||
//The ones that received notification for real estate CHECK_UP_DAYS days from now
|
||||
const date = new Date();
|
||||
const checkUpDate = date.getDate() - CHECK_UP_DAYS;
|
||||
date.setDate(checkUpDate);
|
||||
const dateQuery = {
|
||||
createdAt: {
|
||||
[Op.gte]: date
|
||||
}
|
||||
};
|
||||
|
||||
const excludedMatches = await db.SearchRequestMatch.findAll({
|
||||
attributes: ["searchRequestId"],
|
||||
where: dateQuery,
|
||||
order: [["searchRequestId", "ASC"]]
|
||||
});
|
||||
|
||||
const excludedRequestsAll = excludedMatches.map(match => {
|
||||
return match.dataValues.searchRequestId;
|
||||
});
|
||||
//Removing duplicate search request id-s for optimization
|
||||
const excludedRequests = [...new Set(excludedRequestsAll)];
|
||||
|
||||
const query = {
|
||||
subscribed: true,
|
||||
id: {
|
||||
[Op.notIn]: excludedRequests
|
||||
}
|
||||
};
|
||||
const allRequestsForCheckUp = await db.SearchRequest.findAll({
|
||||
where: query
|
||||
});
|
||||
|
||||
return allRequestsForCheckUp;
|
||||
};
|
||||
|
||||
const addMatches = async matchingRecords => {
|
||||
return await db.SearchRequestMatch.bulkCreate(matchingRecords, {
|
||||
@@ -50,5 +89,6 @@ const addMatches = async matchingRecords => {
|
||||
module.exports = {
|
||||
findRealEstatesForSearchRequest,
|
||||
addMatches,
|
||||
findNotNotifiedMatches
|
||||
findNotNotifiedMatches,
|
||||
findAllRequestsForCheckUp
|
||||
};
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
"use strict";
|
||||
|
||||
const { MAX_REAL_ESTATES_IN_EMAIL, APP_URL } = require("../config/appConfig");
|
||||
const { AD_CATEGORY } = require("../common/enums");
|
||||
const {
|
||||
MAX_REAL_ESTATES_IN_EMAIL,
|
||||
APP_URL,
|
||||
STAGING
|
||||
} = require("../config/appConfig");
|
||||
const { AD_CATEGORY, AD_TYPE, EMAIL_FREQUENCY } = require("../common/enums");
|
||||
|
||||
const generateEmailFooter = searchRequestId => {
|
||||
return `<div>Ako želite prestati dobijati obavještenja za ovu pretragu, <a href="${APP_URL}/odjava/${searchRequestId}">odjavite ovdje</a></div>
|
||||
//Tag to recognize staging from development
|
||||
const stagingTag = STAGING ? "[STAGING] " : "";
|
||||
|
||||
const generateEmailFooter = (searchRequestId, emailFrequencyTitle) => {
|
||||
return ` <div>Trenutno ste prijavljeni da obavještenja o novim nekretninama primate <strong>${emailFrequencyTitle.toLowerCase()} </strong>.</div>
|
||||
<div>Ako želite prestati dobijati obavještenja za ovu pretragu, <a href="${APP_URL}/odjava/${searchRequestId}">odjavite ovdje</a></div>
|
||||
<div>Ako želite pogledati ili promijeniti uslove za ovu pretragu, <a href="${APP_URL}/pregled/${searchRequestId}">pogledajte ovdje</a></div>
|
||||
<br/>
|
||||
<strong>Vaš,<br/>Kivi tim</strong>`;
|
||||
@@ -23,17 +31,24 @@ const generateRealEstateLinks = realEstates => {
|
||||
const generateNotificationEmail = (
|
||||
realEstates,
|
||||
searchRequestId,
|
||||
noAllRealEstates,
|
||||
dailyNotification = false
|
||||
) => {
|
||||
const truncateList = realEstates.length > MAX_REAL_ESTATES_IN_EMAIL;
|
||||
|
||||
const realEstatesToShow = truncateList
|
||||
? realEstates.slice(0, MAX_REAL_ESTATES_IN_EMAIL)
|
||||
: realEstates;
|
||||
|
||||
const allRealEstatesLink = `${APP_URL}/nekretnine/${searchRequestId}`;
|
||||
|
||||
const emailFrequencyTitle = dailyNotification
|
||||
? EMAIL_FREQUENCY.DAILY.title
|
||||
: EMAIL_FREQUENCY.ASAP.title;
|
||||
|
||||
const realEstateLinks = generateRealEstateLinks(realEstatesToShow);
|
||||
const moreRealEstates = `<div>Kompletan spisak nekretnina možete pogledati na <a href="${allRealEstatesLink}">listi nekretnina</a><div>`;
|
||||
const emailFooter = generateEmailFooter(searchRequestId);
|
||||
const moreRealEstates = `<div>Kompletan spisak nekretnina (${noAllRealEstates}) možete pogledati na <a href="${allRealEstatesLink}">listi nekretnina</a><div>`;
|
||||
const emailFooter = generateEmailFooter(searchRequestId, emailFrequencyTitle);
|
||||
const asapMessageBody =
|
||||
realEstates.length > 1
|
||||
? "Pronašli smo nekretnine koje odgovaraju Vašoj pretrazi"
|
||||
@@ -46,7 +61,7 @@ const generateNotificationEmail = (
|
||||
|
||||
const messageBody = dailyNotification ? dailyMessageBody : asapMessageBody;
|
||||
|
||||
return `<h3>Zdravo</h3>
|
||||
return `<h3>${stagingTag}Zdravo</h3>
|
||||
<h4>${messageBody}</h4>
|
||||
<div>
|
||||
${realEstateLinks}
|
||||
@@ -59,6 +74,28 @@ const generateNotificationEmail = (
|
||||
|
||||
const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => {
|
||||
const realEstateType = AD_CATEGORY[searchRequest.realEstateType];
|
||||
let adTypeTitle = "";
|
||||
switch (searchRequest.adType) {
|
||||
case AD_TYPE.AD_TYPE_SALE.stringId:
|
||||
adTypeTitle = AD_TYPE.AD_TYPE_SALE.title;
|
||||
break;
|
||||
case AD_TYPE.AD_TYPE_RENT.stringId:
|
||||
adTypeTitle = AD_TYPE.AD_TYPE_RENT.title;
|
||||
break;
|
||||
default:
|
||||
adTypeTitle = "-";
|
||||
break;
|
||||
}
|
||||
let emailFrequencyTitle;
|
||||
switch (searchRequest.emailFrequency) {
|
||||
case EMAIL_FREQUENCY.ASAP.stringId:
|
||||
emailFrequencyTitle = EMAIL_FREQUENCY.ASAP.title;
|
||||
break;
|
||||
case EMAIL_FREQUENCY.DAILY.stringId:
|
||||
emailFrequencyTitle = EMAIL_FREQUENCY.DAILY.title;
|
||||
break;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
gardenSizeMin,
|
||||
@@ -70,6 +107,7 @@ const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => {
|
||||
} = searchRequest;
|
||||
|
||||
const realEstateLinks = generateRealEstateLinks(matchingRealEstates);
|
||||
|
||||
const instantRealEstatesText = `<br/>
|
||||
<div>
|
||||
U međuvremenu pogledajte neke od nedavno objavljenih nekretnina koje odgovaraju Vašim uslovima pretrage :<br/>
|
||||
@@ -80,13 +118,14 @@ const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => {
|
||||
? `<div><strong>Kvadratura okućnice: Od ${gardenSizeMin} do ${gardenSizeMax} m2</strong></div>`
|
||||
: ``;
|
||||
|
||||
const emailFooter = generateEmailFooter(id);
|
||||
const emailFooter = generateEmailFooter(id, emailFrequencyTitle);
|
||||
|
||||
return `<h3>Zdravo</h3>
|
||||
return `<h3>${stagingTag}Zdravo</h3>
|
||||
<div>Naručili ste da Vam javimo ako se nekretnina sa navedenim uslovima pojavi u oglasima:</div>
|
||||
<br/>
|
||||
<div>
|
||||
<div><strong>Tip nekretnine: </strong>${realEstateType.title}</div>
|
||||
<div><strong>Vrsta oglasa: </strong>${adTypeTitle}</div>
|
||||
<div><strong>Kvadratura nekretnine:</strong> Od ${sizeMin} do ${sizeMax} m2</div>
|
||||
${gardenSize}
|
||||
<div><strong>Cijena:</strong> ${priceMin} do ${priceMax} KM</div>
|
||||
@@ -98,7 +137,7 @@ const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => {
|
||||
|
||||
const generateEmailSubject = (numberOfRealEstates, singleRealEstateTitle) => {
|
||||
if (numberOfRealEstates === 1) {
|
||||
return `Kivi: ${singleRealEstateTitle}`;
|
||||
return `${stagingTag}Kivi: ${singleRealEstateTitle}`;
|
||||
}
|
||||
|
||||
const leastSignificantDigit = numberOfRealEstates % 10;
|
||||
@@ -106,7 +145,7 @@ const generateEmailSubject = (numberOfRealEstates, singleRealEstateTitle) => {
|
||||
const secondLeastSignificantDigit = numberWithoutLastDigit % 10;
|
||||
|
||||
if (leastSignificantDigit === 1 && secondLeastSignificantDigit !== 1) {
|
||||
return `Kivi : ${numberOfRealEstates} nova nekretnina`;
|
||||
return `${stagingTag}Kivi : ${numberOfRealEstates} nova nekretnina`;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -114,14 +153,58 @@ const generateEmailSubject = (numberOfRealEstates, singleRealEstateTitle) => {
|
||||
leastSignificantDigit <= 4 &&
|
||||
secondLeastSignificantDigit !== 1
|
||||
) {
|
||||
return `Kivi: ${numberOfRealEstates} nove nekretnine`;
|
||||
return `${stagingTag}Kivi: ${numberOfRealEstates} nove nekretnine`;
|
||||
}
|
||||
|
||||
return `Kivi: ${numberOfRealEstates} novih nekretnina`;
|
||||
return `${stagingTag}Kivi: ${numberOfRealEstates} novih nekretnina`;
|
||||
};
|
||||
|
||||
const generateCheckUpEmail = searchRequest => {
|
||||
const realEstateType = AD_CATEGORY[searchRequest.realEstateType];
|
||||
const {
|
||||
id,
|
||||
gardenSizeMin,
|
||||
gardenSizeMax,
|
||||
sizeMin,
|
||||
sizeMax,
|
||||
priceMin,
|
||||
priceMax
|
||||
} = searchRequest;
|
||||
|
||||
let emailFrequencyTitle;
|
||||
switch (searchRequest.emailFrequency) {
|
||||
case EMAIL_FREQUENCY.ASAP.stringId:
|
||||
emailFrequencyTitle = EMAIL_FREQUENCY.ASAP.title;
|
||||
break;
|
||||
case EMAIL_FREQUENCY.DAILY.stringId:
|
||||
emailFrequencyTitle = EMAIL_FREQUENCY.DAILY.title;
|
||||
break;
|
||||
}
|
||||
|
||||
const gardenSize = realEstateType.hasGardenSize
|
||||
? `<div><strong>Kvadratura okućnice: Od ${gardenSizeMin} do ${gardenSizeMax} m2</strong></div>`
|
||||
: ``;
|
||||
|
||||
const emailFooter = generateEmailFooter(id, emailFrequencyTitle);
|
||||
|
||||
return `<h3>${stagingTag}Zdravo</h3>
|
||||
<div><strong>Kivi tim traži nekretnine za Vas i kada to ne vidite.</strong></div>
|
||||
<br />
|
||||
<div>Vaša trenutno aktivna pretraga je:</div>
|
||||
<br/>
|
||||
<div>
|
||||
<div><strong>Tip nekretnine: </strong>${realEstateType.title}</div>
|
||||
<div><strong>Kvadratura nekretnine:</strong> Od ${sizeMin} do ${sizeMax} m2</div>
|
||||
${gardenSize}
|
||||
<div><strong>Cijena:</strong> ${priceMin} do ${priceMax} KM</div>
|
||||
</div>
|
||||
<br/>
|
||||
${emailFooter}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateNotificationEmail,
|
||||
generateNewSearchRequestEmail,
|
||||
generateEmailSubject
|
||||
generateEmailSubject,
|
||||
generateCheckUpEmail
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const { getSearchRequest } = require("./db/searchRequest");
|
||||
const { getRealEstateById } = require("./db/realEstate");
|
||||
const { getKiviOriginalById } = require("./db/kiviOriginal");
|
||||
|
||||
const currentSearchRequest = async req => {
|
||||
const searchRequestId =
|
||||
@@ -7,6 +9,23 @@ const currentSearchRequest = async req => {
|
||||
|
||||
return await getSearchRequest(searchRequestId);
|
||||
};
|
||||
module.exports = {
|
||||
currentSearchRequest
|
||||
|
||||
const currentRealEstate = async req => {
|
||||
const realEstateId = req && req.params ? req.params["realEstateId"] : null;
|
||||
if (!realEstateId) return null;
|
||||
|
||||
return await getRealEstateById(parseInt(realEstateId));
|
||||
};
|
||||
const currentKiviRealEstate = async req => {
|
||||
const kiviRealEstateId =
|
||||
req && req.params ? req.params["kiviRealEstateId"] : null;
|
||||
if (!kiviRealEstateId) return null;
|
||||
|
||||
return await getKiviOriginalById(kiviRealEstateId);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
currentSearchRequest,
|
||||
currentRealEstate,
|
||||
currentKiviRealEstate
|
||||
};
|
||||
|
||||
42
app/migrations/20200121000524-add-priceHistory-table.js
Normal file
42
app/migrations/20200121000524-add-priceHistory-table.js
Normal file
@@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
const tableFields = {
|
||||
id: {
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
realEstateId: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false,
|
||||
unique: "uniquePriceRealEstate",
|
||||
references: {
|
||||
model: "RealEstates",
|
||||
key: "id"
|
||||
},
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
},
|
||||
price: {
|
||||
type: Sequelize.REAL,
|
||||
unique: "uniquePriceRealEstate"
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal("NOW()")
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal("NOW()")
|
||||
}
|
||||
};
|
||||
return queryInterface.createTable("PriceHistory", tableFields);
|
||||
},
|
||||
|
||||
down: queryInterface => {
|
||||
return queryInterface.dropTable("PriceHistory", {});
|
||||
}
|
||||
};
|
||||
10
app/migrations/20200121094500-add-constraint-priceHistory.js
Normal file
10
app/migrations/20200121094500-add-constraint-priceHistory.js
Normal file
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) =>
|
||||
queryInterface.addConstraint("PriceHistory", ["realEstateId", "price"], {
|
||||
type: "unique",
|
||||
name: "uniquePriceRealEstate"
|
||||
}),
|
||||
down: queryInterface =>
|
||||
queryInterface.removeConstraint("PriceHistory", "uniquePriceRealEstate")
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
return queryInterface.addColumn("SearchRequests", "includeWithoutPrice", {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: true
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface, Sequelize) => {
|
||||
return queryInterface.removeColumn("SearchRequests", "includeWithoutPrice");
|
||||
}
|
||||
};
|
||||
28
app/migrations/20200203114630-add-kiviOriginal-table.js
Normal file
28
app/migrations/20200203114630-add-kiviOriginal-table.js
Normal file
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
const tableFields = {
|
||||
kiviAdId: {
|
||||
type: Sequelize.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
email: Sequelize.TEXT,
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal("NOW()")
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal("NOW()")
|
||||
}
|
||||
};
|
||||
return queryInterface.createTable("KiviOriginal", tableFields);
|
||||
},
|
||||
|
||||
down: (queryInterface, Sequelize) => {
|
||||
return queryInterface.dropTable("KiviOriginal", {});
|
||||
}
|
||||
};
|
||||
39
app/migrations/20200312210336-add-table-kivi-photos.js
Normal file
39
app/migrations/20200312210336-add-table-kivi-photos.js
Normal file
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
const tableFields = {
|
||||
id: {
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
kiviAdId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: "KiviOriginal",
|
||||
key: "kiviAdId"
|
||||
}
|
||||
},
|
||||
photoUrl: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal("NOW()")
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal("NOW()")
|
||||
}
|
||||
};
|
||||
return queryInterface.createTable("KiviOriginalAdsPhotos", tableFields);
|
||||
},
|
||||
|
||||
down: (queryInterface, Sequelize) => {
|
||||
return queryInterface.dropTable("KiviOriginalAdsPhotos", {});
|
||||
}
|
||||
};
|
||||
21
app/models/kiviOriginal.js
Normal file
21
app/models/kiviOriginal.js
Normal file
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = (sequalize, DataTypes) => {
|
||||
const KiviOriginal = sequalize.define(
|
||||
"KiviOriginal",
|
||||
{
|
||||
kiviAdId: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
email: DataTypes.TEXT
|
||||
},
|
||||
{
|
||||
freezeTableName: true
|
||||
}
|
||||
);
|
||||
|
||||
return KiviOriginal;
|
||||
};
|
||||
41
app/models/kiviOriginalAdsPhotos.js
Normal file
41
app/models/kiviOriginalAdsPhotos.js
Normal file
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = (sequalize, DataTypes) => {
|
||||
const KiviOriginalAdsPhotos = sequalize.define(
|
||||
"KiviOriginalAdsPhotos",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
kiviAdId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: "KiviOriginal",
|
||||
key: "kiviAdId"
|
||||
}
|
||||
},
|
||||
photoUrl: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
{
|
||||
freezeTableName: true
|
||||
}
|
||||
);
|
||||
|
||||
KiviOriginalAdsPhotos.associate = models => {
|
||||
KiviOriginalAdsPhotos.hasMany(models.KiviOriginal, {
|
||||
foreignKey: "kiviAdId",
|
||||
sourceKey: "kiviAdId",
|
||||
targetKey: "kiviAdId",
|
||||
as: "kiviOriginal"
|
||||
});
|
||||
};
|
||||
|
||||
return KiviOriginalAdsPhotos;
|
||||
};
|
||||
44
app/models/priceHistory.js
Normal file
44
app/models/priceHistory.js
Normal file
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = (sequalize, DataTypes) => {
|
||||
const PriceHistory = sequalize.define(
|
||||
"PriceHistory",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
realEstateId: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
unique: "uniquePriceRealEstate",
|
||||
references: {
|
||||
model: "RealEstates",
|
||||
key: "id"
|
||||
},
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "SET NULL"
|
||||
},
|
||||
price: {
|
||||
type: DataTypes.REAL,
|
||||
unique: "uniquePriceRealEstate"
|
||||
}
|
||||
},
|
||||
{
|
||||
freezeTableName: true
|
||||
}
|
||||
);
|
||||
|
||||
PriceHistory.associate = models => {
|
||||
PriceHistory.hasMany(models.RealEstate, {
|
||||
foreignKey: "id",
|
||||
sourceKey: "realEstateId",
|
||||
targetKey: "id",
|
||||
as: "realEstates"
|
||||
});
|
||||
};
|
||||
|
||||
return PriceHistory;
|
||||
};
|
||||
@@ -15,7 +15,15 @@ module.exports = (sequelize, DataTypes) => {
|
||||
allowNull: false,
|
||||
defaultValue: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]],
|
||||
coordinates: [
|
||||
[
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
]
|
||||
],
|
||||
crs: { type: "name", properties: { name: "EPSG:4326" } }
|
||||
}
|
||||
},
|
||||
@@ -71,6 +79,7 @@ module.exports = (sequelize, DataTypes) => {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
includeIncompleteAds: DataTypes.BOOLEAN,
|
||||
includeWithoutPrice: DataTypes.BOOLEAN,
|
||||
balcony: DataTypes.BOOLEAN,
|
||||
elevator: DataTypes.BOOLEAN,
|
||||
newBuilding: DataTypes.BOOLEAN,
|
||||
|
||||
6
app/npmScripts/npmCheckUpNotify.js
Normal file
6
app/npmScripts/npmCheckUpNotify.js
Normal file
@@ -0,0 +1,6 @@
|
||||
"use strict";
|
||||
const { checkUpNotify } = require("../services/notificationService");
|
||||
//For testing pursposes
|
||||
(async () => {
|
||||
await checkUpNotify();
|
||||
})();
|
||||
@@ -154,3 +154,31 @@ h3 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.estates-link {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #cc0033;
|
||||
}
|
||||
|
||||
.custom-col {
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
border: 4px dashed #02adba;
|
||||
border-image: none;
|
||||
max-width: 80%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.dz-progress {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,20 @@ const {
|
||||
getRealEstateTypes,
|
||||
postRealEstateTypes
|
||||
} = require("../controllers/realEstateTypes");
|
||||
const {
|
||||
getPublishTypes,
|
||||
postPublishTypes
|
||||
} = require("../controllers/publishRealEstateTypes");
|
||||
const {
|
||||
getPublishInputs,
|
||||
postPublishInputs
|
||||
} = require("../controllers/publishRealEstate");
|
||||
const {
|
||||
getQueryReview,
|
||||
postQueryReview
|
||||
} = require("../controllers/queryReview");
|
||||
const { getGoAgain } = require("../controllers/goAgain");
|
||||
const { publishSuccess } = require("../controllers/publishSuccess");
|
||||
const { getLocation, postLocation } = require("../controllers/location");
|
||||
const { getUnsubscribe } = require("../controllers/unsubscribe");
|
||||
const { getRealEstates } = require("../controllers/realEstates");
|
||||
@@ -28,6 +37,14 @@ router.get("/vrstanekretnine", getRealEstateTypes);
|
||||
router.post("/vrstanekretnine/:searchRequestId", postRealEstateTypes);
|
||||
router.post("/vrstanekretnine", postRealEstateTypes);
|
||||
|
||||
router.get("/objavinekretninu/:kiviRealEstateId", getPublishTypes);
|
||||
router.get("/objavinekretninu", getPublishTypes);
|
||||
router.post("/objavinekretninu/:kiviRealEstateId", postPublishTypes);
|
||||
router.post("/objavinekretninu", postPublishTypes);
|
||||
|
||||
router.get("/podacionekretnini/:kiviRealEstateId", getPublishInputs);
|
||||
router.post("/podacionekretnini/:kiviRealEstateId", postPublishInputs);
|
||||
|
||||
router.get("/lokacija/:searchRequestId", getLocation);
|
||||
router.post("/lokacija/:searchRequestId", postLocation);
|
||||
|
||||
@@ -41,6 +58,8 @@ router.get("/odjava/:searchRequestId", getUnsubscribe);
|
||||
|
||||
router.get("/ponovo", getGoAgain);
|
||||
|
||||
router.get("/uspjesnaobjava", publishSuccess);
|
||||
|
||||
router.get("/nekretnine/:searchRequestId", getRealEstates);
|
||||
|
||||
router.get("/redirect/:id", getRedirect);
|
||||
|
||||
34
app/seeders/20200121150443-initial-price-history.js
Normal file
34
app/seeders/20200121150443-initial-price-history.js
Normal file
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
|
||||
const { RealEstate } = require("../models");
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
//Reading initial data from RealEstate table in db
|
||||
const realEstateInitialData = await RealEstate.findAll();
|
||||
//Extruding data for table PriceHistory
|
||||
const priceHistoryInitialData = realEstateInitialData.map(realEstate => {
|
||||
//Null values canot be recognized by ignore duplicates in sequalize
|
||||
//Value price = 0 indicates 'cijena na upit'
|
||||
const priceTmp =
|
||||
realEstate.dataValues.price === null ? 0 : realEstate.dataValues.price;
|
||||
|
||||
return {
|
||||
realEstateId: realEstate.dataValues.id,
|
||||
price: priceTmp,
|
||||
createdAt: realEstate.dataValues.createdAt,
|
||||
updatedAt: realEstate.dataValues.updatedAt
|
||||
};
|
||||
});
|
||||
|
||||
return queryInterface.bulkInsert(
|
||||
"PriceHistory",
|
||||
priceHistoryInitialData,
|
||||
{}
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
return queryInterface.bulkDelete("PriceHistory", null, {});
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,8 @@
|
||||
"use strict";
|
||||
const { STAGING } = require("../config/appConfig");
|
||||
|
||||
const stagingTag = STAGING ? "[STAGING] " : "";
|
||||
|
||||
const {
|
||||
matchRealEstates,
|
||||
matchSearchRequest
|
||||
@@ -6,9 +10,14 @@ const {
|
||||
const {
|
||||
generateNotificationEmail,
|
||||
generateNewSearchRequestEmail,
|
||||
generateEmailSubject
|
||||
generateEmailSubject,
|
||||
generateCheckUpEmail
|
||||
} = require("../helpers/emailContentGenerator");
|
||||
const { findNotNotifiedMatches } = require("../helpers/db/searchRequestMatch");
|
||||
const {
|
||||
findNotNotifiedMatches,
|
||||
findAllRequestsForCheckUp,
|
||||
findRealEstatesForSearchRequest
|
||||
} = require("../helpers/db/searchRequestMatch");
|
||||
const { sendEmail } = require("../services/emailService");
|
||||
|
||||
const notifyForNewRealEstates = async newRealEstates => {
|
||||
@@ -21,13 +30,17 @@ const notifyForNewSearchRequest = async searchRequest => {
|
||||
|
||||
const searchRequestId = searchRequest.id;
|
||||
const matchingRealEstates = matches[searchRequestId].realEstates;
|
||||
|
||||
const emailContent = generateNewSearchRequestEmail(
|
||||
searchRequest,
|
||||
matchingRealEstates
|
||||
);
|
||||
const { email } = searchRequest;
|
||||
await sendEmail(email, "Kivi - novi zahtjev za pretragu", emailContent);
|
||||
|
||||
await sendEmail(
|
||||
email,
|
||||
`${stagingTag} Kivi - novi zahtjev za pretragu`,
|
||||
emailContent
|
||||
);
|
||||
};
|
||||
|
||||
const notifyMatches = async (matches, dailyNotification = false) => {
|
||||
@@ -39,10 +52,18 @@ const notifyMatches = async (matches, dailyNotification = false) => {
|
||||
const { email, subscribed } = searchRequest;
|
||||
if (notifyNow && subscribed) {
|
||||
const allMatchingRealEstates = matches[id].realEstates || [];
|
||||
|
||||
//Variable allMatchingRealEstates are real estates that are "new" on the market
|
||||
//the ones that we notify user in this moment, not all that already exists in db
|
||||
//New variable allRealEstates are all real estates that exists in db for search req
|
||||
const allRealEstates = await findRealEstatesForSearchRequest(id);
|
||||
const noAllRealEstates = allRealEstates.length;
|
||||
|
||||
if (allMatchingRealEstates.length > 0) {
|
||||
const emailContent = generateNotificationEmail(
|
||||
allMatchingRealEstates,
|
||||
id,
|
||||
noAllRealEstates,
|
||||
dailyNotification
|
||||
);
|
||||
const emailSubject = generateEmailSubject(
|
||||
@@ -109,8 +130,26 @@ const notifyRequestsWithDailyOption = async () => {
|
||||
await notifyMatches(matches, true);
|
||||
};
|
||||
|
||||
const checkUpNotify = async () => {
|
||||
const searchRequestsForCheckUp = await findAllRequestsForCheckUp();
|
||||
|
||||
const asyncSendEmailActions = [];
|
||||
|
||||
for (const searchRequest of searchRequestsForCheckUp) {
|
||||
const { email } = searchRequest.dataValues;
|
||||
const emailSubject = `${stagingTag}Kivi: Mi tražimo nekretnine za vas!`;
|
||||
const emailContent = generateCheckUpEmail(searchRequest.dataValues);
|
||||
|
||||
const sendEmailPromise = sendEmail(email, emailSubject, emailContent);
|
||||
asyncSendEmailActions.push(sendEmailPromise);
|
||||
sendEmailPromise.catch(err => console.log("[Email Sending Failed]", err));
|
||||
}
|
||||
await Promise.all(asyncSendEmailActions);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
notifyForNewRealEstates,
|
||||
notifyForNewSearchRequest,
|
||||
notifyRequestsWithDailyOption
|
||||
notifyRequestsWithDailyOption,
|
||||
checkUpNotify
|
||||
};
|
||||
|
||||
@@ -9,13 +9,21 @@
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<%= process.env.GA_ID %>');
|
||||
</script>
|
||||
|
||||
|
||||
<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">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.7.0/dropzone.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.7.0/dropzone.js"></script>
|
||||
<script type="text/javascript">
|
||||
Dropzone.autoDiscover = false;
|
||||
</script>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="/assets/main.css">
|
||||
<link rel="stylesheet" href="/assets/segment.css">
|
||||
@@ -47,6 +55,9 @@
|
||||
<% } else { %>
|
||||
<title>Kivi.ba</title>
|
||||
<% } %>
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
50
app/views/publishAdditionalData.ejs
Normal file
50
app/views/publishAdditionalData.ejs
Normal file
@@ -0,0 +1,50 @@
|
||||
<br>
|
||||
<div class="row">
|
||||
<% for (const input of additionalInputInputs){ %>
|
||||
<div class="input-field col s12">
|
||||
<textarea
|
||||
id="<%= input.dbField %>"
|
||||
form="publishForm"
|
||||
name="<%= input.dbField %>"
|
||||
cols="80" rows="15"
|
||||
value="<%= additionalInputValues[input.dbField] !== undefined ? additionalInputValues[input.dbField] : ""%>"
|
||||
></textarea>
|
||||
<label for="<%= input.dbField %>"><%= input.title %></label>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="row">
|
||||
|
||||
<% for (const input of additionalBooleanPublishInputs){ %>
|
||||
<p class="col s6 m4 l4">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" class="filled-in" name="<%= input.dbField %>"
|
||||
<% if (additionalBooleanPublishValues[input.dbField]) { %>
|
||||
checked
|
||||
<% } %>>
|
||||
<span><%= input.title %></span>
|
||||
</label>
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
<br>
|
||||
<% for (const input of additionalSegmentSelectInputs){ %>
|
||||
<div>
|
||||
<label class="checkbox-label"><%= input.title %>: </label><br><br>
|
||||
<span class="segmented small">
|
||||
<% for (const segmentObject of input.values) { %>
|
||||
<% if (segmentObject.id!=="ANY") { %>
|
||||
<label>
|
||||
<input type="radio" name="<%= input.dbField %>" value="<%= segmentObject.id %>"
|
||||
<% if (additionalSegmentSelectValues[input.dbField] === segmentObject.id) { %>
|
||||
checked
|
||||
<% } %>>
|
||||
<span class="label"><%= segmentObject.title %></span>
|
||||
</label>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
49
app/views/publishBasicData.ejs
Normal file
49
app/views/publishBasicData.ejs
Normal file
@@ -0,0 +1,49 @@
|
||||
<br>
|
||||
<div class="row" id="basic-inputs">
|
||||
<% for (const input of basicInputInputs){ %>
|
||||
<div class="input-field col s10 m5 l4">
|
||||
<input
|
||||
id="<%= input.dbField %>"
|
||||
name="<%= input.dbField %>"
|
||||
type="text"
|
||||
value="<%= basicInputValues[input.dbField] !== undefined ? basicInputValues[input.dbField] : ""%>"
|
||||
>
|
||||
<label for="<%= input.dbField %>"><%= input.title %></label>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<% for (const input of basicBooleanPublishInputs){ %>
|
||||
<p>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" class="filled-in" name="<%= input.dbField %>"
|
||||
<% if (basicBooleanPublishValues[input.dbField]) { %>
|
||||
checked
|
||||
<% } %>>
|
||||
<span><%= input.title %></span>
|
||||
</label>
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<% for (const input of basicSegmentSelectInputs){ %>
|
||||
<div>
|
||||
<label class="checkbox-label"><%= input.title %>: </label><br><br>
|
||||
<span class="segmented small">
|
||||
<% for (const segmentObject of input.values) { %>
|
||||
<label>
|
||||
<input type="radio" name="<%= input.dbField %>" value="<%= segmentObject.id %>"
|
||||
<% if (basicSegmentSelectValues[input.dbField] === segmentObject.id) { %>
|
||||
checked
|
||||
<% } %>>
|
||||
<span class="label"><%= segmentObject.title %></span>
|
||||
</label>
|
||||
<% } %>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
|
||||
|
||||
24
app/views/publishEnd.ejs
Normal file
24
app/views/publishEnd.ejs
Normal file
@@ -0,0 +1,24 @@
|
||||
<br>
|
||||
<div class="row center-align">
|
||||
<h3>Vaš oglas je spreman!</h3>
|
||||
Unesite kontakt email i objavite oglas.
|
||||
|
||||
<br>
|
||||
<div class="row center-align input-field col s3 m4 l5 form-group">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
>
|
||||
<div class="messages"></div>
|
||||
<label for="email">Email</label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row center-align">
|
||||
<div class="col s6 push-s3">
|
||||
<a id="submit" href="#" form="publishForm" class="welcome-center-button waves-effect waves-light btn">Objavi oglas</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
192
app/views/publishLocation.ejs
Normal file
192
app/views/publishLocation.ejs
Normal file
@@ -0,0 +1,192 @@
|
||||
<div class="row center-align">
|
||||
<h3>
|
||||
Izaberite lokaciju nekretnine na mapi.
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="row center-align">
|
||||
<div class="col s12 m12 l12 xl12">
|
||||
<input
|
||||
id="autocompleteInput"
|
||||
placeholder="Unesite grad, naselje ili ulicu..."
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row center-align">
|
||||
<div class="col s12">
|
||||
<div id="map"></div>
|
||||
</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" />
|
||||
<input type="hidden" name="lat" id="lat" />
|
||||
<input type="hidden" name="lng" id="lng" />
|
||||
|
||||
<input type="hidden" name="locationInput" id="locationInput" />
|
||||
<input type="hidden" name="locationInputData" id="locationInputData" />
|
||||
|
||||
<script>
|
||||
let autocomplete;
|
||||
let map;
|
||||
let places;
|
||||
let geocoder;
|
||||
let marker =false; //Initialy no marker on map
|
||||
|
||||
function locateMe() {
|
||||
if (navigator.geolocation) {
|
||||
function onLocationSuccess(position) {
|
||||
const coordinates =
|
||||
position && position.coords ? position.coords : null;
|
||||
if (coordinates) {
|
||||
const longitude = coordinates.longitude || null;
|
||||
const latitude = coordinates.latitude || null;
|
||||
|
||||
if (longitude && latitude && map) {
|
||||
map.setCenter({ lat: latitude, lng: longitude });
|
||||
map.setZoom(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(onLocationSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
const BOSNIA_BOUNDS = {
|
||||
north: 45.7,
|
||||
south: 41.69,
|
||||
west: 15.55,
|
||||
east: 20.77
|
||||
};
|
||||
const SARAJEVO_COORDINATES = {
|
||||
lat: 43.85,
|
||||
lng: 18.41
|
||||
};
|
||||
|
||||
const mapElement = document.getElementById("map");
|
||||
const restrictMapPanningToBosniaOnly = {
|
||||
latLngBounds: BOSNIA_BOUNDS,
|
||||
strictBounds: true
|
||||
};
|
||||
const initialMapParams = {
|
||||
center: SARAJEVO_COORDINATES,
|
||||
zoom: 12,
|
||||
restriction: restrictMapPanningToBosniaOnly,
|
||||
mapTypeControl: false,
|
||||
panControl: false,
|
||||
zoomControl: true,
|
||||
streetViewControl: false
|
||||
};
|
||||
map = new google.maps.Map(mapElement, initialMapParams);
|
||||
|
||||
const inputElement = document.getElementById("autocompleteInput");
|
||||
const restrictAutocompleteResultsToBosniaOnly = { country: "ba" };
|
||||
const initialAutocompleteParams = {
|
||||
types: ["geocode"],
|
||||
componentRestrictions: restrictAutocompleteResultsToBosniaOnly,
|
||||
fields: ["geometry", "types", "address_components"]
|
||||
};
|
||||
|
||||
autocomplete = new google.maps.places.Autocomplete(
|
||||
inputElement,
|
||||
initialAutocompleteParams
|
||||
);
|
||||
autocomplete.bindTo("bounds", map);
|
||||
autocomplete.addListener("place_changed", onPlaceChanged);
|
||||
pacSelectFirst(inputElement);
|
||||
addLocateMeButton(map);
|
||||
//Add event listener to position marker on map
|
||||
google.maps.event.addListener(map, 'click', positionMarker);
|
||||
|
||||
}
|
||||
|
||||
function positionMarker(event) {
|
||||
let clickedLocation = event.latLng;
|
||||
if(marker === false){
|
||||
marker = new google.maps.Marker({
|
||||
position: clickedLocation,
|
||||
map: map,
|
||||
draggable: true
|
||||
});
|
||||
google.maps.event.addListener(marker, 'dragend', function(event){
|
||||
markerLocation();
|
||||
});
|
||||
} else{
|
||||
marker.setPosition(clickedLocation);
|
||||
}
|
||||
}
|
||||
|
||||
function addLocateMeButton(map) {
|
||||
var parent = document.createElement("div");
|
||||
parent.className = "locate-me-container";
|
||||
|
||||
var a = document.createElement("a");
|
||||
a.id = "locateMe";
|
||||
a.className = "btn-floating";
|
||||
|
||||
var i = document.createElement("i");
|
||||
i.innerText = "gps_fixed";
|
||||
i.className = "material-icons right";
|
||||
|
||||
a.appendChild(i);
|
||||
a.addEventListener("click", locateMe);
|
||||
parent.appendChild(a);
|
||||
|
||||
map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(parent);
|
||||
}
|
||||
|
||||
function onPlaceChanged() {
|
||||
const place = autocomplete.getPlace();
|
||||
if (place.geometry) {
|
||||
map.fitBounds(place.geometry.viewport);
|
||||
map.setZoom(map.getZoom() + 1);
|
||||
$("#locationInputData").val(JSON.stringify(place));
|
||||
}
|
||||
}
|
||||
|
||||
function pacSelectFirst(input) {
|
||||
// store the original event binding function
|
||||
const _addEventListener = input.addEventListener
|
||||
? input.addEventListener
|
||||
: input.attachEvent;
|
||||
|
||||
function addEventListenerWrapper(type, listener) {
|
||||
// Simulate a 'down arrow' keypress on hitting 'return' when no pac suggestion is selected,
|
||||
// and then trigger the original listener.
|
||||
if (type == "keydown") {
|
||||
const originalListener = listener;
|
||||
listener = function(event) {
|
||||
const suggestionSelected = $(".pac-item-selected").length > 0;
|
||||
if (event.key == "Enter" && !suggestionSelected) {
|
||||
const simulatedDownArrow = $.Event("keydown", {
|
||||
keyCode: 40,
|
||||
which: 40
|
||||
});
|
||||
originalListener.apply(input, [simulatedDownArrow]);
|
||||
}
|
||||
|
||||
originalListener.apply(input, [event]);
|
||||
};
|
||||
}
|
||||
|
||||
_addEventListener.apply(input, [type, listener]);
|
||||
}
|
||||
|
||||
input.addEventListener = addEventListenerWrapper;
|
||||
input.attachEvent = addEventListenerWrapper;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<script
|
||||
src="https://maps.googleapis.com/maps/api/js?key=<%= process.env.API_MAP_KEY %>&language=bs&libraries=places&callback=initMap"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
|
||||
12
app/views/publishPhotos.ejs
Normal file
12
app/views/publishPhotos.ejs
Normal file
@@ -0,0 +1,12 @@
|
||||
<br>
|
||||
|
||||
<div action="/photos-upload" class="dropzone" id="photos-upload">
|
||||
<div class="fallback">
|
||||
<input name="file" type="file" multiple />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<input type="hidden" name="imageUrls" id="imageUrls" value="">
|
||||
290
app/views/publishRealEstate.ejs
Normal file
290
app/views/publishRealEstate.ejs
Normal file
@@ -0,0 +1,290 @@
|
||||
<br>
|
||||
<form id="publishForm" method="POST" novalidate >
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<ul class="tabs">
|
||||
<li class="tab col s3"><a href="#publishBasicData">Osnovni podaci</a></li>
|
||||
<li class="tab col s3"><a href="#publishAdditionalData">Dodatni podaci</a></li>
|
||||
<li class="tab col s2"><a href="#publishLocation">Lokacija</a></li>
|
||||
<li class="tab col s2"><a href="#publishPhotos">Fotografije</a></li>
|
||||
<li class="tab col s2"><a href="#publishEnd">Kraj</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div id="publishBasicData" class="col s12">
|
||||
<%- include("./publishBasicData.ejs") %>
|
||||
</div>
|
||||
<div id="publishAdditionalData" class="col s12">
|
||||
<%- include("./publishAdditionalData.ejs") %>
|
||||
</div>
|
||||
<div id="publishLocation" class="col s12">
|
||||
<%- include("./publishLocation.ejs") %>
|
||||
</div>
|
||||
<div id="publishPhotos" class="col s12">
|
||||
<%- include("./publishPhotos.ejs") %>
|
||||
</div>
|
||||
<div id="publishEnd" class="col s12">
|
||||
<%- include("./publishEnd.ejs") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function getFileName(fileName) {
|
||||
const encodedFileName = (uuidv4() + fileName).replace(/\s+/g, '');
|
||||
return encodedFileName;
|
||||
}
|
||||
|
||||
function upload() {
|
||||
var file = $('#selector')[0].files[0];
|
||||
uploadFile(file)
|
||||
}
|
||||
|
||||
|
||||
async function generateSignedURL(file) {
|
||||
const fileName = getFileName(file.name);
|
||||
const response = await fetch('/generateSignedURL?filename=' + fileName);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response for fetch was not ok.');
|
||||
}
|
||||
let signedUrl = await response.text();
|
||||
signedUrl = signedUrl.replace(/\"/g, "")
|
||||
await uploadFile(file, fileName, signedUrl);
|
||||
return fileName;
|
||||
}
|
||||
|
||||
function uploadFile(file, fileName, url) {
|
||||
return fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: new Headers({'content-type': 'image/*'}),
|
||||
mode: 'cors',
|
||||
body: file
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then (response => {
|
||||
return response;
|
||||
}
|
||||
)
|
||||
.catch(error => $("#status").html(error)
|
||||
)
|
||||
.then(response => {
|
||||
$("#imageUrls").val($("#imageUrls").val()+ fileName+"|");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
$('.tabs').tabs();
|
||||
|
||||
// Manual dropzone init
|
||||
const dropzoneOptions = {
|
||||
url: "/photos-upload", //can be a function that returns url ?
|
||||
autoProcessQueue:false, //not to upload files automaticly
|
||||
method: "put", //or post
|
||||
parallelUploads: 1,
|
||||
uploadMultiple: false,
|
||||
addRemoveLinks: true,
|
||||
maxFilesize: 2, //MB,
|
||||
resizeWidth: 600,
|
||||
maxFiles: 10,
|
||||
acceptedFiles: "image/*",
|
||||
dictDefaultMessage: `<span class="text-center">
|
||||
<h3>Prevuci fotografije ili klikni za dodavanje!</h3>
|
||||
(Maksimalno 10 fotografija.)
|
||||
</span>`,
|
||||
dictResponseError: 'Error uploading file!',
|
||||
dictRemoveFile: 'Izbriši ',
|
||||
dictFileTooBig: 'Fajl je prevelik!',
|
||||
dictInvalidFileType: 'Iabrani fajl nije fotografija!',
|
||||
dictMaxFilesExceeded: 'Dostigli ste maksimalan broj fotografija!'
|
||||
};
|
||||
var photosUploader = new Dropzone('#photos-upload', dropzoneOptions);
|
||||
|
||||
|
||||
//VALIDATION - WiP
|
||||
//Helper validation functions
|
||||
const isValidEmail = $email => {
|
||||
const simpleEmailRegex = /^(([^<>()\[\]\\.,;:\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,}))$/;
|
||||
return $email && $email.length < 250 && simpleEmailRegex.test($email);
|
||||
};
|
||||
|
||||
const isPresent = $input => {
|
||||
return $input && $input!=="" && $input != null;
|
||||
};
|
||||
|
||||
const isNumber = $input => {
|
||||
const simpleNumberRegex = /([0-9]+[.|,][0-9])|([0-9][.|,][0-9]+)|([0-9]+)/;
|
||||
return $input && $input.length <250 && simpleNumberRegex.test($input) && !isNaN($input);
|
||||
};
|
||||
|
||||
const isInteger = $input => {
|
||||
const simpleIntegerRegex = /^([+-]?[1-9]\d*|0)$/;
|
||||
return $input && $input.length <250 && simpleIntegerRegex.test($input);
|
||||
};
|
||||
|
||||
const form = document.querySelector("#publishForm");
|
||||
|
||||
function showErrors(form, errors) {
|
||||
// We loop through all the inputs and show the errors for that input
|
||||
//_.each(form.querySelectorAll("input[name], select[name]"), function(input) {
|
||||
// Since the errors can be null if no errors were found we need to handle
|
||||
// that
|
||||
showErrorsForInput(input, errors && errors[input.name]);
|
||||
// });
|
||||
}
|
||||
|
||||
// Shows the errors for a specific input
|
||||
function showErrorsForInput(input, errors) {
|
||||
// This is the root of the input
|
||||
var formGroup = closestParent(input.parentNode, "form-group"),
|
||||
// Find where the error messages will be insert into
|
||||
messages = formGroup.querySelector(".messages");
|
||||
// First we remove any old messages and resets the classes
|
||||
resetFormGroup(formGroup);
|
||||
// If we have errors
|
||||
if (errors) {
|
||||
// we first mark the group has having errors
|
||||
formGroup.classList.add("has-error");
|
||||
// then we append all the errors
|
||||
$.each(errors, function(error) {
|
||||
addError(messages, errors[error]);
|
||||
});
|
||||
|
||||
} else {
|
||||
// otherwise we simply mark it as success
|
||||
formGroup.classList.add("has-success");
|
||||
}
|
||||
}
|
||||
|
||||
// Recusively finds the closest parent that has the specified class
|
||||
function closestParent(child, className) {
|
||||
if (!child || child == document) {
|
||||
return null;
|
||||
}
|
||||
if (child.classList.contains(className)) {
|
||||
return child;
|
||||
} else {
|
||||
return closestParent(child.parentNode, className);
|
||||
}
|
||||
}
|
||||
|
||||
function resetFormGroup(formGroup) {
|
||||
formGroup.classList.remove("has-error");
|
||||
formGroup.classList.remove("has-success");
|
||||
$.each(formGroup.querySelectorAll(".help-block.error"), function(el) {
|
||||
el.parentNode.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Adds the specified error with the following markup
|
||||
// <p class="help-block error">[message]</p>
|
||||
function addError(messages, error) {
|
||||
var block = document.createElement("p");
|
||||
block.classList.add("help-block");
|
||||
block.classList.add("error");
|
||||
block.innerText = error;
|
||||
messages.appendChild(block);
|
||||
}
|
||||
|
||||
const validate = (input) => {
|
||||
|
||||
let valid=true;
|
||||
let errorMsg =[];
|
||||
let constraint = input.constraint[0];
|
||||
|
||||
switch (constraint) {
|
||||
case "required":
|
||||
valid = isPresent ($(`#${input.dbField}`).val());
|
||||
errorMsg = ["Ovo je obavezno polje."];
|
||||
break;
|
||||
case "numerical":
|
||||
valid = isNumber ($(`#${input.dbField}`).val());
|
||||
errorMsg = ["Unesite brojcanu vrijednost."];
|
||||
break;
|
||||
case "integer":
|
||||
valid = isInteger ($(`#${input.dbField}`).val());
|
||||
errorMsg = ["Unesite cijeli broj."];
|
||||
break;
|
||||
default :
|
||||
valid = true;
|
||||
}
|
||||
if (!valid) {
|
||||
const inputField = document.querySelector(`#${input.dbField}`);
|
||||
showErrorsForInput( inputField, errorMsg);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$("#submit").click( async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (marker) {
|
||||
const currentLocation = marker.getPosition();
|
||||
|
||||
$("#lat").val(currentLocation.lat());
|
||||
|
||||
$("#lng").val(currentLocation.lng());
|
||||
|
||||
$("#locationInput").val(
|
||||
document.getElementById("autocompleteInput").value
|
||||
);
|
||||
} else {
|
||||
$("#lat").val(0);
|
||||
|
||||
$("#lng").val(0);
|
||||
}
|
||||
|
||||
//Tag for checking of error presence
|
||||
let hasErrors = false;
|
||||
//Check if email is valid
|
||||
const validEmail = isValidEmail($("#email").val());
|
||||
//Show messeges for invalid email is present
|
||||
if (!validEmail) {
|
||||
const errorMsgs = ["Unesite validan email."];
|
||||
const email = document.querySelector("#email");
|
||||
showErrorsForInput( email, errorMsgs)
|
||||
hasErrors = true;
|
||||
};
|
||||
//Check if other input fields are valid - vratiti se na ovo!!
|
||||
//const basicInputInputs= document.getElementById("basic-inputs").getElementsByTagName("input");
|
||||
|
||||
//alert(JSON.stringify(""));
|
||||
/*
|
||||
$.each(basicInputInputs, function (input) {
|
||||
alert(input);
|
||||
validate (input);
|
||||
})
|
||||
for (const input of basicInputInputs ) {
|
||||
alert(input.getAttribute(name));
|
||||
|
||||
validate (input);
|
||||
} */
|
||||
const addedFiles = photosUploader.files.filter(file => file.status!=="error");
|
||||
const asyncUpload =[];
|
||||
|
||||
addedFiles.forEach( file => {
|
||||
asyncUpload.push(generateSignedURL(file));
|
||||
})
|
||||
|
||||
if (!hasErrors) {
|
||||
await Promise.all(asyncUpload);
|
||||
$("#publishForm").submit();
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
11
app/views/publishSuccess.ejs
Normal file
11
app/views/publishSuccess.ejs
Normal file
@@ -0,0 +1,11 @@
|
||||
<br>
|
||||
<div class="row center-align">
|
||||
<p>Vaš oglas je spašen u Kivi bazu.</p>
|
||||
<br>
|
||||
<div class="row center-align">
|
||||
<img src="../assets/images/logo.svg" alt="kivi logo" width="160">
|
||||
</div>
|
||||
<br>
|
||||
<p>Poslali smo potvrdni email sa detaljima oglasa na Vašu email adresu.</p>
|
||||
<a href="/" class="">Nova pretraga</a>
|
||||
</div>
|
||||
@@ -9,7 +9,7 @@
|
||||
<% if (selectedAdType === AD_TYPE.AD_TYPE_SALE.id) { %>
|
||||
checked
|
||||
<% } %>>
|
||||
<span class="label"><%= AD_TYPE.AD_TYPE_SALE.title %></span>
|
||||
<span class="label"><%= labelAdType[0] %></span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
@@ -17,7 +17,7 @@
|
||||
<% if (selectedAdType === AD_TYPE.AD_TYPE_RENT.id) { %>
|
||||
checked
|
||||
<% } %>>
|
||||
<span class="label"><%= AD_TYPE.AD_TYPE_RENT.title %></span>
|
||||
<span class="label"><%= labelAdType[1] %></span>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
<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="kivi-color secondary-content">
|
||||
<ul class="collection with-header">
|
||||
<% for(const realEstate of realEstates) { %>
|
||||
<li class="collection-item">
|
||||
<% if(realEstate.adStatus === AD_STATUS.STATUS_VIP) {%>
|
||||
<div>
|
||||
<% //This needs to do redirecting instead of direct link to realestate
|
||||
%>
|
||||
<a href="/redirect/<%= realEstate.id %>" class="estates-link">
|
||||
<%= realEstate.title %>
|
||||
<div class="kivi-color secondary-content">
|
||||
<i class="material-icons">send</i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<%} else { %>
|
||||
<div>
|
||||
<a href="<%= realEstate.url %>" class="estates-link">
|
||||
<%= realEstate.title %>
|
||||
<div class="kivi-color secondary-content">
|
||||
<i class="material-icons">send</i>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<% }%>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,49 @@
|
||||
<br><br>
|
||||
<br /><br />
|
||||
<div class="center">
|
||||
<div class="preloader-wrapper big active center">
|
||||
<div class="kivi-spinner-color spinner-layer spinner-green-only">
|
||||
<div class="circle-clipper left">
|
||||
<div class="circle"></div>
|
||||
</div><div class="gap-patch">
|
||||
<div class="circle"></div>
|
||||
</div><div class="circle-clipper right">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preloader-wrapper big active center">
|
||||
<div class="kivi-spinner-color spinner-layer spinner-green-only">
|
||||
<div class="circle-clipper left">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
<div class="gap-patch">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
<div class="circle-clipper right">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br />
|
||||
<% if(vipAd) { %>
|
||||
<div class="center">
|
||||
<h6>
|
||||
<a href="<%= redirectUrl %>" rel="noreferrer" id="realEstateUrl">Kliknite ovdje ako Vas web preglednik ne preusmjeri automatski</a>
|
||||
</h6>
|
||||
<h6>
|
||||
Ovaj oglas zahtijeva da budete član
|
||||
<a href="https://prostor.ba/" rel="noreferrer">Prostor.ba</a>.
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://prostor.ba/moj-prostor/prijava" rel="noreferrer"
|
||||
>Ulogujte se</a
|
||||
>
|
||||
ili napravite
|
||||
<a href="https://prostor.ba/moj-prostor/registracija" rel="noreferrer"
|
||||
>novi račun</a
|
||||
>, a potom otvorite <a href="<%= redirectUrl %>" rel="noreferrer">oglas</a>.
|
||||
</h6>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="center">
|
||||
<h6>
|
||||
<a href="<%= redirectUrl %>" rel="noreferrer" id="realEstateUrl"
|
||||
>Kliknite ovdje ako Vas web preglednik ne preusmjeri automatski</a
|
||||
>
|
||||
</h6>
|
||||
</div>
|
||||
<% }%>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.getElementById('realEstateUrl').click();
|
||||
}
|
||||
window.onload = function() {
|
||||
document.getElementById("realEstateUrl").click();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<p class="distinguished">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" class="filled-in" name="includeWithoutPrice"
|
||||
checked
|
||||
>
|
||||
<span>Uključi i oglase bez cijene</span>
|
||||
</label>
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<div class="row center-align">
|
||||
|
||||
@@ -18,7 +18,20 @@
|
||||
</div>
|
||||
<input type="hidden" id="adType" name="adType">
|
||||
</form>
|
||||
|
||||
<div class="row center-align">
|
||||
<div>Objavite svoj oglas.</div>
|
||||
</div>
|
||||
<form method="POST" name="welcomePublishForm">
|
||||
<div class="row center-align">
|
||||
<div class="col s5 m4 l3 push-s1 push-m2 push-l3">
|
||||
<a href="#" onclick="publishSaleClick()" class="welcome-center-button btn">Prodaj</a>
|
||||
</div>
|
||||
<div class="col s5 m4 l3 push-s1 push-m2 push-l3">
|
||||
<a href="#" onclick="publishRentClick()" class="welcome-center-button btn">Iznajmi</a>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="publishAdType" name="publishAdType">
|
||||
</form>
|
||||
<script>
|
||||
function saleClick(){
|
||||
$("#adType").val("<%= AD_TYPE.AD_TYPE_SALE.id %>");
|
||||
@@ -29,4 +42,13 @@
|
||||
$("#adType").val("<%= AD_TYPE.AD_TYPE_RENT.id %>");
|
||||
document.welcomeForm.submit();
|
||||
}
|
||||
function publishSaleClick(){
|
||||
$("#publishAdType").val("<%= AD_TYPE.AD_TYPE_SALE.id %>");
|
||||
document.welcomePublishForm.submit();
|
||||
}
|
||||
|
||||
function publishRentClick(){
|
||||
$("#publishAdType").val("<%= AD_TYPE.AD_TYPE_RENT.id %>");
|
||||
document.welcomePublishForm.submit();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,15 +8,22 @@ SEQUELIZE_LOGGING=0- no sequelize logging, 1- log to the console
|
||||
PORT=Port for the app, defaults to 5000
|
||||
APP_BASE_URL=base url for the app
|
||||
|
||||
SETTINGS=Variable to denote development, staging and production
|
||||
|
||||
|
||||
MAX_REAL_ESTATES_IN_EMAIL=Max number of real estates that will be shown in email, others will be truncated and URL with full list will be shwon
|
||||
MAX_REAL_ESTATES_IN_FIRST_EMAIL=Max number of real estates that will be shown in first (welcome) email
|
||||
|
||||
CHECK_UP_DAYS=Check up email is sent after this number of days without notification
|
||||
#=============== GOOGLE ANALYTICS =============#
|
||||
GA_ID=Google Analytics ID
|
||||
|
||||
#=============== GOOGLE MAPS =============#
|
||||
API_MAP_KEY=(your-key-here)
|
||||
|
||||
#=============== GOOGLE STORAGE =============#
|
||||
GOOGLE_APPLICATION_CREDENTIALS="Path to json key file"
|
||||
|
||||
#=============== AWS SDK EMAIL SETTINGS =======#
|
||||
AWS_KEY_ID=(your-key-here)
|
||||
AWS_SECRET_ACCESS_KEY=(your-key-here)
|
||||
@@ -51,6 +58,8 @@ PROSTOR_CRAWLER_AD_CATEGORIES=comma separated list of enum names of categories t
|
||||
PROSTOR_IGNORED_USERNAMES=!!! This is not used for prostor crawler !!!
|
||||
PROSTOR_DELAY_BETWEEN_PAGES=!!! This is not used for prostor crawler !!!
|
||||
PROSTOR_FORCE_CRAWL=Non-zero value will force crawler to crawl all pages without stopping when known real estate is found
|
||||
PROSTOR_LOGIN_EMAIL=Email of valid Prostor.ba account for crawling purposes
|
||||
PROSTOR_LOGIN_PASS=Password of valid Prostor.ba account for crawling purposes
|
||||
#==AKTIDO==
|
||||
AKTIDO_MAX_PAGES=Restrict crawler to this number of pages
|
||||
AKTIDO_MAX_RESULTS_PER_PAGE=Only this number or less results from one page will be scraped and saved
|
||||
@@ -59,3 +68,8 @@ AKTIDO_CRAWLER_AD_CATEGORIES=comma separated list of enum names of categories to
|
||||
AKTIDO_IGNORED_USERNAMES=!!! This is not used for aktido crawler !!!
|
||||
AKTIDO_DELAY_BETWEEN_PAGES=time in miliseconds to wait before indexing next page
|
||||
AKTIDO_FORCE_CRAWL=Non-zero value will force crawler to crawl all pages without stopping when known real estate is found
|
||||
#==SALJIC NEKRETNINE==
|
||||
SALJIC_MAX_RESULTS_PER_PAGE=For Saljic crawler, this represents how many ads are crawled at once
|
||||
SALJIC_CRAWLER_AD_TYPE=enum name of what type of ads should be crawled, check common/enums.js file for valid values
|
||||
SALJIC_CRAWLER_AD_CATEGORIES=comma separated list of enum names of categories to be included, check common/enums.js file for valid values
|
||||
SALJIC_FORCE_CRAWL=Non-zero value will force crawler to crawl all pages without stopping when known real estate is found
|
||||
237
help.js
Normal file
237
help.js
Normal file
@@ -0,0 +1,237 @@
|
||||
(function() {
|
||||
// Before using it we must add the parse and format functions
|
||||
// Here is a sample implementation using moment.js
|
||||
validate.extend(validate.validators.datetime, {
|
||||
// The value is guaranteed not to be null or undefined but otherwise it
|
||||
// could be anything.
|
||||
parse: function(value, options) {
|
||||
return +moment.utc(value);
|
||||
},
|
||||
// Input is a unix timestamp
|
||||
format: function(value, options) {
|
||||
var format = options.dateOnly ? "YYYY-MM-DD" : "YYYY-MM-DD hh:mm:ss";
|
||||
return moment.utc(value).format(format);
|
||||
}
|
||||
});
|
||||
|
||||
// These are the constraints used to validate the form
|
||||
var constraints = {
|
||||
email: {
|
||||
// Email is required
|
||||
presence: true,
|
||||
// and must be an email (duh)
|
||||
email: true
|
||||
},
|
||||
password: {
|
||||
// Password is also required
|
||||
presence: true,
|
||||
// And must be at least 5 characters long
|
||||
length: {
|
||||
minimum: 5
|
||||
}
|
||||
},
|
||||
"confirm-password": {
|
||||
// You need to confirm your password
|
||||
presence: true,
|
||||
// and it needs to be equal to the other password
|
||||
equality: {
|
||||
attribute: "password",
|
||||
message: "^The passwords does not match"
|
||||
}
|
||||
},
|
||||
username: {
|
||||
// You need to pick a username too
|
||||
presence: true,
|
||||
// And it must be between 3 and 20 characters long
|
||||
length: {
|
||||
minimum: 3,
|
||||
maximum: 20
|
||||
},
|
||||
format: {
|
||||
// We don't allow anything that a-z and 0-9
|
||||
pattern: "[a-z0-9]+",
|
||||
// but we don't care if the username is uppercase or lowercase
|
||||
flags: "i",
|
||||
message: "can only contain a-z and 0-9"
|
||||
}
|
||||
},
|
||||
birthdate: {
|
||||
// The user needs to give a birthday
|
||||
presence: true,
|
||||
// and must be born at least 18 years ago
|
||||
date: {
|
||||
latest: moment().subtract(18, "years"),
|
||||
message: "^You must be at least 18 years old to use this service"
|
||||
}
|
||||
},
|
||||
country: {
|
||||
// You also need to input where you live
|
||||
presence: true,
|
||||
// And we restrict the countries supported to Sweden
|
||||
inclusion: {
|
||||
within: ["SE"],
|
||||
// The ^ prevents the field name from being prepended to the error
|
||||
message: "^Sorry, this service is for Sweden only"
|
||||
}
|
||||
},
|
||||
zip: {
|
||||
// Zip is optional but if specified it must be a 5 digit long number
|
||||
format: {
|
||||
pattern: "\\d{5}"
|
||||
}
|
||||
},
|
||||
"number-of-children": {
|
||||
presence: true,
|
||||
// Number of children has to be an integer >= 0
|
||||
numericality: {
|
||||
onlyInteger: true,
|
||||
greaterThanOrEqualTo: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Hook up the form so we can prevent it from being posted
|
||||
var form = document.querySelector("form#main");
|
||||
form.addEventListener("submit", function(ev) {
|
||||
ev.preventDefault();
|
||||
handleFormSubmit(form);
|
||||
});
|
||||
|
||||
// Hook up the inputs to validate on the fly
|
||||
var inputs = document.querySelectorAll("input, textarea, select");
|
||||
for (var i = 0; i < inputs.length; ++i) {
|
||||
inputs.item(i).addEventListener("change", function(ev) {
|
||||
var errors = validate(form, constraints) || {};
|
||||
showErrorsForInput(this, errors[this.name]);
|
||||
});
|
||||
}
|
||||
|
||||
function handleFormSubmit(form, input) {
|
||||
// validate the form against the constraints
|
||||
var errors = validate(form, constraints);
|
||||
// then we update the form to reflect the results
|
||||
showErrors(form, errors || {});
|
||||
if (!errors) {
|
||||
showSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the inputs with the validation errors
|
||||
function showErrors(form, errors) {
|
||||
// We loop through all the inputs and show the errors for that input
|
||||
_.each(form.querySelectorAll("input[name], select[name]"), function(input) {
|
||||
// Since the errors can be null if no errors were found we need to handle
|
||||
// that
|
||||
showErrorsForInput(input, errors && errors[input.name]);
|
||||
});
|
||||
}
|
||||
|
||||
// Shows the errors for a specific input
|
||||
function showErrorsForInput(input, errors) {
|
||||
// This is the root of the input
|
||||
var formGroup = closestParent(input.parentNode, "form-group"),
|
||||
// Find where the error messages will be insert into
|
||||
messages = formGroup.querySelector(".messages");
|
||||
// First we remove any old messages and resets the classes
|
||||
resetFormGroup(formGroup);
|
||||
// If we have errors
|
||||
if (errors) {
|
||||
// we first mark the group has having errors
|
||||
formGroup.classList.add("has-error");
|
||||
// then we append all the errors
|
||||
_.each(errors, function(error) {
|
||||
addError(messages, error);
|
||||
});
|
||||
} else {
|
||||
// otherwise we simply mark it as success
|
||||
formGroup.classList.add("has-success");
|
||||
}
|
||||
}
|
||||
|
||||
// Recusively finds the closest parent that has the specified class
|
||||
function closestParent(child, className) {
|
||||
if (!child || child == document) {
|
||||
return null;
|
||||
}
|
||||
if (child.classList.contains(className)) {
|
||||
return child;
|
||||
} else {
|
||||
return closestParent(child.parentNode, className);
|
||||
}
|
||||
}
|
||||
|
||||
function resetFormGroup(formGroup) {
|
||||
// Remove the success and error classes
|
||||
formGroup.classList.remove("has-error");
|
||||
formGroup.classList.remove("has-success");
|
||||
// and remove any old messages
|
||||
_.each(formGroup.querySelectorAll(".help-block.error"), function(el) {
|
||||
el.parentNode.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Adds the specified error with the following markup
|
||||
// <p class="help-block error">[message]</p>
|
||||
function addError(messages, error) {
|
||||
var block = document.createElement("p");
|
||||
block.classList.add("help-block");
|
||||
block.classList.add("error");
|
||||
block.innerText = error;
|
||||
messages.appendChild(block);
|
||||
}
|
||||
|
||||
function showSuccess() {
|
||||
// We made it \:D/
|
||||
alert("Success!");
|
||||
}
|
||||
})();
|
||||
/////////////////////////////////////////////////
|
||||
const isPresent = $input => {
|
||||
return $input && $input!=="" && $input != null;
|
||||
}
|
||||
|
||||
const isNumber = $input => {
|
||||
const simpleNumberRegex = /[+-]?(?:\d*[.,])?\d+/;
|
||||
return $input && $input.length <250 && simpleNumberRegex.test($input);
|
||||
|
||||
}
|
||||
|
||||
const isInteger = $input => {
|
||||
const simpleIntegerRegex = /^([+-]?[1-9]\d*|0)$/;
|
||||
return $input && $input.length <250 && simpleIntegerRegex.test($input);
|
||||
|
||||
}
|
||||
|
||||
const validate = (input) => {
|
||||
|
||||
const valid;
|
||||
const errorMsg;
|
||||
const constraint = input.constraint[0];
|
||||
|
||||
switch (constraint) {
|
||||
case "required":
|
||||
valid = isPresent ($(`#${input.dbField}`).val());
|
||||
errorMsg = ["Ovo je obavezno polje."];
|
||||
break;
|
||||
case "numerical":
|
||||
valid = isNumber ($(`#${input.dbField}`).val());
|
||||
errorMsg = ["Unesite brojcanu vrijednost."];
|
||||
break;
|
||||
case "integer":
|
||||
valid = isInteger ($(`#${input.dbField}`).val());
|
||||
errorMsg = ["Unesite cijeli broj."];
|
||||
|
||||
break;
|
||||
default :
|
||||
valid = true;
|
||||
}
|
||||
if (!valid) {
|
||||
const inputField = document.querySelector(`#${input.dbField}`);
|
||||
showErrorsForInput( inputField, errorMsg);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
112
help2.js
Normal file
112
help2.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const validatorFunction = () => {
|
||||
// These are the constraints used to validate the form --just email for now!
|
||||
const constraints = {
|
||||
email: {
|
||||
email: {
|
||||
message: "Proba"
|
||||
},
|
||||
// Email is required
|
||||
presence: true,
|
||||
// and must be an email (duh)
|
||||
email: true
|
||||
}
|
||||
};
|
||||
|
||||
// Hook up the inputs to validate on the fly
|
||||
const inputs = document.querySelectorAll("#email");
|
||||
// inputs.on("change", ev => {
|
||||
// const errors = validate(form, constraints) || {};
|
||||
// showErrorsForInput(this, errors[this.name]);
|
||||
// });
|
||||
// var inputs = document.querySelectorAll("input, textarea, select");
|
||||
for (var i = 0; i < inputs.length; ++i) {
|
||||
inputs.item(i).addEventListener("change", function(ev) {
|
||||
var errors = validate(form, constraints) || {};
|
||||
showErrorsForInput(this, errors[this.name]);
|
||||
});
|
||||
}
|
||||
|
||||
const handleFormSubmit = (form, input) => {
|
||||
// validate the form against the constraints
|
||||
const errors = validate(form, constraints);
|
||||
//
|
||||
console.log("handleFormSubmit error:", errors);
|
||||
// then we update the form to reflect the results
|
||||
showErrors(form, errors || {});
|
||||
if (!errors) {
|
||||
showSuccess();
|
||||
}
|
||||
};
|
||||
|
||||
// Updates the inputs with the validation errors
|
||||
const showErrors = (form, errors) => {
|
||||
// We loop through all the inputs and show the errors for that input
|
||||
$.each(form.querySelectorAll("input[name], select[name]"), input => {
|
||||
// Since the errors can be null if no errors were found we need to handle
|
||||
// that
|
||||
showErrorsForInput(input, errors && errors[input.name]);
|
||||
});
|
||||
//showErrorsForInput(email, errors && errors[email]);
|
||||
};
|
||||
|
||||
// Shows the errors for a specific input
|
||||
const showErrorsForInput = (input, errors) => {
|
||||
// This is the root of the input
|
||||
const formGroup = closestParent(input.parentNode, "form-group"),
|
||||
// Find where the error messages will be insert into
|
||||
messages = formGroup.querySelector(".messages");
|
||||
// First we remove any old messages and resets the classes
|
||||
resetFormGroup(formGroup);
|
||||
// If we have errors
|
||||
if (errors) {
|
||||
//
|
||||
console.log("errors:", errors);
|
||||
// we first mark the group has having errors
|
||||
formGroup.classList.add("has-error");
|
||||
// then we append all the errors
|
||||
$.each(errors, error => {
|
||||
addError(messages, errors[error]);
|
||||
});
|
||||
} else {
|
||||
// otherwise we simply mark it as success
|
||||
formGroup.classList.add("has-success");
|
||||
}
|
||||
};
|
||||
|
||||
// Recusively finds the closest parent that has the specified class
|
||||
const closestParent = (child, className) => {
|
||||
if (!child || child == document) {
|
||||
return null;
|
||||
}
|
||||
if (child.classList.contains(className)) {
|
||||
return child;
|
||||
} else {
|
||||
return closestParent(child.parentNode, className);
|
||||
}
|
||||
};
|
||||
|
||||
const resetFormGroup = formGroup => {
|
||||
// Remove the success and error classes
|
||||
formGroup.classList.remove("has-error");
|
||||
formGroup.classList.remove("has-success");
|
||||
// and remove any old messages
|
||||
$.each(formGroup.querySelectorAll(".help-block.error"), el => {
|
||||
el.parentNode.removeChild(el);
|
||||
});
|
||||
};
|
||||
|
||||
// Adds the specified error with the following markup
|
||||
// <p class="help-block error">[message]</p>
|
||||
const addError = (messages, error) => {
|
||||
const block = document.createElement("p");
|
||||
block.classList.add("help-block");
|
||||
block.classList.add("error");
|
||||
block.innerText = error;
|
||||
messages.appendChild(block);
|
||||
};
|
||||
|
||||
const showSuccess = () => {
|
||||
// We made it \:D/
|
||||
alert("Success!");
|
||||
};
|
||||
};
|
||||
43
index.js
43
index.js
@@ -5,6 +5,10 @@ const layout = require("express-layout");
|
||||
const compression = require("compression");
|
||||
const forceSSL = require("./app/helpers/forceSSL");
|
||||
|
||||
const { Storage } = require("@google-cloud/storage");
|
||||
const validate = require("validate.js");
|
||||
const cors = require("cors");
|
||||
|
||||
const {
|
||||
APP_PORT,
|
||||
CRAWLER_INTERVAL,
|
||||
@@ -12,12 +16,15 @@ const {
|
||||
} = require("./app/config/appConfig");
|
||||
const routes = require("./app/routes");
|
||||
const { crawlAll } = require("./app/crawler/crawl");
|
||||
const { checkUpNotify } = require("./app/services/notificationService");
|
||||
const {
|
||||
notifyForNewRealEstates
|
||||
} = require("./app/services/notificationService");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
|
||||
app.use(forceSSL());
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
@@ -45,4 +52,40 @@ const crawl = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(crawl, CRAWLER_INTERVAL * 1000);
|
||||
|
||||
setInterval(checkUpNotify, 1000 * 60 * 60 * 24);
|
||||
|
||||
//Google storage req
|
||||
const PROJECT_ID = "marketalarm";
|
||||
const KEY_FILENAME = ""; //relative path
|
||||
const BUCKET_NAME = "marketalarm-photos";
|
||||
const storage = new Storage();
|
||||
|
||||
const bucket = storage.bucket(BUCKET_NAME);
|
||||
|
||||
app.get("/generateSignedURL", (req, res) => {
|
||||
async function generateSignedUrl() {
|
||||
// console.log("Started server function!");
|
||||
|
||||
const options = {
|
||||
//Tried to define Google ID and private key while debugging
|
||||
version: "v2", //tried v4 also
|
||||
action: "write",
|
||||
contentType: "image/*", //tried without and with specific image/png ex.
|
||||
expires: Date.now() + 86400000
|
||||
};
|
||||
const filename = req.query.filename;
|
||||
|
||||
// console.log("Filename: ", filename);
|
||||
// console.log("Bucket name:", bucket.name);
|
||||
|
||||
const [url] = await bucket.file(filename).getSignedUrl(options);
|
||||
|
||||
//console.log(`The signed url is ${url}.`);
|
||||
|
||||
res.status(200).send(url);
|
||||
}
|
||||
generateSignedUrl().catch(console.error);
|
||||
});
|
||||
|
||||
12
marketalarm-cors.json
Normal file
12
marketalarm-cors.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"origin": ["*"],
|
||||
"responseHeader": [
|
||||
"Content-Type",
|
||||
"Access-Control-Allow-Origin",
|
||||
"x-goog-resumable"
|
||||
],
|
||||
"method": ["GET", "HEAD", "DELETE", "POST", "PUT", "OPTIONS"],
|
||||
"maxAgeSeconds": 3600
|
||||
}
|
||||
]
|
||||
12
marketalarm-d4b71394407e.json
Normal file
12
marketalarm-d4b71394407e.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "marketalarm",
|
||||
"private_key_id": "d4b71394407eb3dba9e431851dab60b198d6985d",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTE6dkFr0bzDXg\n7ghMxkzq8cajqqqes9JZVqsXh+b/kFJYmEImFUILJJZdI080KM2sEYsIapBCxhMP\nFH017f/gfH3jnRbp3c70hghNh8noSTsq7kPA4l25o8GQnJ6AS+nhy8umPjb4KzX9\nkmC6OOD4P8mAmGqhoUv4s2jld1cXNur6NJjCpjEd2cH3SUbI71oA3V/4W8aK4dvS\n660kLY0PRt7mCiITe0hbTUBZY48W2ijZ7wM2r0HUtPG9XEeGMGmNsC+qD2oWxUU3\nvnm7l1fEIUvLYF4GrLRDJDSkpChBXNcWhoGV2AOvuTc+yghU2+lJWqrKcpLlI23E\nlVJjt9UhAgMBAAECggEABatr8sxq+SQOf9hSIe3Me9Kc1nunrC42scFHRKBNxahJ\ndXw5B9FQPh738Cqhk0xEz6hlrln1Agj6HhRIz8U0r9R+z4TRRr6kfnWmBZAMShu0\nC4JW448abpAYx8CQ/CvRmq2GlF+/M+QBeqpLS8gPzyaKTB/5IBaKG8Bn0fXXQZ2e\n7RaTpGx62jq79omPwiKz0PMVBGZrzPu8Z4tW47muV51osdKSNVgsXb4gCZl28zN2\n6zzY1ZK7u89MesY8joILMHm8cw0oyv9o+RVGEa1I1nq2q1A8ftZny4p7kUA/ITZX\nEZ6SCOP87z9HeVCr8lzexcovD8uZCOTYpcfotlSjGQKBgQD1VVGU2bzExiV0XQGc\n8n6m4TR6Y9zwXBiQPe0rXPZhvsj8QMTXk+L0ejCo4m7NF1dEyH6u+qX6wjNL1Hm9\nN/ZuDFqYtd9w6cQ8CtDZ6QZIE60k6tLQhMNRNMvHdMfedq+VOz3LX6TdyTnv8dP3\nbEsD8wIfFd6t5wNgeZkbKsNxBwKBgQDcQQsUppjglGpUoz7lGHKbFcKMPpIj9fMY\nfze1DXeTAtHGxGm2F10WZvxOEs4DCOUllBlarL5xDAJIJHk/NYlgnI6MJXMLro6d\nsb4iNTUuJKdqAijyOaZQUADJpdYKGwu5y66PUOuojWFV9kiamquXduJ9jzOa1vr1\nSJPUy2YGlwKBgQDNJrpgwa8z0QozAy89Ih68x+fNTMLNkAXOYKp6L3OsixCguDyi\nlP0dOSyFnUvQXutQDmS5R8oSJeElURk4HJsKrXP47WVak3DQUK8S+eSR0zpfe6os\nSkjWGFMriEE2i4MKRI7JCULhX8r+FfgNl9YnCEfG3M/oFhzhyO06JYlncwKBgF6n\nBSAGyEQbA+cDkI/bhcToAQdMDHmvxJyOb147P1vKJmSJG/TI7ZQnBd53blkXhYI0\ntwCko+LpCkH+iqyDUVpXbVsE7P/kMB3MuKzyuLvvvJJuAzK1W6e/+daukeEd5lge\nFBI68EsrFt1eTa1DMuKQkJzs4Xx1TrwCSKV2E45ZAoGBAIZkXyAOhwqCxwDF7B69\nt/7CWs0gPGqp6lFO7fgt7jPmcmSEr/xgUbBDFwd7D49jpXVgCEtr1Bd6MItlu/Ns\nXgXyOa5LPQmglF7UtnvuQLASBy5X6boKaf3sz7I5eho1kXczPGQUHfR5e0DaTND3\nTi2NLIAUci8T7hc8mONdeEHD\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "marketalarm-photos-service-acc@marketalarm.iam.gserviceaccount.com",
|
||||
"client_id": "115644068453290488813",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/marketalarm-photos-service-acc%40marketalarm.iam.gserviceaccount.com"
|
||||
}
|
||||
672
package-lock.json
generated
672
package-lock.json
generated
@@ -12,6 +12,92 @@
|
||||
"request": "2.x.x"
|
||||
}
|
||||
},
|
||||
"@google-cloud/common": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz",
|
||||
"integrity": "sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg==",
|
||||
"requires": {
|
||||
"@google-cloud/projectify": "^1.0.0",
|
||||
"@google-cloud/promisify": "^1.0.0",
|
||||
"arrify": "^2.0.0",
|
||||
"duplexify": "^3.6.0",
|
||||
"ent": "^2.2.0",
|
||||
"extend": "^3.0.2",
|
||||
"google-auth-library": "^5.5.0",
|
||||
"retry-request": "^4.0.0",
|
||||
"teeny-request": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@google-cloud/paginator": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz",
|
||||
"integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==",
|
||||
"requires": {
|
||||
"arrify": "^2.0.0",
|
||||
"extend": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@google-cloud/projectify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz",
|
||||
"integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg=="
|
||||
},
|
||||
"@google-cloud/promisify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz",
|
||||
"integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ=="
|
||||
},
|
||||
"@google-cloud/storage": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.5.0.tgz",
|
||||
"integrity": "sha512-ZLFcR6CiP1AnYBA9eTtASF9Dy3wjYmGx+HZiy/LsIPN41wyBTn9yAjIOxRHiteqzX3uQzZ+VJNCB/DmTU33CeQ==",
|
||||
"requires": {
|
||||
"@google-cloud/common": "^2.1.1",
|
||||
"@google-cloud/paginator": "^2.0.0",
|
||||
"@google-cloud/promisify": "^1.0.0",
|
||||
"arrify": "^2.0.0",
|
||||
"compressible": "^2.0.12",
|
||||
"concat-stream": "^2.0.0",
|
||||
"date-and-time": "^0.12.0",
|
||||
"duplexify": "^3.5.0",
|
||||
"extend": "^3.0.2",
|
||||
"gaxios": "^2.0.1",
|
||||
"gcs-resumable-upload": "^2.2.4",
|
||||
"hash-stream-validation": "^0.2.2",
|
||||
"mime": "^2.2.0",
|
||||
"mime-types": "^2.0.8",
|
||||
"onetime": "^5.1.0",
|
||||
"p-limit": "^2.2.0",
|
||||
"pumpify": "^2.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"snakeize": "^0.1.0",
|
||||
"stream-events": "^1.0.1",
|
||||
"through2": "^3.0.0",
|
||||
"xdg-basedir": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mime": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
|
||||
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"xdg-basedir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
|
||||
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sendgrid/client": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.3.0.tgz",
|
||||
@@ -40,6 +126,11 @@
|
||||
"@sendgrid/helpers": "^6.3.0"
|
||||
}
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz",
|
||||
"integrity": "sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA=="
|
||||
},
|
||||
"@types/caseless": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
|
||||
@@ -79,6 +170,14 @@
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"requires": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
|
||||
@@ -88,6 +187,29 @@
|
||||
"negotiator": "0.6.1"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz",
|
||||
"integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==",
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
||||
@@ -176,6 +298,11 @@
|
||||
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
||||
"dev": true
|
||||
},
|
||||
"arrify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
@@ -320,6 +447,11 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
|
||||
"integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
|
||||
@@ -424,6 +556,16 @@
|
||||
"isarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||
},
|
||||
"buffer-writer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
||||
@@ -671,6 +813,17 @@
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"config-chain": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz",
|
||||
@@ -730,6 +883,15 @@
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"requires": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"create-error-class": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
|
||||
@@ -799,6 +961,11 @@
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"date-and-time": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.12.0.tgz",
|
||||
"integrity": "sha512-n2RJIAp93AucgF/U/Rz5WRS2Hjg5Z+QxscaaMCi6pVZT1JpJKRH+C08vyH/lRR1kxNXnPxgo3lWfd+jCb/UcuQ=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -949,6 +1116,41 @@
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
|
||||
"dev": true
|
||||
},
|
||||
"duplexify": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"readable-stream": "^2.0.0",
|
||||
"stream-shift": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -958,6 +1160,14 @@
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"editorconfig": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
|
||||
@@ -997,6 +1207,11 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"ent": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
|
||||
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
|
||||
},
|
||||
"entities": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||
@@ -1074,6 +1289,11 @@
|
||||
"es5-ext": "~0.10.14"
|
||||
}
|
||||
},
|
||||
"event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"events": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||
@@ -1289,6 +1509,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
},
|
||||
"fast-text-encoding": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.1.tgz",
|
||||
"integrity": "sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
||||
@@ -1346,13 +1571,23 @@
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
|
||||
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
@@ -1937,6 +2172,117 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"gaxios": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.2.tgz",
|
||||
"integrity": "sha512-K/+py7UvKRDaEwEKlLiRKrFr+wjGjsMz5qH7Vs549QJS7cpSCOT/BbWL7pzqECflc46FcNPipjSfB+V1m8PAhw==",
|
||||
"requires": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz",
|
||||
"integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==",
|
||||
"requires": {
|
||||
"gaxios": "^2.1.0",
|
||||
"json-bigint": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"gcs-resumable-upload": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.2.tgz",
|
||||
"integrity": "sha512-OPS0iAmPCV+r7PziOIhyxmQOzsazFCy76yYDOS/Z80O/7cuny1KMfqDQa2T0jLaL8EreTU7EMZG5pUuqBKgzHA==",
|
||||
"requires": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"configstore": "^5.0.0",
|
||||
"gaxios": "^2.0.0",
|
||||
"google-auth-library": "^5.0.0",
|
||||
"pumpify": "^2.0.0",
|
||||
"stream-events": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"configstore": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
|
||||
"integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
|
||||
"requires": {
|
||||
"dot-prop": "^5.2.0",
|
||||
"graceful-fs": "^4.1.2",
|
||||
"make-dir": "^3.0.0",
|
||||
"unique-string": "^2.0.0",
|
||||
"write-file-atomic": "^3.0.0",
|
||||
"xdg-basedir": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="
|
||||
},
|
||||
"dot-prop": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
|
||||
"integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
|
||||
"requires": {
|
||||
"is-obj": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-obj": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz",
|
||||
"integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==",
|
||||
"requires": {
|
||||
"semver": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
},
|
||||
"unique-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
||||
"integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
|
||||
"requires": {
|
||||
"crypto-random-string": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
|
||||
"integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
|
||||
"requires": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-typedarray": "^1.0.0",
|
||||
"signal-exit": "^3.0.2",
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"xdg-basedir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
|
||||
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
@@ -2007,6 +2353,45 @@
|
||||
"ini": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"google-auth-library": {
|
||||
"version": "5.10.1",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz",
|
||||
"integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==",
|
||||
"requires": {
|
||||
"arrify": "^2.0.0",
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"fast-text-encoding": "^1.0.0",
|
||||
"gaxios": "^2.1.0",
|
||||
"gcp-metadata": "^3.4.0",
|
||||
"gtoken": "^4.1.0",
|
||||
"jws": "^4.0.0",
|
||||
"lru-cache": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"requires": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"google-p12-pem": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz",
|
||||
"integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==",
|
||||
"requires": {
|
||||
"node-forge": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
||||
@@ -2039,6 +2424,24 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
|
||||
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
|
||||
},
|
||||
"gtoken": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz",
|
||||
"integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==",
|
||||
"requires": {
|
||||
"gaxios": "^2.1.0",
|
||||
"google-p12-pem": "^2.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"mime": "^2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mime": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
|
||||
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
@@ -2090,6 +2493,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"hash-stream-validation": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz",
|
||||
"integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==",
|
||||
"requires": {
|
||||
"through2": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"through2": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
||||
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
|
||||
"requires": {
|
||||
"readable-stream": "~2.3.6",
|
||||
"xtend": "~4.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
@@ -2137,6 +2581,31 @@
|
||||
"statuses": ">= 1.4.0 < 2"
|
||||
}
|
||||
},
|
||||
"http-proxy-agent": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
|
||||
"integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
|
||||
"requires": {
|
||||
"@tootallnate/once": "1",
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
@@ -2147,6 +2616,30 @@
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
|
||||
@@ -2175,8 +2668,7 @@
|
||||
"imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
|
||||
"dev": true
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
|
||||
},
|
||||
"inflection": {
|
||||
"version": "1.12.0",
|
||||
@@ -2470,6 +2962,14 @@
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
|
||||
},
|
||||
"json-bigint": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz",
|
||||
"integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=",
|
||||
"requires": {
|
||||
"bignumber.js": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
@@ -2504,6 +3004,25 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"jwa": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||
"requires": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"requires": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||
@@ -2791,6 +3310,11 @@
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
|
||||
"integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA=="
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz",
|
||||
"integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ=="
|
||||
},
|
||||
"node-schedule": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.2.tgz",
|
||||
@@ -2877,6 +3401,11 @@
|
||||
"resolved": "https://registry.npmjs.org/obj-extend/-/obj-extend-0.1.0.tgz",
|
||||
"integrity": "sha1-u0SKR3X7les0p4H5CLusLfI9u1s="
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"object-copy": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
|
||||
@@ -2952,6 +3481,14 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
|
||||
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
|
||||
"requires": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
@@ -3208,8 +3745,7 @@
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||
},
|
||||
"proto-list": {
|
||||
"version": "1.2.4",
|
||||
@@ -3250,6 +3786,29 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"pumpify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
|
||||
"integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
|
||||
"requires": {
|
||||
"duplexify": "^4.1.1",
|
||||
"inherits": "^2.0.3",
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"duplexify": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz",
|
||||
"integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.4.1",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stream-shift": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -3430,6 +3989,18 @@
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
@@ -3470,6 +4041,30 @@
|
||||
"any-promise": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"retry-request": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz",
|
||||
"integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"through2": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
@@ -3662,6 +4257,11 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"snakeize": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz",
|
||||
"integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
@@ -3858,6 +4458,19 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
|
||||
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
|
||||
},
|
||||
"stream-events": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
||||
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
|
||||
"requires": {
|
||||
"stubs": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"stream-shift": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
|
||||
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
@@ -3904,6 +4517,11 @@
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||
"dev": true
|
||||
},
|
||||
"stubs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@@ -3912,6 +4530,18 @@
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"teeny-request": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.2.tgz",
|
||||
"integrity": "sha512-B6fxA0fSnY/bul06NggdN1nywtr5U5Uvt96pHfTi8pi4MNe6++VUWcAAFBrcMeha94s+gULwA5WvagoSZ+AcYg==",
|
||||
"requires": {
|
||||
"http-proxy-agent": "^4.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"node-fetch": "^2.2.0",
|
||||
"stream-events": "^1.0.5",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"term-size": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
|
||||
@@ -3960,6 +4590,14 @@
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
|
||||
},
|
||||
"through2": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
|
||||
"integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
|
||||
"requires": {
|
||||
"readable-stream": "2 || 3"
|
||||
}
|
||||
},
|
||||
"timed-out": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
|
||||
@@ -4085,6 +4723,19 @@
|
||||
"mime-types": "~2.1.18"
|
||||
}
|
||||
},
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||
"requires": {
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"umzug": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.2.0.tgz",
|
||||
@@ -4287,6 +4938,11 @@
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
},
|
||||
"validate.js": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/validate.js/-/validate.js-0.13.1.tgz",
|
||||
"integrity": "sha512-PnFM3xiZ+kYmLyTiMgTYmU7ZHkjBZz2/+F0DaALc/uUtVzdCt1wAosvYJ5hFQi/hz8O4zb52FQhHZRC+uVkJ+g=="
|
||||
},
|
||||
"validator": {
|
||||
"version": "10.11.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz",
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
"start": "node ./index.js",
|
||||
"start-mon": "nodemon ./index.js",
|
||||
"migrate": "cd app && npx sequelize db:migrate",
|
||||
"seed": "cd app && npx sequelize db:seed:all",
|
||||
"setup": "docker build -t marketalerts . && docker run -e POSTGRES_USER=docker -e POSTGRES_PASSWORD=docker -e POSTGRES_DB=marketalerts --name pg_marketalerts -d -p 5432:5432 marketalerts && sleep 10 && npm run migrate",
|
||||
"docker-start": "docker start pg_marketalerts",
|
||||
"docker-stop": "docker stop pg_marketalerts",
|
||||
"crawl": "cd app/crawler && node npmCrawl.js",
|
||||
"daily-notify": "cd app/npmScripts && node npmDailyNotify.js",
|
||||
"checkup-notify": "cd app/npmScripts && node npmCheckUpNotify.js",
|
||||
"test-search": "cd test && node searchTest.js",
|
||||
"test-olx-scraper": "cd test && node olxScrapeTest.js",
|
||||
"test-rental-scraper": "cd test && node rentalScrapeTest.js"
|
||||
@@ -28,17 +30,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"2checkout-node": "0.0.1",
|
||||
"@google-cloud/storage": "^4.5.0",
|
||||
"@sendgrid/mail": "^6.3.1",
|
||||
"aws-sdk": "^2.422.0",
|
||||
"bluebird": "^3.5.5",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^7.0.0",
|
||||
"ejs": "^2.6.1",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"express": "^4.16.4",
|
||||
"express-ejs-layouts": "^2.5.0",
|
||||
"express-layout": "^0.1.0",
|
||||
"form-data": "^3.0.0",
|
||||
"html-to-text": "^5.1.1",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.26",
|
||||
@@ -48,7 +53,8 @@
|
||||
"prettier": "^1.19.1",
|
||||
"react-step-wizard": "^5.1.0",
|
||||
"sequelize": "^5.18.4",
|
||||
"sequelize-cli": "^5.5.0"
|
||||
"sequelize-cli": "^5.5.0",
|
||||
"validate.js": "^0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.19.0"
|
||||
|
||||
@@ -13,5 +13,5 @@ if (urlToScrape) {
|
||||
})();
|
||||
} else {
|
||||
console.log("No URL to scrape. Use like this : ");
|
||||
console.log("npm run test-olx-scraper -- URL_TO_SCRAPE");
|
||||
console.log("npm run test-rental-scraper -- URL_TO_SCRAPE");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user