Skip to content

Commit

Permalink
Release 1.0 (#15)
Browse files Browse the repository at this point in the history
* Feature/frontend scaffolding (#1)

* Adds react base app
* Adds prettier and extends eslint rules with airbnb's ones
* Fixes reported issue with create react app when running eslint
See on facebook/create-react-app#8936
* Adds lint command to run eslint in the project
* Adds missing jsx and js extensions to eslint command
* Fixes linting errors
* Adds husky, lint-staged and pre-commit and pre-push rules
* Create CHANGELOG.md

* Feature/storybook (#2)

* Adds storybook
* Fixes issue between CRA and storybook
as seen in storybookjs/storybook#4764 (comment)

* Feature/components foundation (#3)

* Adds material ui library
* Adds dark theme
* Adds theme provider to the App main component
* Integrates material ui library with storybook
* Adds prop-types
* Adds themes and theme provider
* Adds window.matchMedia mock in jest configuration

* Feature/layout (#4)

* Updates light theme colors
* Removes padding from storybook stories
* Adds MainLayout component

* Feature/keyboard component (#5)

* Adds material-ui icons
* Adds key component
* Adds eslint rule omission for story files
* Adds keyboard component

* Fix/style improvements (#6)

* Changes themes colors and fixes deprecation message from createMuiTheme
* Solves issue related with 100vh on some mobile devices
* Makes key component responsive and replaces Paper wrapper component
* Update yarn.lock
* Adds css reset to storybook stories
* Removes maxWidth from Keyboard component

* Feature/screen component (#7)

* Adds Suggestions component
* Adds Screen component
* Adds Layout with Keyboard and Screen components story

* Feature/improves screen components (#8)

* Adds word button component
* Disables import/order eslint rule
* Adds ScreenText component
* Modifies Screen component and moves it to organisms

* Feature/local t9 typing (#9)

* Adds T9 keys handler
* Adds type definition file
* Adds useWord hook to handle the state of the words written by the user
* Adds useWriter hook to manage the t9's prediction words
* Ensambles the state and some components into the App component

* Feature/t9 service layer for frontend (#10)

* Adds service layer for T9 word prediction
* Replaces useWriter's fetchSuggestion function

* Update CHANGELOG.md

* Update README.md

* Feature/backend scaffolding (#11)

* Adds express-tool generated scaffold
* Adds eslint, prettier and airbnb linting rules
* Adds lint command
* Adds cors dependency
* Adds jest and supertest
* Adds jest rules for eslint
* Adds simple route test

* Feature/global commands (#12)

* Removes husky and lint-staged from frontend
* Adds root folder commands for both applications
* Splits lint-staged commands

* Backend t9 implementation (#13)

* Adds 10k words english dictionary sorted by global word frequency
* Adds Trie data structure
* Adds t9 key mapping
* Adds t9 service
* Adds nodemon
* Adds command to run tests on both, backend and frontend

* Updates README and CHANGELOG (#14)
  • Loading branch information
mbustosp committed Jun 17, 2021
1 parent ea5c60e commit ab1eb54
Show file tree
Hide file tree
Showing 81 changed files with 47,823 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Intellij
.idea

# Logs
logs
*.log
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2021-06-16

### Added

- Web app capable of handling phonelike keyboard typing, enhanced with T9 word prediction by the use of external services.
- Backend service that provides an endpoint that fetches t9 word predictions based on a 10000 English words dictionary.
- Global commands to handle both application, frontend and backend.
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,49 @@
# kiwi-challenge
Fullstack JS homework

![JS](https://img.icons8.com/nolan/2x/react-native.png)
## Fullstack JS homework

Welcome to my solution attempt of Kiwi's Fullstack JS homework. The project is structured in two parts, one is reserved for the frontend and the other is for the backend.

### The frontend

Consists of a React application which fetches T9 word prediction data from an API. It offers the user a similar experience of typing in old phones using numeric keyboards.

#### Features
- Light / Dark theme based on the user's color schema.
- Nice numeric keyboard.
- T9 mode switch.

### The backend

Consists of a Node/Express application that provides a web API from where the user can fetch T9 word prediction data. The suggestion algorithm
was implemented using Tries.

#### Features

- Word database taken from English word dictionary of 10000 entries.
- Suggestions are sorted by its frequency rank.

#### General

Both applications are configured to use eslint (with airbnb style guide), prettier and husky. There are git hooks that trigger
linting scripts and tests scripts before comitting and pushing, respectively.

### Instructions

#### How to install?
0. Move to the root folder of the project.
1. Run `npm run install` to install the tools.
2. Run `npm run both:install` to install the frontend and backend dependencies.
3. Run `npm run start` to start both applications. By default, the frontend will run in the port `3000` and the backend in the port `3001`

#### How do I run the tests?
0. Move to the root folder of the project.
1. Run `npm run both:test` to install the tools.

#### How do I change the URL of the API the frontend is using?
There is a .env file located in the frontend folder that has the environment variable that sets that value.
0. Move to the frontend folder.
1. Open the .env file.
2. Change the value of the REACT_APP_T9_API_URL variable.

1 change: 1 addition & 0 deletions backend/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
61 changes: 61 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
36 changes: 36 additions & 0 deletions backend/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Core dependencies
*/
const express = require("express");
const path = require("path");

/**
* Middleware dependencies
*/
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const cors = require("cors");

/**
* Routes
*/
const indexRouter = require("./routes/index");

/**
* Application init
*/
const app = express();

app.use(logger("dev"));
app.use(express.json());
app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));

/**
* Set the main router
*/
app.use("/", indexRouter);

module.exports = app;
3 changes: 3 additions & 0 deletions backend/assets/englishDictionary10K.json

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions backend/bin/www.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node
/* eslint-disable no-console */

/**
* Normalize a port into a number, string, or false.
*/

const normalizePort = (portToNormalize) => {
const normalizedPort = parseInt(portToNormalize, 10);

if (Number.isNaN(normalizedPort)) {
// named pipe
return normalizedPort;
}

if (normalizedPort >= 0) {
// port number
return normalizedPort;
}

return false;
};

/**
* Event listener for HTTP server "error" event.
*/

const getOnErrorHandler = (usedPort) => (error) => {
if (error.syscall !== "listen") {
throw error;
}

const bind =
typeof usedPort === "string" ? `Pipe ${usedPort}` : `Port ${usedPort}`;

// handle specific listen errors with friendly messages
switch (error.code) {
case "EACCES":
console.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case "EADDRINUSE":
console.error(`${bind} is already in use`);
process.exit(1);
break;
default:
throw error;
}
};

/**
* Event listener for HTTP server "listening" event.
*/
const getOnListeningHandler = (server, debug) => () => {
const addr = server.address();
const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`;
debug(`Listening on ${bind}`);
};

/**
* Module dependencies.
*/
const http = require("http");
const debug = require("debug")("backend:server");
const app = require("../app");

/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || "3000");

app.set("port", port);

/**
* Create HTTP server.
*/

const server = http.createServer(app);

/**
* Listen on provided port, on all network interfaces.
*/

server.listen(port);
server.on("error", getOnErrorHandler(port));
server.on("listening", getOnListeningHandler(server, debug));
48 changes: 48 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "backend",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "nodemon bin/www.js",
"lint": "eslint . --ext .js",
"test": "jest"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"morgan": "~1.9.1",
"nodemon": "^2.0.7"
},
"eslintConfig": {
"extends": [
"airbnb-base",
"prettier"
],
"plugins": [
"prettier",
"jest"
],
"env": {
"jest/globals": true
}
},
"devDependencies": {
"eslint": "^7.2.0",
"eslint-config-airbnb-base": "14.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-prettier": "^3.4.0",
"jest": "^27.0.4",
"prettier": "^2.3.1",
"supertest": "^6.1.3"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
]
}
}
13 changes: 13 additions & 0 deletions backend/routes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const express = require("express");
const { getSuggestions } = require("../service/t9");

const router = express.Router();

/* GET */
router.get("/", (req, res) => {
const digits = req.query.input;
const suggestions = getSuggestions(digits);
return res.send(suggestions);
});

module.exports = router;
12 changes: 12 additions & 0 deletions backend/routes/routes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const request = require("supertest");
const app = require("../app");

describe("GET endpoints", () => {
describe("/", () => {
it("should retrieve the T9 prediction words", async () => {
const res = await request(app).get("/").query({ input: "2" });
expect(res.statusCode).toEqual(200);
expect(res.body).toStrictEqual(["a", "c", "b"]);
});
});
});
16 changes: 16 additions & 0 deletions backend/service/t9.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Words are sorted by frequency. More info: https://github.com/first20hours/google-10000-english
const { words } = require("../assets/englishDictionary10K.json");
const Trie = require("../utils/Trie");

const suggestionsTrie = new Trie();

// Load dictionary into the Trie
words.forEach((word, idx) => {
suggestionsTrie.insert(word, idx);
});

// Exhibit the getSuggestions method
const getSuggestions = (digits) =>
suggestionsTrie.getSuggestions(digits).reverse();

module.exports = { getSuggestions };
16 changes: 16 additions & 0 deletions backend/service/t9.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable global-require */
describe("t9 service", () => {
it("has getSuggestion method", () => {
const t9 = require("./t9");
expect(
Object.prototype.hasOwnProperty.call(t9, "getSuggestions")
).toBeTruthy();
});

describe("getSuggestions", () => {
it("returns hello when 4-3-5-5-6 is pressed", () => {
const t9 = require("./t9");
expect(t9.getSuggestions("43556")).toBeTruthy();
});
});
});
Loading

0 comments on commit ab1eb54

Please sign in to comment.