├── .gitignore
├── requirements.txt
├── Animation.gif
├── static
├── img
│ ├── home.png
│ └── home_selected.png
├── js
│ ├── property_information.js
│ ├── map_operations.js
│ └── map.js
└── css
│ └── style.css
├── images
└── app_preview_image.png
├── creator.py
├── README.md
├── LICENSE
├── marketplace.json
├── templates
├── base.html
└── demo
│ ├── about.html
│ └── index.html
├── helper.html
└── app.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | venv
3 | helper.html
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | redis
2 | flask
3 | flask-socketio
4 | rejson
--------------------------------------------------------------------------------
/Animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/apartani-realtime-search/main/Animation.gif
--------------------------------------------------------------------------------
/static/img/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/apartani-realtime-search/main/static/img/home.png
--------------------------------------------------------------------------------
/images/app_preview_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/apartani-realtime-search/main/images/app_preview_image.png
--------------------------------------------------------------------------------
/static/img/home_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/apartani-realtime-search/main/static/img/home_selected.png
--------------------------------------------------------------------------------
/creator.py:
--------------------------------------------------------------------------------
1 | ## This script creates random properties
2 |
3 |
4 | import json
5 | import string
6 | import random
7 | data = []
8 |
9 |
10 | letters = string.ascii_lowercase
11 |
12 | def creator(pid):
13 | im = ''.join(random.choice(letters) for i in range(4))
14 | rooms = int(random.uniform(3, 5))
15 | data.append({
16 | "id": pid,
17 | "img": f"https://www.{im}.com/{im}.jpg",
18 | "area": int(random.uniform(10, 100)),
19 | "rooms": rooms,
20 | "baths": rooms - int(random.uniform(1, 2)),
21 | "lat" : -74.039882+random.uniform(-0.05, 0.05),
22 | "lon" : 4.6971232+random.uniform(-0.05, 0.05),
23 | "price" : int(random.uniform(120, 500))*1000
24 | })
25 |
26 | for i in range(1, 1500):
27 | pr = ''.join(random.choice(letters) for i in range(5))
28 | creator('pr-' + str(pr))
29 |
30 | with open('data.json', 'w') as outfile:
31 | json.dump(data, outfile)
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Apartani realtime search
2 |
3 | Real time house search using Socket.io, Flask and Redis JSON database
4 |
5 | ## Steps to run
6 |
7 |
8 | ### 1. Install and run Redis JSON
9 | ```
10 | docker run -p 6379:6379 --name redis-redisjson redislabs/rejson:latest
11 | ```
12 |
13 | ### 2. Clone this repository
14 | ```
15 | git clone https://github.com/redis-developer/apartani-realtime-search.git
16 | ```
17 |
18 | ```
19 | cd apartani-realtime-search
20 | ```
21 |
22 | ### 3. Create virutalenv
23 |
24 | ```
25 | virtualenv venv
26 | ```
27 |
28 | ### 4. Activate the enviroment
29 | ```
30 | #Linux
31 | source venv/bin/activate
32 |
33 | #Windows
34 | source venv/Scripts/activate
35 | ```
36 | ### 5. Install requirements
37 | ```
38 | pip install -r requirements.txt
39 | ```
40 | ### 6. Install the required modules
41 |
42 | ```
43 | pip install flask
44 | pip install rejson
45 | pip install flask_socketio
46 | ```
47 |
48 | ### 7. Run the app
49 |
50 | ```
51 | python app.py
52 | ```
53 |
54 | ### 8. View the app
55 |
56 | ```
57 | localhost:5000
58 | ```
59 | _the web app was designed for mobile view_
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Redis Developer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/marketplace.json:
--------------------------------------------------------------------------------
1 | {
2 | "app_name": "Apartani Realtime Search",
3 | "description": "Real-time house search using Socket.io, Flask and Redis",
4 | "rank": "390",
5 | "type": "Full App",
6 | "contributed_by": "Community",
7 | "repo_url": "https://github.com/redis-developer/apartani-realtime-search",
8 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/apartani-realtime-search/master/images/app_preview_image.png",
9 | "download_url": "https://github.com/redis-developer/apartani-realtime-search/archive/refs/heads/main.zip",
10 | "hosted_url": "",
11 | "quick_deploy": "false",
12 | "deploy_buttons": [],
13 | "language": [
14 | "Python",
15 | "JavaScript"
16 | ],
17 | "redis_commands": [
18 | "GeoRadius"
19 | ],
20 | "redis_use_cases": [
21 | "Caching",
22 | "JSON"
23 | ],
24 | "redis_features": [
25 | "JSON"
26 | ],
27 | "app_image_urls": [
28 | "https://i.ibb.co/T27Kwj0/home.png"
29 | ],
30 | "youtube_url": "https://www.youtube.com/watch?v=zz2HjVrYfNE",
31 | "special_tags": [
32 | "Hackathon"
33 | ],
34 | "verticals": [
35 | "Financial Services"
36 | ],
37 | "markdown": "https://raw.githubusercontent.com/redis-developer/apartani-realtime-search/main/README.md"
38 | }
--------------------------------------------------------------------------------
/static/js/property_information.js:
--------------------------------------------------------------------------------
1 | //Test images for testing purposes
2 | const stockImages = [
3 | "https://c8.alamy.com/comp/C1G52K/art-deco-apartment-block-dar-es-salaam-tanzania-C1G52K.jpg",
4 | "https://st2.depositphotos.com/1015412/7702/i/600/depositphotos_77024653-stock-photo-apartments.jpg",
5 | "https://thumbs.dreamstime.com/b/modern-apartment-building-5569745.jpg",
6 | "https://st2.depositphotos.com/1658611/6903/i/950/depositphotos_69030443-stock-photo-typical-suburban-apartment-building.jpg",
7 | "https://t4.ftcdn.net/jpg/01/22/80/93/360_F_122809314_qqF2qH028iOn3FC43M0TL40hdXz6B5Mr.jpg",
8 | "https://s.realpage.com/wp-content/uploads/sites/20/2016/02/shutterstock_141042463-1-e1619105338909.jpg",
9 | "https://pixnio.com/free-images/2017/10/21/2017-10-21-07-59-49.jpg",
10 | ];
11 | var numClicks = 0;
12 | socket.on("property_result", function (data) {
13 | document.getElementById("selection-price").innerText = "$" + data.price;
14 | document.getElementById("selection-area").innerText = data.area;
15 | document.getElementById("selection-bathrooms").innerText = data.baths;
16 | document.getElementById("selection-rooms").innerText = data.rooms;
17 | document.getElementById("selection-img").src = stockImages[numClicks];
18 | //Loop over stock images
19 | numClicks++;
20 | if (numClicks >= stockImages.length) {
21 | numClicks = 0
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/static/css/style.css:
--------------------------------------------------------------------------------
1 | *{
2 | font-family: 'KoHo', sans-serif;
3 | }
4 |
5 | .navbar-brand{
6 | display: flex;
7 | }
8 |
9 | #map-container {
10 | width: 100%;
11 | height: 240px;
12 | }
13 |
14 | #realtime-search {
15 | display: flex;
16 | flex-direction: column;
17 | margin-top: 25px;
18 | }
19 |
20 | .marker {
21 | background-image: url("../img/home.png");
22 | background-size: cover;
23 | width: 30px;
24 | height: 30px;
25 | cursor: pointer;
26 | }
27 |
28 | .marker-selected {
29 | background-image: url("../img/home_selected.png");
30 | }
31 |
32 | .mapboxgl-popup {
33 | max-width: 200px;
34 | }
35 |
36 | .img-search {
37 | height: 120px;
38 | width: 100%;
39 | object-fit: cover;
40 | }
41 |
42 | .search-result .card-title {
43 | color: black;
44 | }
45 |
46 | .search-result .card-text {
47 | color: rgb(66, 66, 66);
48 | }
49 |
50 | .card-search {
51 | margin: 15px;
52 | max-width: 540px;
53 | }
54 |
55 | .link-card {
56 | font-size: small;
57 | }
58 |
59 | .coordinates {
60 | background: rgba(0, 0, 0, 0.5);
61 | color: #fff;
62 | position: absolute;
63 | bottom: 40px;
64 | left: 10px;
65 | padding: 5px 10px;
66 | margin: 0;
67 | font-size: 11px;
68 | line-height: 18px;
69 | border-radius: 3px;
70 | display: none;
71 | }
72 |
73 | .control-container {
74 | width: 200px;
75 | }
76 |
77 | .control {
78 | display: flex;
79 | justify-content: space-evenly;
80 | }
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% block head %}{% endblock %}
9 |
10 |
11 |
12 |
13 |
14 |
15 | {% block header %}{% endblock %}
16 |
17 | {% block main %} {% endblock %}
18 |
19 | {% block footer %}{% endblock %}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
34 |
35 |
--------------------------------------------------------------------------------
/static/js/map_operations.js:
--------------------------------------------------------------------------------
1 | var socket = io("ws://localhost:5000/rt-search");
2 |
3 | function send_mov(mov) {
4 | var prevLat = point.features[0].geometry.coordinates[0];
5 | var prevLon = point.features[0].geometry.coordinates[1];
6 | var newcoords;
7 | switch (mov) {
8 | case "up":
9 | newcoords = [prevLat, prevLon + 0.0002];
10 | break;
11 | case "down":
12 | newcoords = [prevLat, prevLon - 0.0002];
13 | break;
14 | case "left":
15 | newcoords = [prevLat - 0.0002, prevLon];
16 | break;
17 | case "right":
18 | newcoords = [prevLat + 0.0002, prevLon];
19 | break;
20 | default:
21 | newcoords = [prevLat, prevLon];
22 | break;
23 | }
24 |
25 | socket.emit("position", {
26 | user: "example",
27 | lat: newcoords[0],
28 | lon: newcoords[1],
29 | });
30 | point.features[0].geometry.coordinates = newcoords;
31 | map.getSource("pointpos").setData(point);
32 | map.flyTo({ center: newcoords });
33 | }
34 |
35 | socket.on("update_prop", function (data) {
36 | let newProps = [];
37 |
38 | data.forEach((prop) => {
39 | if (document.getElementById(prop[0]) == null) {
40 | add_new(prop[0], prop[1][0], prop[1][1]);
41 | }
42 |
43 | newProps.push(prop[0]);
44 | });
45 |
46 | //i need to delete all houses that were added but are far away
47 | const added = document.querySelectorAll('*[id^="pr-"]');
48 |
49 | for (var elem of added) {
50 | if (!newProps.includes(elem.id)) {
51 | var oldProp = document.getElementById(elem.id);
52 | oldProp.parentNode.removeChild(oldProp);
53 | }
54 | }
55 | });
56 | socket.on("update_prop", function (data) {
57 | let newProps = [];
58 |
59 | data.forEach((prop) => {
60 | //Add new if doesn't exists
61 | if (document.getElementById(prop[0]) == null) {
62 | add_new(prop[0], prop[1][0], prop[1][1]);
63 | }
64 |
65 | newProps.push(prop[0]);
66 | });
67 |
68 | //Delete all houses that were added but are far away
69 | const added = document.querySelectorAll('*[id^="pr-"]');
70 |
71 | for (var elem of added) {
72 | if (!newProps.includes(elem.id)) {
73 | var oldProp = document.getElementById(elem.id);
74 | oldProp.parentNode.removeChild(oldProp);
75 | }
76 | }
77 | });
78 |
--------------------------------------------------------------------------------
/templates/demo/about.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block head %}
4 |
5 | Apartani
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 | {% endblock %}
15 |
16 |
17 |
18 | {% block header %}
19 |
20 |
21 |
22 |
23 |
24 |
{% block
26 | title %}Apartani{% endblock %}
27 |
29 |
30 |
31 |
32 |
33 |
34 | Home
35 |
36 |
37 | About
38 |
39 |
40 |
41 |
42 |
43 |
44 | {% endblock %}
45 |
46 |
47 | {% block main %}
48 |
49 |
50 |
51 |
52 | Hi! I'm Julián Úsuga, an statistics student from Colombia, i started this project from zero
53 | with zero knowledge of Redis and Socket.io, i plan to continue improving and growing this project (apartani).
54 | This project can be used as a template or you can contribute to the project here .
56 | Thank you Redis. 😊
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {% endblock %}
67 |
68 | {% block footer %}
69 | © Copyright 2021 Apartani
70 | {% endblock %}
--------------------------------------------------------------------------------
/static/js/map.js:
--------------------------------------------------------------------------------
1 | var map;
2 | mapboxgl.accessToken =
3 | "pk.eyJ1IjoianVsaWFudXN1IiwiYSI6ImNrbm56dWU3ZjEzZ2Uyb21vMXRpaHp0bDMifQ.jOaoVuPdkcgQcpgq7yHz1Q";
4 |
5 | var initial_point = [-74.039882, 4.697];
6 |
7 | console.info("Started map");
8 | map = new mapboxgl.Map({
9 | container: "map-container",
10 | style: "mapbox://styles/mapbox/streets-v10",
11 | center: initial_point,
12 | zoom: 15,
13 | });
14 |
15 | const template = document.getElementById("template");
16 |
17 | var point = {
18 | type: "FeatureCollection",
19 | features: [
20 | {
21 | type: "Feature",
22 | properties: {},
23 | geometry: {
24 | type: "Point",
25 | coordinates: initial_point,
26 | },
27 | },
28 | ],
29 | };
30 |
31 | map.on("load", function () {
32 | map.addSource("pointpos", {
33 | type: "geojson",
34 | data: point,
35 | });
36 |
37 | map.addLayer({
38 | id: "pointpos",
39 | source: "pointpos",
40 | type: "circle",
41 | paint: {
42 | "circle-radius": 10,
43 | "circle-color": "#3887be",
44 | },
45 | });
46 | });
47 |
48 | let selectionId = "";
49 |
50 | function create_template() {
51 | const selection = template.content.cloneNode(true);
52 |
53 | return selection;
54 | }
55 |
56 | function replace_selection(newSelection) {
57 |
58 |
59 | socket.emit("property", {
60 | user: "example",
61 | selection: newSelection
62 | });
63 |
64 |
65 | var propertySelection = document.getElementById("properties-list");
66 |
67 | const selection = create_template();
68 |
69 | if (selectionId == "") {
70 | propertySelection.appendChild(selection);
71 | document.getElementById(newSelection).classList.add("marker-selected")
72 | } else {
73 | document.getElementById(newSelection).classList.add("marker-selected")
74 | document.getElementById(selectionId).classList.remove("marker-selected")
75 | propertySelection.innerHTML = null;
76 | propertySelection.appendChild(selection);
77 | }
78 | selectionId = newSelection;
79 | }
80 |
81 | function add_new(pid, lat, lon) {
82 |
83 | var el = document.createElement("div");
84 | el.classList = "marker";
85 | el.id = pid;
86 |
87 | el.addEventListener("click", function () {
88 | replace_selection(el.id);
89 | });
90 |
91 | new mapboxgl.Marker(el).setLngLat([lat, lon]).addTo(map);
92 | }
93 |
--------------------------------------------------------------------------------
/helper.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
14 |
15 |
16 |
17 |
40 |
41 |
42 |
67 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | from rejson import Client, Path
2 | from flask import Flask, render_template, request, session
3 | from flask_socketio import SocketIO, emit
4 | import json
5 |
6 |
7 | # Connect to redis json client
8 | rj = Client(host="127.0.0.1", port=6379, decode_responses=True)
9 |
10 | # Gets one property with id
11 | def getproperty(pid):
12 | """ Deletes properties by Id """
13 | prop = rj.jsonget(pid)
14 | return prop
15 |
16 |
17 | def addproperty(pid, img, price, area, rooms, baths, lat, lon):
18 | """ Adds each property to Redis JSON """
19 | prop = {
20 | "id": pid,
21 | "img": img,
22 | "price": price,
23 | "area": area,
24 | "rooms": rooms,
25 | "baths": baths
26 | }
27 | # adds json object
28 | rj.jsonset(pid, Path.rootPath(), prop)
29 | # adds geo location of property
30 | rj.geoadd("properties", lat, lon, pid)
31 |
32 |
33 | def delproperty(pid):
34 | """ Deletes properties by Id """
35 | rj.zrem("properties", pid)
36 | rj.jsondel(pid)
37 |
38 |
39 | # Loads all the properties from the data.json file
40 | with open("data.json", "r") as data:
41 | data = json.load(data)
42 | for i in data:
43 | addproperty(
44 | i["id"], i["img"], i["price"], i["area"], i["rooms"], i["baths"], i["lat"], i["lon"]
45 | )
46 |
47 |
48 | # Start Flask app
49 | app = Flask(__name__)
50 | app.config["SECRET_KEY"] = "secret!"
51 |
52 | # Socket io client
53 | socketio = SocketIO()
54 | socketio.init_app(app)
55 |
56 |
57 | # Socket that detects changes in user position
58 | @socketio.on("position", namespace="/rt-search")
59 | def position(data):
60 | """ Called when user sends new position """
61 |
62 | result = rj.georadius(
63 | # I need to leave the "" paramenter to dont get the distances from database
64 | # Get properties that are 500m away in radius of my current location
65 | "properties",
66 | data["lat"],
67 | data["lon"],
68 | "500",
69 | "m",
70 | "",
71 | "WITHCOORD",
72 | )
73 |
74 | # Emit propierties to one user
75 | emit("update_prop", result, room=request.sid)
76 |
77 |
78 | # Socket that returns properties
79 | @socketio.on("property", namespace="/rt-search")
80 | def property(data):
81 | """ Called when user clicks on a property """
82 |
83 | result = getproperty(data["selection"])
84 |
85 | # Emit propierties to one user
86 | emit("property_result", result, room=request.sid)
87 |
88 |
89 | # Index Route
90 | @app.route("/")
91 | def index():
92 | return render_template("demo/index.html")
93 |
94 | # About Route
95 | @app.route("/about")
96 | def about():
97 | return render_template("demo/about.html")
98 |
99 |
100 | # Start app
101 | if __name__ == "__main__":
102 |
103 | #Go to http://localhost:5000 😊
104 | socketio.run(app, debug=True, host="localhost")
105 |
--------------------------------------------------------------------------------
/templates/demo/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block head %}
4 |
5 | Apartani
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 | {% endblock %}
17 |
18 |
19 |
20 | {% block header %}
21 |
22 |
23 |
24 |
25 |
26 |
{% block
27 | title %}Apartani{% endblock %}
28 |
30 |
31 |
32 |
33 |
34 |
35 | Home
36 |
37 |
38 | About
39 |
40 |
41 |
42 |
43 |
44 |
45 | {% endblock %}
46 |
47 |
48 | {% block main %}
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
64 |
65 |
66 |
68 |
70 |
71 |
72 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
99 |
100 |
101 | {% endblock %}
102 |
103 | {% block footer %}
104 | © Copyright 2021 Apartani
105 | {% endblock %}
--------------------------------------------------------------------------------