├── api ├── requirements.txt ├── package.json ├── serverless.yml ├── handler.py └── google.py ├── web ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── src │ ├── setupTests.js │ ├── index.js │ ├── index.css │ ├── reportWebVitals.js │ ├── randomRestaurant.js │ ├── App.css │ ├── consts.js │ ├── columns.js │ └── App.js ├── .eslintrc.js ├── package.json └── README.md ├── README.md └── .gitignore /api/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/wolt-explorer/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/wolt-explorer/HEAD/web/public/logo192.png -------------------------------------------------------------------------------- /web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/wolt-explorer/HEAD/web/public/logo512.png -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wolt-explorer 2 | 3 | Wolt restaurants explorer that allows advanced filtering and sorting 🍔 🍕 🥗 🍣. 4 | 5 | Project is hosted on [https://woltex.ranrib.com](https://woltex.ranrib.com). 6 | -------------------------------------------------------------------------------- /web/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | 14 | reportWebVitals(); 15 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wolt-explorer-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "scottyjs": "^1.10.2", 13 | "serverless": "^2.59.0", 14 | "serverless-python-requirements": "^5.4.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /api/serverless.yml: -------------------------------------------------------------------------------- 1 | service: wolt-explorer-api 2 | 3 | provider: 4 | name: aws 5 | runtime: python3.7 6 | lambdaHashingVersion: '20201221' 7 | 8 | custom: 9 | pythonRequirements: 10 | pythonBin: python3 11 | 12 | functions: 13 | get-resteraunts: 14 | handler: handler.get_resteraunts 15 | events: 16 | - httpApi: 17 | path: / 18 | method: get 19 | 20 | plugins: 21 | - serverless-python-requirements 22 | -------------------------------------------------------------------------------- /web/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ 4 | getCLS, getFID, getFCP, getLCP, getTTFB, 5 | }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb', 9 | ], 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | ecmaVersion: 12, 15 | sourceType: 'module', 16 | }, 17 | plugins: [ 18 | 'react', 19 | ], 20 | rules: { 21 | 'react/jsx-filename-extension': 0, 22 | 'react/prop-types': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Wolt Explorer", 3 | "name": "Wolt Explorer", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /api/handler.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | 5 | def get_resteraunts(event, context): 6 | with open('out.json') as google_data_file: 7 | google_data = json.loads(google_data_file.read()) 8 | response = requests.get( 9 | 'https://restaurant-api.wolt.com/v1/pages/restaurants?' 10 | f'lat={event["queryStringParameters"]["lat"]}&' 11 | f'lon={event["queryStringParameters"]["lon"]}' 12 | ).json() 13 | for resteraunt in response['sections'][1]['items']: 14 | resteraunt['google'] = google_data.get(resteraunt['track_id'], {}) 15 | return { 16 | "statusCode": 200, 17 | "body": json.dumps(response), 18 | "headers": { 19 | "Access-Control-Allow-Headers" : "Content-Type", 20 | "Access-Control-Allow-Origin": "*", 21 | "Access-Control-Allow-Methods": "OPTIONS,GET" 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Wolt Explorer 15 | 16 | 17 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /web/src/randomRestaurant.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | 4 | function RandomRestaurant(props) { 5 | const { filteredResteraunts, idx } = props; 6 | const randRestaurant = filteredResteraunts[idx]; 7 | return ( 8 |
9 |

10 | 11 | { randRestaurant.venue.name } 12 | 13 |

14 |

{ randRestaurant.venue.short_description }

15 |

16 | Rating: 17 | {' '} 18 | { 19 | randRestaurant.venue.rating !== undefined ? ( 20 | randRestaurant.venue.rating.score 21 | ) : ('') 22 | } 23 | {' | '} 24 | Tag: 25 | {' '} 26 | { randRestaurant.venue.tags[0] } 27 | {' | '} 28 | Delivery: 29 | {' '} 30 | { randRestaurant.venue.estimate } 31 | {' '} 32 | min 33 |

34 | 35 | {randRestaurant.venue.name} 43 | 44 |
45 | ); 46 | } 47 | export default RandomRestaurant; 48 | -------------------------------------------------------------------------------- /api/google.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | def get_resteraunts(): 5 | key = open('key.txt').read() 6 | response = requests.get( 7 | 'https://restaurant-api.wolt.com/v1/pages/restaurants?lat=32.0633549&lon=34.769285' 8 | ).json() 9 | gscores = {} 10 | 11 | for rest in response['sections'][1]['items']: 12 | name = rest['venue']['name'].split(' | ')[0] 13 | address = rest['venue']['address'] 14 | track_id = rest['track_id'] 15 | lat = rest['venue']['location'][1] 16 | lon = rest['venue']['location'][0] 17 | rest_res = requests.get( 18 | f'https://maps.googleapis.com/maps/api/place/findplacefromtext/json?fields=formatted_address%2Cname%2Crating%2Cprice_level&input={name}&inputtype=textquery&locationbias=point%3A{lat}%2C{lon}&key={key}' 19 | ).json() 20 | 21 | if len(rest_res['candidates']) == 0: 22 | print('didnt find', name) 23 | continue 24 | 25 | rest_res = rest_res['candidates'][0] 26 | 27 | print(name, address, '==', rest_res['name'], rest_res['formatted_address']) 28 | 29 | price = rest_res.get('price_level') 30 | rating = rest_res.get('rating') 31 | if not price and not rating: continue 32 | gscores[track_id] = {} 33 | if price: 34 | gscores[track_id]['p'] = price 35 | if rating: 36 | gscores[track_id]['r'] = rating 37 | 38 | with open('out.json', 'w') as out_file: 39 | out_file.write(json.dumps(gscores)) 40 | import ipdb;ipdb.set_trace() 41 | 42 | get_resteraunts() -------------------------------------------------------------------------------- /web/src/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | .category-tag { 4 | text-transform: capitalize; 5 | } 6 | 7 | .ant-dropdown-menu { 8 | text-transform: capitalize; 9 | } 10 | 11 | .navbar { 12 | display: flex; 13 | } 14 | 15 | .location-indication { 16 | margin-top: 5px; 17 | margin-bottom: 5px; 18 | margin-left: 5px; 19 | cursor: unset; 20 | } 21 | 22 | .information { 23 | margin-top: 5px; 24 | margin-bottom: 5px; 25 | margin-left: 5px; 26 | } 27 | .random { 28 | margin-top: 5px; 29 | margin-bottom: 5px; 30 | margin-left: 5px; 31 | } 32 | 33 | .location-indication:hover { 34 | color: rgba(0, 0, 0, 0.85); 35 | border-color: #d9d9d9; 36 | background: #fff; 37 | } 38 | 39 | .App { 40 | text-align: center; 41 | margin: 10px 40px 40px 40px; 42 | } 43 | 44 | h1 { 45 | margin-bottom: 0.1em; 46 | } 47 | 48 | .input-search { 49 | margin-top: 5px; 50 | margin-bottom: 5px; 51 | } 52 | 53 | .App-logo { 54 | height: 40vmin; 55 | pointer-events: none; 56 | } 57 | 58 | @media (prefers-reduced-motion: no-preference) { 59 | .App-logo { 60 | animation: App-logo-spin infinite 20s linear; 61 | } 62 | } 63 | 64 | .App-header { 65 | background-color: #282c34; 66 | min-height: 100vh; 67 | display: flex; 68 | flex-direction: column; 69 | align-items: center; 70 | justify-content: center; 71 | font-size: calc(10px + 2vmin); 72 | color: white; 73 | } 74 | 75 | .App-link { 76 | color: #61dafb; 77 | } 78 | 79 | @keyframes App-logo-spin { 80 | from { 81 | transform: rotate(0deg); 82 | } 83 | to { 84 | transform: rotate(360deg); 85 | } 86 | } 87 | .random-restaurant{ 88 | color: black; 89 | line-height: 1; 90 | font-size: medium; 91 | text-align: center; 92 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wolt-explorer", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://woltex.ranrib.com", 6 | "author": "Ran Ribenzaft ", 7 | "dependencies": { 8 | "@ant-design/icons": "^4.6.4", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.1.0", 11 | "@testing-library/user-event": "^12.1.10", 12 | "antd": "^4.20.5", 13 | "axios": "^0.21.4", 14 | "geolib": "^3.3.1", 15 | "node-emoji": "^1.11.0", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2", 18 | "react-highlight-words": "^0.17.0", 19 | "react-scripts": "^4.0.3", 20 | "web-vitals": "^1.0.1" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject", 27 | "lint": "eslint ./src", 28 | "lint:fix": "eslint ./src --fix", 29 | "deploy": "npm run build && scotty --spa --source ./build --bucket" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "@typescript-eslint/eslint-plugin": "^4.31.1", 45 | "@typescript-eslint/parser": "^4.31.1", 46 | "babel-eslint": "^10.1.0", 47 | "eslint": "^7.32.0", 48 | "eslint-config-airbnb": "^18.2.1", 49 | "eslint-config-react-app": "^6.0.0", 50 | "eslint-plugin-flowtype": "^5.10.0", 51 | "eslint-plugin-import": "^2.24.2", 52 | "eslint-plugin-jsx-a11y": "^6.4.1", 53 | "eslint-plugin-react": "^7.25.3", 54 | "eslint-plugin-react-hooks": "^4.2.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | web/yarn.lock 8 | web/package-lock.json 9 | api/package-lock.json 10 | api/key.txt 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | .DS_Store 111 | -------------------------------------------------------------------------------- /web/src/consts.js: -------------------------------------------------------------------------------- 1 | import emoji from 'node-emoji'; 2 | 3 | export const apiUrl = 'https://ki9e8xgx8f.execute-api.us-east-1.amazonaws.com'; 4 | 5 | // Dizengoff Center 6 | export const defaultLat = 32.075386; 7 | export const defaultLon = 34.775170; 8 | 9 | // Emoji list can be found here - https://raw.githubusercontent.com/omnidan/node-emoji/master/lib/emoji.json 10 | export const emojiCategories = { 11 | italian: 'spaghetti', 12 | 'Central Asian': 'bento', 13 | asian: 'bento', 14 | american: 'flag-us', 15 | bakery: 'croissant', 16 | bbq: 'meat_on_bone', 17 | breakfast: 'fried_egg', 18 | burger: 'hamburger', 19 | café: 'coffee', 20 | chinese: 'takeout_box', 21 | dessert: 'cookie', 22 | fish: 'tropical_fish', 23 | french: 'flag-fr', 24 | bistro: 'flag-fr', 25 | 'fried chicken': 'poultry_leg', 26 | greek: 'flag-gr', 27 | grill: 'meat_on_bone', 28 | healthy: 'green_salad', 29 | hungarian: 'flag-hu', 30 | 'ice cream': 'ice_cream', 31 | indian: 'flag-in', 32 | japanese: 'flag-jp', 33 | mexican: 'flag-mx', 34 | noodles: 'spaghetti', 35 | pasta: 'spaghetti', 36 | pastry: 'croissant', 37 | salad: 'green_salad', 38 | thai: 'flag-th', 39 | vegan: 'seedling', 40 | vegetarian: 'seedling', 41 | sweets: 'lollipop', 42 | 'bubble tea': 'tropical_drink', 43 | mediterranean: 'stuffed_flatbread', 44 | 'middle eastern': 'stuffed_flatbread', 45 | pita: 'stuffed_flatbread', 46 | poke: 'bowl_with_spoon', 47 | bowl: 'bowl_with_spoon', 48 | soup: 'bowl_with_spoon', 49 | shawarma: 'tamale', 50 | crepes: 'pancakes', 51 | homemade: 'house_with_garden', 52 | local: 'house_with_garden', 53 | juice: 'orange', 54 | kebab: 'tamale', 55 | khachapuri: 'flatbread', 56 | kids: 'baby', 57 | kosher: 'star_of_david', 58 | smoothie: 'tropical_drink', 59 | 'street food': 'stuffed_flatbread', 60 | beyondmeat: 'seedling', 61 | drinks: 'cocktail', 62 | georgian: 'flag-ge', 63 | fruit: 'apple', 64 | }; 65 | 66 | export const categoryNameCorrection = { 67 | bbq: 'BBQ', 68 | }; 69 | 70 | export const formatCategory = (category) => { 71 | try { 72 | return `${(emoji.search(emojiCategories[category] || category)[0] || { emoji: '' }).emoji} ${categoryNameCorrection[category] || category}`; 73 | } catch (err) { 74 | return category; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /web/src/columns.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getDistance } from 'geolib'; 3 | import Highlighter from 'react-highlight-words'; 4 | import { 5 | Badge, Tag, Tooltip, Rate, 6 | } from 'antd'; 7 | import { 8 | CheckCircleOutlined, CloseCircleOutlined, DollarTwoTone, DollarOutlined, StarFilled, 9 | } from '@ant-design/icons'; 10 | import { formatCategory } from './consts'; 11 | 12 | const columns = (latitude, longitude, categories, search) => [ 13 | { 14 | title: 'Restaurant', 15 | dataIndex: ['venue', 'name'], 16 | render: (name, row) => ( 17 |
18 | } placement="right"> 19 | 20 | 25 | 26 | 27 | {' '} 28 | { 29 | row.venue.badges && row.venue.badges[0] && row.venue.badges[0].text === 'New' 30 | && 31 | } 32 |
33 | 38 |
39 | ), 40 | filters: [ 41 | { 42 | text: ( 43 | <> 44 | 45 | {' '} 46 | New on Wolt 47 | 48 | ), 49 | value: 'New', 50 | }, 51 | ], 52 | onFilter: (value, row) => ( 53 | row.venue.badges && row.venue.badges[0] && row.venue.badges[0].text === value 54 | ), 55 | }, 56 | { 57 | title: 'Delivery', 58 | dataIndex: ['venue', 'estimate'], 59 | sorter: (a, b) => a.venue.estimate - b.venue.estimate, 60 | render: (estimate) => `${estimate} min`, 61 | sortDirections: ['ascend', 'descend'], 62 | defaultSortOrder: 'ascend', 63 | }, 64 | { 65 | title: 'Distance', 66 | dataIndex: ['venue', 'location'], 67 | render: (location, row) => `${(getDistance({ latitude, longitude }, { latitude: location[1], longitude: location[0] }) / 1000).toFixed(1)} km (${row.venue.delivery_price})`, 68 | sorter: (a, b) => getDistance( 69 | { latitude, longitude }, { latitude: a.venue.location[1], longitude: a.venue.location[0] }, 70 | ) - getDistance( 71 | { latitude, longitude }, { latitude: b.venue.location[1], longitude: b.venue.location[0] }, 72 | ), 73 | sortDirections: ['ascend', 'descend'], 74 | }, 75 | { 76 | title: 'Wolt Price', 77 | dataIndex: ['venue', 'price_range'], 78 | sorter: (a, b) => a.venue.price_range - b.venue.price_range, 79 | sortDirections: ['descend', 'ascend'], 80 | render: (range) => (index < range ? : )} />, 81 | filters: [ 82 | { 83 | text: (index < 1 ? : )} />, 84 | value: 1, 85 | }, 86 | { 87 | text: (index < 2 ? : )} />, 88 | value: 2, 89 | }, 90 | { 91 | text: (index < 3 ? : )} />, 92 | value: 3, 93 | }, 94 | { 95 | text: (index < 4 ? : )} />, 96 | value: 4, 97 | }, 98 | ], 99 | onFilter: (value, row) => row.venue.price_range === value, 100 | }, 101 | { 102 | title: 'Google Price', 103 | dataIndex: ['google', 'p'], 104 | sorter: (a, b) => a.google.p - b.google.p, 105 | sortDirections: ['descend', 'ascend'], 106 | render: (range) => (index < range ? : )} />, 107 | filters: [ 108 | { 109 | text: (index < 1 ? : )} />, 110 | value: 1, 111 | }, 112 | { 113 | text: (index < 2 ? : )} />, 114 | value: 2, 115 | }, 116 | { 117 | text: (index < 3 ? : )} />, 118 | value: 3, 119 | }, 120 | { 121 | text: (index < 4 ? : )} />, 122 | value: 4, 123 | }, 124 | ], 125 | onFilter: (value, row) => row.google.p === value, 126 | }, 127 | { 128 | title: 'Wolt Rating', 129 | dataIndex: ['venue', 'rating', 'score'], 130 | render: (score) => score || '-', 131 | sorter: (a, b) => { 132 | const venueA = (a.venue.rating && parseFloat(a.venue.rating.score, 10)) || 0; 133 | const venueB = (b.venue.rating && parseFloat(b.venue.rating.score, 10)) || 0; 134 | return venueA - venueB; 135 | }, 136 | sortDirections: ['descend', 'ascend'], 137 | }, 138 | { 139 | title: 'Google Rating', 140 | dataIndex: ['google', 'r'], 141 | render: (score) => (score ? score * 2 : '-'), 142 | sorter: (a, b) => { 143 | const venueA = (a.google.r && parseFloat(a.google.r, 10)) || 0; 144 | const venueB = (b.google.r && parseFloat(b.google.r, 10)) || 0; 145 | return venueA - venueB; 146 | }, 147 | sortDirections: ['descend', 'ascend'], 148 | }, 149 | { 150 | title: 'Tag', 151 | dataIndex: ['venue', 'tags'], 152 | render: (tags) => (tags 153 | ? ( 154 | 155 | 156 | 157 | ) : '' 158 | ), 159 | filters: categories, 160 | filterMode: 'tree', 161 | filterSearch: (input, row) => ( 162 | row.venue.tags[0].toString().toLowerCase() === input.toLowerCase() 163 | ), 164 | onFilter: (value, row) => ( 165 | row.venue.tags[0] && row.venue.tags[0].toString().toLowerCase() === value.toLowerCase() 166 | ), 167 | className: 'tag-column', 168 | }, 169 | { 170 | title: 'Online', 171 | dataIndex: ['venue', 'online'], 172 | render: (isOnline) => : } />, 173 | filters: [ 174 | { 175 | text: 'Online', 176 | value: 'true', 177 | }, 178 | { 179 | text: 'Offline', 180 | value: 'false', 181 | }, 182 | ], 183 | onFilter: (value, row) => row.venue.online.toString() === value, 184 | defaultFilteredValue: ['true'], 185 | }, 186 | ]; 187 | 188 | export default columns; 189 | -------------------------------------------------------------------------------- /web/src/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /* eslint-disable react/jsx-no-comment-textnodes */ 3 | /* eslint-disable react/button-has-type */ 4 | import React, { useState, useEffect, useCallback } from 'react'; 5 | import { 6 | Table, Input, Button, Tooltip, Modal, 7 | } from 'antd'; 8 | import axios from 'axios'; 9 | import { 10 | AimOutlined, 11 | GithubOutlined, 12 | InfoCircleOutlined, 13 | FilterOutlined, 14 | SortAscendingOutlined, 15 | } from '@ant-design/icons'; 16 | import { 17 | apiUrl, defaultLat, defaultLon, formatCategory, 18 | } from './consts'; 19 | import columns from './columns'; 20 | import RandomRestaurant from './randomRestaurant'; 21 | import './App.css'; 22 | 23 | let fetched = false; 24 | 25 | function App() { 26 | const [error, setError] = useState(''); 27 | const [restaurants, setRestaurants] = useState([]); 28 | const [locationEnabled, setLocationEnabled] = useState(false); 29 | const [categories, setCategories] = useState([]); 30 | const [loading, setLoading] = useState(true); 31 | const [search, setSearch] = useState(''); 32 | const [randIdx, setRandIdx] = useState(0); 33 | const [location, setLocation] = useState({ 34 | lat: defaultLat, 35 | lon: defaultLon, 36 | }); 37 | const [isInfoModalVisible, setIsInfoModalVisible] = useState(false); 38 | const [isRandomModalVisible, setIsRandomModalVisible] = useState(false); 39 | const fetchData = useCallback(async (locationData) => { 40 | if (fetched) return; 41 | fetched = true; 42 | const response = await axios.get( 43 | `${apiUrl}/?lat=${locationData.lat}&lon=${locationData.lon}`, 44 | ); 45 | if (!response.data.sections[1].items) { 46 | setError(response.data.sections[1].title); 47 | setLoading(false); 48 | return; 49 | } 50 | const categoriesSorted = Array.from( 51 | new Set(response.data.sections[1].items.map(({ venue }) => venue.tags[0])), 52 | ); 53 | categoriesSorted.sort(); 54 | setCategories( 55 | categoriesSorted 56 | .filter((name) => name) 57 | .map((name) => ({ 58 | text: formatCategory(name), 59 | value: name, 60 | })), 61 | ); 62 | setRestaurants(response.data.sections[1].items); 63 | setLoading(false); 64 | }, []); 65 | 66 | useEffect(() => { 67 | navigator.geolocation.getCurrentPosition( 68 | async (position) => { 69 | fetchData({ 70 | lat: position.coords.latitude, 71 | lon: position.coords.longitude, 72 | }); 73 | setLocation({ 74 | lat: position.coords.latitude, 75 | lon: position.coords.longitude, 76 | }); 77 | setLocationEnabled(true); 78 | }, 79 | async () => { 80 | fetchData(location); 81 | }, 82 | ); 83 | }, [fetchData]); 84 | 85 | const filteredResteraunts = restaurants.filter( 86 | (rest) => rest.venue.name?.toLowerCase().includes(search.toLowerCase()) 87 | || rest.venue.short_description?.toLowerCase().includes(search.toLowerCase()) 88 | || (rest.venue.tags?.length > 0 89 | && rest.venue.tags[0].toLowerCase().includes(search.toLowerCase())), 90 | ); 91 | 92 | const showInfoModal = () => { 93 | setIsInfoModalVisible(true); 94 | }; 95 | const showRandomModal = () => { 96 | setIsRandomModalVisible(true); 97 | }; 98 | 99 | const handleOk = () => { 100 | setIsInfoModalVisible(false); 101 | setIsRandomModalVisible(false); 102 | }; 103 | 104 | const handleCancel = () => { 105 | setIsInfoModalVisible(false); 106 | setIsRandomModalVisible(false); 107 | }; 108 | const randomIdx = (max) => { 109 | setRandIdx(Math.floor(Math.random() * max)); 110 | }; 111 | return ( 112 |
113 |

Wolt Explorer

114 |
115 | setSearch(e.target.value)} 121 | allowClear 122 | /> 123 | 127 |
128 | 131 |
149 |
150 |
151 | {!error 152 | ? ( 153 | row.venue.id} 156 | dataSource={filteredResteraunts} 157 | columns={columns(location.lat, location.lon, categories, search)} 158 | loading={loading} 159 | /> 160 | ) 161 | : (

{error}

)} 162 |
163 | This is an open source project hosted on 164 | {' '} 165 | 170 | 171 | {' '} 172 | GitHub 173 | 174 | . 175 |
176 | We are not affiliated, associated, authorized, endorsed by, or in any way 177 | officially connected with 178 | {' '} 179 | 180 | Wolt 181 | 182 | . 183 |
184 |
185 | 191 |

192 | Wolt Explorer allows advanced filtering and sorting on Wolt 193 | restaurants. 194 |

195 |

196 | Searching can be done by typing into the text search, or by filtering 197 | data in the table columns. 198 | 199 |

200 |

201 | Sorting can be done by clicking on the column header. 202 | 203 |

204 |

205 | Distance and delivery time is based on your location. Make sure your 206 | location is enabled (Green) or disabled (Red). 207 | 208 |

209 |
210 | 216 | { 217 | filteredResteraunts.length > 0 ? ( 218 |
219 | 220 | 230 |
231 | ) : ( 232 | '' 233 | ) 234 | } 235 |
236 | 237 | ); 238 | } 239 | 240 | export default App; 241 | --------------------------------------------------------------------------------