Compare commits

..

15 Commits

Author SHA1 Message Date
Senad Uka
f53d60ab0f Untested version 2019-03-20 05:59:23 +01:00
Senad Uka
6821054494 Added aws sdk to node modules 2019-03-18 05:11:47 +01:00
Senad Uka
cb34890583 Connect to heroku's mysql 2019-03-15 10:31:56 +01:00
Senad Uka
2669aaa9eb Now serving whole app 2019-03-14 16:50:25 +01:00
Senad Uka
13a9886292 Fix the path 2019-03-14 05:57:17 +01:00
Senad Uka
a6df827cd1 Updated Procfile 2019-03-14 05:53:02 +01:00
Senad Uka
b67b15c4b9 Package version bump 2019-03-14 05:45:58 +01:00
Senad Uka
a4ed76e29b Updated to newest node, refactored for heroku 2019-03-14 05:41:06 +01:00
Senad Uka
7fc24add1f Package json 2019-03-13 05:43:48 +01:00
Senad Uka
b1a08a7a57 Started porting to heroku 2019-03-12 05:12:02 +01:00
=
1f7063f94e Assume email when creating notifications 2019-03-08 07:33:19 -08:00
=
8a1e406f43 Don't show actual results just the counts 2019-03-07 12:49:19 -08:00
Edin
01b864d75b Merge branch 'remove-payment-step' into 'master'
Remove payment step

See merge request saburly/marketalarm/web!1
2019-03-06 19:32:34 +00:00
=
b7cb61b53b Don't contact TCO api 2019-03-06 11:31:10 -08:00
=
a957293029 Removed card info inputs
Left to do:
  - Remove TCO integration calls
  - Actually save notification
  - Display a nice message
2019-03-05 09:31:04 -08:00
55 changed files with 1450 additions and 3862 deletions

1
.gitignore vendored Normal file
View File

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

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: node ./index.js

1
backend/context.json Normal file
View File

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

View File

@@ -1,8 +1,4 @@
const Sequelize = require("sequelize"); const Sequelize = require("sequelize");
const sequelize = new Sequelize("sql7276322", "sql7276322", "RS53ihYlg9", { const sequelize = new Sequelize(process.env.JAWSDB_URL);
host: "sql7.freemysqlhosting.net",
dialect: "mysql",
operatorsAliases: false
});
module.exports = sequelize; module.exports = sequelize;

1
backend/deploy.env Normal file
View File

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

5
backend/event.json Normal file
View File

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

View File

@@ -0,0 +1,36 @@
{
"EventSourceMappings": [
{
"EventSourceArn": "your event source arn",
"StartingPosition": "LATEST",
"BatchSize": 100,
"Enabled": true
}
],
"ScheduleEvents": [
{
"ScheduleName": "node-lambda-test-schedule",
"ScheduleState": "ENABLED",
"ScheduleExpression": "rate(1 hour)",
"Input":
{
"key1": "value",
"key2": "value"
}
}
],
"S3Events": [{
"Bucket": "BUCKET_NAME",
"Events": [
"s3:ObjectCreated:*"
],
"Filter": {
"Key": {
"FilterRules": [{
"Name": "prefix",
"Value": "STRING_VALUE"
}]
}
}
}]
}

1168
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ehvan Gradanin",
"license": "ISC",
"dependencies": {
"2checkout-node": "0.0.1",
"@sendgrid/mail": "^6.3.1",
"cheerio": "^1.0.0-rc.2",
"express": "^4.16.4",
"mysql2": "^1.6.4",
"node-fetch": "^2.3.0",
"sequelize": "^4.42.0"
}
}

View File

@@ -1,10 +1,8 @@
const scrapTheItems = require("./scraptheitems"); const scrapTheItems = require("./scraptheitems");
const convertToDate = require("./convertToDate"); const convertToDate = require("./convertToDate");
const sgMail = require("@sendgrid/mail"); const AWS = require('aws-sdk');
// should be process.env.SENDGRID_API_KEY AWS.config.update({region: 'eu-central-1'});
sgMail.setApiKey(
"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"
);
async function sendNotification(marketAlert) { async function sendNotification(marketAlert) {
const { id, email, olx_url, last_date } = marketAlert; const { id, email, olx_url, last_date } = marketAlert;
@@ -18,16 +16,43 @@ async function sendNotification(marketAlert) {
(mes, item) => mes + `<strong>${item.url} i ${item.price}</strong>`, (mes, item) => mes + `<strong>${item.url} i ${item.price}</strong>`,
"" ""
); );
const msg = {
to: email, // Create sendEmail params
from: "test@example.com", var params = {
subject: "Market Alert", Destination: { /* required */
text: "New items on olx", CcAddresses: [
html: message ],
ToAddresses: [
email
]
},
Message: { /* required */
Body: { /* required */
Html: {
Charset: "UTF-8",
Data: message
},
Text: {
Charset: "UTF-8",
Data: message // TODO: convert to text
}
},
Subject: {
Charset: 'UTF-8',
Data: 'Javimi alert'
}
},
Source: 'info@saburly.com', /* required */
ReplyToAddresses: [
'info@saburly.com',
],
}; };
if (message) { if (message) {
await sgMail.send(msg); const sendPromise = new AWS.SES({apiVersion: '2010-12-01'}).sendEmail(params).promise();
await sendPromise;
return { id, date: String(convertToDate(lastDate)) }; return { id, date: String(convertToDate(lastDate)) };
} }
} }
module.exports = sendNotification; module.exports = sendNotification;

View File

@@ -20,6 +20,10 @@ const appStyle = theme => ({
zIndex: "1", zIndex: "1",
backgroundColor: "#272727", backgroundColor: "#272727",
backgroundImage: "linear-gradient(180deg,#272727, #21525f)" backgroundImage: "linear-gradient(180deg,#272727, #21525f)"
},
itemsCountTitle: {
textAlign: 'center',
color: 'white'
} }
}); });

View File

@@ -2,19 +2,19 @@ import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { ITEMS_CHANGED, USER_DATA_CHANGED } from "constants/actionTypes"; import { ITEMS_CHANGED, USER_DATA_CHANGED } from "../constants/actionTypes";
import { areObjectEqual } from "utils/helpers"; import { areObjectEqual } from "../utils/helpers";
import { createOlxLink } from "utils/createOlxLink"; import { createOlxLink } from "../utils/createOlxLink";
import axios from "axios"; import axios from "axios";
import image from "assets/img/sidebar-1.jpg"; import image from "../assets/img/sidebar-1.jpg";
import logo from "assets/img/reactlogo.png"; import logo from "../assets/img/reactlogo.png";
import Sidebar from "components/Sidebar.js"; import Sidebar from "../components/Sidebar.js";
import ItemsContainer from "./items/itemscontainer/ItemsContainer"; import ItemsContainer from "./items/itemscontainer/ItemsContainer";
import NotificationModal from "./NotificationModal"; import NotificationModal from "./NotificationModal";
import dashboardStyle from "assets/dashboardStyle.js"; import dashboardStyle from "../assets/dashboardStyle.js";
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
@@ -84,9 +84,8 @@ class App extends React.Component {
return ( return (
<div className={classes.wrapper}> <div className={classes.wrapper}>
<Sidebar logoText={"Market Alarm"} logo={logo} image={image} /> <Sidebar logoText={"Market Alarm"} logo={logo} image={image} />
<div className={classes.mainPanel}> <div className={classes.mainPanel}>
<ItemsContainer /> {items.length && <h3 className={classes.itemsCountTitle}>Pronađeno {items.length} nekretnina. Napravite notifikaciju i primite vise detalja na vas emailu adresu.</h3>}
{items.length ? <NotificationModal /> : null} {items.length ? <NotificationModal /> : null}
</div> </div>
</div> </div>

View File

@@ -5,8 +5,8 @@ import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions"; import DialogActions from "@material-ui/core/DialogActions";
import Slide from "@material-ui/core/Slide"; import Slide from "@material-ui/core/Slide";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import { notificationmodalwrapper } from "utils/notificationmodalwrapper"; import { notificationmodalwrapper } from "../utils/notificationmodalwrapper";
import modalStyle from "assets/modalStyle.js"; import modalStyle from "../assets/modalStyle.js";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import Close from "@material-ui/icons/Close"; import Close from "@material-ui/icons/Close";
@@ -62,7 +62,7 @@ class NotificationModal extends React.Component {
}; };
successCallback = data => { successCallback = data => {
token = data.response.token.token; //token = data.response.token.token;
const { const {
userdata: { email, last_date, olx_url } userdata: { email, last_date, olx_url }
} = this.props; } = this.props;
@@ -73,18 +73,10 @@ class NotificationModal extends React.Component {
last_date, last_date,
olx_url olx_url
}) })
.then(response => .then(response => {
axios this.handleClose();
.post("/payforalert", { alert("Market Alert Created");
email, })
token
})
.then(response => {
this.handleClose();
alert("Market Alert Created");
})
.catch(error => console.log(error))
)
.catch(error => console.log(error)); .catch(error => console.log(error));
}; };
@@ -114,7 +106,8 @@ class NotificationModal extends React.Component {
}; };
handleSaveMarketAlert = () => { handleSaveMarketAlert = () => {
this.tokenRequest(); this.successCallback();
//this.tokenRequest();
}; };
render() { render() {
@@ -161,82 +154,22 @@ class NotificationModal extends React.Component {
id="classic-modal-slide-description" id="classic-modal-slide-description"
className={classes.modalBody} className={classes.modalBody}
> >
<FormControlLabel <div>
className={classes.whiteText} <Input
control={ className={classes.inputStyle}
<Checkbox placeholder="Email"
className={classes.checkBoxStyle} inputProps={{
checked={this.isChecked("emailChecked")} "aria-label": "Email"
type={"checkbox"} }}
value={""} type="email"
onChange={() => this.optionChange("emailChecked")} onChange={this.handleEmail}
/> />
} <Input
label={<Typography style={{ color: "white" }}>Email</Typography>} className={classes.inputStyle}
/> type="hidden"
value={token}
{this.isChecked("emailChecked") ? ( />
<div> </div>
<Input
className={classes.inputStyle}
placeholder="Email"
inputProps={{
"aria-label": "Email"
}}
type="email"
onChange={this.handleEmail}
/>
<Input
className={classes.inputStyle}
type="hidden"
value={token}
/>
<Input
className={classes.inputStyle}
placeholder="Card Number"
inputProps={{
"aria-label": "Card Number"
}}
required
autoComplete="off"
type="number"
onChange={e => this.handleInput(e, "ccNo")}
/>
<Input
className={classes.inputStyle}
placeholder="Expiration Year"
inputProps={{
"aria-label": "Expiration Year"
}}
required
autoComplete="off"
type="number"
onChange={e => this.handleInput(e, "expYear")}
/>
<Input
className={classes.inputStyle}
placeholder="Expiration Month"
inputProps={{
"aria-label": "Expiration Month"
}}
required
autoComplete="off"
type="number"
onChange={e => this.handleInput(e, "expMonth")}
/>
<Input
className={classes.inputStyle}
placeholder="CVV"
inputProps={{
"aria-label": "CVV"
}}
required
autoComplete="off"
type="number"
onChange={e => this.handleInput(e, "cvv")}
/>
</div>
) : null}
</DialogContent> </DialogContent>
<DialogActions className={classes.modalFooter}> <DialogActions className={classes.modalFooter}>
{validEmail ? ( {validEmail ? (

View File

@@ -12,16 +12,15 @@ import ExpandMore from "@material-ui/icons/ExpandMore";
import Collapse from "@material-ui/core/Collapse"; import Collapse from "@material-ui/core/Collapse";
import StarBorder from "@material-ui/icons/StarBorder"; import StarBorder from "@material-ui/icons/StarBorder";
import sidebarStyle from "assets/sidebarStyle.js"; import sidebarStyle from "../assets/sidebarStyle.js";
import CollapseWrapperStyled from "components/widgets/CollapseWrapperStyled"; import CollapseWrapperStyled from "../components/widgets/CollapseWrapperStyled";
import DeepCategoryWrapper from "./widgets/DeepCategoryWrapper"; import DeepCategoryWrapper from "./widgets/DeepCategoryWrapper";
import { hoc } from "utils/helpers"; import { hoc } from "../utils/helpers";
import * as Vozila from "./categories/Vozila";
import * as Nekretnine from "./categories/Nekretnine"; import * as Nekretnine from "./categories/Nekretnine";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { CATEGORY_SELECT } from "constants/actionTypes"; import { CATEGORY_SELECT } from "../constants/actionTypes";
const options = [ const options = [
{ value: "Nekretnine", label: "Nekretnine" } { value: "Nekretnine", label: "Nekretnine" }

View File

@@ -1,20 +0,0 @@
import React from "react";
import Automobili from "../subcategories/vozila/Automobili";
import Motocikli from "../subcategories/vozila/Motocikli";
const options = [
{ value: 18, label: "Automobili" },
{ value: 21, label: "Motocikli" }
];
const depth = 0;
const childrenComponents = {
18: <Automobili />,
21: <Motocikli />
};
export const properties = {
options,
depth,
childrenComponents
};

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import * as Filters from "./AllFiltersDefined"; import * as Filters from "./AllFiltersDefined";
import CheckboxAndRadioWrapper from "components/widgets/CheckboxAndRadioWrapper"; import CheckboxAndRadioWrapper from "../../widgets/CheckboxAndRadioWrapper";
import SelectDisplayCheckboxWrapper from "components/widgets/SelectDisplayCheckboxWrapper"; import SelectDisplayCheckboxWrapper from "../../widgets/SelectDisplayCheckboxWrapper";
import RangeWrapper from "components/widgets/RangeWrapper"; import RangeWrapper from "../../widgets/RangeWrapper";
import DropDownWrapper from "components/widgets/DropDownWrapper"; import DropDownWrapper from "../../widgets/DropDownWrapper";
const Kanton = ( const Kanton = (
<DropDownWrapper componentName="Lokacija" {...Filters.lokacijaOptions} /> <DropDownWrapper componentName="Lokacija" {...Filters.lokacijaOptions} />

View File

@@ -1,4 +1,4 @@
import { WrapAll } from "components/widgets/CollapseWrapperAll"; import { WrapAll } from "../../widgets/CollapseWrapperAll";
import { import {
Kanton, Kanton,
Grad, Grad,
@@ -7,7 +7,7 @@ import {
Vrsta, Vrsta,
Kvadratura, Kvadratura,
DodatnoZaKucu DodatnoZaKucu
} from "components/filters/NekretnineFilter/index"; } from "../../filters/NekretnineFilter/index";
const KuceFilters = [ const KuceFilters = [
Kanton, Kanton,

View File

@@ -1,4 +1,4 @@
import { WrapAll } from "components/widgets/CollapseWrapperAll"; import { WrapAll } from "../../widgets/CollapseWrapperAll";
import { import {
Kanton, Kanton,
Grad, Grad,
@@ -7,7 +7,7 @@ import {
Vrsta, Vrsta,
Kvadratura, Kvadratura,
DodatnoZaStan DodatnoZaStan
} from "components/filters/NekretnineFilter/index"; } from "../../filters/NekretnineFilter/index";
const StanFilters = [ const StanFilters = [
Kanton, Kanton,

View File

@@ -1,29 +0,0 @@
import { WrapAll } from "components/widgets/CollapseWrapperAll";
import {
Proizvodac,
Kanton,
Grad,
Cijena,
Stanje,
Vrsta,
Godiste,
KilometraMin,
KilometraMax,
Gorivo
} from "components/filters/VozilaFilter/index";
const AutomobiliFilters = [
Proizvodac,
Kanton,
Grad,
Cijena,
Stanje,
Vrsta,
Godiste,
KilometraMin,
KilometraMax,
Gorivo
];
const Automobili = () => WrapAll(AutomobiliFilters);
export default Automobili;

View File

@@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import { optionchangewrapper } from "utils/optionchangewrapper"; import { optionchangewrapper } from "../../utils/optionchangewrapper";
import Checkbox from "@material-ui/core/Checkbox"; import Checkbox from "@material-ui/core/Checkbox";
import Radio from "@material-ui/core/Radio"; import Radio from "@material-ui/core/Radio";
import "assets/checkboxAndRadioStyle.css"; import "../../assets/checkboxAndRadioStyle.css";
class CheckboxAndRadioWrapper extends React.Component { class CheckboxAndRadioWrapper extends React.Component {
optionChange = (option, optionName, type) => { optionChange = (option, optionName, type) => {

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { OPTION_EXPAND_CHANGE } from "constants/actionTypes"; import { OPTION_EXPAND_CHANGE } from "../../constants/actionTypes";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import sidebarStyle from "assets/sidebarStyle.js"; import sidebarStyle from "../../assets/sidebarStyle.js";
import Collapse from "@material-ui/core/Collapse"; import Collapse from "@material-ui/core/Collapse";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import { subcategorywrapper } from "utils/subcategorywrapper"; import { subcategorywrapper } from "../../utils/subcategorywrapper";
import { hoc } from "utils/helpers"; import { hoc } from "../../utils/helpers";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import sidebarStyle from "assets/sidebarStyle.js"; import sidebarStyle from "../../assets/sidebarStyle.js";
import List from "@material-ui/core/List"; import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";

View File

@@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import { optionchangewrapper } from "utils/optionchangewrapper"; import { optionchangewrapper } from "../../utils/optionchangewrapper";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import List from "@material-ui/core/List"; import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
import sidebarStyle from "assets/sidebarStyle.js"; import sidebarStyle from "../../assets/sidebarStyle.js";
import StarBorder from "@material-ui/icons/StarBorder"; import StarBorder from "@material-ui/icons/StarBorder";
class DropdownWrapper extends React.Component { class DropdownWrapper extends React.Component {

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Range } from "rc-slider"; import { Range } from "rc-slider";
import { optionchangewrapper } from "utils/optionchangewrapper"; import { optionchangewrapper } from "../../utils/optionchangewrapper";
import "assets/rangeStyle.css"; import "../../assets/rangeStyle.css";
import "rc-slider/assets/index.css"; import "rc-slider/assets/index.css";
import Input from "@material-ui/core/Input"; import Input from "@material-ui/core/Input";

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import CheckboxAndRadioWrapper from "components/widgets/CheckboxAndRadioWrapper"; import CheckboxAndRadioWrapper from "../../components/widgets/CheckboxAndRadioWrapper";
import { optionchangewrapper } from "utils/optionchangewrapper"; import { optionchangewrapper } from "../../utils/optionchangewrapper";
class SelectDisplayCheckboxWrapper extends React.Component { class SelectDisplayCheckboxWrapper extends React.Component {
render() { render() {

View File

@@ -1,4 +1,4 @@
import { CATEGORY_SELECT } from "constants/actionTypes"; import { CATEGORY_SELECT } from "../constants/actionTypes";
export default (state = null, action) => { export default (state = null, action) => {
switch (action.type) { switch (action.type) {

View File

@@ -3,7 +3,7 @@ import {
SUBCATEGORY_SELECT, SUBCATEGORY_SELECT,
CATEGORY_SELECT, CATEGORY_SELECT,
OPTION_CHANGE OPTION_CHANGE
} from "constants/actionTypes"; } from "../constants/actionTypes";
export default (state = [], action) => { export default (state = [], action) => {
switch (action.type) { switch (action.type) {

View File

@@ -1,4 +1,4 @@
import { MODAL_CLOSE, MODAL_OPEN } from "constants/actionTypes"; import { MODAL_CLOSE, MODAL_OPEN } from "../constants/actionTypes";
export default (state = false, action) => { export default (state = false, action) => {
switch (action.type) { switch (action.type) {

View File

@@ -2,7 +2,7 @@ import {
SUBCATEGORY_SELECT, SUBCATEGORY_SELECT,
CATEGORY_SELECT, CATEGORY_SELECT,
OPTION_CHANGE OPTION_CHANGE
} from "constants/actionTypes"; } from "../constants/actionTypes";
export default (state = {}, action) => { export default (state = {}, action) => {
switch (action.type) { switch (action.type) {

View File

@@ -1,4 +1,4 @@
import { SUBCATEGORY_SELECT, CATEGORY_SELECT } from "constants/actionTypes"; import { SUBCATEGORY_SELECT, CATEGORY_SELECT } from "../constants/actionTypes";
export default (state = {}, action) => { export default (state = {}, action) => {
switch (action.type) { switch (action.type) {

View File

@@ -2,7 +2,7 @@ import {
OPTION_EXPAND_CHANGE, OPTION_EXPAND_CHANGE,
CATEGORY_SELECT, CATEGORY_SELECT,
SUBCATEGORY_SELECT SUBCATEGORY_SELECT
} from "constants/actionTypes"; } from "../constants/actionTypes";
export default (state = {}, action) => { export default (state = {}, action) => {
switch (action.type) { switch (action.type) {

View File

@@ -1,4 +1,4 @@
import { USER_DATA_CHANGED } from "constants/actionTypes"; import { USER_DATA_CHANGED } from "../constants/actionTypes";
export default (state = {}, action) => { export default (state = {}, action) => {
switch (action.type) { switch (action.type) {

View File

@@ -3,7 +3,7 @@ import {
MODAL_CLOSE, MODAL_CLOSE,
MODAL_OPEN, MODAL_OPEN,
USER_DATA_CHANGED USER_DATA_CHANGED
} from "constants/actionTypes"; } from "../constants/actionTypes";
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {

View File

@@ -1,5 +1,5 @@
import { connect } from "react-redux"; import { connect } from "react-redux";
import { OPTION_CHANGE } from "constants/actionTypes"; import { OPTION_CHANGE } from "../constants/actionTypes";
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {

View File

@@ -1,5 +1,5 @@
import { connect } from "react-redux"; import { connect } from "react-redux";
import { SUBCATEGORY_SELECT } from "constants/actionTypes"; import { SUBCATEGORY_SELECT } from "../constants/actionTypes";
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {

View File

@@ -1,10 +1,10 @@
let express = require("express"); let express = require("express");
const path = require("path"); const path = require("path");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
const MarketAlert = require("./MarketAlert"); const MarketAlert = require("./backend/MarketAlert");
const sendNotification = require("./utils/sendnotification"); const sendNotification = require("./backend/utils/sendnotification");
const scrapTheItems = require("./utils/scraptheitems"); const scrapTheItems = require("./backend/utils/scraptheitems");
const sequelize = require("./db.js"); const sequelize = require("./backend/db.js");
const Twocheckout = require("2checkout-node"); const Twocheckout = require("2checkout-node");
const app = express(); const app = express();
@@ -49,15 +49,17 @@ app.get("/items/:url", async (req, res) => {
app.post("/marketalerts", function(req, res) { app.post("/marketalerts", function(req, res) {
const { email, last_date, olx_url } = req.body; const { email, last_date, olx_url } = req.body;
console.log(email, last_date, olx_url); console.log(email, last_date, olx_url);
res.json({ message: "Market Alert Created!" }); sequelize.sync().then(() => {
// sequelize.sync().then(() => MarketAlert.create({
// MarketAlert.create({ olx_url,
// olx_url, last_date,
// last_date, email
// email })
// }) .then(() => {
// ); res.json({ message: "Market Alert Created!" });
// res.json({ message: "Market Alert Created!" }); })
.catch(e => console.error(e));
});
}); });
app.post("/payforalert", function(request, response) { app.post("/payforalert", function(request, response) {
@@ -93,4 +95,11 @@ app.post("/payforalert", function(request, response) {
}); });
}); });
app.use(express.static(path.join(__dirname, 'frontend-react/build')));
// Anything that doesn't match the above, send back index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname + '/frontend-react/build/index.html'))
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`)); app.listen(port, () => console.log(`Example app listening on port ${port}!`));

6
ma-api/.gitignore vendored
View File

@@ -1,6 +0,0 @@
# package directories
node_modules
jspm_packages
# Serverless directories
.serverless

View File

@@ -1,6 +0,0 @@
# How to deploy automatically:
1. set up aws cli with aws configure
2. cd ma-api
3. serverless deploy

View File

@@ -1,83 +0,0 @@
'use strict';
const path = require("path");
const bodyParser = require("body-parser");
const MarketAlert = require("./lib/MarketAlert");
const sendNotification = require("./lib/sendnotification");
const scrapTheItems = require("./lib/scraptheitems");
const sequelize = require("./lib/db.js");
const Twocheckout = require("2checkout-node");
module.exports.sendnotification = async (event, context) => {
let marketAlerts = await MarketAlert.findAll();
let lastDateUpdate = await Promise.all(
marketAlerts
.map(marketAlert => {
const { id, email, olx_url, last_date } = marketAlert.dataValues;
return { id, email, olx_url, last_date };
})
.map(sendNotification)
);
lastDateUpdate = lastDateUpdate.filter(Boolean(dateUpdate));
lastDateUpdate.length &&
lastDateUpdate.forEach(dateUpdate =>
MarketAlert.update(
{ last_date: dateUpdate.date },
{ where: { id: dateUpdate.id } }
)
);
return {
statusCode: 200,
body: JSON.stringify({
message: 'Notifications sent',
input: event,
}),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
module.exports.getitems = async (event, context) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Get Items',
input: event,
}),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
module.exports.marketalerts = async (event, context) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Market alerts',
input: event,
}),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
module.exports.payforalert = async (event, context) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Pay for alert',
input: event,
}),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};

View File

@@ -1,17 +0,0 @@
const Sequelize = require("sequelize");
const sequelize = require("./db.js");
const MarketAlert = sequelize.define("market_alert", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
olx_url: Sequelize.STRING,
last_date: Sequelize.STRING,
email: {
type: Sequelize.STRING,
allowNull: false
}
});
module.exports = MarketAlert;

View File

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

View File

@@ -1,23 +0,0 @@
{
"development": {
"username": "javimistaging",
"password": "10MinutaSvaki*Dan",
"database": "database_development",
"host": "javimi-staging.cluster-c52xdqogwrl2.eu-central-1.rds.amazonaws.com",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}

View File

@@ -1,13 +0,0 @@
function convertToDate(date) {
const [dan, mjesec, godina] = date
.split(". u ")[0]
.split(".")
.map(el => Number(el));
const [sati, minute] = date
.split(". u ")[1]
.split(":")
.map(el => Number(el));
return new Date(godina, mjesec, dan, sati, minute);
}
module.exports = convertToDate;

View File

@@ -1,8 +0,0 @@
const Sequelize = require("sequelize");
const sequelize = new Sequelize("sql7276322", "sql7276322@localhost", "RS53ihYlg9", {
host: "sql7.freemysqlhosting.net",
dialect: "mysql",
operatorsAliases: false
});
module.exports = sequelize;

View File

@@ -1,96 +0,0 @@
let express = require("express");
const path = require("path");
const bodyParser = require("body-parser");
const MarketAlert = require("./MarketAlert");
const sendNotification = require("./utils/sendnotification");
const scrapTheItems = require("./utils/scraptheitems");
const sequelize = require("./db.js");
const Twocheckout = require("2checkout-node");
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const port = process.env.PORT || 5000;
app.get("/sendnotifications", async function(req, res) {
let marketAlerts = await MarketAlert.findAll();
let lastDateUpdate = await Promise.all(
marketAlerts
.map(marketAlert => {
const { id, email, olx_url, last_date } = marketAlert.dataValues;
return { id, email, olx_url, last_date };
})
.map(sendNotification)
);
lastDateUpdate = lastDateUpdate.filter(Boolean(dateUpdate));
lastDateUpdate.length &&
lastDateUpdate.forEach(dateUpdate =>
MarketAlert.update(
{ last_date: dateUpdate.date },
{ where: { id: dateUpdate.id } }
)
);
});
app.get("/items/:url", async (req, res) => {
let url =
"https://www.olx.ba/pretraga?" +
req.params.url +
"&sort_order=desc&sort_po=datum";
let appts = await scrapTheItems(url);
res.json({
last_date: appts[0] && appts[0].date,
items: appts
});
});
app.post("/marketalerts", function(req, res) {
const { email, last_date, olx_url } = req.body;
console.log(email, last_date, olx_url);
res.json({ message: "Market Alert Created!" });
// sequelize.sync().then(() =>
// MarketAlert.create({
// olx_url,
// last_date,
// email
// })
// );
// res.json({ message: "Market Alert Created!" });
});
app.post("/payforalert", function(request, response) {
let tco = new Twocheckout({
sellerId: "901402692",
privateKey: "A28DCE5F-9292-405C-8161-F84D8BB83AFC",
sandbox: true
});
let params = {
merchantOrderId: "123",
token: request.body.token,
currency: "USD",
total: "2.00",
billingAddr: {
name: "Testing Tester",
addrLine1: "123 Test St",
city: "Sarajevo",
state: "BiH",
zipCode: "71000",
country: "BiH",
email: request.body.email,
phoneNumber: "5555555555"
}
};
tco.checkout.authorize(params, function(error, data) {
if (error) {
response.send(error.message);
} else {
response.send(data.response.responseMsg);
}
});
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`));

View File

@@ -1,27 +0,0 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('MarketAlerts', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
olx_url: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('MarketAlerts');
}
};

View File

@@ -1,37 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

View File

@@ -1,10 +0,0 @@
'use strict';
module.exports = (sequelize, DataTypes) => {
const MarketAlert = sequelize.define('MarketAlert', {
olx_url: DataTypes.STRING
}, {});
MarketAlert.associate = function(models) {
// associations can be defined here
};
return MarketAlert;
};

View File

@@ -1,42 +0,0 @@
let fetch = require("node-fetch");
let cheerio = require("cheerio");
const areThereAnyNewItems = require("./arethereanynewitems");
async function scrapTheItems(url, controlDate, noNewItems = false) {
let items = [];
let response = await fetch(url);
const body = await response.text();
const $ = cheerio.load(body);
$("#rezultatipretrage")
.find(".listitem")
.each(async (index, elem) => {
if (noNewItems) return;
const itemDate = $(elem)
.find(".cijena > .datum > div")
.first()
.attr("data-cijelidatum");
if (controlDate && !areThereAnyNewItems(itemDate, controlDate)) {
noNewItems = true;
return;
}
const id = $(elem)
.find("a")
.first()
.attr("href");
const cijena = $(elem)
.find(".cijena > .datum > span")
.first()
.text();
const image = $(elem)
.find("a > .slika > img")
.first()
.attr("src");
items.push({ url: id, price: cijena, image, date: itemDate });
});
return items;
}
module.exports = scrapTheItems;

View File

@@ -1,33 +0,0 @@
const scrapTheItems = require("./scraptheitems");
const convertToDate = require("./convertToDate");
const sgMail = require("@sendgrid/mail");
// should be process.env.SENDGRID_API_KEY
sgMail.setApiKey(
"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"
);
async function sendNotification(marketAlert) {
const { id, email, olx_url, last_date } = marketAlert;
let url =
"https://www.olx.ba/pretraga?" + olx_url + "&sort_order=desc&sort_po=datum";
let newItems = await scrapTheItems(url);
let lastDate = newItems.length && newItems[0].date;
let message =
newItems.length &&
newItems.reduce(
(mes, item) => mes + `<strong>${item.url} i ${item.price}</strong>`,
""
);
const msg = {
to: email,
from: "test@example.com",
subject: "Market Alert",
text: "New items on olx",
html: message
};
if (message) {
await sgMail.send(msg);
return { id, date: String(convertToDate(lastDate)) };
}
}
module.exports = sendNotification;

1918
ma-api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ehvan Gradanin",
"license": "ISC",
"dependencies": {
"2checkout-node": "0.0.1",
"@sendgrid/mail": "^6.3.1",
"cheerio": "^1.0.0-rc.2",
"express": "^4.16.4",
"mysql2": "^1.6.4",
"node-fetch": "^2.3.0",
"sequelize": "^4.42.0",
"sequelize-cli": "^5.4.0"
}
}

View File

@@ -1,109 +0,0 @@
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
# docs.serverless.com
#
# Happy Coding!
service: ma-api # NOTE: update this with your service name
# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"
provider:
name: aws
runtime: nodejs8.10
# you can overwrite defaults here
# stage: dev
# region: us-east-1
# you can add statements to the Lambda function's IAM Role here
# iamRoleStatements:
# - Effect: "Allow"
# Action:
# - "s3:ListBucket"
# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] }
# - Effect: "Allow"
# Action:
# - "s3:PutObject"
# Resource:
# Fn::Join:
# - ""
# - - "arn:aws:s3:::"
# - "Ref" : "ServerlessDeploymentBucket"
# - "/*"
# you can define service wide environment variables here
# environment:
# variable1: value1
# you can add packaging information here
#package:
# include:
# - include-me.js
# - include-me-dir/**
# exclude:
# - exclude-me.js
# - exclude-me-dir/**
functions:
sendnotification:
handler: handler.sendnotification
events:
- http:
path: notifications/send
method: post
# The following are a few example events you can configure
# NOTE: Please make sure to change your handler code to work with those events
# Check the event documentation for details
# events:
# - http:
# path: users/create
# method: get
# - websocket: $connect
# - s3: ${env:BUCKET}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
# - cloudwatchEvent:
# event:
# source:
# - "aws.ec2"
# detail-type:
# - "EC2 Instance State-change Notification"
# detail:
# state:
# - pending
# - cloudwatchLog: '/aws/lambda/hello'
# - cognitoUserPool:
# pool: MyUserPool
# trigger: PreSignUp
# Define function environment variables here
# environment:
# variable2: value2
# you can add CloudFormation resource templates here
#resources:
# Resources:
# NewResource:
# Type: AWS::S3::Bucket
# Properties:
# BucketName: my-new-bucket
# Outputs:
# NewOutput:
# Description: "Description for the output"
# Value: "Some output value"

1244
package-lock.json generated

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "marketalarm",
"version": "1.0.1",
"description": "Market Alarm",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node ./index.js",
"heroku-postbuild": "cd frontend-react && npm install && npm run build"
},
"repository": {
"type": "git",
"url": "git@gitlab.com:saburly/marketalarm/web.git"
},
"author": "Saburlije",
"license": "ISC",
"engines": {
"node": "11.10.x"
},
"dependencies": {
"2checkout-node": "0.0.1",
"@sendgrid/mail": "^6.3.1",
"aws-sdk": "^2.422.0",
"cheerio": "^1.0.0-rc.2",
"express": "^4.16.4",
"mysql2": "^1.6.4",
"node-fetch": "^2.3.0",
"sequelize": "^4.42.0"
}
}