├── src ├── public │ └── images │ │ └── favicon.ico └── index.ts ├── jest.config.js ├── .vscode ├── extensions.json ├── generic.code-snippets ├── settings.json ├── jest.code-snippets └── launch.json ├── .prettierrc.js ├── views ├── css │ └── index.css └── index.pug ├── .editorconfig ├── tsconfig.json ├── .eslintrc.js ├── README.md ├── .gitignore └── package.json /src/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoriviera/thefactspace/HEAD/src/public/images/favicon.ico -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "msjsdiag.debugger-for-chrome" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 4, 7 | }; 8 | -------------------------------------------------------------------------------- /views/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Shippori Mincho", "serif" 3 | } 4 | 5 | a { 6 | text-decoration: underline; 7 | } 8 | 9 | .fact { 10 | line-height: 1.2; 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | -------------------------------------------------------------------------------- /.vscode/generic.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "console-debug-generic": { 3 | "scope": "javascript,typescript,javascriptreact,typescriptreact", 4 | "prefix": "console log debug", 5 | "description": "Console debug message with 'REMOVEME' prefix", 6 | "body": [ 7 | "console.debug(`REMOVEME: ${1}`${0});" 8 | ] 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "baseUrl": "src", 7 | "sourceMap": true, 8 | "target": "es6", 9 | "lib": [ 10 | "dom", 11 | "esnext" 12 | ], 13 | "outDir": "dist", 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "isolatedModules": true, 17 | "resolveJsonModule": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit", 5 | "source.addMissingImports": "explicit" 6 | }, 7 | "eslint.validate": [ 8 | "javascript", 9 | "javascriptreact", 10 | "typescript", 11 | "typescriptreact" 12 | ], 13 | "files.exclude": { 14 | "**/node_modules": true 15 | }, 16 | "typescript.tsdk": "node_modules/typescript/lib", 17 | "files.autoSave": "afterDelay" 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["@typescript-eslint"], 4 | extends: [ 5 | // Recommended ts/es-lint rules 6 | 'plugin:@typescript-eslint/recommended', 7 | // Disable ESLint rules conflicting with Prettier 8 | 'prettier/@typescript-eslint', 9 | // Displays Prettier errors as ESLint errors - must be last plugin in 'extends' array 10 | 'plugin:prettier/recommended', 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 6, 14 | sourceType: "module", 15 | ecmaFeatures: { 16 | modules: true, 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/jest.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "describe-jest": { 3 | "scope": "javascript,typescript,javascriptreact,typescriptreact", 4 | "prefix": "describe - jest", 5 | "body": [ 6 | "describe('${1}', () => {", 7 | "\t${0}", 8 | "});" 9 | ], 10 | "description": "Jest Test Describe" 11 | }, 12 | "it-jest": { 13 | "scope": "javascript,typescript,javascriptreact,typescriptreact", 14 | "prefix": "it - jest", 15 | "body": [ 16 | "it('${1}', () => {", 17 | "\t// ARRANGE", 18 | "\t${0}", 19 | "\t", 20 | "\t// ACT", 21 | "\t", 22 | "\t// ASSERT", 23 | "\t", 24 | "});" 25 | ], 26 | "description": "Jest Test It Arrange Act Assert" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Facts API 2 | 3 | A RESTful API which returns useless, random facts gathered from all over the Internet. 4 | 5 | The `facts-api` project provides a website which serves random facts, accessible at `/`, along with a JSON endpoint at `/random`. Users can also find a particular fact by its index using `/facts/:index`. 6 | 7 | ![Screenshot of Fact #1734](https://user-images.githubusercontent.com/11467778/109699389-6da17a80-7b88-11eb-9c6b-79e725bc244f.png) 8 | 9 | Feel free to contribute your own facts by making a pull request! If you notice anything incorrect, then simply create an issue. You're also able to clone this repo and use it to serve any niche facts you please! 10 | 11 | This TypeScript project uses [Express](https://expressjs.com/) to serve requests and [Pug](https://pugjs.org/api/getting-started.html) to render the website. 12 | 13 | To run this project, 14 | - Clone the repository using `git clone https://github.com/leoriviera/thefactspace.git facts`, 15 | - Enter the folder using `cd facts` and install dependencies using `npm install`, 16 | - Build the project using `npm run build`, 17 | - Run on `http://localhost:3000` using `node src/index.js`. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # OSX 64 | .DS_Store 65 | 66 | # project specific 67 | dist 68 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Node Inspector", 6 | "type": "node", 7 | "request": "launch", 8 | "args": ["${workspaceRoot}/src/index.ts"], 9 | "runtimeArgs": ["-r", "ts-node/register"], 10 | "cwd": "${workspaceRoot}", 11 | "protocol": "inspector", 12 | "internalConsoleOptions": "neverOpen" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Jest Current File", 18 | "program": "${workspaceFolder}/node_modules/.bin/jest", 19 | "args": [ 20 | "${file}", 21 | "--config", 22 | "jest.config.js" 23 | ], 24 | "console": "integratedTerminal", 25 | "internalConsoleOptions": "neverOpen", 26 | "disableOptimisticBPs": true, 27 | "cwd": "${workspaceFolder}" 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "Jest All", 33 | "program": "${workspaceFolder}/node_modules/.bin/jest", 34 | "args": [ 35 | "--runInBand", 36 | "--bail" 37 | ], 38 | "console": "integratedTerminal", 39 | "internalConsoleOptions": "neverOpen", 40 | "disableOptimisticBPs": true, 41 | "cwd": "${workspaceFolder}" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='X-UA-Compatible' content='IE=edge') 6 | meta(name='viewport' content='width=device-width, initial-scale=1') 7 | meta(name='description' content='Enjoy a random fact on every refresh. By Leo.') 8 | 9 | link(rel='stylesheet' href='https://fonts.googleapis.com/css2?family=Shippori+Mincho:wght@400;500;600&display=swap') 10 | link(rel='stylesheet' href='https://unpkg.com/bulma@0.9.0/css/bulma.min.css') 11 | 12 | style 13 | include ./css/index.css 14 | 15 | title The Fact Space: Facts Nobody Asked For 16 | 17 | body 18 | section.hero.is-fullheight.is-black 19 | div.hero-body 20 | div.container.has-text-centered 21 | div.column.is-6.is-offset-3 22 | h1.has-text-weight-light 23 | span.is-size-4.is-uppercase 24 | | Fact ##{index} 25 | span.is-size-5 26 | | from 27 | span.is-size-5.is-italic 28 | | #[a(href=source) here]. 29 | h2.fact.is-size-2.has-text-weight-semibold.mb-4 #{text} 30 | h4.is-size-6 An #[a(href="https://github.com/leoriviera/facts-api") open source project] by Leo. Call the endpoint #[a(href='./random') /random] to get one of #{factCount} facts. 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-boilerplate", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "license": "UNLICENSED", 6 | "private": true, 7 | "scripts": { 8 | "start": "ts-node src/index", 9 | "clean": "rimraf dist/", 10 | "copy-files": "copyfiles -u 1 src/**/*.* -e src/**/*.ts dist/", 11 | "build": "npm run clean && tsc && npm run copy-files", 12 | "lint": "eslint src/**", 13 | "lint:tsc": "tsc --noEmit", 14 | "test": "jest", 15 | "test:watch": "jest --watchAll" 16 | }, 17 | "importSort": { 18 | ".ts, .tsx": { 19 | "parser": "typescript", 20 | "style": "module-scoped" 21 | } 22 | }, 23 | "eslintIgnore": [ 24 | "/**/*.js" 25 | ], 26 | "dependencies": { 27 | "@types/jest": "26.0.15", 28 | "@types/node": "14.14.3", 29 | "@typescript-eslint/eslint-plugin": "4.5.0", 30 | "@typescript-eslint/parser": "4.5.0", 31 | "dotenv": "^8.2.0", 32 | "eslint": "7.12.0", 33 | "eslint-config-prettier": "6.14.0", 34 | "eslint-plugin-prettier": "3.1.4", 35 | "express": "^4.17.1", 36 | "import-sort": "6.0.0", 37 | "import-sort-cli": "6.0.0", 38 | "import-sort-parser-typescript": "6.0.0", 39 | "import-sort-style-module-scoped": "1.0.3", 40 | "jest": "26.6.1", 41 | "prettier": "2.1.2", 42 | "prettier-plugin-import-sort": "0.0.6", 43 | "pug": "^3.0.2", 44 | "serve-favicon": "^2.5.0", 45 | "ts-jest": "26.4.2", 46 | "ts-node": "^9.1.1", 47 | "typescript": "4.0.3" 48 | }, 49 | "devDependencies": { 50 | "@types/express": "^4.17.11", 51 | "@types/serve-favicon": "^2.5.2", 52 | "copyfiles": "^2.4.1", 53 | "rimraf": "^3.0.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import dotenv from 'dotenv'; 4 | import express from 'express'; 5 | import favicon from 'serve-favicon'; 6 | 7 | import facts from './facts'; 8 | 9 | dotenv.config(); 10 | 11 | type Fact = { 12 | text: string; 13 | source: string; 14 | index: number; 15 | }; 16 | 17 | // Get the total number of facts 18 | const factCount = facts.length; 19 | 20 | const getFact = (index: number): Fact | undefined => { 21 | // Get fact at index 22 | const fact = facts[index]; 23 | 24 | if (fact) { 25 | // Return destructured fact with index 26 | return { 27 | index, 28 | ...fact, 29 | }; 30 | } 31 | 32 | return undefined; 33 | }; 34 | 35 | // Get a random fact 36 | const getRandomFact = () => { 37 | // Generate random number between 0 and facts.length - 1 38 | const index = Math.floor(Math.random() * factCount); 39 | 40 | // Get fact at index 41 | const fact = getFact(index); 42 | 43 | // Return fact 44 | return fact; 45 | }; 46 | 47 | // Create an Express app, and specify pug as the view engine 48 | const app = express(); 49 | app.set('view engine', 'pug'); 50 | app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.ico'))); 51 | 52 | // On request to '/'... 53 | app.get('/', (_, res) => { 54 | // Get a random fact 55 | const fact = getRandomFact(); 56 | 57 | // Render index.pug with fact data and number of facts 58 | res.render('index', { 59 | ...fact, 60 | factCount, 61 | }); 62 | }); 63 | 64 | // Fetch a random fact 65 | app.get('/random', (req, res) => { 66 | // Get a fact 67 | const fact = getRandomFact(); 68 | 69 | // Return the fact object 70 | res.json(fact); 71 | }); 72 | 73 | // When fetching a fact by index... 74 | app.get('/fact/:index?', (req, res) => { 75 | // Extract index paramater and convert to integer 76 | const { index } = req.params; 77 | const indexInt = parseInt(index); 78 | 79 | // If the index is not an integer 80 | // (so, undefined, or has text)... 81 | if (isNaN(indexInt)) { 82 | // Redirect to a random fact instead 83 | res.redirect('/random'); 84 | return; 85 | } 86 | 87 | // Try to get the the fact 88 | const fact = getFact(indexInt); 89 | 90 | // If the fact is undefined... 91 | if (fact === undefined) { 92 | // Send error 93 | res.status(404).json({ 94 | index: indexInt, 95 | text: `Fact #${indexInt} hasn't been added, yet!`, 96 | }); 97 | return; 98 | } 99 | 100 | // If the fact is not undefined, 101 | // (and in the array's bounds), 102 | // send fact 103 | res.json(fact); 104 | return; 105 | }); 106 | 107 | app.get('/facts/:number', (req, res) => { 108 | // Set pagination multiple 109 | const multiple = 100; 110 | const { number } = req.params; 111 | 112 | // Get page number 113 | const pageNumber = parseInt(number); 114 | 115 | if (isNaN(pageNumber) || pageNumber < 0) { 116 | res.status(404).json({ 117 | text: "This page doesn't exist!", 118 | }); 119 | return; 120 | } 121 | 122 | const startIndex = pageNumber * multiple; 123 | const endIndex = startIndex + multiple; 124 | const factPage = facts.slice(startIndex, endIndex); 125 | res.json(factPage); 126 | }); 127 | 128 | // Get the port 129 | const port = process.env.PORT || 3000; 130 | 131 | // Bind app to port, and print message on successful listening 132 | app.listen(port, () => { 133 | console.log(`Hello! This server's listening on ${port}.`); 134 | }); 135 | --------------------------------------------------------------------------------