├── .docs └── website.png ├── .env.example ├── .eslintrc.json ├── .gitignore ├── Procfile ├── README.md ├── build ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config └── index.js ├── db ├── activities.json ├── facts.json ├── riddles.json └── websites.json ├── license ├── package-lock.json ├── package.json ├── scripts ├── overwriteDBCollection.js ├── retrieveDBCollection.js └── utils.js ├── server.js ├── src ├── backend │ ├── controllers │ │ ├── activities.js │ │ ├── facts.js │ │ ├── riddles.js │ │ ├── suggestions.js │ │ ├── utils.js │ │ └── websites.js │ ├── keen │ │ └── index.js │ ├── middleware │ │ ├── express.js │ │ └── index.js │ ├── models │ │ ├── Activity.js │ │ ├── Fact.js │ │ ├── Riddle.js │ │ ├── Suggestion.js │ │ ├── Website.js │ │ └── index.js │ └── routes │ │ ├── error.js │ │ ├── index.js │ │ ├── v1 │ │ ├── activities.js │ │ ├── index.js │ │ ├── masks.js │ │ └── suggestions.js │ │ └── v2 │ │ ├── activities.js │ │ ├── facts.js │ │ ├── index.js │ │ ├── masks.js │ │ ├── riddles.js │ │ ├── suggestions.js │ │ └── websites.js └── frontend │ ├── App.vue │ ├── components │ ├── Bottombar.vue │ ├── DocumentationEndpoint.vue │ ├── Info.vue │ ├── Intro.vue │ ├── ResponseDescription.vue │ └── Topbar.vue │ ├── main.js │ ├── pages │ ├── About.vue │ ├── Contributing.vue │ ├── Documentation.vue │ ├── Error.vue │ └── Home.vue │ └── router │ └── index.js ├── static ├── favicon.ico └── index.html └── test ├── backend ├── integration │ ├── v1.activities.test.js │ ├── v1.suggestions.test.js │ ├── v2.activities.test.js │ ├── v2.facts.test.js │ ├── v2.riddles.test.js │ ├── v2.suggestions.test.js │ └── v2.websites.test.js └── utils │ ├── models.js │ ├── mongo.js │ └── server.js └── db ├── activities.test.js ├── facts.test.js ├── riddles.test.js └── websites.test.js /.docs/website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewthoennes/Bored-API/a978ac490c4c3aff555e7453ad8e577b658f8864/.docs/website.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | KEEN_PROJECT_ID= 3 | KEEN_WRITE_KEY= 4 | MONGODB_URI= 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "sourceType": "module", 10 | "allowImportExportEverywhere": true 11 | }, 12 | "rules": { 13 | "array-bracket-newline": ["error", "consistent"], 14 | "array-bracket-spacing": ["error", "never"], 15 | "brace-style": ["error", "stroustrup", {"allowSingleLine": true}], 16 | "block-spacing": ["error", "always"], 17 | "camelcase": ["error", {"properties": "always"}], 18 | "comma-spacing": ["error", {"before": false, "after": true}], 19 | "indent": ["error", "tab"], 20 | "semi": ["error", "always"], 21 | "semi-style": ["error", "last"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | dist/ 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | .env 16 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bored API 2 | A free and simple API to help you find something better to do 3 | 4 | ![Bored API Website](./.docs/website.png) 5 | 6 | ## About 7 | This project is an MEVN (MongoDB, Express.js, Vue.js, and Node.js) web app that has a goal of creating a simple way to find things to do. You do not need an API key to use this API, just query the endpoint to get data. All activities served by the API can be found [here](./activities.json). 8 | 9 | --- 10 | ## Endpoints 11 | The full documentation can be found [here](https://www.boredapi.com/documentation), but listed below are a few of the endpoints. 12 | 13 | #### Random event 14 | Gets a random event 15 | ``` 16 | /api/activity/ 17 | ``` 18 | Response: 19 | ``` 20 | { 21 | "activity": "Learn Express.js", 22 | "accessibility": 0.25, 23 | "type": "education", 24 | "participants": 1, 25 | "price": 0.1, 26 | "link": "https://expressjs.com/", 27 | "key": "3943506" 28 | } 29 | ``` 30 | 31 | #### Get by type 32 | Query for events by a certain type 33 | ``` 34 | /api/activity?type=:type 35 | ``` 36 | Response: 37 | ``` 38 | { 39 | "activity": "Learn how to play a new sport", 40 | "accessibility": 0.2, 41 | "type": "sports", 42 | "participants": 1, 43 | "price": 0.1, 44 | "key": "5808228" 45 | } 46 | ``` 47 | 48 | --- 49 | ## Using 50 | To set up your own Bored API, clone the app, start your MongoDB instance, and run: 51 | ```bash 52 | npm install 53 | npm start 54 | # Started on port 8080 55 | ``` 56 | 57 | --- 58 | ## Contributing 59 | All help is welcome! A pull request or a new issue would be very appreciated. If you want to add more activities, I've created a UI on the [website](https://www.boredapi.com/contributing) to make suggesting easy. 60 | 61 | --- 62 | ## Usage 63 | The Bored API has been used in many other applications and projects: 64 | 65 | * [I'm Bored Alexa skill](https://www.amazon.com/gp/product/B07GDL9MP4?ie=UTF8&ref-suffix=ss_rw) 66 | * [Python wrapper](https://pypi.org/project/bored/) 67 | * [Kotlin wrapper](https://gitlab.com/CMDR_Tvis/bored-api) 68 | * [React app](https://github.com/CDAracena/Im-Bored) 69 | * [Vue app](https://github.com/emilsgulbis/BoredApp) 70 | * [iOS app](https://apps.apple.com/us/app/bored-find-what-to-do/id1475656469) 71 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { VueLoaderPlugin } = require('vue-loader'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | function resolve (dir) { 6 | return path.join(__dirname, '..', dir) 7 | } 8 | 9 | module.exports = { 10 | entry: { 11 | app: './src/frontend/main.js' 12 | }, 13 | output: { 14 | path: resolve('dist'), 15 | filename: '[name].js' 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.vue', '.json'], 19 | alias: { 20 | 'vue$': 'vue/dist/vue.esm.js', 21 | '@': resolve('src/frontend'), 22 | } 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.vue$/, 28 | loader: 'vue-loader' 29 | }, 30 | { 31 | test: /\.js$/, 32 | exclude: /node_modules/, 33 | use: { 34 | loader: 'babel-loader', 35 | options: { 36 | babelrc: false, 37 | presets: [ 38 | '@babel/preset-env' 39 | ], 40 | cacheDirectory: true 41 | } 42 | } 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [ 47 | 'vue-style-loader', 48 | { 49 | loader: 'css-loader' 50 | } 51 | ] 52 | } 53 | ] 54 | }, 55 | plugins: [ 56 | new VueLoaderPlugin(), 57 | new CopyWebpackPlugin([ 58 | { 59 | from: resolve('static'), 60 | to: resolve('dist'), 61 | ignore: ['.*'] 62 | } 63 | ]) 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const baseWebpackConfig = require('./webpack.base.conf'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = merge(baseWebpackConfig, { 11 | mode: 'development', 12 | output: { 13 | path: resolve('dist'), 14 | filename: 'js/[name].[chunkhash].js', 15 | chunkFilename: 'js/[id].[chunkhash].js' 16 | }, 17 | plugins: [ 18 | new HtmlWebpackPlugin({ 19 | template: resolve('static/index.html'), 20 | inject: true 21 | }), 22 | ] 23 | }); 24 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const baseWebpackConfig = require('./webpack.base.conf'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = merge(baseWebpackConfig, { 12 | mode: 'production', 13 | output: { 14 | path: resolve('dist'), 15 | filename: 'js/[name].[chunkhash].js', 16 | chunkFilename: 'js/[id].[chunkhash].js' 17 | }, 18 | plugins: [ 19 | new HtmlWebpackPlugin({ 20 | template: resolve('static/index.html'), 21 | inject: true, 22 | minify: { 23 | removeComments: true, 24 | collapseWhitespace: true, 25 | removeAttributeQuotes: true 26 | } 27 | }), 28 | /* new BundleAnalyzerPlugin() */ 29 | ] 30 | }); 31 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 8080, 3 | database: 'mongodb://localhost:27017/boredapi' 4 | } 5 | -------------------------------------------------------------------------------- /db/activities.json: -------------------------------------------------------------------------------- 1 | {"activity":"Learn Express.js","availability":0.25,"type":"education","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://expressjs.com/","key":"3943506"} 2 | {"activity":"Learn to greet someone in a new language","availability":0.2,"type":"education","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link": "","key":"4704256"} 3 | {"activity":"Learn how to play a new sport","availability":0.2,"type":"recreational","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"5808228"} 4 | {"activity":"Text a friend you haven't talked to in a long time","availability":0.2,"type":"social","participants":2,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6081071"} 5 | {"activity":"Learn a new programming language","availability":0.25,"type":"education","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5881028"} 6 | {"activity":"Learn how to fold a paper crane","availability":0.05,"type":"education","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link": "","key":"3136036"} 7 | {"activity":"Find a DIY to do","availability":0.3,"type":"diy","participants":1,"price":0.4,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4981819"} 8 | {"activity":"Learn about the Golden Ratio","availability":0.2,"type":"education","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Golden_ratio","key":"2095681"} 9 | {"activity":"Volunteer at a local animal shelter","availability":0.5,"type":"charity","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"1382389"} 10 | {"activity":"Play a game of Monopoly","availability":0.3,"type":"social","participants":4,"price":0.2,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"1408058"} 11 | {"activity":"Bake pastries for you and your neighbor","availability":0.3,"type":"cooking","participants":1,"price":0.4,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"8125168"} 12 | {"activity":"Bake something you've never tried before","availability":0.3,"type":"cooking","participants":1,"price":0.4,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"5665663"} 13 | {"activity":"Take your dog on a walk","availability":0.2,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9318514"} 14 | {"activity":"Meditate for five minutes","availability":0.05,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3699502"} 15 | {"activity":"Start a book you've never gotten around to reading","availability":0.1,"type":"relaxation","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"7114122"} 16 | {"activity":"Take a caffeine nap","availability":0.08,"type":"relaxation","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":false,"link":"","key":"5092652"} 17 | {"activity":"Take a bubble bath","availability":0.1,"type":"relaxation","participants":1,"price":0.15,"accessibility":"'Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2581372"} 18 | {"activity":"Go to a nail salon","availability":0.5,"type":"relaxation","participants":1,"price":0.4,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"7526324"} 19 | {"activity":"Listen to your favorite album","availability":0.2,"type":"music","participants":1,"price":0.08,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3136729"} 20 | {"activity":"Learn to play a new instrument","availability":0.6,"type":"music","participants":1,"price":0.55,"accessibility":"Major challenges","duration":"hours","kidFriendly":true,"link":"","key":"3192099"} 21 | {"activity":"Teach your dog a new trick","availability":0.15,"type":"relaxation","participants":1,"price":0.05,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1668223"} 22 | {"activity":"Make a to-do list for your week","availability":0.05,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"5920481"} 23 | {"activity":"Go swimming with a friend","availability":0.1,"type":"social","participants":2,"price":0.1,"accessibility":"Major challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1505028"} 24 | {"activity":"Go on a long drive with no music","availability":0.2,"type":"relaxation","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"4290333"} 25 | {"activity":"Watch a movie you'd never usually watch","availability":0.15,"type":"relaxation","participants":1,"price":0.15,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"9212950"} 26 | {"activity":"Go see a movie in theaters with a few friends","availability":0.3,"type":"social","participants":4,"price":0.2,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"5262759"} 27 | {"activity":"Draw and color a Mandala","availability":0.1,"type":"relaxation","participants":1,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Mandala","key":"4614092"} 28 | {"activity":"Rearrange and organize your room","availability":0.15,"type":"busywork","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6197243"} 29 | {"activity":"Pot some plants and put them around your house","availability":0.3,"type":"relaxation","participants":1,"price":0.4,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6613330"} 30 | {"activity":"Plan a vacation you've always wanted to take","availability":0.05,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"7265395"} 31 | {"activity":"Take your cat on a walk","availability":0.1,"type":"relaxation","participants":1,"price":0.02,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"5940465"} 32 | {"activity":"Have a football scrimmage with some friends","availability":0.2,"type":"social","participants":8,"price":0,"accessibility":"Major challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1638604"} 33 | {"activity":"Fill out a basketball bracket","availability":0.1,"type":"recreational","participants":1,"price":0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"7806284"} 34 | {"activity":"Play a game of tennis with a friend","availability":0.4,"type":"social","participants":2,"price":0.1,"accessibility":"Major challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1093640"} 35 | {"activity":"Catch up with a friend over a lunch date","availability":0.15,"type":"social","participants":2,"price":0.2,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5590133"} 36 | {"activity":"Learn how to iceskate or rollerskate","availability":0.25,"type":"recreational","participants":1,"price":0.1,"accessibility":"Major challenges","duration":"days","kidFriendly":true,"link":"","key":"5947957"} 37 | {"activity":"Go to a concert with local artists with some friends","availability":0.3,"type":"social","participants":3,"price":0.4,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"2211716"} 38 | {"activity":"Explore the nightlife of your city","availability":0.32,"type":"social","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":false,"link":"","key":"2237769"} 39 | {"activity":"Fix something that's broken in your house","availability":0.3,"type":"diy","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6925988"} 40 | {"activity":"Wash your car","availability":0.15,"type":"busywork","participants":1,"price":0.05,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1017771"} 41 | {"activity":"Find a charity and donate to it","availability":0.1,"type":"charity","participants":1,"price":0.4,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1488053"} 42 | {"activity":"Hold a yard sale","availability":0.1,"type":"social","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"days","kidFriendly":true,"link":"","key":"1432113"} 43 | {"activity":"Donate blood at a local blood center","availability":0.35,"type":"charity","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":false,"link":"https://www.redcross.org/give-blood","key":"6509779"} 44 | {"activity":"Volunteer and help out at a senior center","availability":0.1,"type":"charity","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"3920096"} 45 | {"activity":"Shop at support your local farmers market","availability":0.1,"type":"relaxation","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8979931"} 46 | {"activity":"Learn a new recipe","availability":0.05,"type":"cooking","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6553978"} 47 | {"activity":"Create a cookbook with your favorite recipes","availability":0.05,"type":"cooking","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"1947449"} 48 | {"activity":"Create a compost pile","availability":0.15,"type":"diy","participants":1,"price":0.0,"accessibility":"Major challenges","duration":"hours","kidFriendly":true,"link":"","key":"8631548"} 49 | {"activity":"Volunteer at your local food bank","availability":0.1,"type":"charity","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"2055368"} 50 | {"activity":"Create or update your resume","availability":0.1,"type":"busywork","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9364041"} 51 | {"activity":"Paint the first thing you see","availability":0.2,"type":"recreational","participants":1,"price":0.25,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1162360"} 52 | {"activity":"Start a blog for something you're passionate about","availability":0.1,"type":"recreational","participants":1,"price":0.05,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"8364626"} 53 | {"activity":"Start a garden","availability":0.35,"type":"recreational","participants":1,"price":0.3,"accessibility":"Major challenges","duration":"hours","kidFriendly":true,"link":"","key":"1934228"} 54 | {"activity":"Clean out your closet and donate the clothes you've outgrown","availability":0.1,"type":"charity","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9026787"} 55 | {"activity":"Catch up on world news","availability":0.07,"type":"recreational","participants":1,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6596257"} 56 | {"activity":"Create a personal website","availability":0.12,"type":"recreational","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"3141417"} 57 | {"activity":"Listen to a new podcast","availability":0.12,"type":"relaxation","participants":1,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4124860"} 58 | {"activity":"Have a paper airplane contest with some friends","availability":0.05,"type":"social","participants":4,"price":0.02,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8557562"} 59 | {"activity":"Learn calligraphy","availability":0.1,"type":"education","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"weeks","kidFriendly":true,"link":"","key":"4565537"} 60 | {"activity":"Start a collection","availability":0.5,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"1718657"} 61 | {"activity":"Go to a local thrift shop","availability":0.2,"type":"recreational","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8503795"} 62 | {"activity":"Make a couch fort","availability":0.08,"type":"recreational","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2352669"} 63 | {"activity":"Pick up litter around your favorite park","availability":0.05,"type":"charity","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4894697"} 64 | {"activity":"Buy a new house decoration","availability":0.3,"type":"recreational","participants":1,"price":0.4,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3456114"} 65 | {"activity":"Write a thank you letter to an influential person in your life","availability":0.1,"type":"social","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4101229"} 66 | {"activity":"Clean out your car","availability":0.08,"type":"busywork","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2896176"} 67 | {"activity":"Write a short story","availability":0.1,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"6301585"} 68 | {"activity":"Do something nice for someone you care about","availability":0.1,"type":"social","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8321894"} 69 | {"activity":"Think of a new business idea","availability":0.05,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6808057"} 70 | {"activity":"Clean out your garage","availability":0.1,"type":"busywork","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"7023703"} 71 | {"activity":"Learn to sew on a button","availability":0.1,"type":"education","participants":1,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8731971"} 72 | {"activity":"Learn how to french braid hair","availability":0.1,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8926492"} 73 | {"activity":"Learn how to whistle with your fingers","availability":0.0,"type":"education","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2790297"} 74 | {"activity":"Learn to write with your nondominant hand","availability":0.02,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1645485"} 75 | {"activity":"Make bread from scratch","availability":0.2,"type":"cooking","participants":1,"price":0.2,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"4809815"} 76 | {"activity":"Make a budget","availability":0.1,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4379552"} 77 | {"activity":"Learn how to write in shorthand","availability":0.1,"type":"education","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"days","kidFriendly":true,"link":"","key":"6778219"} 78 | {"activity":"Make a simple musical instrument","availability":0.25,"type":"music","participants":1,"price":0.4,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"7091374"} 79 | {"activity":"Go to the gym","availability":0.1,"type":"recreational","participants":1,"price":0.2,"accessibility":"Minor challenges","duration":"hours","kidFriendly":false,"link":"","key":"4387026"} 80 | {"activity":"Try a food you don't like","availability":0.05,"type":"recreational","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6693574"} 81 | {"activity":"Conquer one of your fears","availability":0.1,"type":"recreational","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8344539"} 82 | {"activity":"Go to a concert with some friends","availability":0.4,"type":"social","participants":4,"price":0.6,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"4558850"} 83 | {"activity":"Go to the library and find an interesting book","availability":0.2,"type":"relaxation","participants":1,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8253550"} 84 | {"activity":"Go to an escape room","availability":0.3,"type":"social","participants":4,"price":0.5,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"5585829"} 85 | {"activity":"Go to a karaoke bar with some friends","availability":0.35,"type":"social","participants":4,"price":0.5,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"9072906"} 86 | {"activity":"Repaint a room in your house","availability":0.4,"type":"recreational","participants":1,"price":0.3,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"4877086"} 87 | {"activity":"Pull a harmless prank on one of your friends","availability":0.2,"type":"social","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1288934"} 88 | {"activity":"Take a spontaneous road trip with some friends","availability":0.3,"type":"social","participants":4,"price":0.2,"accessibility":"Few to no challenges","duration":"days","kidFriendly":true,"link":"","key":"2085321"} 89 | {"activity":"Go stargazing","availability":0.1,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8832605"} 90 | {"activity":"Invite some friends over for a game night","availability":0.2,"type":"social","participants":4,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"6613428"} 91 | {"activity":"Make homemade ice cream","availability":0.2,"type":"cooking","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3818400"} 92 | {"activity":"Start a daily journal","availability":0.0,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8779876"} 93 | {"activity":"Go to a music festival with some friends","availability":0.2,"type":"social","participants":4,"price":0.4,"accessibility":"Minor challenges","duration":"days","kidFriendly":true,"link":"","key":"6482790"} 94 | {"activity":"Make a bucket list","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2735499"} 95 | {"activity":"Binge watch a trending series","availability":0.2,"type":"recreational","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5881647"} 96 | {"activity":"Learn how to make a website","availability":0.3,"type":"education","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"9924423"} 97 | {"activity":"Create and follow a savings plan","availability":0.2,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9366464"} 98 | {"activity":"Watch a classic movie","availability":0.1,"type":"recreational","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8081693"} 99 | {"activity":"Plan a trip to another country","availability":0.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5554727"} 100 | {"activity":"Learn how the internet works","availability":0.1,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9414706"} 101 | {"activity":"Take a hike at a local park","availability":0.1,"type":"recreational","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"8724324"} 102 | {"activity":"Make tie dye shirts","availability":0.2,"type":"diy","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8092359"} 103 | {"activity":"Make a scrapbook with pictures of your favorite memories","availability":0.1,"type":"diy","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5554174"} 104 | {"activity":"Have a picnic with some friends","availability":0.1,"type":"social","participants":3,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"6813070"} 105 | {"activity":"Have a bonfire with your close friends","availability":0.1,"type":"social","participants":4,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"8442249"} 106 | {"activity":"Memorize the fifty states and their capitals","availability":0.0,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4179309"} 107 | {"activity":"Take a class at your local community center that interests you","availability":0.15,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8750692"} 108 | {"activity":"Resolve a problem you've been putting off","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9999999"} 109 | {"activity":"Make a new friend","availability":0.0,"type":"social","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1000000"} 110 | {"activity":"Learn origami","availability":0.3,"type":"education","participants":1,"price":0.2,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8394738"} 111 | {"activity":"Learn how to use a french press","availability":0.3,"type":"recreational","participants":1,"price":0.3,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/French_press","key":"4522866"} 112 | {"activity":"Read a formal research paper on an interesting subject","availability":0.1,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3352474"} 113 | {"activity":"Listen to a new music genre","availability":0,"type":"music","participants":1,"price":0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4708863"} 114 | {"activity":"Volunteer at your local food pantry","availability":0.1,"type":"charity","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"1878070"} 115 | {"activity":"Learn how to make an Alexa skill","availability":0.1,"type":"education","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"https://developer.amazon.com/en-US/docs/alexa/custom-skills/steps-to-build-a-custom-skill.html","key":"1592381"} 116 | {"activity":"Surprise your significant other with something considerate","availability":0.0,"type":"social","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6204657"} 117 | {"activity":"Learn Kotlin","availability":0.8,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://kotlinlang.org/","key":"3950821"} 118 | {"activity":"Go for a run","availability":0.9,"type":"recreational","participants":1,"price":0.0,"accessibility":"Major challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6852505"} 119 | {"activity":"Learn woodworking","availability":0.3,"type":"diy","participants":1,"price":0.3,"accessibility":"Minor challenges","duration":"weeks","kidFriendly":true,"link":"","key":"9216391"} 120 | {"activity":"Start a band","availability":0.8,"type":"music","participants":4,"price":0.3,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"5675880"} 121 | {"activity":"Cook something together with someone","availability":0.8,"type":"cooking","participants":2,"price":0.3,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"1799120"} 122 | {"activity":"Have a jam session with your friends","availability":0.3,"type":"music","participants":5,"price":0.1,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"2715253"} 123 | {"activity":"Learn GraphQL","availability":0.8,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://graphql.org/","key":"2167064"} 124 | {"activity":"Play basketball with a group of friends","availability":0.7,"type":"social","participants":5,"price":0.0,"accessibility":"Major challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8683473"} 125 | {"activity":"Learn the Chinese erhu","availability":0.4,"type":"music","participants":1,"price":0.6,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"2742452"} 126 | {"activity":"Start a webinar on a topic of your choice","availability":0.9,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"6826029"} 127 | {"activity":"Learn how to use an Arduino","availability":0.7,"type":"education","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Arduino","key":"8264223"} 128 | {"activity":"Go see a Broadway production","availability":0.3,"type":"recreational","participants":4,"price":0.8,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"4448913"} 129 | {"activity":"Learn Javascript","availability":0.9,"type":"education","participants":1,"price":0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"3469378"} 130 | {"activity":"Visit your past teachers","availability":0.7,"type":"social","participants":1,"price":0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8238918"} 131 | {"activity":"Research a topic you're interested in","availability":0.9,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"3561421"} 132 | {"activity":"Listen to music you haven't heard in a while","availability":0.9,"type":"music","participants":1,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4296813"} 133 | {"activity":"Go on a fishing trip with some friends","availability":0.4,"type":"social","participants":3,"price":0.4,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"3149232"} 134 | {"activity":"Bake a pie with some friends","availability":0.3,"type":"cooking","participants":3,"price":0.3,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"3141592"} 135 | {"activity":"Do yoga","availability":0.9,"type":"recreational","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4688012"} 136 | {"activity":"Visit a nearby museum","availability":0.7,"type":"recreational","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5490351"} 137 | {"activity":"Have a photo session with some friends","availability":0.8,"type":"social","participants":4,"price":0.05,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3305912"} 138 | {"activity":"Watch the sunset or the sunrise","availability":1.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4748214"} 139 | {"activity":"Play a volleyball match with some friends","availability":0.3,"type":"social","participants":4,"price":0.0,"accessibility":"Major challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4306710"} 140 | {"activity":"Host a movie marathon with some friends","availability":0.0,"type":"social","participants":3,"price":0.1,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5914292"} 141 | {"activity":"Hold a video game tournament with some friends","availability":0.1,"type":"social","participants":4,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"2300257"} 142 | {"activity":"Write a poem","availability":0.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8688620"} 143 | {"activity":"Take a nap","availability":0.0,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6184514"} 144 | {"activity":"Mow your lawn","availability":0.3,"type":"busywork","participants":1,"price":0.1,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3590127"} 145 | {"activity":"Practice coding in your favorite lanaguage","availability":0.1,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"7096020"} 146 | {"activity":"Write a song","availability":0.0,"type":"music","participants":1,"price":0.0,"accessibility":"Few to no challenges'","duration":"minutes","kidFriendly":true,"link":"","key":"5188388"} 147 | {"activity":"Play a video game","availability":0.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"5534113"} 148 | {"activity":"Clean out your refrigerator","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9324336"} 149 | {"activity":"Study a foreign language","availability":0.1,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"weeks","kidFriendly":true,"link":"","key":"9765530"} 150 | {"activity":"Learn the NATO phonetic alphabet","availability":0.0,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/NATO_phonetic_alphabet","key":"6706598"} 151 | {"activity":"Solve a Rubik's cube","availability":0.1,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"4151544"} 152 | {"activity":"Make your own LEGO creation","availability":0.1,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1129748"} 153 | {"activity":"Plant a tree","availability":0.3,"type":"recreational","participants":1,"price":0.3,"accessibility":"Minor challenges","duration":"hours","kidFriendly":true,"link":"","key":"1942393"} 154 | {"activity":"Contribute code or a monetary donation to an open-source software project","availability":0.0,"type":"charity","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://github.com/explore","key":"7687030"} 155 | {"activity":"Uninstall unused apps from your devices","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2850593"} 156 | {"activity":"Prepare a dish from a foreign culture","availability":0.3,"type":"cooking","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8061754"} 157 | {"activity":"Patronize a local independent restaurant","availability":0.1,"type":"recreational","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"5319204"} 158 | {"activity":"Give your pet ten minutes of focused attention","availability":0.0,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9937387"} 159 | {"activity":"Explore a park you have never been to before","availability":0.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8159356"} 160 | {"activity":"Configure two-factor authentication on your accounts","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Multi-factor_authentication","key":"1572120"} 161 | {"activity":"Create a meal plan for the coming week","availability":0.0,"type":"cooking","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3491470"} 162 | {"activity":"Watch a Khan Academy lecture on a subject of your choosing","availability":0.0,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://www.khanacademy.org/","key":"7154873"} 163 | {"activity":"Shred old documents you don't need anymore","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2430066"} 164 | {"activity":"Learn about a distributed version control system such as Git","availability":0.0,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Distributed_version_control","key":"9303608"} 165 | {"activity":"Write a note of appreciation to someone","availability":0.0,"type":"social","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"1770521"} 166 | {"activity":"Draw something interesting","availability":0.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8033599"} 167 | {"activity":"Learn and play a new card game","availability":0.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://www.pagat.com","key":"9660022"} 168 | {"activity":"Write a list of things you are grateful for","availability":0.0,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2062010"} 169 | {"activity":"Organize a bookshelf","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"6098037"} 170 | {"activity":"Organize a cluttered drawer","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9714586"} 171 | {"activity":"Organize your music collection","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"3151646"} 172 | {"activity":"Organize your movie collection","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"6378359"} 173 | {"activity":"Learn Morse code","availability":0.0,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Morse_code","key":"3646173"} 174 | {"activity":"Go for a walk","availability":0.1,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4286250"} 175 | {"activity":"Mow your neighbor's lawn","availability":0.2,"type":"charity","participants":1,"price":0.0,"accessibility":"Minor challenges","duration":"minutes","kidFriendly":false,"link":"","key":"1303906"} 176 | {"activity":"Compliment someone","availability":0.0,"type":"social","participants":2,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9149470"} 177 | {"activity":"Draft your living will","availability":0.5,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":false,"link":"https://www.investopedia.com/terms/l/livingwill.asp","key":"2360432"} 178 | {"activity":"Prepare a 72-hour kit","availability":0.5,"type":"busywork","participants":1,"price":0.5,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://www.ready.gov/kit","key":"4266522"} 179 | {"activity":"Color","availability":0.0,"type":"relaxation","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"5322987"} 180 | {"activity":"Organize your pantry","availability":0.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"3954882"} 181 | {"activity":"Back up important computer files","availability":0.2,"type":"busywork","participants":1,"price":0.2,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":false,"link":"","key":"9081214"} 182 | {"activity":"Memorize a favorite quote or poem","availability":0.8,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"9008639"} 183 | {"activity":"Do something you used to do as a kid","availability":0.8,"type":"relaxation","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"8827573"} 184 | {"activity":"Organize your basement","availability":0.9,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"","key":"8203595"} 185 | {"activity":"Sit in the dark and listen to your favorite music with no distractions","availability":1.0,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":false,"link":"","key":"9908721"} 186 | {"activity":"Do a jigsaw puzzle","availability":1.0,"type":"recreational","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Jigsaw_puzzle","key":"8550768"} 187 | {"activity":"Look at pictures and videos of cute animals","availability":1.0,"type":"relaxation","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2565076"} 188 | {"activity":"Write a handwritten letter to somebody","availability":0.8,"type":"social","participants":1,"price":0.1,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"2277801"} 189 | {"activity":"Match your storage containers with their lids","availability":1.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4940907"} 190 | {"activity":"Start a family tree","availability":1.0,"type":"social","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Family_tree","key":"6825484"} 191 | {"activity":"Look at your finances and find one way to save money","availability":1.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":false,"link":"","key":"5977626"} 192 | {"activity":"Organize your dresser","availability":1.0,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"7556665"} 193 | {"activity":"Learn the periodic table","availability":0.6,"type":"education","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Periodic_table","key":"3621244"} 194 | {"activity":"Improve your touch typing","availability":0.8,"type":"busywork","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":false,"link":"https://en.wikipedia.org/wiki/Touch_typing","key":"2526437"} 195 | {"activity":"Donate to your local food bank","availability":0.8,"type":"charity","participants":1,"price":0.5,"accessibility":"Few to no challenges","duration":"minutes","kidFriendly":true,"link":"","key":"4150284"} 196 | {"activity":"Learn how to beatbox","availability":1.0,"type":"recreational","participants":1,"price":0.0,"accessibility":"Few to no challenges","duration":"hours","kidFriendly":true,"link":"https://en.wikipedia.org/wiki/Beatboxing","key":"8731710"} 197 | -------------------------------------------------------------------------------- /db/facts.json: -------------------------------------------------------------------------------- 1 | {"fact":"The first computer was invented in the 1940s.","source":"https://bestlifeonline.com/random-fun-facts","key":"8929851"} 2 | {"fact":"The unicorn is the national animal of Scotland.","source":"https://bestlifeonline.com/random-fun-facts/","key":"4920184"} 3 | {"fact":"Playing the accordion was once required for teachers in North Korea.","source":"https://bestlifeonline.com/random-fun-facts/","key":"1848104"} 4 | {"fact":"Water makes different pouring sounds depending on its temperature.","source":"https://bestlifeonline.com/random-fun-facts/","key":"2562345"} 5 | -------------------------------------------------------------------------------- /db/riddles.json: -------------------------------------------------------------------------------- 1 | {"question":"You live in a one story house made entirely of redwood. What color would the stairs be?","answer":"What stairs? You live in a one-story house.","difficulty":"easy","source":"https://www.riddles.com/","key":"3684252"} 2 | {"question":"What has six faces, but does not wear makeup, has twenty-one eyes, but cannot see? What is it?","answer":"A die.","difficulty":"easy","source":"https://www.riddles.com/","key":"7643252"} 3 | {"question":"What can you catch but never throw?","answer":"A cold.","difficulty":"normal","source":"https://www.riddles.com/","key":"7974324"} 4 | {"question":"Who is that with a neck and no head, two arms and no hands? What is it?","answer":"A shirt.","difficulty":"hard","source":"https://www.riddles.com/","key":"2347324"} 5 | -------------------------------------------------------------------------------- /db/websites.json: -------------------------------------------------------------------------------- 1 | {"url":"https://weirdorconfusing.com","description":"Random random being sold.","key":"4298747"} 2 | {"url":"http://eelslap.com","description":"Eel slap.","key":"8738873"} 3 | {"url":"https://heeeeeeeey.com","description":"Hey there.","key":"9723037"} 4 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Drew Thoennes 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boredapi", 3 | "version": "1.1.0", 4 | "description": "A simple and free API to find things to do when you're bored", 5 | "scripts": { 6 | "build-dev": "npm run clean; webpack --inline --progress --color --config build/webpack.dev.conf.js", 7 | "build": "npm run clean; webpack --inline --progress --color --config build/webpack.prod.conf.js", 8 | "start-dev": "export NODE_ENV=development; npm run build-dev && node server.js", 9 | "start": "export NODE_ENV=production; npm run build && node server.js", 10 | "clean": "rm -r dist/*", 11 | "test-i": "mocha test/backend/integration --timeout 60000", 12 | "test-db": "mocha test/db --timeout 60000", 13 | "test": "npm run test-i && npm run test-db" 14 | }, 15 | "license": "MIT", 16 | "_moduleAliases": { 17 | "@": "./", 18 | "@b": "./src/backend", 19 | "@f": "./src/frontend", 20 | "@t": "./test", 21 | "@s": "./scripts" 22 | }, 23 | "dependencies": { 24 | "@hapi/joi": "^17.1.1", 25 | "body-parser": "^1.19.0", 26 | "chalk": "^4.0.0", 27 | "dotenv": "^8.2.0", 28 | "express": "^4.17.1", 29 | "keen-tracking": "^4.4.1", 30 | "module-alias": "^2.2.2", 31 | "mongoose": "^5.9.9", 32 | "vue": "^2.6.11", 33 | "vue-notification": "^1.3.20", 34 | "vue-resource": "^1.5.1", 35 | "vue-router": "^3.1.6" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.9.0", 39 | "@babel/preset-env": "^7.9.5", 40 | "ajv": "^6.12.0", 41 | "babel-eslint": "^10.1.0", 42 | "babel-loader": "^8.0.6", 43 | "chai": "^4.2.0", 44 | "chai-as-promised": "^7.1.1", 45 | "chai-http": "^4.3.0", 46 | "copy-webpack-plugin": "^5.1.1", 47 | "css-loader": "^3.5.2", 48 | "eslint": "^7.0.0", 49 | "faker": "^4.1.0", 50 | "html-webpack-plugin": "^4.2.0", 51 | "mongodb-memory-server": "^6.5.2", 52 | "sinon": "^9.0.2", 53 | "vue-loader": "^15.9.1", 54 | "vue-style-loader": "^4.1.2", 55 | "vue-template-compiler": "^2.6.11", 56 | "webpack": "^4.42.1", 57 | "webpack-bundle-analyzer": "^3.6.1", 58 | "webpack-cli": "^3.3.11", 59 | "webpack-merge": "^4.2.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /scripts/overwriteDBCollection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * overwriteDBCollection.js 3 | * Overwrites a collection from your mLab instance\ 4 | * 5 | * node overwriteDBCollection.js 6 | * 7 | * @arg collection The name of the collection to be retrieved 8 | * @arg input The JSON to be used to overwite the collection 9 | */ 10 | 11 | require('module-alias/register'); 12 | const chalk = require('chalk') 13 | const { 14 | exec, 15 | MONGODB_HOST, 16 | MONGODB_DB, 17 | MONGODB_USERNAME, 18 | MONGODB_PASSWORD 19 | } = require('@s/utils'); 20 | 21 | if (process.argv.length < 4) { 22 | console.log(chalk.red('Invalid format: Missing necessary arguments')) 23 | return; 24 | }; 25 | 26 | exec(`mongoimport -h ${MONGODB_HOST} -d ${MONGODB_DB} -c ${process.argv[2]} -u ${MONGODB_USERNAME} -p ${MONGODB_PASSWORD} --file ${process.argv[3]} --drop`).then((stdout, stderr) => { 27 | if (stderr && stderr !== '') throw new Error(stderr); 28 | 29 | console.log(chalk.green(`Successfully overwrote collection`)); 30 | }).catch(err => { 31 | console.log(chalk.red(err)); 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/retrieveDBCollection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * retrieveDBCollection.js 3 | * Retrieves a collection from your mLab instance specified by the MONGODB_URI in the .env 4 | * 5 | * node retrieveDBCollection.js 6 | * 7 | * @arg collection The name of the collection to be retrieved 8 | * @arg output The name of the output file 9 | */ 10 | 11 | require('module-alias/register'); 12 | const chalk = require('chalk') 13 | const { 14 | exec, 15 | MONGODB_HOST, 16 | MONGODB_DB, 17 | MONGODB_USERNAME, 18 | MONGODB_PASSWORD 19 | } = require('@s/utils'); 20 | 21 | if (process.argv.length < 4) { 22 | console.log(chalk.red('Invalid format: Missing necessary arguments')) 23 | return; 24 | }; 25 | 26 | exec(`mongoexport -h ${MONGODB_HOST} -d ${MONGODB_DB} -c ${process.argv[2]} -u ${MONGODB_USERNAME} -p ${MONGODB_PASSWORD} -o ${process.argv[3]}`).then((stdout, stderr) => { 27 | if (stderr && stderr !== '') throw new Error(stderr); 28 | 29 | console.log(chalk.green(`Successfully retrieved collection as ${process.argv[3]}`)); 30 | }).catch(err => { 31 | console.log(chalk.red(err)); 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const fs = require('fs'); 3 | const util = require('util'); 4 | 5 | require('dotenv').config({path: __dirname + '/../.env'}); 6 | 7 | const matches = (/mongodb:\/\/([^:]*):([^@]*)@([^\/]*)\/(.*)/g).exec(process.env.MONGODB_URI); 8 | if (!matches || matches.length < 5) throw new Error('Cannot parse MONGODB_URI due to invalid format'); 9 | 10 | module.exports = { 11 | exec: util.promisify(require('child_process').exec), 12 | MONGODB_USERNAME: matches[1], 13 | MONGODB_PASSWORD: matches[2], 14 | MONGODB_HOST: matches[3], 15 | MONGODB_DB: matches[4] 16 | }; 17 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | var express = require('express'); 3 | var path = require('path'); 4 | var bodyParser = require('body-parser'); 5 | var mongoose = require('mongoose'); 6 | var chalk = require('chalk'); 7 | var config = require('./config'); 8 | 9 | require('dotenv').config(); 10 | 11 | app = express(); 12 | 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({extended: false})); 15 | 16 | // Allow CORS 17 | app.use(function(req, res, next) { 18 | res.header('Access-Control-Allow-Origin', '*'); 19 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 20 | next(); 21 | }); 22 | 23 | app.get('/favicon.ico', (req, res) => { 24 | res.sendFile(`${__dirname}/static/favicon.ico`); 25 | }); 26 | 27 | // Backend API routes 28 | app.use(require('./src/backend/routes')()); 29 | 30 | // Catch any errors 31 | app.use((err, req, res, next) => { 32 | if (err) { 33 | res.error(`There was an error parsing the request: ${err}`); 34 | return; 35 | } 36 | 37 | next(); 38 | }); 39 | 40 | // Frontend endpoints 41 | app.use(express.static(__dirname + '/dist')); 42 | app.use('/', express.static(__dirname + '/dist')); 43 | 44 | // Catch all for frontend routes 45 | app.all('/*', function(req, res) { 46 | res.sendFile(path.join(__dirname, '/dist', '/index.html')); 47 | }); 48 | 49 | const PORT = process.env.PORT || config.port; 50 | app.listen(PORT); 51 | 52 | console.log(chalk.green('Started on port ' + PORT)); 53 | 54 | // Connect to MongoDB 55 | const DATABASE = process.env.NODE_ENV === 'production' ? process.env.MONGODB_URI : config.database; 56 | 57 | mongoose.Promise = global.Promise; 58 | mongoose.connect(DATABASE, { useNewUrlParser: true, useUnifiedTopology: true }) 59 | .then(res => { 60 | console.log(chalk.green('Connected to MongoDB: ' + DATABASE)); 61 | }).catch(err => { 62 | console.log(chalk.red('Error connecting to MongoDB: ' + err)); 63 | } 64 | ); 65 | -------------------------------------------------------------------------------- /src/backend/controllers/activities.js: -------------------------------------------------------------------------------- 1 | const Activity = require('@b/models/Activity'); 2 | 3 | exports.findActivity = params => { 4 | return Activity.findOne(params).then(activity => { 5 | if (!activity) { 6 | throw new Error('No activities found with the specified parameters'); 7 | } 8 | 9 | return activity; 10 | }); 11 | }; 12 | 13 | exports.findRandomActivity = params => { 14 | return Activity.countDocuments(params).then(count => { 15 | if (!count || count === 0) throw new Error('No activity found with the specified parameters'); 16 | 17 | return Activity.findOne(params).skip(Math.floor(Math.random() * count)); 18 | }).then(activity => { 19 | if (!activity) throw new Error('No activity found with the specified parameters'); 20 | 21 | return activity; 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/backend/controllers/facts.js: -------------------------------------------------------------------------------- 1 | const Fact = require('@b/models/Fact'); 2 | 3 | exports.findFactByKey = key => { 4 | return Fact.findOne({'key': key}).then(fact => { 5 | if (!fact) { 6 | throw new Error('No facts found with the specified parameters'); 7 | } 8 | 9 | return fact; 10 | }); 11 | }; 12 | 13 | exports.findRandomFact = () => { 14 | return Fact.countDocuments().then(count => { 15 | if (!count || count === 0) throw new Error('No facts found'); 16 | 17 | return Fact.findOne().skip(Math.floor(Math.random() * count)); 18 | }).then(fact => { 19 | if (!fact) throw new Error('No facts found'); 20 | 21 | return fact; 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/backend/controllers/riddles.js: -------------------------------------------------------------------------------- 1 | const Riddle = require('@b/models/Riddle'); 2 | 3 | exports.findRiddle = params => { 4 | return Riddle.findOne(params).then(riddle => { 5 | if (!riddle) { 6 | throw new Error('No riddles found with the specified parameters'); 7 | } 8 | 9 | return riddle; 10 | }); 11 | }; 12 | 13 | exports.findRandomRiddle = params => { 14 | return Riddle.countDocuments(params).then(count => { 15 | if (!count || count === 0) throw new Error('No riddles found with the specified query'); 16 | 17 | return Riddle.findOne(params).skip(Math.floor(Math.random() * count)); 18 | }).then(riddle => { 19 | if (!riddle) throw new Error('No riddles found with the specified query'); 20 | 21 | return riddle; 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/backend/controllers/suggestions.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewthoennes/Bored-API/a978ac490c4c3aff555e7453ad8e577b658f8864/src/backend/controllers/suggestions.js -------------------------------------------------------------------------------- /src/backend/controllers/utils.js: -------------------------------------------------------------------------------- 1 | exports.stringSanitize = (input) => { 2 | return input.replace(/[|&;$%@"<>()+,]/g, ""); 3 | }; 4 | 5 | exports.generateKey = () => { 6 | return String(1000000 + Math.floor(Math.random() * 9000000)); 7 | }; 8 | -------------------------------------------------------------------------------- /src/backend/controllers/websites.js: -------------------------------------------------------------------------------- 1 | const Website = require('@b/models/Website'); 2 | 3 | exports.findWebsiteByKey = key => { 4 | return Website.findOne({'key': key}).then(website => { 5 | if (!website) { 6 | throw new Error('No websites found with the specified parameters'); 7 | } 8 | 9 | return website; 10 | }); 11 | }; 12 | 13 | exports.findRandomWebsite = () => { 14 | return Website.countDocuments().then(count => { 15 | if (!count || count === 0) throw new Error('No websites found'); 16 | 17 | return Website.findOne().skip(Math.floor(Math.random() * count)); 18 | }).then(website => { 19 | if (!website) throw new Error('No websites found'); 20 | 21 | return website; 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/backend/keen/index.js: -------------------------------------------------------------------------------- 1 | const Keen = require('keen-tracking'); 2 | 3 | const client = new Keen({ 4 | projectId: process.env.KEEN_PROJECT_ID, 5 | writeKey: process.env.KEEN_WRITE_KEY 6 | }); 7 | 8 | exports.logQuery = (type, query) => { 9 | if (process.env.NODE_ENV === 'dev') return; 10 | 11 | return client.recordEvent('query', { 12 | type, 13 | query 14 | }, err => { 15 | if (err) console.log(err); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/backend/middleware/express.js: -------------------------------------------------------------------------------- 1 | module.exports = function(router) { 2 | // Add res.error method for error reporting 3 | router.use((req, res, next) => { 4 | res.error = function(err) { 5 | res.json({'error': err}); 6 | }; 7 | 8 | next(); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/backend/middleware/index.js: -------------------------------------------------------------------------------- 1 | const expressMiddleware = require('./express'); 2 | 3 | exports.validate = (schema) => { 4 | return (req, res, next) => { 5 | let err = schema.validate(req.body).error; 6 | if (err != null) { 7 | res.error(`Invalid fields: ${err}`); 8 | return; 9 | } 10 | 11 | next(); 12 | }; 13 | } 14 | 15 | exports.express = function(router) { 16 | // Add res.error method for error reporting 17 | router.use((req, res, next) => { 18 | res.error = function(err) { 19 | res.json({'error': err}); 20 | }; 21 | 22 | next(); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/backend/models/Activity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const activitySchema = new mongoose.Schema({ 4 | activity: { 5 | type: String, 6 | trim: true, 7 | required: true 8 | }, 9 | type: { 10 | type: String, 11 | enum: ['charity', 'cooking', 'music', 'diy', 'education', 'social', 'busywork', 'recreational', 'relaxation'], 12 | required: true 13 | }, 14 | participants: { // 1 - n 15 | type: Number, 16 | required: true 17 | }, 18 | price: { // 0.0 - 1.0 19 | type: Number, 20 | required: true 21 | }, 22 | availability: { // 0.0 - 1.0 23 | type: Number, 24 | required: true 25 | }, 26 | accessibility: { 27 | type: String, 28 | enum: ['Few to no challenges', 'Minor challenges', 'Major challenges'] 29 | }, 30 | duration: { 31 | type: String, 32 | enum: ['minutes', 'hours', 'days', 'weeks'], 33 | default: 'minutes', 34 | required: true 35 | }, 36 | kidFriendly: { 37 | type: Boolean, 38 | default: false, 39 | required: true 40 | }, 41 | link: { // URL to resource 42 | type: String 43 | }, 44 | key: { 45 | type: String, 46 | required: true 47 | } 48 | }, { 49 | collection: 'activities' 50 | }); 51 | 52 | module.exports = mongoose.model('Activity', activitySchema); 53 | -------------------------------------------------------------------------------- /src/backend/models/Fact.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const factSchema = new mongoose.Schema({ 4 | fact: { 5 | type: String, 6 | required: true 7 | }, 8 | source: { 9 | type: String 10 | }, 11 | key: { 12 | type: String, 13 | required: true 14 | } 15 | }, { 16 | collection: 'facts' 17 | }); 18 | 19 | module.exports = mongoose.model('Fact', factSchema); 20 | -------------------------------------------------------------------------------- /src/backend/models/Riddle.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const riddleSchema = new mongoose.Schema({ 4 | question: { 5 | type: String, 6 | required: true 7 | }, 8 | answer: { 9 | type: String, 10 | required: true 11 | }, 12 | difficulty: { 13 | type: String, 14 | enum: ['easy', 'normal', 'hard'], 15 | default: 'normal', 16 | required: true 17 | }, 18 | source: { 19 | type: String 20 | }, 21 | key: { 22 | type: String, 23 | required: true 24 | } 25 | }, { 26 | collection: 'riddles' 27 | }); 28 | 29 | module.exports = mongoose.model('Riddle', riddleSchema); 30 | -------------------------------------------------------------------------------- /src/backend/models/Suggestion.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const suggestionSchema = new mongoose.Schema({ 4 | profile: { 5 | agent: { 6 | type: String 7 | } 8 | } 9 | }, { 10 | collection: 'suggestions' 11 | }); 12 | 13 | const suggestionModel = mongoose.model('Suggestion', suggestionSchema); 14 | 15 | const ActivitySuggestion = suggestionModel.discriminator('ActivitySuggestion', new mongoose.Schema({ 16 | activity: { 17 | activity: { 18 | type: String, 19 | required: true 20 | }, 21 | type: { 22 | type: String 23 | }, 24 | participants: { 25 | type: Number 26 | } 27 | } 28 | })); 29 | 30 | const FactSuggestion = suggestionModel.discriminator('FactSuggestion', new mongoose.Schema({ 31 | fact: { 32 | fact: { 33 | type: String, 34 | required: true 35 | }, 36 | source: { 37 | type: String 38 | } 39 | }, 40 | })); 41 | 42 | const RiddleSuggestion = suggestionModel.discriminator('RiddleSuggestion', new mongoose.Schema({ 43 | riddle: { 44 | question: { 45 | type: String, 46 | required: true 47 | }, 48 | answer: { 49 | type: String, 50 | required: true 51 | }, 52 | source: { 53 | type: String 54 | } 55 | } 56 | })); 57 | 58 | const WebsiteSuggestion = suggestionModel.discriminator('WebsiteSuggestion', new mongoose.Schema({ 59 | website: { 60 | url: { 61 | type: String, 62 | required: true 63 | }, 64 | description: { 65 | type: String, 66 | required: true 67 | } 68 | } 69 | })); 70 | 71 | module.exports = { 72 | ActivitySuggestion, 73 | FactSuggestion, 74 | RiddleSuggestion, 75 | WebsiteSuggestion 76 | } 77 | -------------------------------------------------------------------------------- /src/backend/models/Website.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const websiteSchema = new mongoose.Schema({ 4 | url: { 5 | type: String, 6 | required: true 7 | }, 8 | description: { 9 | type: String, 10 | required: true 11 | }, 12 | key: { 13 | type: String, 14 | required: true 15 | } 16 | }, { 17 | collection: 'websites' 18 | }); 19 | 20 | module.exports = mongoose.model('Website', websiteSchema); 21 | -------------------------------------------------------------------------------- /src/backend/models/index.js: -------------------------------------------------------------------------------- 1 | const Activity = require('./Activity'); 2 | const Fact = require('./Fact'); 3 | const Riddle = require('./Riddle'); 4 | const Website = require('./Website'); 5 | const { 6 | ActivitySuggestion, 7 | FactSuggestion, 8 | RiddleSuggestion, 9 | WebsiteSuggestion 10 | } = require('./Suggestion'); 11 | 12 | module.exports = { 13 | Activity, 14 | ActivitySuggestion, 15 | FactSuggestion, 16 | RiddleSuggestion, 17 | WebsiteSuggestion, 18 | Fact, 19 | Riddle, 20 | Website 21 | }; 22 | -------------------------------------------------------------------------------- /src/backend/routes/error.js: -------------------------------------------------------------------------------- 1 | module.exports = function(router) { 2 | router.get('/api/*', (req, res) => { 3 | res.error('Endpoint not found'); 4 | }); 5 | 6 | router.post('/api/*', (req, res) => { 7 | res.error('Endpoint not found'); 8 | }); 9 | 10 | router.put('/api/*', (req, res) => { 11 | res.error('Endpoint not found'); 12 | }); 13 | 14 | router.delete('/api/*', (req, res) => { 15 | res.error('Endpoint not found'); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/backend/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const {express: expressMiddlware} = require('@b/middleware'); 3 | 4 | module.exports = function() { 5 | let router = express.Router(); 6 | 7 | expressMiddlware(router); 8 | 9 | router.get('/api', (req, res) => { 10 | res.json({'message': 'Bored API'}); 11 | }); 12 | 13 | require('./v1')(router); 14 | require('./v2')(router); 15 | require('./error')(router); 16 | 17 | return router; 18 | }; 19 | -------------------------------------------------------------------------------- /src/backend/routes/v1/activities.js: -------------------------------------------------------------------------------- 1 | const {logQuery} = require('@b/keen'); 2 | const activitiesController = require('@b/controllers/activities'); 3 | const {unmaskActivity, maskActivity} = require('@b/routes/v1/masks'); 4 | 5 | module.exports = function(router) { 6 | router.get(['/api/activity/', '/api/v1/activity/'], (req, res) => { 7 | logQuery('activity', req.query); 8 | 9 | // Transform query data to be used on database 10 | req.query = unmaskActivity(req.query); 11 | 12 | // Aggregate the mins and maxes 13 | const ranges = ['price', 'participants', 'availability'] 14 | .filter(range => req.query[`min${range}`] || req.query[`max${range}`]) // Filter out ranges that aren't specified 15 | .map(range => { 16 | return { 17 | [range]: Object.assign( 18 | {}, 19 | ...[`max${range}`, `min${range}`] // Add min and max if defined 20 | .filter(key => req.query[key]) 21 | .map(key => ({[key.substr(0, 3) === 'max' ? '$lte': '$gte']: req.query[key]})) 22 | ) 23 | }; 24 | } 25 | ); 26 | 27 | // Assign filters to query database with 28 | const params = Object.assign( 29 | {}, 30 | ...['key', 'type', 'participants', 'price', 'availability'] 31 | .filter(key => req.query[key]) 32 | .map(key => ({[key]: req.query[key]})), 33 | ...ranges // Ranges override concrete values (e.g., minprice overrides price) 34 | ); 35 | 36 | activitiesController.findRandomActivity(params).then(activity => { 37 | res.json(maskActivity(activity)); 38 | }).catch(err => { 39 | if (err.name === 'CastError') { 40 | 41 | res.error('Failed to query due to error in arguments'); 42 | } 43 | else { 44 | res.error(err.message || 'There was an error querying for activity'); 45 | } 46 | }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/backend/routes/v1/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | module.exports = function(router) { 4 | 5 | require('./activities')(router); 6 | require('./suggestions')(router); 7 | 8 | return router; 9 | }; 10 | -------------------------------------------------------------------------------- /src/backend/routes/v1/masks.js: -------------------------------------------------------------------------------- 1 | exports.unmaskActivity = activity => { 2 | return !activity ? {} : Object.assign( 3 | {}, 4 | ...[ 5 | 'activity', 6 | 'type', 7 | 'participants', 8 | 'minparticipants', 9 | 'maxparticipants', 10 | 'price', 11 | 'minprice', 12 | 'maxprice', 13 | 'link', 14 | 'key' 15 | ] 16 | .filter(key => activity[key] !== undefined) 17 | .map(key => ({[key]: activity[key]})), 18 | ...[ 19 | {name: 'availability', filter: 'accessibility', action: () => {return activity.accessibility}}, 20 | {name: 'minavailability', filter: 'minaccessibility', action: () => {return activity.minaccessibility}}, 21 | {name: 'maxavailability', filter: 'maxaccessibility', action: () => {return activity.maxaccessibility}} 22 | ].filter(key => activity[key.filter || key.name] !== undefined) 23 | .map(field => ({[field.name]: field.action() || ''})) 24 | ); 25 | }; 26 | 27 | exports.maskActivity = activity => { 28 | return !activity ? {} : Object.assign( 29 | {}, 30 | ...['activity', 'type', 'participants', 'price', 'link', 'key'] 31 | .map(key => ({[key]: activity[key] !== undefined ? activity[key] : ''})), 32 | ...[ 33 | {name: 'accessibility', filter: 'availability', action: () => {return activity.availability}} 34 | ].filter(key => activity[key.filter] !== undefined) 35 | .map(field => ({[field.name]: field.action()})) 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/backend/routes/v1/suggestions.js: -------------------------------------------------------------------------------- 1 | const {ActivitySuggestion} = require('@b/models'); 2 | const utilsController = require('@b/controllers/utils'); 3 | const joi = require('@hapi/joi'); 4 | const middleware = require('@b/middleware'); 5 | 6 | const defaultTypes = ['education', 'recreational', 'social', 'diy', 'charity', 'cooking', 'relaxation', 'music', 'busywork']; 7 | 8 | const suggestionSchema = joi.object().keys({ 9 | activity: joi.string().required(), 10 | type: joi.string().valid(...defaultTypes).required(), 11 | participants: joi.number().min(1).required() 12 | }).required(); 13 | 14 | 15 | module.exports = function(router) { 16 | router.post(['/api/suggestion/', '/api/v1/suggestion/'], middleware.validate(suggestionSchema), (req, res) => { 17 | let suggestion = { 18 | profile: { 19 | agent: req.header('user-agent') 20 | }, 21 | activity: { 22 | activity: utilsController.stringSanitize(req.body.activity), 23 | type: req.body.type, 24 | participants: parseInt(req.body.participants) 25 | } 26 | }; 27 | 28 | ActivitySuggestion.create(suggestion, err => { 29 | if (err) { 30 | res.error(err); 31 | return; 32 | } 33 | 34 | res.send({'message': 'Successfully created suggestion'}); 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/backend/routes/v2/activities.js: -------------------------------------------------------------------------------- 1 | const {logQuery} = require('@b/keen'); 2 | const activitiesController = require('@b/controllers/activities'); 3 | const {unmaskActivity, maskActivity} = require('@b/routes/v2/masks'); 4 | 5 | module.exports = function(router) { 6 | router.get('/api/v2/activities(/:key)?', (req, res) => { 7 | logQuery('activity', req.query); 8 | 9 | // Transform query data to be used on database 10 | req.query = unmaskActivity(req.query); 11 | 12 | // Aggregate the mins and maxes 13 | const ranges = ['price', 'participants', 'availability'] 14 | .filter(range => req.query[`min${range}`] || req.query[`max${range}`]) // Filter out ranges that aren't specified 15 | .map(range => { 16 | return { 17 | [range]: Object.assign( 18 | {}, 19 | ...[`max${range}`, `min${range}`] // Add min and max if defined 20 | .filter(key => req.query[key]) 21 | .map(key => ({[key.substr(0, 3) === 'max' ? '$lte': '$gte']: req.query[key]})) 22 | ) 23 | }; 24 | } 25 | ); 26 | 27 | // Assign filters to query database 28 | const params = Object.assign( 29 | {}, 30 | ...['type', 'participants', 'price', 'availability'] 31 | .filter(key => req.query[key]) 32 | .map(key => ({[key]: req.query[key]})), 33 | ...ranges // Ranges override concrete values (e.g., minprice overrides price) 34 | ); 35 | 36 | if (req.params.key) { 37 | return activitiesController.findActivity({'key': req.params.key, ...params}).then(activity => { 38 | res.json({'activity': maskActivity(activity)}); 39 | }).catch(err => { 40 | res.error(err.message || 'There was an error querying for activity'); 41 | }); 42 | } 43 | 44 | activitiesController.findRandomActivity(params).then(activity => { 45 | res.json({'activity': maskActivity(activity)}); 46 | }).catch(err => { 47 | if (err.name === 'CastError') { 48 | res.error('Failed to query due to error in arguments'); 49 | } 50 | else { 51 | res.error(err.message || 'There was an error querying for activity'); 52 | } 53 | }); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /src/backend/routes/v2/facts.js: -------------------------------------------------------------------------------- 1 | const {logQuery} = require('@b/keen'); 2 | const factsController = require('@b/controllers/facts'); 3 | const {maskFact} = require('@b/routes/v2/masks'); 4 | 5 | module.exports = function(router) { 6 | router.get('/api/v2/facts(/:key)?', (req, res) => { 7 | logQuery('fact', req.query); 8 | 9 | if (req.params.key) { 10 | return factsController.findFactByKey(req.params.key).then(fact => { 11 | res.json({'fact': maskFact(fact)}); 12 | }).catch(err => { 13 | res.error(err); 14 | }); 15 | } 16 | 17 | factsController.findRandomFact().then(fact => { 18 | res.json({'fact': maskFact(fact)}); 19 | }).catch(err => { 20 | if (err.name === 'CastError') { 21 | res.error('Failed to query due to error in arguments'); 22 | } 23 | else { 24 | res.error(err.message || 'There was an error querying for fact'); 25 | } 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/backend/routes/v2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(router) { 2 | 3 | require('./activities')(router); 4 | require('./facts')(router); 5 | require('./riddles')(router); 6 | require('./websites')(router); 7 | require('./suggestions')(router); 8 | 9 | return router; 10 | }; 11 | -------------------------------------------------------------------------------- /src/backend/routes/v2/masks.js: -------------------------------------------------------------------------------- 1 | const unmaskPrice = (activity, field) => { 2 | return activity[field] === undefined ? 0.1 : (activity[field].length - 1) * 0.25 || 0.1; 3 | }; 4 | 5 | const maskPrice = (activity, field) => { 6 | return activity[field] === undefined ? '$' : '$'.repeat(1 + (activity.price / 0.25)); 7 | }; 8 | 9 | exports.unmaskActivity = activity => { 10 | return !activity ? {} : Object.assign( 11 | {}, 12 | ...[ 13 | 'activity', 14 | 'availability', 15 | 'minavailability', 16 | 'maxavailability', 17 | 'type', 18 | 'participants', 19 | 'minparticipants', 20 | 'maxparticipants', 21 | 'link', 22 | 'key' 23 | ].filter(key => activity[key] !== undefined) 24 | .map(key => ({[key]: activity[key]})), 25 | ...[ 26 | {name: 'price', action: () => unmaskPrice(activity, 'price')}, 27 | {name: 'minprice', action: () => unmaskPrice(activity, 'minprice')}, 28 | {name: 'maxprice', action: () => unmaskPrice(activity, 'maxprice')} 29 | ].filter(key => activity[key.filter || key.name] !== undefined) 30 | .map(field => ({[field.name]: field.action() || '0'})) 31 | ); 32 | } 33 | 34 | exports.maskActivity = activity => { 35 | return !activity ? {} : Object.assign( 36 | {}, 37 | ...['activity', 'accessibility', 'type', 'participants', 'link', 'key', 'duration', 'kidFriendly'] 38 | .map(key => ({[key]: activity[key] ? activity[key] : activity[key] === 0 ? '0' : ''})), 39 | ...[ 40 | {name: 'price', action: () => maskPrice(activity, 'price')} 41 | ].map(field => ({[field.name]: field.action() || ''})) 42 | ); 43 | } 44 | 45 | exports.maskFact = fact => { 46 | if (!fact) return {}; 47 | 48 | let masked = fact.toObject(); 49 | 50 | delete masked._id; 51 | delete masked.__v; 52 | 53 | return masked; 54 | }; 55 | 56 | exports.maskRiddle = riddle => { 57 | if (!riddle) return {}; 58 | 59 | let masked = riddle.toObject(); 60 | 61 | delete masked._id; 62 | delete masked.__v; 63 | 64 | return masked; 65 | }; 66 | 67 | exports.maskWebsite = website => { 68 | if (!website) return {}; 69 | 70 | let masked = website.toObject(); 71 | 72 | delete masked._id; 73 | delete masked.__v; 74 | 75 | return masked; 76 | }; 77 | 78 | exports.unmaskActivityPrice = unmaskPrice; 79 | exports.maskActivityPrice = maskPrice; 80 | -------------------------------------------------------------------------------- /src/backend/routes/v2/riddles.js: -------------------------------------------------------------------------------- 1 | const {logQuery} = require('@b/keen'); 2 | const riddlesController = require('@b/controllers/riddles'); 3 | const {maskRiddle} = require('@b/routes/v2/masks'); 4 | 5 | module.exports = function(router) { 6 | router.get('/api/v2/riddles(/:key)?', (req, res) => { 7 | logQuery('riddle', req.query); 8 | 9 | // Assign filters to query database 10 | const params = Object.assign( 11 | {}, 12 | ...['difficulty'] 13 | .filter(key => req.query[key]) 14 | .map(key => ({[key]: req.query[key]})), 15 | ); 16 | 17 | if (req.params.key) { 18 | return riddlesController.findRiddle({'key': req.params.key, ...params}).then(riddle => { 19 | res.json({'riddle': maskRiddle(riddle)}); 20 | }).catch(err => { 21 | res.error(err); 22 | }); 23 | } 24 | 25 | riddlesController.findRandomRiddle(params).then(riddle => { 26 | res.json({'riddle': maskRiddle(riddle)}); 27 | }).catch(err => { 28 | if (err.name === 'CastError') { 29 | res.error('Failed to query due to error in arguments'); 30 | } 31 | else { 32 | res.error(err.message || 'There was an error querying for riddle'); 33 | } 34 | }); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/backend/routes/v2/suggestions.js: -------------------------------------------------------------------------------- 1 | const { 2 | ActivitySuggestion, 3 | FactSuggestion, 4 | RiddleSuggestion, 5 | WebsiteSuggestion, 6 | } = require('@b/models'); 7 | const utilsController = require('@b/controllers/utils'); 8 | const joi = require('@hapi/joi'); 9 | const middleware = require('@b/middleware'); 10 | 11 | const defaultTypes = ['education', 'recreational', 'social', 'diy', 'charity', 'cooking', 'relaxation', 'music', 'busywork']; 12 | 13 | const suggestionSchema = joi.object().keys({ 14 | activity: joi.object().keys({ 15 | activity: joi.string().required(), 16 | type: joi.string().valid(...defaultTypes).required(), 17 | participants: joi.number().min(1).required() 18 | }), 19 | fact: joi.object().keys({ 20 | fact: joi.string().required(), 21 | source: joi.string().uri().required() 22 | }), 23 | riddle: joi.object().keys({ 24 | question: joi.string().required(), 25 | answer: joi.string().required(), 26 | source: joi.string().uri().required() 27 | }), 28 | website: joi.object().keys({ 29 | url: joi.string().uri().required(), 30 | description: joi.string().required() 31 | }) 32 | }).xor('activity', 'fact', 'riddle', 'website').required(); 33 | 34 | module.exports = function(router) { 35 | router.post('/api/v2/suggestions', middleware.validate(suggestionSchema), (req, res) => { 36 | let type; 37 | let model; 38 | 39 | switch (type = Object.keys(req.body)[0]) { 40 | case 'activity': 41 | model = ActivitySuggestion; 42 | break; 43 | case 'fact': 44 | model = FactSuggestion; 45 | break; 46 | case 'riddle': 47 | model = RiddleSuggestion; 48 | break; 49 | case 'website': 50 | model = WebsiteSuggestion; 51 | break; 52 | } 53 | 54 | let suggestion = { 55 | profile: { 56 | agent: req.header('user-agent') 57 | }, 58 | type, 59 | ...req.body 60 | }; 61 | 62 | model.create(suggestion, err => { 63 | if (err) { 64 | res.error(err); 65 | return; 66 | } 67 | 68 | res.send({'message': 'Successfully created suggestion'}); 69 | }); 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /src/backend/routes/v2/websites.js: -------------------------------------------------------------------------------- 1 | const {logQuery} = require('@b/keen'); 2 | const websitesController = require('@b/controllers/websites'); 3 | const {maskWebsite} = require('@b/routes/v2/masks'); 4 | 5 | module.exports = function(router) { 6 | router.get('/api/v2/websites(/:key)?', (req, res) => { 7 | logQuery('website', req.query); 8 | 9 | if (req.params.key) { 10 | return websitesController.findWebsiteByKey(req.params.key).then(website => { 11 | res.json({'website': maskWebsite(website)}); 12 | }).catch(err => { 13 | res.error(err); 14 | }); 15 | } 16 | 17 | websitesController.findRandomWebsite().then(website => { 18 | res.json({'website': maskWebsite(website)}); 19 | }).catch(err => { 20 | if (err.name === 'CastError') { 21 | res.error('Failed to query due to error in arguments'); 22 | } 23 | else { 24 | res.error(err.message || 'There was an error querying for website'); 25 | } 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/frontend/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 140 | -------------------------------------------------------------------------------- /src/frontend/components/Bottombar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 37 | 38 | 117 | -------------------------------------------------------------------------------- /src/frontend/components/DocumentationEndpoint.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | 38 | 47 | -------------------------------------------------------------------------------- /src/frontend/components/Info.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | 27 | 52 | -------------------------------------------------------------------------------- /src/frontend/components/Intro.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | 42 | 65 | -------------------------------------------------------------------------------- /src/frontend/components/ResponseDescription.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 49 | -------------------------------------------------------------------------------- /src/frontend/components/Topbar.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 65 | 66 | 184 | -------------------------------------------------------------------------------- /src/frontend/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App'; 3 | import router from './router'; 4 | 5 | import VueResource from 'vue-resource'; 6 | import VueNotification from 'vue-notification'; 7 | 8 | Vue.config.productionTip = false; 9 | Vue.use(VueResource); 10 | Vue.use(VueNotification); 11 | 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | components: { App }, 16 | template: '' 17 | }); 18 | -------------------------------------------------------------------------------- /src/frontend/pages/About.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 88 | 89 | 127 | -------------------------------------------------------------------------------- /src/frontend/pages/Contributing.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 228 | 229 | 392 | -------------------------------------------------------------------------------- /src/frontend/pages/Documentation.vue: -------------------------------------------------------------------------------- 1 | 145 | 146 | 183 | 184 | 342 | -------------------------------------------------------------------------------- /src/frontend/pages/Error.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | 37 | 64 | -------------------------------------------------------------------------------- /src/frontend/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 71 | 72 | 165 | -------------------------------------------------------------------------------- /src/frontend/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | Vue.use(Router); 5 | 6 | import HomePage from '@/pages/Home'; 7 | import AboutPage from '@/pages/About'; 8 | import DocumentationPage from '@/pages/Documentation'; 9 | import ContributingPage from '@/pages/Contributing'; 10 | import ErrorPage from '@/pages/Error'; 11 | 12 | const router = new Router({ 13 | hashbang: false, 14 | history: true, 15 | mode: 'history', 16 | routes: [ 17 | { 18 | path: '/', 19 | name: 'Home', 20 | component: HomePage 21 | }, 22 | { 23 | path: '/about', 24 | name: 'About', 25 | component: AboutPage 26 | }, 27 | { 28 | path: '/documentation', 29 | name: 'Documentation', 30 | component: DocumentationPage 31 | }, 32 | { 33 | path: '/contributing', 34 | name: 'Contributing', 35 | component: ContributingPage 36 | }, 37 | { 38 | path: '/*', 39 | name: 'Error', 40 | component: ErrorPage 41 | } 42 | ] 43 | }); 44 | 45 | export default router; 46 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewthoennes/Bored-API/a978ac490c4c3aff555e7453ad8e577b658f8864/static/favicon.ico -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Bored API 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /test/backend/integration/v1.activities.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const chaiHttp = require('chai-http'); 5 | const expect = chai.expect; 6 | const models = require('@t/backend/utils/models'); 7 | const server = require('@t/backend/utils/server'); 8 | const mongo = require('@t/backend/utils/mongo'); 9 | 10 | let app; 11 | 12 | chai.use(chaiAsPromised); 13 | chai.use(chaiHttp); 14 | 15 | const prune = activity => { 16 | let pruned = activity.toObject(); 17 | 18 | delete pruned._id; 19 | delete pruned.__v; 20 | delete pruned.duration; 21 | delete pruned.kidFriendly; 22 | 23 | return pruned; 24 | }; 25 | 26 | const unmask = activity => { 27 | let unmasked = Object.assign({}, activity); 28 | 29 | unmasked.availability = unmasked.accessibility; 30 | delete unmasked.accessibility; 31 | 32 | return unmasked; 33 | } 34 | 35 | describe('Activities v1 routes should work as expected', () => { 36 | before(() => { 37 | app = server.getNewApp(); 38 | }); 39 | 40 | beforeEach(done => { 41 | mongo.beforeEach().then(() => done()); 42 | }); 43 | 44 | afterEach(done => { 45 | mongo.afterEach().then(() => done()); 46 | }); 47 | 48 | after(() => server.killSession()); 49 | 50 | it('/api/activity GET should work as expected', done => { 51 | let activity; 52 | 53 | models.createV1Activity().then(created => { 54 | activity = prune(created); 55 | 56 | return chai.request(app).get('/api/activity'); 57 | }).then(res => { 58 | expect(unmask(res.body)).to.eql(activity); 59 | 60 | done(); 61 | }).catch(err => done(err)); 62 | }); 63 | 64 | it('/api/activity?key={} GET should work as expected', done => { 65 | let activity; 66 | 67 | Promise.all([models.createV1Activity(), models.createV1Activity(), models.createV1Activity()]).then(created => { 68 | activity = prune(created[0]); 69 | 70 | return chai.request(app).get(`/api/activity?key=${activity.key}`); 71 | }).then(res => { 72 | expect(unmask(res.body)).to.eql(activity); 73 | 74 | done(); 75 | }).catch(err => done(err)); 76 | }); 77 | 78 | it('/api/activity?type={} GET should work as expected', done => { 79 | let activity; 80 | 81 | Promise.all([models.createV1Activity({type: 'education'}), models.createV1Activity({type: 'recreational'}), models.createV1Activity({type: 'social'})]).then(created => { 82 | activity = prune(created[0]); 83 | 84 | return chai.request(app).get(`/api/activity?type=${activity.type}`); 85 | }).then(res => { 86 | expect(unmask(res.body)).to.eql(activity); 87 | 88 | done(); 89 | }).catch(err => done(err)); 90 | }); 91 | 92 | it('/api/activity?participants={} GET should work as expected', done => { 93 | let activity; 94 | 95 | Promise.all([models.createV1Activity({participants: 1}), models.createV1Activity({participants: 2}), models.createV1Activity({participants: 3})]).then(created => { 96 | activity = prune(created[0]); 97 | 98 | return chai.request(app).get(`/api/activity?participants=${activity.participants}`); 99 | }).then(res => { 100 | expect(unmask(res.body)).to.eql(activity); 101 | 102 | done(); 103 | }).catch(err => done(err)); 104 | }); 105 | 106 | it('/api/activity?price={} GET should work as expected', done => { 107 | let activity; 108 | 109 | Promise.all([models.createV1Activity({price: 0.1}), models.createV1Activity({price: 0.5}), models.createV1Activity({price: 0.7})]).then(created => { 110 | activity = prune(created[0]); 111 | 112 | return chai.request(app).get(`/api/activity?price=${activity.price}`); 113 | }).then(res => { 114 | expect(unmask(res.body)).to.eql(activity); 115 | 116 | done(); 117 | }).catch(err => done(err)); 118 | }); 119 | 120 | it('/api/activity?minprice={} GET should work as expected', done => { 121 | let activity; 122 | 123 | Promise.all([models.createV1Activity({price: 0.1}), models.createV1Activity({price: 0.2}), models.createV1Activity({price: 0.7})]).then(created => { 124 | activity = prune(created[2]); 125 | 126 | return chai.request(app).get('/api/activity?minprice=0.7'); 127 | }).then(res => { 128 | expect(unmask(res.body)).to.eql(activity); 129 | 130 | done(); 131 | }).catch(err => done(err)); 132 | }); 133 | 134 | it('/api/activity?maxprice={} GET should work as expected', done => { 135 | let activity; 136 | 137 | Promise.all([models.createV1Activity({price: 0.1}), models.createV1Activity({price: 0.2}), models.createV1Activity({price: 0.7})]).then(created => { 138 | activity = prune(created[0]); 139 | 140 | return chai.request(app).get('/api/activity?maxprice=0.1'); 141 | }).then(res => { 142 | expect(unmask(res.body)).to.eql(activity); 143 | 144 | done(); 145 | }).catch(err => done(err)); 146 | }); 147 | 148 | it('/api/activity?minprice={}&maxprice={} GET should return error if range is invalid', done => { 149 | Promise.all([models.createV1Activity(), models.createV1Activity(), models.createV1Activity()]).then(created => { 150 | return chai.request(app).get('/api/activity?minprice=0.9&maxprice=0.1'); 151 | }).then(res => { 152 | expect(res.body).to.have.property('error'); 153 | 154 | done(); 155 | }).catch(err => done(err)); 156 | }); 157 | 158 | it('/api/activity?minprice={}&maxprice={} GET should work as expected', done => { 159 | let activity; 160 | 161 | Promise.all([models.createV1Activity({price: 0.1}), models.createV1Activity({price: 0.3}), models.createV1Activity({price: 0.7})]).then(created => { 162 | activity = prune(created[1]); 163 | 164 | return chai.request(app).get('/api/activity?minprice=0.2&maxprice=0.5'); 165 | }).then(res => { 166 | expect(unmask(res.body)).to.eql(activity); 167 | 168 | done(); 169 | }).catch(err => done(err)); 170 | }); 171 | 172 | it('/api/activity?price={}&minprice={}&maxprice={} GET should allow the range to override the specified value', done => { 173 | let activity; 174 | 175 | Promise.all([models.createV1Activity({price: 0.1}), models.createV1Activity({price: 0.3}), models.createV1Activity({price: 0.7})]).then(created => { 176 | activity = prune(created[1]); 177 | 178 | return chai.request(app).get('/api/activity?price=0.1&minprice=0.2&maxprice=0.5'); 179 | }).then(res => { 180 | expect(unmask(res.body)).to.eql(activity); 181 | 182 | done(); 183 | }).catch(err => done(err)); 184 | }); 185 | 186 | it('/api/activity?accessibility={} GET should work as expected', done => { 187 | let activity; 188 | 189 | Promise.all([models.createV1Activity({availability: 0.1}), models.createV1Activity({availability: 0.5}), models.createV1Activity({availability: 0.7})]).then(created => { 190 | activity = prune(created[0]); 191 | 192 | return chai.request(app).get(`/api/activity?accessibility=${activity.availability}`); 193 | }).then(res => { 194 | expect(unmask(res.body)).to.eql(activity); 195 | 196 | done(); 197 | }).catch(err => done(err)); 198 | }); 199 | 200 | it('/api/activity?minaccessibility={} GET should work as expected', done => { 201 | let activity; 202 | 203 | Promise.all([models.createV1Activity({availability: 0.1}), models.createV1Activity({availability: 0.2}), models.createV1Activity({availability: 0.7})]).then(created => { 204 | activity = prune(created[2]); 205 | 206 | return chai.request(app).get('/api/activity?minaccessibility=0.6'); 207 | }).then(res => { 208 | expect(unmask(res.body)).to.eql(activity); 209 | 210 | done(); 211 | }).catch(err => done(err)); 212 | }); 213 | 214 | it('/api/activity?maxaccessibility={} GET should work as expected', done => { 215 | let activity; 216 | 217 | Promise.all([models.createV1Activity({availability: 0.1}), models.createV1Activity({availability: 0.2}), models.createV1Activity({availability: 0.7})]).then(created => { 218 | activity = prune(created[0]); 219 | 220 | return chai.request(app).get('/api/activity?maxaccessibility=0.1'); 221 | }).then(res => { 222 | expect(unmask(res.body)).to.eql(activity); 223 | 224 | done(); 225 | }).catch(err => done(err)); 226 | }); 227 | 228 | it('/api/activity?minaccessibility={}&maxaccessibility={} GET should return error if range is invalid', done => { 229 | Promise.all([models.createV1Activity(), models.createV1Activity(), models.createV1Activity()]).then(created => { 230 | return chai.request(app).get('/api/activity?minaccessibility=0.9&maxaccessibility=0.1'); 231 | }).then(res => { 232 | expect(res.body).to.have.property('error'); 233 | 234 | done(); 235 | }).catch(err => done(err)); 236 | }); 237 | 238 | it('/api/activity?minaccessibility={}&maxaccessibility={} GET should work as expected', done => { 239 | let activity; 240 | 241 | Promise.all([models.createV1Activity({availability: 0.1}), models.createV1Activity({availability: 0.3}), models.createV1Activity({availability: 0.7})]).then(created => { 242 | activity = prune(created[1]); 243 | 244 | return chai.request(app).get('/api/activity?minaccessibility=0.2&maxaccessibility=0.5'); 245 | }).then(res => { 246 | expect(unmask(res.body)).to.eql(activity); 247 | 248 | done(); 249 | }).catch(err => done(err)); 250 | }); 251 | 252 | it('/api/activity?accessibility={}&minaccessibility={}&maxaccessibility={} GET should allow the range to override the specified value', done => { 253 | let activity; 254 | 255 | Promise.all([models.createV1Activity({availability: 0.1}), models.createV1Activity({availability: 0.3}), models.createV1Activity({availability: 0.7})]).then(created => { 256 | activity = prune(created[1]); 257 | 258 | return chai.request(app).get('/api/activity?accessibility=0.1&minaccessibility=0.2&maxaccessibility=0.5'); 259 | }).then(res => { 260 | expect(unmask(res.body)).to.eql(activity); 261 | 262 | done(); 263 | }).catch(err => done(err)); 264 | }); 265 | }); 266 | -------------------------------------------------------------------------------- /test/backend/integration/v1.suggestions.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const chaiHttp = require('chai-http'); 5 | const expect = chai.expect; 6 | const faker = require('faker'); 7 | 8 | const server = require('@t/backend/utils/server'); 9 | const mongo = require('@t/backend/utils/mongo'); 10 | 11 | let app; 12 | 13 | chai.use(chaiAsPromised); 14 | chai.use(chaiHttp); 15 | 16 | const createSuggestion = () => { 17 | return { 18 | activity: faker.random.words(), 19 | type: faker.random.objectElement(['education', 'recreational', 'social', 'diy', 'charity', 'cooking', 'relaxation', 'music', 'busywork']), 20 | participants: faker.random.number({min: 1, max: 10}) 21 | } 22 | }; 23 | 24 | describe('Suggestions v1 routes should work as expected', () => { 25 | before(() => { 26 | app = server.getNewApp(); 27 | }); 28 | 29 | beforeEach(done => { 30 | mongo.beforeEach().then(() => done()); 31 | }); 32 | 33 | afterEach(done => { 34 | mongo.afterEach().then(() => done()); 35 | }); 36 | 37 | after(() => server.killSession()); 38 | 39 | it('/api/suggestion POST should fail if missing fields', done => { 40 | let suggestion = { 41 | activity: 'Missing other fields' 42 | }; 43 | 44 | chai.request(app).post('/api/suggestion').send(suggestion).then(res => { 45 | expect(res.body).to.have.property('error'); 46 | 47 | done(); 48 | }).catch(err => done(err)); 49 | }); 50 | 51 | it('/api/suggestion POST should fail if activity is not a string', done => { 52 | let suggestion = { 53 | activity: 1, 54 | type: 'education', 55 | participants: 1 56 | }; 57 | 58 | chai.request(app).post('/api/suggestion').send(suggestion).then(res => { 59 | expect(res.body).to.have.property('error'); 60 | 61 | done(); 62 | }).catch(err => done(err)); 63 | }); 64 | 65 | it('/api/suggestion POST should fail if type is invalid', done => { 66 | let suggestion = { 67 | activity: 'Bubble wrap the house', 68 | type: 'prank', 69 | participants: 1 70 | }; 71 | 72 | chai.request(app).post('/api/suggestion').send(suggestion).then(res => { 73 | expect(res.body).to.have.property('error'); 74 | 75 | done(); 76 | }).catch(err => done(err)); 77 | }); 78 | 79 | it('/api/suggestion POST should fail if participants is not a number', done => { 80 | let suggestion = { 81 | activity: 'Sing in the shower', 82 | type: 'relaxation', 83 | participants: 'Just me' 84 | }; 85 | 86 | chai.request(app).post('/api/suggestion').send(suggestion).then(res => { 87 | expect(res.body).to.have.property('error'); 88 | 89 | done(); 90 | }).catch(err => done(err)); 91 | }); 92 | 93 | it('/api/suggestion POST should fail if participants is less than one', done => { 94 | let suggestion = { 95 | activity: 'Exist', 96 | type: 'diy', 97 | participants: -1 98 | }; 99 | 100 | chai.request(app).post('/api/suggestion').send(suggestion).then(res => { 101 | expect(res.body).to.have.property('error'); 102 | 103 | done(); 104 | }).catch(err => done(err)); 105 | }); 106 | 107 | it('/api/suggestion POST should work as expected', done => { 108 | let suggestion = createSuggestion(); 109 | 110 | chai.request(app).post('/api/suggestion').send(suggestion).then(res => { 111 | expect(res.body).to.have.property('message'); 112 | 113 | done(); 114 | }).catch(err => done(err)); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/backend/integration/v2.activities.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const chaiHttp = require('chai-http'); 5 | const expect = chai.expect; 6 | const models = require('@t/backend/utils/models'); 7 | const server = require('@t/backend/utils/server'); 8 | const mongo = require('@t/backend/utils/mongo'); 9 | const { 10 | maskActivity, 11 | maskActivityPrice 12 | } = require('@b/routes/v2/masks'); 13 | 14 | let app; 15 | 16 | chai.use(chaiAsPromised); 17 | chai.use(chaiHttp); 18 | 19 | const prune = activity => { 20 | let pruned = activity.toObject(); 21 | 22 | delete pruned._id; 23 | delete pruned.__v; 24 | 25 | return pruned; 26 | }; 27 | 28 | describe('Activities v2 routes should work as expected', () => { 29 | before(() => { 30 | app = server.getNewApp(); 31 | }); 32 | 33 | beforeEach(done => { 34 | mongo.beforeEach().then(() => done()); 35 | }); 36 | 37 | afterEach(done => { 38 | mongo.afterEach().then(() => done()); 39 | }); 40 | 41 | after(() => server.killSession()); 42 | 43 | it('/api/v2/activities GET should work as expected', done => { 44 | let activity; 45 | 46 | models.createV2Activity().then(created => { 47 | activity = prune(created); 48 | 49 | return chai.request(app).get('/api/v2/activities'); 50 | }).then(res => { 51 | expect(res.body).to.have.property('activity'); 52 | expect(res.body.activity).to.eql(maskActivity(activity)); 53 | 54 | done(); 55 | }).catch(err => done(err)); 56 | }); 57 | 58 | it('/api/v2/activities/:key GET should work as expected', done => { 59 | let activity; 60 | 61 | Promise.all([models.createV2Activity(), models.createV2Activity(), models.createV2Activity()]).then(created => { 62 | activity = prune(created[0]); 63 | 64 | return chai.request(app).get(`/api/v2/activities/${activity.key}`); 65 | }).then(res => { 66 | expect(res.body).to.have.property('activity'); 67 | expect(res.body.activity).to.eql(maskActivity(activity)); 68 | 69 | done(); 70 | }).catch(err => done(err)); 71 | }); 72 | 73 | it('/api/v2/activities?type={} GET should work as expected', done => { 74 | let activity; 75 | 76 | Promise.all([models.createV2Activity({type: 'education'}), models.createV2Activity({type: 'recreational'}), models.createV2Activity({type: 'social'})]).then(created => { 77 | activity = prune(created[0]); 78 | 79 | return chai.request(app).get(`/api/v2/activities?type=${activity.type}`); 80 | }).then(res => { 81 | expect(res.body).to.have.property('activity'); 82 | expect(res.body.activity).to.eql(maskActivity(activity)); 83 | 84 | done(); 85 | }).catch(err => done(err)); 86 | }); 87 | 88 | it('/api/v2/activities?participants={} GET should work as expected', done => { 89 | let activity; 90 | 91 | Promise.all([models.createV2Activity({participants: 1}), models.createV2Activity({participants: 2}), models.createV2Activity({participants: 3})]).then(created => { 92 | activity = prune(created[0]); 93 | 94 | return chai.request(app).get(`/api/v2/activities?participants=${activity.participants}`); 95 | }).then(res => { 96 | expect(res.body).to.have.property('activity'); 97 | expect(res.body.activity).to.eql(maskActivity(activity)); 98 | 99 | done(); 100 | }).catch(err => done(err)); 101 | }); 102 | 103 | it('/api/v2/activities?price={} GET should work as expected', done => { 104 | let activity; 105 | 106 | Promise.all([models.createV2Activity({price: 0.1}), models.createV2Activity({price: 0.5}), models.createV2Activity({price: 0.7})]).then(created => { 107 | activity = prune(created[0]); 108 | 109 | return chai.request(app).get(`/api/v2/activities?price=${maskActivity({price: activity.price}).price}`); 110 | }).then(res => { 111 | expect(res.body).to.have.property('activity'); 112 | expect(res.body.activity).to.eql(maskActivity(activity)); 113 | 114 | done(); 115 | }).catch(err => done(err)); 116 | }); 117 | 118 | it('/api/v2/activities?minprice={} GET should work as expected', done => { 119 | let activity; 120 | 121 | Promise.all([models.createV2Activity({price: 0.1}), models.createV2Activity({price: 0.2}), models.createV2Activity({price: 0.7})]).then(created => { 122 | activity = prune(created[2]); 123 | 124 | return chai.request(app).get(`/api/v2/activities?minprice=${maskActivityPrice(activity, 'price')}`); 125 | }).then(res => { 126 | expect(res.body).to.have.property('activity'); 127 | expect(res.body.activity).to.eql(maskActivity(activity)); 128 | 129 | done(); 130 | }).catch(err => done(err)); 131 | }); 132 | 133 | it('/api/v2/activities?maxprice={} GET should work as expected', done => { 134 | let activity; 135 | 136 | Promise.all([models.createV2Activity({price: 0.1}), models.createV2Activity({price: 0.2}), models.createV2Activity({price: 0.7})]).then(created => { 137 | activity = prune(created[0]); 138 | 139 | return chai.request(app).get(`/api/v2/activities?maxprice=${maskActivityPrice(activity, 'price')}`); 140 | }).then(res => { 141 | expect(res.body).to.have.property('activity'); 142 | expect(res.body.activity).to.eql(maskActivity(activity)); 143 | 144 | done(); 145 | }).catch(err => done(err)); 146 | }); 147 | 148 | it('/api/v2/activities?minprice={}&maxprice={} GET should return error if range is invalid', done => { 149 | Promise.all([models.createV2Activity(), models.createV2Activity(), models.createV2Activity()]).then(created => { 150 | return chai.request(app).get('/api/v2/activities?minprice=$$$&maxprice=$'); 151 | }).then(res => { 152 | expect(res.body).to.have.property('error'); 153 | 154 | done(); 155 | }).catch(err => done(err)); 156 | }); 157 | 158 | it('/api/v2/activities?minprice={}&maxprice={} GET should work as expected', done => { 159 | let activity; 160 | 161 | Promise.all([models.createV2Activity({price: 0.1}), models.createV2Activity({price: 0.7}), models.createV2Activity({price: 0.9})]).then(created => { 162 | activity = prune(created[0]); 163 | 164 | return chai.request(app).get('/api/v2/activities?minprice=$&maxprice=$$'); 165 | }).then(res => { 166 | expect(res.body).to.have.property('activity'); 167 | expect(res.body.activity).to.eql(maskActivity(activity)); 168 | 169 | done(); 170 | }).catch(err => done(err)); 171 | }); 172 | 173 | it('/api/v2/activities?price={}&minprice={}&maxprice={} GET should allow the range to override the specified value', done => { 174 | let activity; 175 | 176 | Promise.all([models.createV2Activity({price: 0.1}), models.createV2Activity({price: 0.7}), models.createV2Activity({price: 0.9})]).then(created => { 177 | activity = prune(created[0]); 178 | 179 | return chai.request(app).get('/api/v2/activities?price=0.9&minprice=$&maxprice=$$'); 180 | }).then(res => { 181 | expect(res.body).to.have.property('activity'); 182 | expect(res.body.activity).to.eql(maskActivity(activity)); 183 | 184 | done(); 185 | }).catch(err => done(err)); 186 | }); 187 | 188 | it('/api/v2/activities?availability={} GET should work as expected', done => { 189 | let activity; 190 | 191 | Promise.all([models.createV2Activity({availability: 0.1}), models.createV2Activity({availability: 0.5}), models.createV2Activity({availability: 0.7})]).then(created => { 192 | activity = prune(created[0]); 193 | 194 | return chai.request(app).get(`/api/v2/activities?availability=${activity.availability}`); 195 | }).then(res => { 196 | expect(res.body).to.have.property('activity'); 197 | expect(res.body.activity).to.eql(maskActivity(activity)); 198 | 199 | done(); 200 | }).catch(err => done(err)); 201 | }); 202 | 203 | it('/api/v2/activities?minavailability={} GET should work as expected', done => { 204 | let activity; 205 | 206 | Promise.all([models.createV2Activity({availability: 0.1}), models.createV2Activity({availability: 0.2}), models.createV2Activity({availability: 0.7})]).then(created => { 207 | activity = prune(created[2]); 208 | 209 | return chai.request(app).get('/api/v2/activities?minavailability=0.6'); 210 | }).then(res => { 211 | expect(res.body).to.have.property('activity'); 212 | expect(res.body.activity).to.eql(maskActivity(activity)); 213 | 214 | done(); 215 | }).catch(err => done(err)); 216 | }); 217 | 218 | it('/api/v2/activities?maxavailability={} GET should work as expected', done => { 219 | let activity; 220 | 221 | Promise.all([models.createV2Activity({availability: 0.1}), models.createV2Activity({availability: 0.2}), models.createV2Activity({availability: 0.7})]).then(created => { 222 | activity = prune(created[0]); 223 | 224 | return chai.request(app).get('/api/v2/activities?maxavailability=0.1'); 225 | }).then(res => { 226 | expect(res.body).to.have.property('activity'); 227 | expect(res.body.activity).to.eql(maskActivity(activity)); 228 | 229 | done(); 230 | }).catch(err => done(err)); 231 | }); 232 | 233 | it('/api/v2/activities?minavailability={}&maxavailability={} GET should return error if range is invalid', done => { 234 | Promise.all([models.createV2Activity(), models.createV2Activity(), models.createV2Activity()]).then(created => { 235 | return chai.request(app).get('/api/v2/activities?minavailability=0.9&maxavailability=0.1'); 236 | }).then(res => { 237 | expect(res.body).to.have.property('error'); 238 | 239 | done(); 240 | }).catch(err => done(err)); 241 | }); 242 | 243 | it('/api/v2/activities?minavailability={}&maxavailability={} GET should work as expected', done => { 244 | let activity; 245 | 246 | Promise.all([models.createV2Activity({availability: 0.1}), models.createV2Activity({availability: 0.3}), models.createV2Activity({availability: 0.7})]).then(created => { 247 | activity = prune(created[1]); 248 | 249 | return chai.request(app).get('/api/v2/activities?minavailability=0.2&maxavailability=0.5'); 250 | }).then(res => { 251 | expect(res.body).to.have.property('activity'); 252 | expect(res.body.activity).to.eql(maskActivity(activity)); 253 | 254 | done(); 255 | }).catch(err => done(err)); 256 | }); 257 | 258 | it('/api/v2/activities?availability={}&minavailability={}&maxavailability={} GET should allow the range to override the specified value', done => { 259 | let activity; 260 | 261 | Promise.all([models.createV2Activity({availability: 0.1}), models.createV2Activity({availability: 0.3}), models.createV2Activity({availability: 0.7})]).then(created => { 262 | activity = prune(created[1]); 263 | 264 | return chai.request(app).get('/api/v2/activities?availability=0.1&minavailability=0.2&maxavailability=0.5'); 265 | }).then(res => { 266 | expect(res.body).to.have.property('activity'); 267 | expect(res.body.activity).to.eql(maskActivity(activity)); 268 | 269 | done(); 270 | }).catch(err => done(err)); 271 | }); 272 | }); 273 | -------------------------------------------------------------------------------- /test/backend/integration/v2.facts.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const chaiHttp = require('chai-http'); 5 | const expect = chai.expect; 6 | const models = require('@t/backend/utils/models'); 7 | const server = require('@t/backend/utils/server'); 8 | const mongo = require('@t/backend/utils/mongo'); 9 | 10 | let app; 11 | 12 | chai.use(chaiAsPromised); 13 | chai.use(chaiHttp); 14 | 15 | const prune = fact => { 16 | let pruned = fact.toObject(); 17 | 18 | delete pruned._id; 19 | delete pruned.__v; 20 | 21 | return pruned; 22 | }; 23 | 24 | describe('Facts v2 routes should work as expected', () => { 25 | before(() => { 26 | app = server.getNewApp(); 27 | }); 28 | 29 | beforeEach(done => { 30 | mongo.beforeEach().then(() => done()); 31 | }); 32 | 33 | afterEach(done => { 34 | mongo.afterEach().then(() => done()); 35 | }); 36 | 37 | after(() => server.killSession()); 38 | 39 | it('/api/v2/facts GET should work as expected', done => { 40 | let fact; 41 | 42 | models.createV2Fact().then(created => { 43 | fact = prune(created); 44 | 45 | return chai.request(app).get('/api/v2/facts'); 46 | }).then(res => { 47 | expect(res.body).to.have.property('fact'); 48 | expect(res.body.fact).to.eql(fact); 49 | 50 | done(); 51 | }).catch(err => done(err)); 52 | }); 53 | 54 | it('/api/v2/facts/:key GET should work as expected', done => { 55 | let fact; 56 | 57 | Promise.all([models.createV2Fact(), models.createV2Fact(), models.createV2Fact()]).then(created => { 58 | fact = prune(created[0]); 59 | 60 | return chai.request(app).get(`/api/v2/facts/${fact.key}`); 61 | }).then(res => { 62 | expect(res.body).to.have.property('fact'); 63 | expect(res.body.fact).to.eql(fact); 64 | 65 | done(); 66 | }).catch(err => done(err)); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/backend/integration/v2.riddles.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const chaiHttp = require('chai-http'); 5 | const expect = chai.expect; 6 | const models = require('@t/backend/utils/models'); 7 | const server = require('@t/backend/utils/server'); 8 | const mongo = require('@t/backend/utils/mongo'); 9 | 10 | let app; 11 | 12 | chai.use(chaiAsPromised); 13 | chai.use(chaiHttp); 14 | 15 | const prune = riddle => { 16 | let pruned = riddle.toObject(); 17 | 18 | delete pruned._id; 19 | delete pruned.__v; 20 | 21 | return pruned; 22 | }; 23 | 24 | describe('Riddles v2 routes should work as expected', () => { 25 | before(() => { 26 | app = server.getNewApp(); 27 | }); 28 | 29 | beforeEach(done => { 30 | mongo.beforeEach().then(() => done()); 31 | }); 32 | 33 | afterEach(done => { 34 | mongo.afterEach().then(() => done()); 35 | }); 36 | 37 | after(() => server.killSession()); 38 | 39 | it('/api/v2/riddles GET should work as expected', done => { 40 | let riddle; 41 | 42 | models.createV2Riddle().then(created => { 43 | riddle = prune(created); 44 | 45 | return chai.request(app).get('/api/v2/riddles'); 46 | }).then(res => { 47 | expect(res.body).to.have.property('riddle'); 48 | expect(res.body.riddle).to.eql(riddle); 49 | 50 | done(); 51 | }).catch(err => done(err)); 52 | }); 53 | 54 | it('/api/v2/riddles/:key GET should work as expected', done => { 55 | let riddle; 56 | 57 | Promise.all([models.createV2Riddle(), models.createV2Riddle(), models.createV2Riddle()]).then(created => { 58 | riddle = prune(created[0]); 59 | 60 | return chai.request(app).get(`/api/v2/riddles/${riddle.key}`); 61 | }).then(res => { 62 | expect(res.body).to.have.property('riddle'); 63 | expect(res.body.riddle).to.eql(riddle); 64 | 65 | done(); 66 | }).catch(err => done(err)); 67 | }); 68 | 69 | it('/api/v2/riddles?difficulty={} GET should return an error if the difficulty is invalid', done => { 70 | let riddle; 71 | 72 | Promise.all([models.createV2Riddle({'difficulty': 'easy'}), models.createV2Riddle({'difficulty': 'normal'}), models.createV2Riddle({'difficulty': 'hard'})]).then(created => { 73 | riddle = prune(created[0]); 74 | 75 | return chai.request(app).get(`/api/v2/riddle?difficulty=invalid`); 76 | }).then(res => { 77 | expect(res.body).to.have.property('error'); 78 | 79 | done(); 80 | }).catch(err => done(err)); 81 | }); 82 | 83 | it('/api/v2/riddles?difficulty={} GET should work as expected', done => { 84 | let riddle; 85 | 86 | Promise.all([models.createV2Riddle({'difficulty': 'easy'}), models.createV2Riddle({'difficulty': 'normal'}), models.createV2Riddle({'difficulty': 'hard'})]).then(created => { 87 | riddle = prune(created[0]); 88 | 89 | return chai.request(app).get(`/api/v2/riddles?difficulty=${riddle.difficulty}`); 90 | }).then(res => { 91 | expect(res.body).to.have.property('riddle'); 92 | expect(res.body.riddle).to.eql(riddle); 93 | 94 | done(); 95 | }).catch(err => done(err)); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/backend/integration/v2.suggestions.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const chaiHttp = require('chai-http'); 5 | const expect = chai.expect; 6 | const models = require('@t/backend/utils/models'); 7 | 8 | const server = require('@t/backend/utils/server'); 9 | const mongo = require('@t/backend/utils/mongo'); 10 | 11 | let app; 12 | 13 | chai.use(chaiAsPromised); 14 | chai.use(chaiHttp); 15 | 16 | describe('Suggestions v2 routes should work as expected', () => { 17 | before(() => { 18 | app = server.getNewApp(); 19 | }); 20 | 21 | beforeEach(done => { 22 | mongo.beforeEach().then(() => done()); 23 | }); 24 | 25 | afterEach(done => { 26 | mongo.afterEach().then(() => done()); 27 | }); 28 | 29 | after(() => server.killSession()); 30 | 31 | it('/api/v2/suggestions POST for activities should fail if missing fields', done => { 32 | let suggestion = { 33 | activity: { 34 | activity: 'Missing other fields' 35 | } 36 | }; 37 | 38 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 39 | expect(res.body).to.have.property('error'); 40 | 41 | done(); 42 | }).catch(err => done(err)); 43 | }); 44 | 45 | it('/api/v2/suggestions POST for activites should fail if activity is not a string', done => { 46 | let suggestion = { 47 | activity: { 48 | activity: 1, 49 | type: 'education', 50 | participants: 1 51 | } 52 | }; 53 | 54 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 55 | expect(res.body).to.have.property('error'); 56 | 57 | done(); 58 | }).catch(err => done(err)); 59 | }); 60 | 61 | it('/api/v2/suggestions POST for activities should fail if type is invalid', done => { 62 | let suggestion = { 63 | activity: { 64 | activity: 'Bubble wrap the house', 65 | type: 'prank', 66 | participants: 1 67 | } 68 | 69 | }; 70 | 71 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 72 | expect(res.body).to.have.property('error'); 73 | 74 | done(); 75 | }).catch(err => done(err)); 76 | }); 77 | 78 | it('/api/v2/suggestions POST for activities should fail if participants is not a number', done => { 79 | let suggestion = { 80 | activity: { 81 | activity: 'Sing in the shower', 82 | type: 'relaxation', 83 | participants: 'Just me' 84 | } 85 | }; 86 | 87 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 88 | expect(res.body).to.have.property('error'); 89 | 90 | done(); 91 | }).catch(err => done(err)); 92 | }); 93 | 94 | it('/api/v2/suggestions POST for activities should fail if participants is less than one', done => { 95 | let suggestion = { 96 | activity: { 97 | activity: 'Exist', 98 | type: 'diy', 99 | participants: -1 100 | } 101 | }; 102 | 103 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 104 | expect(res.body).to.have.property('error'); 105 | 106 | done(); 107 | }).catch(err => done(err)); 108 | }); 109 | 110 | it('/api/v2/suggestions POST for activities should work as expected', done => { 111 | let suggestion = models.createV2Suggestion('activity', {}, true); 112 | 113 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 114 | expect(res.body).to.have.property('message'); 115 | 116 | done(); 117 | }).catch(err => done(err)); 118 | }); 119 | 120 | it('/api/v2/suggestions POST for facts should fail if missing fields', done => { 121 | let suggestion = {fact: {}}; 122 | 123 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 124 | expect(res.body).to.have.property('error'); 125 | 126 | done(); 127 | }).catch(err => done(err)); 128 | }); 129 | 130 | it('/api/v2/suggestions POST for facts should fail if fact is not a string', done => { 131 | let suggestion = { 132 | fact: { 133 | fact: 100101, 134 | source: 'https://google.com' 135 | } 136 | }; 137 | 138 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 139 | expect(res.body).to.have.property('error'); 140 | 141 | done(); 142 | }).catch(err => done(err)); 143 | }); 144 | 145 | it('/api/v2/suggestions POST for facts should fail if source is not a url', done => { 146 | let suggestion = { 147 | fact: { 148 | fact: 'Tabs >>> spaces', 149 | source: 'everywhere' 150 | } 151 | }; 152 | 153 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 154 | expect(res.body).to.have.property('error'); 155 | 156 | done(); 157 | }).catch(err => done(err)); 158 | }); 159 | 160 | it('/api/v2/suggestions POST for facts should work as expected', done => { 161 | let suggestion = models.createV2Suggestion('fact', {}, true); 162 | 163 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 164 | expect(res.body).to.have.property('message'); 165 | 166 | done(); 167 | }).catch(err => done(err)); 168 | }); 169 | 170 | it('/api/v2/suggestions POST for riddles should fail if missing fields', done => { 171 | let suggestion = {riddle: {}}; 172 | 173 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 174 | expect(res.body).to.have.property('error'); 175 | 176 | done(); 177 | }).catch(err => done(err)); 178 | }); 179 | 180 | it('/api/v2/suggestions POST for riddles should fail if source is not a url', done => { 181 | let suggestion = { 182 | riddle: { 183 | question: 'What english word has three consecutive double letters?', 184 | answer: 'Bookkeeper', 185 | url: 'big brain energy' 186 | } 187 | }; 188 | 189 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 190 | expect(res.body).to.have.property('error'); 191 | 192 | done(); 193 | }).catch(err => done(err)); 194 | }); 195 | 196 | it('/api/v2/suggestions POST for riddles should work as expected', done => { 197 | let suggestion = models.createV2Suggestion('riddle', {}, true); 198 | 199 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 200 | expect(res.body).to.have.property('message'); 201 | 202 | done(); 203 | }).catch(err => done(err)); 204 | }); 205 | 206 | it('/api/v2/suggestions POST for websites should fail if missing fields', done => { 207 | let suggestion = {website: {}}; 208 | 209 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 210 | expect(res.body).to.have.property('error'); 211 | 212 | done(); 213 | }).catch(err => done(err)); 214 | }); 215 | 216 | it('/api/v2/suggestions POST for websites should fail if url is not a url', done => { 217 | let suggestion = {website: 218 | { 219 | url: 'not a url', 220 | description: 'Does something cool, but I don\'t know what' 221 | } 222 | }; 223 | 224 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 225 | expect(res.body).to.have.property('error'); 226 | 227 | done(); 228 | }).catch(err => done(err)); 229 | }); 230 | 231 | it('/api/v2/suggestions POST for websites should work as expected', done => { 232 | let suggestion = models.createV2Suggestion('website', {}, true); 233 | 234 | chai.request(app).post('/api/v2/suggestions').send(suggestion).then(res => { 235 | expect(res.body).to.have.property('message'); 236 | 237 | done(); 238 | }).catch(err => done(err)); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /test/backend/integration/v2.websites.test.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const chaiHttp = require('chai-http'); 5 | const expect = chai.expect; 6 | const models = require('@t/backend/utils/models'); 7 | const server = require('@t/backend/utils/server'); 8 | const mongo = require('@t/backend/utils/mongo'); 9 | 10 | let app; 11 | 12 | chai.use(chaiAsPromised); 13 | chai.use(chaiHttp); 14 | 15 | const prune = website => { 16 | let pruned = website.toObject(); 17 | 18 | delete pruned._id; 19 | delete pruned.__v; 20 | 21 | return pruned; 22 | }; 23 | 24 | describe('Websites v2 routes should work as expected', () => { 25 | before(() => { 26 | app = server.getNewApp(); 27 | }); 28 | 29 | beforeEach(done => { 30 | mongo.beforeEach().then(() => done()); 31 | }); 32 | 33 | afterEach(done => { 34 | mongo.afterEach().then(() => done()); 35 | }); 36 | 37 | after(() => server.killSession()); 38 | 39 | it('/api/v2/websites GET should work as expected', done => { 40 | let website; 41 | 42 | models.createV2Website().then(created => { 43 | website = prune(created); 44 | 45 | return chai.request(app).get('/api/v2/websites'); 46 | }).then(res => { 47 | expect(res.body).to.have.property('website'); 48 | expect(res.body.website).to.eql(website); 49 | 50 | done(); 51 | }).catch(err => done(err)); 52 | }); 53 | 54 | it('/api/v2/websites/:key GET should work as expected', done => { 55 | let website; 56 | 57 | Promise.all([models.createV2Website(), models.createV2Fact(), models.createV2Fact()]).then(created => { 58 | website = prune(created[0]); 59 | 60 | return chai.request(app).get(`/api/v2/websites/${website.key}`); 61 | }).then(res => { 62 | expect(res.body).to.have.property('website'); 63 | expect(res.body.website).to.eql(website); 64 | 65 | done(); 66 | }).catch(err => done(err)); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/backend/utils/models.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const { 3 | Activity, 4 | Fact, 5 | Riddle, 6 | Website, 7 | ActivitySuggestion, 8 | FactSuggestion, 9 | RiddleSuggestion, 10 | WebsiteSuggestion 11 | } = require('@b/models'); 12 | 13 | exports.createV1Activity = params => { 14 | const activity = Object.assign({ 15 | activity: faker.random.words(), 16 | availability: faker.random.number({min: 0.1, max: 1, precision: 0.1}), 17 | type: faker.random.objectElement(['education', 'recreational', 'social', 'diy', 'charity', 'cooking', 'relaxation', 'music', 'busywork']), 18 | participants: faker.random.number({min: 1, max: 5}), 19 | price: faker.random.number({min: 0.1, max: 1, precision: 0.1}), 20 | duration: faker.random.objectElement(['minutes', 'hours', 'days', 'weeks']), 21 | kidFriendly: faker.random.boolean(), 22 | link: faker.internet.url(), 23 | key: faker.random.number({min: 1000000, max: 9999999}) 24 | }, params); 25 | 26 | return new Activity(activity).save(); 27 | }; 28 | 29 | exports.createV2Activity = params => { 30 | const activity = Object.assign({ 31 | activity: faker.random.words(), 32 | availability: faker.random.number({min: 0.1, max: 1, precision: 0.1}), 33 | accessibility: faker.random.objectElement(['Few to no challenges', 'Minor challenges', 'Major challenges']), 34 | type: faker.random.objectElement(['education', 'recreational', 'social', 'diy', 'charity', 'cooking', 'relaxation', 'music', 'busywork']), 35 | participants: faker.random.number({min: 1, max: 5}), 36 | price: faker.random.number({min: 0.1, max: 1, precision: 0.1}), 37 | duration: faker.random.objectElement(['minutes', 'hours', 'days', 'weeks']), 38 | kidFriendly: faker.random.boolean(), 39 | link: faker.internet.url(), 40 | key: faker.random.number({min: 1000000, max: 9999999}) 41 | }, params); 42 | 43 | return new Activity(activity).save(); 44 | }; 45 | 46 | exports.createV2Fact = params => { 47 | const fact = Object.assign({ 48 | fact: faker.random.words(), 49 | source: faker.internet.url(), 50 | key: faker.random.number({min: 1000000, max: 9999999}) 51 | }, params); 52 | 53 | return new Fact(fact).save(); 54 | }; 55 | 56 | exports.createV2Riddle = params => { 57 | const riddle = Object.assign({ 58 | question: faker.random.words(), 59 | answer: faker.random.words(), 60 | difficulty: faker.random.objectElement(['easy', 'normal', 'hard']), 61 | source: faker.internet.url(), 62 | key: faker.random.number({min: 1000000, max: 9999999}) 63 | }, params); 64 | 65 | return new Riddle(riddle).save(); 66 | }; 67 | 68 | exports.createV2Website = params => { 69 | const website = Object.assign({ 70 | url: faker.internet.url(), 71 | description: faker.random.words(), 72 | key: faker.random.number({min: 1000000, max: 9999999}) 73 | }, params); 74 | 75 | return new Website(website).save(); 76 | }; 77 | 78 | exports.createV2Suggestion = (type, params = {}, dry) => { 79 | let defaultValues; 80 | let suggestionModel; 81 | 82 | switch (type) { 83 | case 'activity': 84 | defaultValues = Object.assign({ 85 | activity: faker.random.words(), 86 | type: faker.random.objectElement(['education', 'recreational', 'social', 'diy', 'charity', 'cooking', 'relaxation', 'music', 'busywork']), 87 | participants: faker.random.number({min: 1, max: 10}) 88 | }); 89 | suggestionModel = ActivitySuggestion; 90 | break; 91 | 92 | case 'fact': 93 | defaultValues = Object.assign({ 94 | fact: faker.random.words(), 95 | source: faker.internet.url() 96 | }); 97 | suggestionModel = FactSuggestion; 98 | break; 99 | 100 | case 'riddle': 101 | defaultValues = Object.assign({ 102 | question: faker.random.words(), 103 | answer: faker.random.words(), 104 | source: faker.internet.url() 105 | }); 106 | suggestionModel = RiddleSuggestion; 107 | break; 108 | 109 | case 'website': 110 | defaultValues = Object.assign({ 111 | url: faker.internet.url(), 112 | description: faker.random.words() 113 | }); 114 | suggestionModel = WebsiteSuggestion; 115 | break; 116 | 117 | default: 118 | throw new Error(`createV2Suggestion: Invalid type '${type}'`); 119 | } 120 | 121 | const suggestion = Object.assign(defaultValues, params); 122 | 123 | if (dry) return {[type]: suggestion}; 124 | return new suggestionModel({[type]: suggestion}).save(); 125 | } 126 | -------------------------------------------------------------------------------- /test/backend/utils/mongo.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const MongoMemoryServer = require('mongodb-memory-server').MongoMemoryServer; 3 | const mongoose = require('mongoose'); 4 | const faker = require('faker'); 5 | 6 | const Activity = require('@b/models/Activity'); 7 | 8 | let server; 9 | 10 | exports.beforeEach = () => { 11 | server = new MongoMemoryServer(); 12 | return server.getConnectionString().then(uri => { 13 | return mongoose.connect(uri, { 14 | useCreateIndex: true, 15 | useNewUrlParser: true, 16 | useUnifiedTopology: true, 17 | useFindAndModify: false 18 | }); 19 | }); 20 | }; 21 | 22 | exports.afterEach = () => { 23 | return mongoose.disconnect().then(() => { 24 | return server.stop(); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /test/backend/utils/server.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register'); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | require('dotenv').config({path: __dirname + '/../../../.env'}); 5 | 6 | let app; 7 | let session; 8 | 9 | const createApp = () => { 10 | app = express(); 11 | 12 | app.use(bodyParser.json()); 13 | app.use(bodyParser.urlencoded({extended: false})); 14 | 15 | app.use(function(err, req, res, next) { 16 | if (err instanceof SyntaxError) { 17 | res.error('Invalid JSON'); 18 | return; 19 | } 20 | 21 | next(); 22 | }); 23 | 24 | app.use(require('../../../src/backend/routes')()); 25 | 26 | return app; 27 | } 28 | 29 | const createSession = () => { 30 | if (app) { 31 | session = app.listen(42014); 32 | } 33 | } 34 | 35 | const getApp = () => { 36 | if (!app) { 37 | return createApp(); 38 | } 39 | } 40 | 41 | const getNewApp = () => { 42 | killSession(); 43 | createApp(); 44 | createSession(); 45 | 46 | return app; 47 | } 48 | 49 | const killSession = () => { 50 | if (session) { 51 | return session.close(); 52 | } 53 | 54 | return; 55 | } 56 | 57 | module.exports = { 58 | getApp, 59 | getNewApp, 60 | killSession 61 | } 62 | -------------------------------------------------------------------------------- /test/db/activities.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const joi = require('@hapi/joi'); 4 | 5 | const activitySchema = joi.object().keys({ 6 | activity: joi.string().required(), 7 | type: joi.string().allow('charity', 'cooking', 'music', 'diy', 'education', 'social', 'busywork', 'recreational', 'relaxation').required(), 8 | participants: joi.number().min(1).required(), 9 | price: joi.number().min(0).max(1).required(), 10 | availability: joi.number().min(0).max(1).required(), 11 | accessibility: joi.string().allow('Few to no challenges', 'Minor challenges', 'Major challenges').required(), 12 | duration: joi.string().allow('minutes', 'hours', 'days', 'weeks').required(), 13 | kidFriendly: joi.boolean().required(), 14 | link: joi.string().uri().allow('').optional(), 15 | key: joi.string().length(7).required() 16 | }).required(); 17 | 18 | describe('Check that activities are valid and well formatted', () => { 19 | let unchangedFacts; 20 | let activities; 21 | 22 | before(() => { 23 | unchangedFacts = fs.readFileSync(path.join(__dirname, '../../db/activities.json'), 'utf8') 24 | .split(/\r?\n/) 25 | .filter(activity => activity.length > 0); 26 | }); 27 | 28 | beforeEach(() => activities = unchangedFacts); 29 | 30 | 31 | it('Each line should be valid JSON', done => { 32 | for (let index in activities) { 33 | let activity = activities[index]; 34 | 35 | try { 36 | JSON.parse(activity); 37 | } catch (err) { 38 | err.message = `Error on line ${++index}: ${err.message}`; 39 | done(err); 40 | return; 41 | } 42 | } 43 | 44 | done(); 45 | }); 46 | 47 | it('Each line should match the schema', done => { 48 | for (let index in activities) { 49 | let activity; 50 | 51 | try { 52 | activity = JSON.parse(activities[index]); 53 | } catch (err) { 54 | err.message = `Error on line ${++index}: ${err.message}`; 55 | done(err); 56 | return; 57 | } 58 | 59 | let err = activitySchema.validate(activity).error; 60 | 61 | if (err) { 62 | err.message = `Error on line ${++index}: ${err.message}`; 63 | done(err); 64 | return; 65 | } 66 | } 67 | 68 | done(); 69 | }); 70 | 71 | it('Each key should be unique', done => { 72 | let keys = []; 73 | 74 | for (let index in activities) { 75 | let activity; 76 | 77 | try { 78 | activity = JSON.parse(activities[index]); 79 | } catch (err) { 80 | err.message = `Error on line ${++index}: ${err.message}`; 81 | done(err); 82 | return; 83 | } 84 | 85 | if (keys.includes(activity.key)) { 86 | done(new Error(`Error on line ${++index}: Duplicate key`)); 87 | return; 88 | } 89 | 90 | keys.push(activity.key); 91 | } 92 | 93 | done(); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/db/facts.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const joi = require('@hapi/joi'); 4 | 5 | const factSchema = joi.object().keys({ 6 | fact: joi.string().required(), 7 | source: joi.string().uri().allow('').optional(), 8 | key: joi.string().length(7).required() 9 | }).required(); 10 | 11 | describe('Check that facts are valid and well formatted', () => { 12 | let unchangedFacts; 13 | let facts; 14 | 15 | before(() => { 16 | unchangedFacts = fs.readFileSync(path.join(__dirname, '../../db/facts.json'), 'utf8') 17 | .split(/\r?\n/) 18 | .filter(fact => fact.length > 0); 19 | }); 20 | 21 | beforeEach(() => facts = unchangedFacts); 22 | 23 | 24 | it('Each line should be valid JSON', done => { 25 | for (let index in facts) { 26 | let fact = facts[index]; 27 | 28 | try { 29 | JSON.parse(fact); 30 | } catch (err) { 31 | err.message = `Error on line ${++index}: ${err.message}`; 32 | done(err); 33 | return; 34 | } 35 | } 36 | 37 | done(); 38 | }); 39 | 40 | it('Each line should match the schema', done => { 41 | for (let index in facts) { 42 | let fact; 43 | 44 | try { 45 | fact = JSON.parse(facts[index]); 46 | } catch (err) { 47 | err.message = `Error on line ${++index}: ${err.message}`; 48 | done(err); 49 | return; 50 | } 51 | 52 | let err = factSchema.validate(fact).error; 53 | 54 | if (err) { 55 | err.message = `Error on line ${++index}: ${err.message}`; 56 | done(err); 57 | return; 58 | } 59 | } 60 | 61 | done(); 62 | }); 63 | 64 | it('Each key should be unique', done => { 65 | let keys = []; 66 | 67 | for (let index in facts) { 68 | let fact; 69 | 70 | try { 71 | fact = JSON.parse(facts[index]); 72 | } catch (err) { 73 | err.message = `Error on line ${++index}: ${err.message}`; 74 | done(err); 75 | return; 76 | } 77 | 78 | if (keys.includes(fact.key)) { 79 | done(new Error(`Error on line ${++index}: Duplicate key`)); 80 | return; 81 | } 82 | 83 | keys.push(fact.key); 84 | } 85 | 86 | done(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/db/riddles.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const joi = require('@hapi/joi'); 4 | 5 | const riddleSchema = joi.object().keys({ 6 | question: joi.string().required(), 7 | answer: joi.string().required(), 8 | difficulty: joi.string().valid('easy', 'normal', 'hard'), 9 | source: joi.string().uri().allow('').optional(), 10 | key: joi.string().length(7).required() 11 | }).required(); 12 | 13 | describe('Check that riddles are valid and well formatted', () => { 14 | let unchangedRiddles; 15 | let riddles; 16 | 17 | before(() => { 18 | unchangedRiddles = fs.readFileSync(path.join(__dirname, '../../db/riddles.json'), 'utf8') 19 | .split(/\r?\n/) 20 | .filter(riddle => riddle.length > 0); 21 | }); 22 | 23 | beforeEach(() => riddles = unchangedRiddles); 24 | 25 | 26 | it('Each line should be valid JSON', done => { 27 | for (let index in riddles) { 28 | let riddle = riddles[index]; 29 | 30 | try { 31 | JSON.parse(riddle); 32 | } catch (err) { 33 | err.message = `Error on line ${++index}: ${err.message}`; 34 | done(err); 35 | return; 36 | } 37 | } 38 | 39 | done(); 40 | }); 41 | 42 | it('Each line should match the schema', done => { 43 | for (let index in riddles) { 44 | let riddle; 45 | 46 | try { 47 | riddle = JSON.parse(riddles[index]); 48 | } catch (err) { 49 | err.message = `Error on line ${++index}: ${err.message}`; 50 | done(err); 51 | return; 52 | } 53 | 54 | let err = riddleSchema.validate(riddle).error; 55 | 56 | if (err) { 57 | err.message = `Error on line ${++index}: ${err.message}`; 58 | done(err); 59 | return; 60 | } 61 | } 62 | 63 | done(); 64 | }); 65 | 66 | it('Each key should be unique', done => { 67 | let keys = []; 68 | 69 | for (let index in riddles) { 70 | let riddle; 71 | 72 | try { 73 | riddle = JSON.parse(riddles[index]); 74 | } catch (err) { 75 | err.message = `Error on line ${++index}: ${err.message}`; 76 | done(err); 77 | return; 78 | } 79 | 80 | if (keys.includes(riddle.key)) { 81 | done(new Error(`Error on line ${++index}: Duplicate key`)); 82 | return; 83 | } 84 | 85 | keys.push(riddle.key); 86 | } 87 | 88 | done(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/db/websites.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const joi = require('@hapi/joi'); 4 | 5 | const websiteSchema = joi.object().keys({ 6 | url: joi.string().uri().allow('').required(), 7 | description: joi.string().required(), 8 | key: joi.string().length(7).required() 9 | }).required(); 10 | 11 | describe('Check that websites are valid and well formatted', () => { 12 | let unchangedWebsites; 13 | let websites; 14 | 15 | before(() => { 16 | unchangedWebsites = fs.readFileSync(path.join(__dirname, '../../db/websites.json'), 'utf8') 17 | .split(/\r?\n/) 18 | .filter(website => website.length > 0); 19 | }); 20 | 21 | beforeEach(() => websites = unchangedWebsites); 22 | 23 | 24 | it('Each line should be valid JSON', done => { 25 | for (let index in websites) { 26 | let website = websites[index]; 27 | 28 | try { 29 | JSON.parse(website); 30 | } catch (err) { 31 | err.message = `Error on line ${++index}: ${err.message}`; 32 | done(err); 33 | return; 34 | } 35 | } 36 | 37 | done(); 38 | }); 39 | 40 | it('Each line should match the schema', done => { 41 | for (let index in websites) { 42 | let website; 43 | 44 | try { 45 | website = JSON.parse(websites[index]); 46 | } catch (err) { 47 | err.message = `Error on line ${++index}: ${err.message}`; 48 | done(err); 49 | return; 50 | } 51 | 52 | let err = websiteSchema.validate(website).error; 53 | 54 | if (err) { 55 | err.message = `Error on line ${++index}: ${err.message}`; 56 | done(err); 57 | return; 58 | } 59 | } 60 | 61 | done(); 62 | }); 63 | 64 | it('Each key should be unique', done => { 65 | let keys = []; 66 | 67 | for (let index in websites) { 68 | let website; 69 | 70 | try { 71 | website = JSON.parse(websites[index]); 72 | } catch (err) { 73 | err.message = `Error on line ${++index}: ${err.message}`; 74 | done(err); 75 | return; 76 | } 77 | 78 | if (keys.includes(website.key)) { 79 | done(new Error(`Error on line ${++index}: Duplicate key`)); 80 | return; 81 | } 82 | 83 | keys.push(website.key); 84 | } 85 | 86 | done(); 87 | }); 88 | }); 89 | --------------------------------------------------------------------------------