basic frontend demo with react and redux
This commit is contained in:
71
frontend-react/src/components/App.js
Normal file
71
frontend-react/src/components/App.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import Select from "react-select";
|
||||
import { connect } from "react-redux";
|
||||
import { CATEGORY_SELECT, ITEMS_CHANGED } from "constants/actionTypes";
|
||||
import { hoc } from "utils/hoc";
|
||||
import { createOlxLink } from "utils/createOlxLink";
|
||||
import axios from "axios";
|
||||
|
||||
import Vozila from "./categories/Vozila";
|
||||
import Nekretnine from "./categories/Nekretnine";
|
||||
import ItemsContainer from "./items/itemscontainer/ItemsContainer";
|
||||
|
||||
const options = [
|
||||
{ value: "Vozila", label: "Vozila" },
|
||||
{ value: "Nekretnine", label: "Nekretnine" }
|
||||
];
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
category: state.category,
|
||||
options: state.options,
|
||||
subcategory: state.subcategory,
|
||||
items: state.items
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onCategoryChanged: option => dispatch({ type: CATEGORY_SELECT, option }),
|
||||
onItemsChanged: items => dispatch({ type: ITEMS_CHANGED, items })
|
||||
});
|
||||
|
||||
class App extends React.Component {
|
||||
handleChange = selectedOption => {
|
||||
this.props.onCategoryChanged(selectedOption);
|
||||
};
|
||||
|
||||
getDataFromOlx = () => {
|
||||
const { category, options, subcategory, onItemsChanged } = this.props;
|
||||
let url = createOlxLink(category, subcategory, options);
|
||||
url = encodeURI(url);
|
||||
axios
|
||||
.get(`/api/${url}`)
|
||||
.then(response => onItemsChanged(response.data))
|
||||
.catch(error => console.log(error));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { category } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
value={category}
|
||||
onChange={this.handleChange}
|
||||
options={options}
|
||||
/>
|
||||
{hoc(category && category.value, {
|
||||
Vozila: <Vozila />,
|
||||
Nekretnine: <Nekretnine />
|
||||
})}
|
||||
<button onClick={this.getDataFromOlx}>Get Data from OLX </button>
|
||||
<ItemsContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App);
|
||||
38
frontend-react/src/components/categories/Nekretnine.js
Normal file
38
frontend-react/src/components/categories/Nekretnine.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import Select from "react-select";
|
||||
import { subcategorywrapper } from "utils/subcategorywrapper";
|
||||
import { hoc } from "utils/hoc";
|
||||
|
||||
import Stanovi from "../subcategories/nekretnine/Stanovi";
|
||||
import Kuce from "../subcategories/nekretnine/Kuce";
|
||||
|
||||
const options = [
|
||||
{ value: "Stanovi", label: "Stanovi" },
|
||||
{ value: "Kuce", label: "Kuce" }
|
||||
];
|
||||
|
||||
class Nekretnine extends React.Component {
|
||||
handleChange = selectedOption => {
|
||||
this.props.onSubCategoryChanged(selectedOption);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { subcategory } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
value={subcategory}
|
||||
onChange={this.handleChange}
|
||||
options={options}
|
||||
/>
|
||||
{hoc(subcategory && subcategory.value, {
|
||||
Stanovi: <Stanovi />,
|
||||
Kuce: <Kuce />
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default subcategorywrapper(Nekretnine);
|
||||
37
frontend-react/src/components/categories/Vozila.js
Normal file
37
frontend-react/src/components/categories/Vozila.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import Select from "react-select";
|
||||
import Automobili from "../subcategories/vozila/Automobili";
|
||||
import Motocikli from "../subcategories/vozila/Motocikli";
|
||||
import { subcategorywrapper } from "utils/subcategorywrapper";
|
||||
import { hoc } from "utils/hoc";
|
||||
|
||||
const options = [
|
||||
{ value: "Automobili", label: "Automobili" },
|
||||
{ value: "Motocikli", label: "Motocikli" }
|
||||
];
|
||||
|
||||
class Vozila extends React.Component {
|
||||
handleChange = selectedOption => {
|
||||
this.props.onSubCategoryChanged(selectedOption);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { subcategory } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
value={subcategory}
|
||||
onChange={this.handleChange}
|
||||
options={options}
|
||||
/>
|
||||
{hoc(subcategory && subcategory.value, {
|
||||
Automobili: <Automobili />,
|
||||
Motocikli: <Motocikli />
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default subcategorywrapper(Vozila);
|
||||
@@ -0,0 +1,6 @@
|
||||
export const rangeOptions = {
|
||||
min: 0,
|
||||
max: 100000,
|
||||
defaultValues: [0, 100000],
|
||||
step: 100
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import CheckboxAndRadioWrapper from "components/widgets/CheckboxAndRadioWrapper";
|
||||
|
||||
const elements = [
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Uknjizeno",
|
||||
optionName: "uknjizeno"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Namjesteno",
|
||||
optionName: "namjesteno"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Nedavno adaptirano",
|
||||
optionName: "nedavno_adaptirano"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Garaza",
|
||||
optionName: "garaza"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Balkon",
|
||||
optionName: "balkon"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Voda",
|
||||
optionName: "voda"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Plin",
|
||||
optionName: "plin"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Bazen",
|
||||
optionName: "bazen"
|
||||
}
|
||||
];
|
||||
class KuceFilter extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span>Dodatno za kuce</span>
|
||||
<CheckboxAndRadioWrapper
|
||||
componentName="dodatno-za-kuce"
|
||||
elements={elements}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default KuceFilter;
|
||||
@@ -0,0 +1,8 @@
|
||||
export const lokacijaOptions = {
|
||||
choices: [
|
||||
{ value: "9", label: "Sarajevo" },
|
||||
{ value: "2", label: "Posavski" }
|
||||
],
|
||||
value: "kanton",
|
||||
optionName: "kanton"
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import CheckboxAndRadioWrapper from "components/widgets/CheckboxAndRadioWrapper";
|
||||
|
||||
const elements = [
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Novogradnja",
|
||||
optionName: "novogradnja"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Namjesten",
|
||||
optionName: "namjesten"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Nedavno adaptiran",
|
||||
optionName: "Nedavno_adaptiran"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Uknjizeno",
|
||||
optionName: "uknjizeno"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Lift",
|
||||
optionName: "lift"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Balkon",
|
||||
optionName: "balkon"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Parking",
|
||||
optionName: "parking"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Plin",
|
||||
optionName: "plin"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "kablovska",
|
||||
optionName: "kablovska"
|
||||
}
|
||||
];
|
||||
|
||||
class StanoviFilter extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span>Dodatno za stan</span>
|
||||
<CheckboxAndRadioWrapper
|
||||
componentName="dodatno-za-stan"
|
||||
elements={elements}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default StanoviFilter;
|
||||
@@ -0,0 +1,6 @@
|
||||
export const rangeOptions = {
|
||||
min: 0,
|
||||
max: 1000,
|
||||
defaultValues: [0, new Date().getFullYear()],
|
||||
step: 1
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
export const elements = [
|
||||
{
|
||||
value: "",
|
||||
name: "Sve",
|
||||
type: "radio",
|
||||
optionName: "vrsta"
|
||||
},
|
||||
{
|
||||
value: "samoprodaja",
|
||||
name: "Prodaja",
|
||||
type: "radio",
|
||||
optionName: "vrsta"
|
||||
},
|
||||
{
|
||||
value: "samoizdavanje",
|
||||
name: "Iznajmljivanje",
|
||||
type: "radio",
|
||||
optionName: "vrsta"
|
||||
},
|
||||
{
|
||||
value: "samopotraznja",
|
||||
name: "Potražnja",
|
||||
type: "radio",
|
||||
optionName: "vrsta"
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import * as Vrsta from "./Vrsta";
|
||||
import * as Lokacija from "./Lokacija";
|
||||
import * as Cijena from "./Cijena";
|
||||
import * as Velicina from "./Velicina";
|
||||
import CheckboxAndRadioWrapper from "components/widgets/CheckboxAndRadioWrapper";
|
||||
import RangeWrapper from "components/widgets/RangeWrapper";
|
||||
import SelectWrapper from "components/widgets/SelectWrapper";
|
||||
|
||||
class NekretnineFilter extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<CheckboxAndRadioWrapper
|
||||
componentName="vrsta"
|
||||
elements={Vrsta.elements}
|
||||
/>
|
||||
<SelectWrapper {...Lokacija.lokacijaOptions} />
|
||||
<RangeWrapper {...Cijena.rangeOptions} optionName="cijena" />
|
||||
<RangeWrapper {...Velicina.rangeOptions} optionName="velicina" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default NekretnineFilter;
|
||||
@@ -0,0 +1,6 @@
|
||||
export const rangeOptions = {
|
||||
min: 0,
|
||||
max: 100000,
|
||||
defaultValues: [0, 100000],
|
||||
step: 100
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const rangeOptions = {
|
||||
min: 1960,
|
||||
max: new Date().getFullYear(),
|
||||
defaultValues: [1960, new Date().getFullYear()],
|
||||
step: 1
|
||||
};
|
||||
27
frontend-react/src/components/filters/VozilaFilter/Gorivo.js
Normal file
27
frontend-react/src/components/filters/VozilaFilter/Gorivo.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export const elements = [
|
||||
{
|
||||
name: "Dizel",
|
||||
optionName: "gorivo_select_dizel",
|
||||
type: "checkbox"
|
||||
},
|
||||
{
|
||||
name: "Benzin",
|
||||
optionName: "gorivo_select_benzin",
|
||||
type: "checkbox"
|
||||
},
|
||||
{
|
||||
name: "Plin",
|
||||
optionName: "gorivo_select_plin",
|
||||
type: "checkbox"
|
||||
},
|
||||
{
|
||||
name: "Hibrid",
|
||||
optionName: "gorivo_select_hibrid",
|
||||
type: "checkbox"
|
||||
},
|
||||
{
|
||||
name: "Elektro",
|
||||
optionName: "gorivo_select_elektro",
|
||||
type: "checkbox"
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
export const kilometrazaOptions = {
|
||||
kilometraMin: {
|
||||
choices: [{ value: 5000, label: "5000" }, { value: 10000, label: "10000" }],
|
||||
value: "kilometrazaMin",
|
||||
optionName: "kilometrazaMin"
|
||||
},
|
||||
kilometraMax: {
|
||||
choices: [
|
||||
{ value: 15000, label: "15000" },
|
||||
{ value: 200000, label: "200000" }
|
||||
],
|
||||
value: "kilometrazaMax",
|
||||
optionName: "kilometrazaMax"
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const lokacijaOptions = {
|
||||
choices: [
|
||||
{ value: "9", label: "Sarajevo" },
|
||||
{ value: "2", label: "Posavski" }
|
||||
],
|
||||
value: "kanton",
|
||||
optionName: "kanton"
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export const proizvodacOptions = {
|
||||
choices: [{ value: "1900", label: "Audi" }, { value: "9000", label: "Ford" }],
|
||||
value: "proizvodac",
|
||||
optionName: "proizvodac"
|
||||
};
|
||||
25
frontend-react/src/components/filters/VozilaFilter/Stanje.js
Normal file
25
frontend-react/src/components/filters/VozilaFilter/Stanje.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const elements = [
|
||||
{
|
||||
type: "radio",
|
||||
value: "",
|
||||
name: "Nova i polovna vozila",
|
||||
optionName: "stanje"
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
value: 1,
|
||||
name: "Nova",
|
||||
optionName: "stanje"
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
value: 2,
|
||||
name: "Polovna vozila",
|
||||
optionName: "stanje"
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "Udarena vozila",
|
||||
optionName: "udaren_checkbox"
|
||||
}
|
||||
];
|
||||
35
frontend-react/src/components/filters/VozilaFilter/index.js
Normal file
35
frontend-react/src/components/filters/VozilaFilter/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import * as Stanje from "./Stanje";
|
||||
import * as Proizvodac from "./Proizvodac";
|
||||
import * as Cijena from "./Cijena";
|
||||
import * as Lokacija from "./Lokacija";
|
||||
import * as Godiste from "./Godiste";
|
||||
import * as Kilometraza from "./Kilometraza";
|
||||
import * as Gorivo from "./Gorivo";
|
||||
import CheckboxAndRadioWrapper from "components/widgets/CheckboxAndRadioWrapper";
|
||||
import RangeWrapper from "components/widgets/RangeWrapper";
|
||||
import SelectWrapper from "components/widgets/SelectWrapper";
|
||||
|
||||
class VozilaFilter extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<CheckboxAndRadioWrapper
|
||||
componentName="stanje"
|
||||
elements={Stanje.elements}
|
||||
/>
|
||||
<SelectWrapper {...Proizvodac.proizvodacOptions} />
|
||||
<RangeWrapper {...Cijena.rangeOptions} optionName="cijena" />
|
||||
<SelectWrapper {...Lokacija.lokacijaOptions} />
|
||||
<RangeWrapper {...Godiste.rangeOptions} optionName="godiste" />
|
||||
<SelectWrapper {...Kilometraza.kilometrazaOptions.kilometraMin} />
|
||||
<SelectWrapper {...Kilometraza.kilometrazaOptions.kilometraMax} />
|
||||
<CheckboxAndRadioWrapper
|
||||
componentName="gorivo"
|
||||
elements={Gorivo.elements}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default VozilaFilter;
|
||||
36
frontend-react/src/components/items/itemcard/ItemCard.css
Normal file
36
frontend-react/src/components/items/itemcard/ItemCard.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.item {
|
||||
display: grid;
|
||||
border: solid #877ad2 1px;
|
||||
border-radius: 10px;
|
||||
max-width: 360px;
|
||||
min-height: 300px;
|
||||
grid-template-areas:
|
||||
"image"
|
||||
"title";
|
||||
margin: 3%;
|
||||
background-color: #dbdbd7;
|
||||
}
|
||||
|
||||
.item > a > img {
|
||||
width: 70%;
|
||||
height: 60%;
|
||||
max-height: 200px;
|
||||
grid-area: image;
|
||||
padding: 8% 8% 0 8%;
|
||||
}
|
||||
.item > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.basic-info {
|
||||
grid-area: title;
|
||||
width: 85%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.basic-info > h3 {
|
||||
font-size: 90%;
|
||||
margin: 5%;
|
||||
margin-bottom: 0px;
|
||||
color: #332390;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
20
frontend-react/src/components/items/itemcard/ItemCard.js
Normal file
20
frontend-react/src/components/items/itemcard/ItemCard.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React, { Component } from "react";
|
||||
import "./ItemCard.css";
|
||||
|
||||
class ItemCard extends Component {
|
||||
render() {
|
||||
const { url, price, image } = this.props;
|
||||
return (
|
||||
<div className="item">
|
||||
<a href={url}>
|
||||
<img alt={image} src={image} />
|
||||
<div className="basic-info">
|
||||
<h3>{price}</h3>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ItemCard;
|
||||
@@ -0,0 +1,8 @@
|
||||
.items-list {
|
||||
margin: 5%;
|
||||
margin-top: 8%;
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
grid-template-rows: repeat(2, 320px);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import ItemCard from "../itemcard/ItemCard";
|
||||
import "./ItemsContainer.css";
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
items: state.items
|
||||
};
|
||||
};
|
||||
|
||||
class ItemContainer extends Component {
|
||||
renderItems() {
|
||||
let items = this.props.items;
|
||||
return items.map((item, index) => {
|
||||
return <ItemCard {...item} key={index} />;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="items-list">{this.renderItems()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default connect(mapStateToProps)(ItemContainer);
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import NekretnineFilter from "components/filters/NekretnineFilter/index";
|
||||
import KuceFilter from "components/filters/NekretnineFilter/KuceFilter/index";
|
||||
class Kuce extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span>Kuce</span>
|
||||
<NekretnineFilter />
|
||||
<KuceFilter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Kuce;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import NekretnineFilter from "components/filters/NekretnineFilter/index";
|
||||
import StanoviFilter from "components/filters/NekretnineFilter/StanoviFilter/index";
|
||||
|
||||
class Stanovi extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span>Stanovi</span>
|
||||
<NekretnineFilter />
|
||||
<StanoviFilter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Stanovi;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import VozilaFilter from "components/filters/VozilaFilter/index";
|
||||
|
||||
class Automobili extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span>Auto</span>
|
||||
<VozilaFilter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Automobili;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import VozilaFilter from "components/filters/VozilaFilter/index";
|
||||
|
||||
class Motocikli extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span>Motocikli</span>
|
||||
<VozilaFilter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Motocikli;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { optionchangewrapper } from "utils/optionchangewrapper";
|
||||
|
||||
class CheckboxAndRadioWrapper extends React.Component {
|
||||
optionChange = (option, optionName, type) => {
|
||||
const optionTypePicker = {
|
||||
radio: option.target.value,
|
||||
checkbox: option.target.checked
|
||||
};
|
||||
const { onOptionChanged } = this.props;
|
||||
onOptionChanged({
|
||||
optionName,
|
||||
optionValue: optionTypePicker[type]
|
||||
});
|
||||
};
|
||||
isChecked = (type, value, optionName) => {
|
||||
const { options } = this.props;
|
||||
return type === "checkbox"
|
||||
? value
|
||||
: options.hasOwnProperty(optionName) &&
|
||||
options[optionName] === String(value);
|
||||
};
|
||||
renderElements = (elements, componentName) => {
|
||||
return elements.map(({ type, value, name, optionName } = {}) => (
|
||||
<label key={name + type}>
|
||||
<input
|
||||
name={type === "radio" ? `${componentName}-radio` : ""}
|
||||
type={type}
|
||||
value={value}
|
||||
checked={this.isChecked(type, value, optionName)}
|
||||
onChange={option => this.optionChange(option, optionName, type)}
|
||||
/>
|
||||
{name}
|
||||
</label>
|
||||
));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { elements, componentName } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<span>{componentName}</span>
|
||||
{this.renderElements(elements, componentName)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default optionchangewrapper(CheckboxAndRadioWrapper);
|
||||
52
frontend-react/src/components/widgets/RangeWrapper.js
Normal file
52
frontend-react/src/components/widgets/RangeWrapper.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { Range } from "rc-slider";
|
||||
import { optionchangewrapper } from "utils/optionchangewrapper";
|
||||
import "rc-slider/assets/index.css";
|
||||
|
||||
class RangeWrapper extends React.Component {
|
||||
handleRangeChange = ([min, max] = this.props.defaultValues) => {
|
||||
this.inputMin.value = min;
|
||||
this.inputMax.value = max;
|
||||
const { onOptionChanged, optionName } = this.props;
|
||||
onOptionChanged({
|
||||
optionName,
|
||||
optionValue: [min, max]
|
||||
});
|
||||
};
|
||||
|
||||
handleInputChange = () => {
|
||||
const { onOptionChanged, optionName } = this.props;
|
||||
onOptionChanged({
|
||||
optionName,
|
||||
optionValue: [this.inputMin.value, this.inputMax.value]
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { step, defaultValues, min, max } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Range
|
||||
min={min}
|
||||
max={max}
|
||||
defaultValue={defaultValues}
|
||||
step={step}
|
||||
onAfterChange={this.handleRangeChange}
|
||||
/>
|
||||
<input
|
||||
ref={node => {
|
||||
this.inputMin = node;
|
||||
}}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
<input
|
||||
ref={node => {
|
||||
this.inputMax = node;
|
||||
}}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default optionchangewrapper(RangeWrapper);
|
||||
25
frontend-react/src/components/widgets/SelectWrapper.js
Normal file
25
frontend-react/src/components/widgets/SelectWrapper.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import Select from "react-select";
|
||||
import { optionchangewrapper } from "utils/optionchangewrapper";
|
||||
|
||||
class SelectWrapper extends React.Component {
|
||||
handleOptionChange = selectedOption => {
|
||||
const { onOptionChanged, optionName } = this.props;
|
||||
onOptionChanged({
|
||||
optionName,
|
||||
optionValue: selectedOption
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let { value, options, choices } = this.props;
|
||||
return (
|
||||
<Select
|
||||
value={options[value]}
|
||||
onChange={this.handleOptionChange}
|
||||
options={choices}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default optionchangewrapper(SelectWrapper);
|
||||
4
frontend-react/src/constants/actionTypes.js
Normal file
4
frontend-react/src/constants/actionTypes.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const CATEGORY_SELECT = "CATEGORY_SELECT";
|
||||
export const SUBCATEGORY_SELECT = "SUBCATEGORY_SELECT";
|
||||
export const OPTION_CHANGE = "OPTION_CHANGE";
|
||||
export const ITEMS_CHANGED = "ITEMS_CHANGED";
|
||||
13
frontend-react/src/index.js
Normal file
13
frontend-react/src/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import React from "react";
|
||||
import { store } from "./store";
|
||||
|
||||
import App from "./components/App";
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
12
frontend-react/src/reducer.js
Normal file
12
frontend-react/src/reducer.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import category from "./reducers/category";
|
||||
import subcategory from "./reducers/subcategory";
|
||||
import options from "./reducers/options";
|
||||
import items from "./reducers/items";
|
||||
import { combineReducers } from "redux";
|
||||
|
||||
export default combineReducers({
|
||||
category,
|
||||
subcategory,
|
||||
options,
|
||||
items
|
||||
});
|
||||
10
frontend-react/src/reducers/category.js
Normal file
10
frontend-react/src/reducers/category.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { CATEGORY_SELECT } from "constants/actionTypes";
|
||||
|
||||
export default (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case CATEGORY_SELECT:
|
||||
return action.option;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
19
frontend-react/src/reducers/items.js
Normal file
19
frontend-react/src/reducers/items.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
ITEMS_CHANGED,
|
||||
SUBCATEGORY_SELECT,
|
||||
CATEGORY_SELECT,
|
||||
OPTION_CHANGE
|
||||
} from "constants/actionTypes";
|
||||
|
||||
export default (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case ITEMS_CHANGED:
|
||||
return action.items;
|
||||
case CATEGORY_SELECT:
|
||||
case SUBCATEGORY_SELECT:
|
||||
case OPTION_CHANGE:
|
||||
return [];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
18
frontend-react/src/reducers/options.js
Normal file
18
frontend-react/src/reducers/options.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
SUBCATEGORY_SELECT,
|
||||
CATEGORY_SELECT,
|
||||
OPTION_CHANGE
|
||||
} from "constants/actionTypes";
|
||||
|
||||
export default (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case OPTION_CHANGE:
|
||||
return { ...state, [action.optionName]: action.optionValue };
|
||||
case SUBCATEGORY_SELECT:
|
||||
return {};
|
||||
case CATEGORY_SELECT:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
12
frontend-react/src/reducers/subcategory.js
Normal file
12
frontend-react/src/reducers/subcategory.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SUBCATEGORY_SELECT, CATEGORY_SELECT } from "constants/actionTypes";
|
||||
|
||||
export default (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case SUBCATEGORY_SELECT:
|
||||
return action.option;
|
||||
case CATEGORY_SELECT:
|
||||
return null;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
135
frontend-react/src/serviceWorker.js
Executable file
135
frontend-react/src/serviceWorker.js
Executable file
@@ -0,0 +1,135 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read http://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit http://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
11
frontend-react/src/store.js
Normal file
11
frontend-react/src/store.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { applyMiddleware, createStore } from "redux";
|
||||
import { createLogger } from "redux-logger";
|
||||
import { composeWithDevTools } from "redux-devtools-extension/developmentOnly";
|
||||
import reducer from "./reducer";
|
||||
|
||||
const getMiddleware = () => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return applyMiddleware(createLogger());
|
||||
}
|
||||
};
|
||||
export const store = createStore(reducer, composeWithDevTools(getMiddleware()));
|
||||
63
frontend-react/src/utils/createOlxLink.js
Normal file
63
frontend-react/src/utils/createOlxLink.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/*category: {value: "Vozila", label: "Vozila"}
|
||||
options:
|
||||
cijena: (2) [6500, 70500]
|
||||
godiste: (2) [2004, 2017]
|
||||
gorivo_select_benzin: true
|
||||
gorivo_select_dizel: true
|
||||
kanton: {value: "9", label: "Sarajevo"}
|
||||
kilometrazaMax: {value: 20000, label: "20000"}
|
||||
kilometrazaMin: {value: 5000, label: "5000"}
|
||||
proizvodac: {value: "1900", label: "Audi"}
|
||||
stanje: ""
|
||||
subcategory: {value: "Automobili", label: "Automobili"}
|
||||
|
||||
|
||||
https://www.olx.ba/pretraga?
|
||||
kategorija=18&stanje=&v_b=1900
|
||||
&od=6500&do=70500
|
||||
&kanton=9&
|
||||
godiste_min=2004&godiste_max=2017
|
||||
&kilometra-a_min=5000&kilometra-a_max=50000
|
||||
&gorivo_select_dizel=Dizel&gorivo_select_benzin=Benzin
|
||||
|
||||
{
|
||||
"Automobili": 18,
|
||||
"v_b": "proizvodac",
|
||||
|
||||
}*/
|
||||
function AutomobiliLinkCreator(options) {
|
||||
const [od, do_] = options.cijena;
|
||||
const [godiste_min, godiste_max] = options.godiste;
|
||||
const goriva = [
|
||||
"gorivo_select_benzin",
|
||||
"gorivo_select_dizel",
|
||||
"gorivo_select_plin",
|
||||
"gorivo_select_hibrid",
|
||||
"gorivo_selector_elektro"
|
||||
]
|
||||
.filter(gorivo => options.hasOwnProperty(gorivo))
|
||||
.reduce(
|
||||
(izborGoriva, gorivo) =>
|
||||
izborGoriva +
|
||||
"" +
|
||||
options[gorivo] +
|
||||
"=" +
|
||||
options[gorivo] +
|
||||
"&",
|
||||
""
|
||||
);
|
||||
return `kategorija=18&stanje=${options.stanje}&v_b=${
|
||||
options.proizvodac.value
|
||||
}&od=${od}&do=${do_}&kanton=${
|
||||
options.kanton.value
|
||||
}&godiste_min=${godiste_min}&godiste_max=${godiste_max}&kilometra-a_min=${
|
||||
options.kilometrazaMin.value
|
||||
}&kilometra-a_max=${options.kilometrazaMax.value}&${goriva}`;
|
||||
}
|
||||
const mappingFunctios = {
|
||||
Automobili: AutomobiliLinkCreator
|
||||
};
|
||||
|
||||
export const createOlxLink = (category, subcategory, options) => {
|
||||
return mappingFunctios[subcategory.value](options);
|
||||
};
|
||||
1
frontend-react/src/utils/hoc.js
Normal file
1
frontend-react/src/utils/hoc.js
Normal file
@@ -0,0 +1 @@
|
||||
export const hoc = (option, componentList) => componentList[option] || null;
|
||||
18
frontend-react/src/utils/optionchangewrapper.js
Normal file
18
frontend-react/src/utils/optionchangewrapper.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { connect } from "react-redux";
|
||||
import { OPTION_CHANGE } from "constants/actionTypes";
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
options: state.options
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onOptionChanged: option => dispatch({ type: OPTION_CHANGE, ...option })
|
||||
});
|
||||
|
||||
export const optionchangewrapper = component =>
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(component);
|
||||
19
frontend-react/src/utils/subcategorywrapper.js
Normal file
19
frontend-react/src/utils/subcategorywrapper.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { connect } from "react-redux";
|
||||
import { SUBCATEGORY_SELECT } from "constants/actionTypes";
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
subcategory: state.subcategory
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubCategoryChanged: option =>
|
||||
dispatch({ type: SUBCATEGORY_SELECT, option })
|
||||
});
|
||||
|
||||
export const subcategorywrapper = component =>
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(component);
|
||||
Reference in New Issue
Block a user