├── .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 | 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 | 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 | 41 | 42 |
43 | 66 |
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 | 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 | 100 | 101 | {% endblock %} 102 | 103 | {% block footer %} 104 |

© Copyright 2021 Apartani

105 | {% endblock %} --------------------------------------------------------------------------------