├── .dockerignore
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE.md
├── README.md
├── images.d.ts
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── server
├── build
│ ├── routes
│ │ ├── outline.js
│ │ └── search.js
│ └── server.js
├── routes
│ ├── outline.ts
│ └── search.ts
├── server.ts
├── tsconfig.json
└── tslint.json
├── src
├── App.css
├── App.tsx
├── actions
│ └── index.ts
├── components
│ ├── About.tsx
│ ├── Loader.tsx
│ ├── Outline.tsx
│ ├── Result.tsx
│ ├── ResultsList.tsx
│ ├── ResultsView.tsx
│ └── SearchBox.tsx
├── containers
│ └── Home.tsx
├── images
│ ├── back-button.svg
│ ├── document.svg
│ ├── glass.svg
│ └── imgLoader.gif
├── index.css
├── index.tsx
├── logo.svg
├── reducers
│ ├── counterReducer.ts
│ ├── index.ts
│ ├── loadingStatus.ts
│ ├── outlineReducer.ts
│ ├── queryReducer.ts
│ ├── resultsReducer.ts
│ └── screenshotsReducer.ts
├── registerServiceWorker.ts
├── reset.min.css
└── style
│ ├── About.css
│ ├── Loader.css
│ ├── Outline.css
│ ├── Result.css
│ ├── ResultsView.css
│ └── SearchBox.css
├── test
└── index.js
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
├── tslint.json
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | test
3 | .vscode
4 | .idea
5 | .git
6 | .gitignore
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | #editors
7 | .vscode
8 | .idea
9 |
10 | # testing
11 | /coverage
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | install:
5 | - yarn global add mocha && yarn add chai puppeteer
6 | script:
7 | - mocha
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10.1.0
2 | COPY . .
3 | RUN yarn install --production && yarn build
4 | WORKDIR ./server
5 | EXPOSE 1337
6 | CMD node ./build/server.js
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 JoshuaRabiu
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # spresso-search
2 | [](https://travis-ci.com/JoshuaRabiu/spresso-search)
3 | [](https://github.com/prettier/prettier)
4 |
5 | 
6 |
7 | >Visual metasearch engine built with React, Redux, Express, and TypeScript.
8 |
9 | [Live link to site](http://spresso-search.herokuapp.com/)
10 |
11 | ## About
12 | Spresso Search scrapes search results from Google using the [node x-ray](https://github.com/matthewmueller/x-ray) library, and uses the same library to scrape obtain meta-information on webpages (preview images, favicons). There is a screenshot feature, which takes screenshots of sites that don't have meta preview images in their HTML. There is also a text-outline feature, powered by [node-unfluff](https://github.com/ageitgey/node-unfluff), which scrapes text content from web pages(ideal for articles & other text-rich pages), allowing the user to read the contents of a web page in clean, formatted text and without leaving the Spresso Search site.
13 |
14 | ## Running Locally
15 | To run Spresso Search locally, first clone the repo with: `git clone https://github.com/JoshuaRabiu/spresso-search.git`
16 |
17 |
18 | Then `cd` into its directory: `cd spresso-search`
19 |
20 | Install the dependencies with `yarn install`
21 |
22 | Then run `yarn start` to run the client side code. The app should be visible on port 3000.
23 |
24 | Open a new terminal tab/window in the same directory, and run `cd server` to go into the server directory.
25 |
26 | Run `node ./build/server.js` to start the server. The app is now ready for use.
27 |
28 | If making any modifications to the server's TypeScript code, you should start the TypeScript compiler in watch mode with
29 |
30 | `tsc -w` so your changes can be tracked in the JS build.
31 |
32 |
33 | ## License
34 | [MIT](https://github.com/JoshuaRabiu/spresso-search/blob/master/LICENSE.md)
35 |
--------------------------------------------------------------------------------
/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg'
2 | declare module '*.png'
3 | declare module '*.jpg'
4 | declare module '*.jpeg'
5 | declare module '*.gif'
6 | declare module '*.bmp'
7 | declare module '*.tiff'
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spresso-ts",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/express": "^4.16.0",
7 | "@types/rc-tooltip": "^3.4.12",
8 | "@types/react-infinite-scroll-component": "^4.2.3",
9 | "@types/react-redux": "^6.0.9",
10 | "@types/react-router-dom": "^4.3.1",
11 | "@types/redux-logger": "^3.0.6",
12 | "axios": "^0.18.0",
13 | "body-parser": "^1.18.3",
14 | "chai": "^4.2.0",
15 | "compression": "^1.7.3",
16 | "express": "^4.16.3",
17 | "git": "^0.1.5",
18 | "morgan": "^1.9.0",
19 | "puppeteer": "^1.9.0",
20 | "rc-tooltip": "^3.7.3",
21 | "react": "^16.4.2",
22 | "react-dom": "^16.4.2",
23 | "react-infinite-scroll-component": "^4.2.0",
24 | "react-redux": "^5.0.7",
25 | "react-router-dom": "^4.3.1",
26 | "react-scripts-ts": "3.1.0",
27 | "redux": "^4.0.1",
28 | "redux-thunk": "^2.3.0",
29 | "request": "^2.88.0",
30 | "styled-components": "^3.4.5",
31 | "unfluff": "^3.2.0"
32 | },
33 | "scripts": {
34 | "start": "react-scripts-ts start",
35 | "build": "react-scripts-ts build",
36 | "test": "react-scripts-ts test --env=jsdom",
37 | "eject": "react-scripts-ts eject"
38 | },
39 | "devDependencies": {
40 | "@types/jest": "^23.3.5",
41 | "@types/node": "^10.11.7",
42 | "@types/react": "^16.4.16",
43 | "@types/react-dom": "^16.0.9",
44 | "typescript": "^3.1.3"
45 | },
46 | "proxy": "http://127.0.0.1:1337"
47 | }
48 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JoshuaRabiu/spresso-search/dc2fee6c3f4b53f766f8dfb36638383df3617f9d/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/server/build/routes/outline.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const { Request, Response, Router } = require('express');
4 | const request = require('request');
5 | const unfluff = require('unfluff');
6 | const router = Router();
7 | router.post('/:site(*)', (req, response) => {
8 | const site = req.params.site;
9 | request(site, (err, res, body) => {
10 | const data = unfluff(body);
11 | response.send({
12 | title: data.title,
13 | text: data.text
14 | });
15 | });
16 | });
17 | exports.OutlineController = router;
18 |
--------------------------------------------------------------------------------
/server/build/routes/search.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const { Router } = require('express');
4 | const path = require('path');
5 | const { get } = require('axios');
6 | const router = Router();
7 | router.get('*', (req, res, next) => {
8 | res.sendFile(path.resolve('../', 'build/index.html'));
9 | });
10 | router.post('/:query/:start?', async (req, res) => {
11 | const { query, start } = req.params;
12 | try {
13 | const { data: { items } } = await get(`
14 | https://www.googleapis.com/customsearch/v1?key=AIzaSyBHU6yrWSnBlxoQXreinayGeLuWlyTyaLI&cx=000950167190857528722:vf0rypkbf0w&start=${start
15 | ? start
16 | : 10}&q=${encodeURI(query)}`);
17 | res.send(items);
18 | }
19 | catch (error) {
20 | console.error(error);
21 | }
22 | });
23 | exports.SearchController = router;
24 |
--------------------------------------------------------------------------------
/server/build/server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const logger = require('morgan');
5 | const { SearchController } = require('./routes/search.js');
6 | const { OutlineController } = require('./routes/outline.js');
7 | const path = require('path');
8 | const compression = require('compression');
9 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
10 | const app = express();
11 | app.use(compression());
12 | app.use(express.static(path.resolve('../', 'build')));
13 | app.use(bodyParser.text());
14 | app.use(logger('dev'));
15 | app.use('/search', SearchController);
16 | app.use('/outline', OutlineController);
17 | const port = process.env.PORT || 1337;
18 | app.listen(port, () => {
19 | console.log(`App listening on port ${port}`);
20 | });
21 |
--------------------------------------------------------------------------------
/server/routes/outline.ts:
--------------------------------------------------------------------------------
1 | const { Request, Response, Router } = require('express');
2 | import * as express from 'express';
3 | const request = require('request');
4 | const unfluff = require('unfluff');
5 |
6 | const router: any = Router();
7 |
8 | router.post('/:site(*)', (req: express.Request, response: express.Response) => {
9 | const site = req.params.site;
10 | request(site, (err: Error, res: any, body: any) => {
11 | const data = unfluff(body);
12 | response.send({
13 | title: data.title,
14 | text: data.text
15 | });
16 | });
17 | });
18 |
19 | export const OutlineController = router;
--------------------------------------------------------------------------------
/server/routes/search.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | const { Router } = require('express');
3 | const path = require('path');
4 | const { get } = require('axios');
5 |
6 | const router: express.Router = Router();
7 |
8 | router.get('*', (req: express.Request, res: express.Response, next: express.NextFunction): void => {
9 | res.sendFile(path.resolve('../', 'build/index.html'));
10 | });
11 |
12 | router.post('/:query/:start?', async (req: express.Request, res: express.Response) => {
13 | const { query, start } = req.params;
14 | try {
15 | const { data: { items } } = await get(`
16 | https://www.googleapis.com/customsearch/v1?key=AIzaSyBHU6yrWSnBlxoQXreinayGeLuWlyTyaLI&cx=000950167190857528722:vf0rypkbf0w&start=${start
17 | ? start
18 | : 10}&q=${encodeURI(query)}`);
19 |
20 | res.send(items);
21 | } catch (error) {
22 | console.error(error);
23 | }
24 | });
25 |
26 | export const SearchController = router;
27 |
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const logger = require('morgan');
4 | const { SearchController } = require('./routes/search.js');
5 | const { OutlineController } = require('./routes/outline.js');
6 | const path = require('path');
7 | const compression = require('compression');
8 |
9 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
10 |
11 | const app = express();
12 | app.use(compression());
13 | app.use(express.static(path.resolve('../', 'build')));
14 | app.use(bodyParser.text());
15 | app.use(logger('dev'));
16 | app.use('/search', SearchController);
17 | app.use('/outline', OutlineController);
18 |
19 | const port = process.env.PORT || 1337;
20 |
21 | app.listen(port, () => {
22 | console.log(`App listening on port ${port}`);
23 | });
24 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | // "lib": [], /* Specify library files to be included in the compilation. */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | "outDir": "./build", /* Redirect output structure to the directory. */
15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "composite": true, /* Enable project compilation */
17 | // "removeComments": true, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 |
23 | /* Strict Type-Checking Options */
24 | "strict": true, /* Enable all strict type-checking options. */
25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
31 |
32 | /* Additional Checks */
33 | // "noUnusedLocals": true, /* Report errors on unused locals. */
34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
37 |
38 | /* Module Resolution Options */
39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
43 | // "typeRoots": [], /* List of folders to include type definitions from. */
44 | // "types": [], /* Type declaration files to be included in compilation. */
45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
48 |
49 | /* Source Map Options */
50 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
51 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
54 |
55 | /* Experimental Options */
56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
58 | }
59 | }
--------------------------------------------------------------------------------
/server/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "jsRules": {},
7 | "rules": {
8 | "quotemark": [true, "single", "jsx-double"]
9 | },
10 | "rulesDirectory": []
11 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .about{
2 | float: right;
3 | padding: 15px;
4 | }
5 | .about:hover{
6 | text-decoration: underline;
7 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Home from './containers/Home';
3 | import { About } from './components/About';
4 | import { Switch, Route } from 'react-router-dom';
5 | import './App.css'
6 |
7 | export const App: React.StatelessComponent = ():JSX.Element => (
8 |
9 |
10 |
11 |
12 |
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { store } from '../reducers';
3 |
4 | export const setQuery = (e: any): void => {
5 | store.dispatch({ type: 'SET_QUERY', payload: encodeURI(e.target.value) });
6 | };
7 |
8 | export const handleKey = (e: any, reset?: string): void => {
9 | if (e.key === 'Enter') {
10 | if (reset) {
11 | store.dispatch({ type: 'RESET_RESULTS' });
12 | }
13 | search();
14 | }
15 | };
16 |
17 | export const search = (reset?: any): void => {
18 | store.dispatch((dispatch: any): any => {
19 | if (reset) {
20 | dispatch({ type: 'RESET_RESULTS' });
21 | }
22 | dispatch({ type: 'LOADING_STATUS', payload: true });
23 | axios
24 | .post(`/search/${store.getState().query}`)
25 | .then((res: any) => dispatch({ type: 'SEND_RESULTS', payload: res.data }))
26 | .then(() => {
27 | dispatch({ type: 'LOADING_STATUS', payload: false });
28 | screenGrab();
29 | });
30 | });
31 | };
32 |
33 | const screenGrab = (): void => {
34 | const arr = [];
35 | const results: any = store.getState().results;
36 | for (let i = 0; i < results.length; i++) {
37 | if (!!results[i].image === false) {
38 | arr.push(results[i].link);
39 | }
40 | }
41 | getScreenshot(arr);
42 | };
43 |
44 | const getScreenshot = (links: string[]): void => {
45 | const len = links.length;
46 | for (let i = 0; i < len; i++) {
47 | const link = links[i];
48 | const formattedLink = encodeURIComponent(link);
49 | axios
50 | .get(`https://www.googleapis.com/pagespeedonline/v1/runPagespeed?screenshot=true&url=${formattedLink}`)
51 | .then(res => {
52 | const rawData = res.data.screenshot;
53 | if (rawData) {
54 | const imgData = rawData.data.replace(/_/g, '/').replace(/-/g, '+');
55 | const screenshot = 'data:' + rawData.mime_type + ';base64,' + imgData;
56 | store.dispatch({ type: 'SEND_SCREENSHOTS', payload: { link, screenshot } });
57 | }
58 | });
59 | }
60 | };
61 |
62 | export const nextPage = (): void => {
63 | store.dispatch((dispatch: any): any => {
64 | dispatch({ type: 'INCREMENT' });
65 | axios.post(`/search/${store.getState().query}/${store.getState().counter}`).then(res => {
66 | dispatch({ type: 'SEND_RESULTS', payload: res.data });
67 | screenGrab();
68 | });
69 | });
70 | };
71 |
72 | export const outline = (site: string): void => {
73 | store.dispatch((dispatch: any): any => {
74 | dispatch({ type: 'OUTLINE_LOADING' });
75 | axios.post(`/outline/${site}`).then(res => dispatch({ type: 'OUTLINE', payload: res.data }));
76 | });
77 | };
78 |
--------------------------------------------------------------------------------
/src/components/About.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import '../style/About.css';
4 |
5 | export const About: React.StatelessComponent = (): JSX.Element => (
6 |
7 |
8 |
Spresso
9 |
Search
10 |
11 |
12 |
About
13 |
14 |
15 | Spresso Search is a visual metasearch engine built using React, Redux and Express. It scrapes results from Google and obtains their meta-data
16 | using the node x-ray library. It also has a text-outline feature (powered by node-unfluff ) that allows you to read the contents of a web page in clean, formatted text without leaving the Spresso Search site.
17 |
18 |
23 |
24 |
25 | );
26 |
--------------------------------------------------------------------------------
/src/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import '../style/Loader.css';
3 |
4 | // Source: https://codepen.io/rbv912/pen/dYbqLQ
5 |
6 | export const Loader: React.StatelessComponent = (): JSX.Element => (
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/components/Outline.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Loader } from './Loader';
3 | import '../style/Outline.css';
4 |
5 | interface IOutlineProp {
6 | outline?: {
7 | title?: string;
8 | text?: string;
9 | };
10 | }
11 |
12 | export const Outline: React.StatelessComponent = ({ outline }: IOutlineProp): JSX.Element => {
13 | if (outline === 'outline loading...') {
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 | return (
21 |
22 |
{!!outline ? null : 'Spresso Text-Outline\n v.1.0.0'}
23 |
{!outline ? null : outline.title}
24 |
{!outline ? null : outline.text}
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/Result.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Tooltip from 'rc-tooltip';
3 | import 'rc-tooltip/assets/bootstrap_white.css';
4 | import '../style/Result.css';
5 | import { outline } from '../actions';
6 | import imgLoader from '../images/imgLoader.gif';
7 | import document from '../images/document.svg';
8 |
9 | export interface IData {
10 | title: string;
11 | link: string;
12 | snippet?: string;
13 | favicon?: string;
14 | pagemap?: {cse_image?: any};
15 | }
16 |
17 | interface IResultProps {
18 | data: IData;
19 | screenshots: string[];
20 | }
21 |
22 | const resolveImage = (pagemap: IData['pagemap'], link: string, screenshots: any[]) => {
23 | if (pagemap && pagemap.cse_image) {
24 | const [{src}] = pagemap.cse_image;
25 | return src;
26 | } else {
27 | for (let i = 0; i < screenshots.length; i++) {
28 | if (screenshots[i].link === link) {
29 | return screenshots[i].screenshot;
30 | }
31 | }
32 | return imgLoader;
33 | }
34 | };
35 |
36 | export const Result = ({ data, screenshots }: IResultProps) => (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
48 |
53 |
}
57 | >
58 |
outline(data.link)} />
59 |
60 |
63 |
64 |
65 | );
66 |
--------------------------------------------------------------------------------
/src/components/ResultsList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as InfiniteScroll from 'react-infinite-scroll-component';
3 | import { nextPage } from '../actions/index';
4 | import { Result } from './Result';
5 | import { IData } from './Result';
6 |
7 | interface IResultsListProps {
8 | results: any[];
9 | screenshots: string[];
10 | }
11 |
12 | export const ResultsList = ({ results, screenshots }: IResultsListProps): JSX.Element => {
13 | const ResultsArray = [];
14 | const len = results.length;
15 |
16 | for (let i = 0; i < len; i++) {
17 | ResultsArray.push( );
18 | }
19 |
20 | return (
21 | Loading...}
26 | >
27 | {ResultsArray}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/ResultsView.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ResultsList } from './ResultsList';
3 | import { Link } from 'react-router-dom';
4 | import { Outline } from './Outline';
5 | import { handleKey, search, setQuery } from '../actions';
6 | import '../style/ResultsView.css';
7 | import glass from '../images/glass.svg';
8 | import { Loader } from './Loader';
9 |
10 | export const ResultsView: React.StatelessComponent = ({ results, outline, screenshots, query, loadingStatus }: any): JSX.Element => {
11 | const mobile: string[] = ['Android', 'webOS', 'iPhone', 'iPad', 'iPod', 'BlackBerry'];
12 | return (
13 |
14 |
15 |
16 |
Spresso
17 |
Search
18 |
19 |
handleKey(e, 'reset')}
22 | onChange={e => setQuery(e)}
23 | />
24 |
search('reset')} className="glass" alt="magnifying glass" src={glass} />
25 |
26 | About
27 |
28 |
29 |
30 | {loadingStatus === true
31 | ?
32 | :
33 | }
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/SearchBox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import '../style/SearchBox.css';
4 | import { search, handleKey, setQuery } from '../actions/index';
5 | import glass from '../images/glass.svg';
6 |
7 | export const SearchBox: React.StatelessComponent = (): JSX.Element => (
8 |
9 |
10 | About
11 |
12 |
13 |
14 |
Spresso Search
15 |
setQuery(e)} onKeyPress={e => handleKey(e)} autoFocus={true} />
16 |
17 |
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/containers/Home.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { connect } from 'react-redux';
3 | import { SearchBox } from '../components/SearchBox';
4 | import { ResultsView } from '../components/ResultsView';
5 |
6 | export interface IHomeProps {
7 | results?: any[];
8 | loadingStatus?: boolean;
9 | outline?: any;
10 | query: string;
11 | counter?: number;
12 | screenshots?: any;
13 | }
14 |
15 | export const Home: React.StatelessComponent = ({
16 | results,
17 | loadingStatus,
18 | outline,
19 | query,
20 | screenshots
21 | }: IHomeProps) => {
22 | if (loadingStatus === true) {
23 | return ;
24 | } else if (loadingStatus === false && !!results) {
25 | return (
26 |
33 | );
34 | }
35 | return ;
36 | };
37 |
38 | const mapStateToProps = (state: IHomeProps): IHomeProps => {
39 | return {
40 | results: state.results,
41 | loadingStatus: state.loadingStatus,
42 | outline: state.outline,
43 | query: state.query,
44 | counter: state.counter,
45 | screenshots: state.screenshots
46 | };
47 | };
48 |
49 | export default connect(mapStateToProps)(Home);
50 |
--------------------------------------------------------------------------------
/src/images/back-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/images/document.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/images/glass.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/images/imgLoader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JoshuaRabiu/spresso-search/dc2fee6c3f4b53f766f8dfb36638383df3617f9d/src/images/imgLoader.gif
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "europa";
3 | src: url("https://use.typekit.net/af/4eabcf/00000000000000003b9b12fd/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff2"), url("https://use.typekit.net/af/4eabcf/00000000000000003b9b12fd/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("woff"), url("https://use.typekit.net/af/4eabcf/00000000000000003b9b12fd/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3") format("opentype");
4 | font-style: normal;
5 | font-weight: 400;
6 | }
7 | *{
8 | font-family: europa;
9 | font-size: 16px;
10 | }
11 | body {
12 | background: #fff;
13 | }
14 | a{
15 | text-decoration: none;
16 | color: inherit;
17 | }
18 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import './reset.min.css';
5 | import './index.css';
6 | import { App } from './App';
7 | import { Provider } from 'react-redux';
8 | import { store } from './reducers/index';
9 | import registerServiceWorker from './registerServiceWorker';
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | );
19 | registerServiceWorker();
20 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/reducers/counterReducer.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from '.';
2 |
3 | const initialState = 0;
4 | export const counterReducer = (state = initialState, action: IAction) => {
5 | switch (action.type) {
6 | case 'INCREMENT':
7 | return state += 10;
8 | case 'RESET_RESULTS':
9 | return initialState;
10 | default:
11 | return state;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers, applyMiddleware, createStore } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { counterReducer } from './counterReducer';
4 | import { loadingStatusReducer } from './loadingStatus';
5 | import { outlineReducer } from './outlineReducer';
6 | import { queryReducer } from './queryReducer';
7 | import { resultsReducer } from './resultsReducer';
8 | import { screenshotsReducer } from './screenshotsReducer';
9 |
10 | export interface IAction {
11 | type: string;
12 | payload?: any;
13 | }
14 |
15 | export const rootReducer = combineReducers({
16 | results: resultsReducer,
17 | loadingStatus: loadingStatusReducer,
18 | outline: outlineReducer,
19 | query: queryReducer,
20 | counter: counterReducer,
21 | screenshots: screenshotsReducer
22 | })
23 |
24 | const middleware = applyMiddleware(thunk)
25 | export const store = createStore(rootReducer, middleware)
26 |
--------------------------------------------------------------------------------
/src/reducers/loadingStatus.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from '.';
2 |
3 | export const loadingStatusReducer = (state = false, action: IAction) => {
4 | switch (action.type) {
5 | case 'LOADING_STATUS':
6 | return action.payload;
7 | default:
8 | return state;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/reducers/outlineReducer.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from '.';
2 |
3 | export const outlineReducer = (state = '', action: IAction) => {
4 | switch (action.type) {
5 | case 'OUTLINE':
6 | return action.payload;
7 | case 'OUTLINE_LOADING':
8 | return 'outline loading...';
9 | default:
10 | return state;
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/reducers/queryReducer.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from '.';
2 |
3 | export const queryReducer = (state = '', action: IAction) => {
4 | switch (action.type) {
5 | case 'SET_QUERY':
6 | return action.payload;
7 | default:
8 | return state;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/reducers/resultsReducer.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from '.';
2 |
3 | const initialState = '';
4 |
5 | export const resultsReducer = (state = initialState, action: IAction) => {
6 | switch (action.type) {
7 | case 'SEND_RESULTS':
8 | return !!state === true ? state.concat(action.payload) : action.payload;
9 | case 'RESET_RESULTS':
10 | return initialState;
11 | default:
12 | return state;
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/reducers/screenshotsReducer.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from '.';
2 |
3 | export const screenshotsReducer = (state = [], action: IAction) => {
4 | switch (action.type) {
5 | case 'SEND_SCREENSHOTS':
6 | return state.concat(action.payload);
7 | default:
8 | return state;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-console
2 | // In production, we register a service worker to serve assets from local cache.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on the 'N+1' visit to a page, since previously
7 | // cached resources are updated in the background.
8 |
9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
10 | // This link also includes instructions on opting out of this behavior.
11 |
12 | const isLocalhost = Boolean(
13 | window.location.hostname === 'localhost' ||
14 | // [::1] is the IPv6 localhost address.
15 | window.location.hostname === '[::1]' ||
16 | // 127.0.0.1/8 is considered localhost for IPv4.
17 | window.location.hostname.match(
18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
19 | )
20 | );
21 |
22 | export default function register() {
23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
24 | // The URL constructor is available in all browsers that support SW.
25 | const publicUrl = new URL(
26 | process.env.PUBLIC_URL!,
27 | window.location.toString()
28 | );
29 | if (publicUrl.origin !== window.location.origin) {
30 | // Our service worker won't work if PUBLIC_URL is on a different origin
31 | // from what our page is served on. This might happen if a CDN is used to
32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
33 | return;
34 | }
35 |
36 | window.addEventListener('load', () => {
37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
38 |
39 | if (isLocalhost) {
40 | // This is running on localhost. Lets check if a service worker still exists or not.
41 | checkValidServiceWorker(swUrl);
42 |
43 | // Add some additional logging to localhost, pointing developers to the
44 | // service worker/PWA documentation.
45 | navigator.serviceWorker.ready.then(() => {
46 | console.log(
47 | 'This web app is being served cache-first by a service ' +
48 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
49 | );
50 | });
51 | } else {
52 | // Is not local host. Just register service worker
53 | registerValidSW(swUrl);
54 | }
55 | });
56 | }
57 | }
58 |
59 | function registerValidSW(swUrl: string) {
60 | navigator.serviceWorker
61 | .register(swUrl)
62 | .then(registration => {
63 | registration.onupdatefound = () => {
64 | const installingWorker = registration.installing;
65 | if (installingWorker) {
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the old content will have been purged and
70 | // the fresh content will have been added to the cache.
71 | // It's the perfect time to display a 'New content is
72 | // available; please refresh.' message in your web app.
73 | console.log('New content is available; please refresh.');
74 | } else {
75 | // At this point, everything has been precached.
76 | // It's the perfect time to display a
77 | // 'Content is cached for offline use.' message.
78 | console.log('Content is cached for offline use.');
79 | }
80 | }
81 | };
82 | }
83 | };
84 | })
85 | .catch(error => {
86 | console.error('Error during service worker registration:', error);
87 | });
88 | }
89 |
90 | function checkValidServiceWorker(swUrl: string) {
91 | // Check if the service worker can be found. If it can't reload the page.
92 | fetch(swUrl)
93 | .then(response => {
94 | // Ensure service worker exists, and that we really are getting a JS file.
95 | if (
96 | response.status === 404 ||
97 | response.headers.get('content-type')!.indexOf('javascript') === -1
98 | ) {
99 | // No service worker found. Probably a different app. Reload the page.
100 | navigator.serviceWorker.ready.then(registration => {
101 | registration.unregister().then(() => {
102 | window.location.reload();
103 | });
104 | });
105 | } else {
106 | // Service worker found. Proceed as normal.
107 | registerValidSW(swUrl);
108 | }
109 | })
110 | .catch(() => {
111 | console.log(
112 | 'No internet connection found. App is running in offline mode.'
113 | );
114 | });
115 | }
116 |
117 | export function unregister() {
118 | if ('serviceWorker' in navigator) {
119 | navigator.serviceWorker.ready.then(registration => {
120 | registration.unregister();
121 | });
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/reset.min.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
--------------------------------------------------------------------------------
/src/style/About.css:
--------------------------------------------------------------------------------
1 | .home-1,
2 | .home-2 {
3 | display: inline-block;
4 | }
5 | .home-1 {
6 | background-image: linear-gradient(to right, #ff5c1c 0%, #ffc04b 100%);
7 | -webkit-background-clip: text;
8 | -webkit-text-fill-color: transparent;
9 | margin-left: 23px;
10 | }
11 | .home-2 {
12 | color: #545454;
13 | padding-left: 5.5px;
14 | }
15 | .center {
16 | position: absolute;
17 | top: 50%;
18 | left: 50%;
19 | margin-right: -50%;
20 | transform: translate(-50%, -50%);
21 | width: 45vw;
22 | }
23 | .center a {
24 | color: #ffc04b;
25 | }
26 | .center a:hover {
27 | text-decoration: underline;
28 | }
29 | .center h4 {
30 | display: block;
31 | }
32 | ul,
33 | li {
34 | list-style: none;
35 | }
36 | ul,
37 | li::before {
38 | content: "- ";
39 | }
40 |
--------------------------------------------------------------------------------
/src/style/Loader.css:
--------------------------------------------------------------------------------
1 | .loader-5 {
2 | height: 32px;
3 | width: 32px;
4 | margin: auto;
5 | position: absolute;
6 | top: 0;
7 | left: 0;
8 | bottom: 80px;
9 | right: 0;
10 | -webkit-animation: loader-5-1 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
11 | animation: loader-5-1 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
12 | }
13 | @-webkit-keyframes loader-5-1 {
14 | 0% {
15 | -webkit-transform: rotate(0deg);
16 | }
17 | 100% {
18 | -webkit-transform: rotate(360deg);
19 | }
20 | }
21 | @keyframes loader-5-1 {
22 | 0% {
23 | transform: rotate(0deg);
24 | }
25 | 100% {
26 | transform: rotate(360deg);
27 | }
28 | }
29 | .loader-5::before {
30 | content: "";
31 | display: block;
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | bottom: 0;
36 | right: auto;
37 | margin: auto;
38 | width: 8px;
39 | height: 8px;
40 | background: #ffa500;
41 | border-radius: 50%;
42 | -webkit-animation: loader-5-2 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
43 | animation: loader-5-2 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
44 | }
45 | @-webkit-keyframes loader-5-2 {
46 | 0% {
47 | -webkit-transform: translate3d(0, 0, 0) scale(1);
48 | }
49 | 50% {
50 | -webkit-transform: translate3d(24px, 0, 0) scale(.5);
51 | }
52 | 100% {
53 | -webkit-transform: translate3d(0, 0, 0) scale(1);
54 | }
55 | }
56 | @keyframes loader-5-2 {
57 | 0% {
58 | transform: translate3d(0, 0, 0) scale(1);
59 | }
60 | 50% {
61 | transform: translate3d(24px, 0, 0) scale(.5);
62 | }
63 | 100% {
64 | transform: translate3d(0, 0, 0) scale(1);
65 | }
66 | }
67 | .loader-5::after {
68 | content: "";
69 | display: block;
70 | position: absolute;
71 | top: 0;
72 | left: auto;
73 | bottom: 0;
74 | right: 0;
75 | margin: auto;
76 | width: 8px;
77 | height: 8px;
78 | background: #ffa500;
79 | border-radius: 50%;
80 | -webkit-animation: loader-5-3 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
81 | animation: loader-5-3 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
82 | }
83 | @-webkit-keyframes loader-5-3 {
84 | 0% {
85 | -webkit-transform: translate3d(0, 0, 0) scale(1);
86 | }
87 | 50% {
88 | -webkit-transform: translate3d(-24px, 0, 0) scale(.5);
89 | }
90 | 100% {
91 | -webkit-transform: translate3d(0, 0, 0) scale(1);
92 | }
93 | }
94 | @keyframes loader-5-3 {
95 | 0% {
96 | transform: translate3d(0, 0, 0) scale(1);
97 | }
98 | 50% {
99 | transform: translate3d(-24px, 0, 0) scale(.5);
100 | }
101 | 100% {
102 | transform: translate3d(0, 0, 0) scale(1);
103 | }
104 | }
105 | .loader-5 span {
106 | display: block;
107 | position: absolute;
108 | top: 0;
109 | left: 0;
110 | bottom: 0;
111 | right: 0;
112 | margin: auto;
113 | height: 32px;
114 | width: 32px;
115 | }
116 | .loader-5 span::before {
117 | content: "";
118 | display: block;
119 | position: absolute;
120 | top: 0;
121 | left: 0;
122 | bottom: auto;
123 | right: 0;
124 | margin: auto;
125 | width: 8px;
126 | height: 8px;
127 | background: #ffa500;
128 | border-radius: 50%;
129 | -webkit-animation: loader-5-4 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
130 | animation: loader-5-4 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
131 | }
132 | @-webkit-keyframes loader-5-4 {
133 | 0% {
134 | -webkit-transform: translate3d(0, 0, 0) scale(1);
135 | }
136 | 50% {
137 | -webkit-transform: translate3d(0, 24px, 0) scale(.5);
138 | }
139 | 100% {
140 | -webkit-transform: translate3d(0, 0, 0) scale(1);
141 | }
142 | }
143 | @keyframes loader-5-4 {
144 | 0% {
145 | transform: translate3d(0, 0, 0) scale(1);
146 | }
147 | 50% {
148 | transform: translate3d(0, 24px, 0) scale(.5);
149 | }
150 | 100% {
151 | transform: translate3d(0, 0, 0) scale(1);
152 | }
153 | }
154 | .loader-5 span::after {
155 | content: "";
156 | display: block;
157 | position: absolute;
158 | top: auto;
159 | left: 0;
160 | bottom: 0;
161 | right: 0;
162 | margin: auto;
163 | width: 8px;
164 | height: 8px;
165 | background: #ffa500;
166 | border-radius: 50%;
167 | -webkit-animation: loader-5-5 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
168 | animation: loader-5-5 2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
169 | }
170 | @-webkit-keyframes loader-5-5 {
171 | 0% {
172 | -webkit-transform: translate3d(0, 0, 0) scale(1);
173 | }
174 | 50% {
175 | -webkit-transform: translate3d(0, -24px, 0) scale(.5);
176 | }
177 | 100% {
178 | -webkit-transform: translate3d(0, 0, 0) scale(1);
179 | }
180 | }
181 | @keyframes loader-5-5 {
182 | 0% {
183 | transform: translate3d(0, 0, 0) scale(1);
184 | }
185 | 50% {
186 | transform: translate3d(0, -24px, 0) scale(.5);
187 | }
188 | 100% {
189 | transform: translate3d(0, 0, 0) scale(1);
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/style/Outline.css:
--------------------------------------------------------------------------------
1 | .outline {
2 | font-size: 16px;
3 | float: right;
4 | position: sticky;
5 | position: -webkit-sticky;
6 | top: 75px;
7 | display: inline-block;
8 | background: #fff;
9 | border: .009px solid #f2f2f2;
10 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15);
11 | width: 41vw;
12 | height: 84vh;
13 | border-radius: 20px;
14 | margin-right: 15px;
15 | overflow: scroll;
16 | max-width: 100%;
17 | overflow-x: hidden;
18 | padding: 20px;
19 | line-height: 19pt;
20 | text-align: justify;
21 | }
22 | .outline h3 {
23 | font-style: italic;
24 | }
25 | .outline p {
26 | color: #545454;
27 | }
28 | .placeholder {
29 | opacity: 0.4;
30 | position: absolute;
31 | top: 50%;
32 | left: 50%;
33 | margin-right: -50%;
34 | transform: translate(-50%, -50%);
35 | }
36 | @media only screen and (min-device-width: 320px) and (max-device-width: 1024px) {
37 | .outline,
38 | .placeholder {
39 | display: none;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/style/Result.css:
--------------------------------------------------------------------------------
1 | h4 a {
2 | text-decoration: none;
3 | color: #ffa500;
4 | }
5 |
6 | h4 a:hover {
7 | text-decoration: underline;
8 | }
9 | h4 a:visited {
10 | color: #962a00;
11 | }
12 | .card {
13 | display: flex;
14 | position: relative;
15 | height: 174px;
16 | width: 50.5vw;
17 | border-radius: 20px;
18 | margin: 15px;
19 | background: #fff;
20 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15);
21 | transition: all 300ms ease;
22 | }
23 | .card:hover {
24 | transform: scale(1.01);
25 | box-shadow: 0 15px 30px 0 rgba(0, 0, 0, 0.15), 0 5px 15px 0 rgba(0, 0, 0, 0.08);
26 | }
27 | .card-body {
28 | display: inline-block;
29 | margin-left: 8px;
30 | margin-right: 8px;
31 | border-bottom: 6px solid transparent;
32 | text-overflow: ellipsis;
33 | line-height: normal;
34 | overflow: hidden;
35 | }
36 | .title {
37 | color: #ffa500;
38 | max-width: 22vw;
39 | text-overflow: ellipsis;
40 | overflow: hidden;
41 | }
42 | .title:visited{
43 | color: #962a00;
44 | }
45 | h4 {
46 | display: inline-block;
47 | white-space: nowrap;
48 | max-width: 45ch;
49 | }
50 | .preview {
51 | width: 21vw;
52 | height: 100%;
53 | border-top-left-radius: 20px;
54 | border-bottom-left-radius: 20px;
55 | }
56 | .favicon {
57 | position: relative;
58 | top: 1vh;
59 | width: 18px;
60 | height: 18px;
61 | float: left;
62 | clear: both;
63 | margin: 13px;
64 | }
65 | .wrap {
66 | margin-top: -21px;
67 | }
68 | .description {
69 | color: #545454;
70 | min-height: 0;
71 | padding: 0 10px 10px 10px;
72 | }
73 | .icon {
74 | position: relative;
75 | bottom: 27px;
76 | vertical-align: bottom;
77 | display: inline-block;
78 | padding-left: 10px;
79 | width: 17px;
80 | height: 17px;
81 | cursor: pointer;
82 | }
83 | @media only screen and (min-device-width: 320px) and (max-device-width: 480px) {
84 | * {
85 | font-size: 13px;
86 | }
87 | .top-bar {
88 | text-align: center;
89 | }
90 | .glass {
91 | height: 27px;
92 | }
93 | input {
94 | font-size: 16px;
95 | }
96 | .card {
97 | margin: 10px 5px 10px 3px;
98 | width: 98vw;
99 | }
100 | .card-body {
101 | border-bottom: 10px solid transparent;
102 | }
103 | .title {
104 | max-width: 18ch;
105 | text-overflow: ellipsis;
106 | overflow: hidden;
107 | }
108 | .preview {
109 | min-width: 41vw;
110 | height: 174px;
111 | }
112 | .about-bar {
113 | display: none;
114 | }
115 | .icon {
116 | display: none;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/style/ResultsView.css:
--------------------------------------------------------------------------------
1 | .invisible {
2 | display: none;
3 | }
4 | .loading-text {
5 | color: #545454;
6 | margin: 15px;
7 | }
8 | .top-bar {
9 | position: sticky;
10 | top: 0px;
11 | z-index: 500;
12 | background: #fff;
13 | height: 60px;
14 | width: 100%;
15 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.10);
16 | }
17 | .top-bar input {
18 | margin-left: 5.4vw;
19 | height: 5.5vh;
20 | margin-top: 7px;
21 | box-shadow: none;
22 | border: 1px solid lightgrey;
23 | display: inline-flex;
24 | }
25 | .top-bar h3 {
26 | display: inline;
27 | }
28 | .top-bar h3:hover {
29 | cursor: pointer;
30 | }
31 | .heading-1 {
32 | margin-left: 23px;
33 | margin-right: 5.5px;
34 | background-image: linear-gradient(to right, #ff5c1c 0%, #ffc04b 100%);
35 | -webkit-background-clip: text;
36 | -webkit-text-fill-color: transparent;
37 | }
38 | .heading-2 {
39 | color: #545454;
40 | }
41 | .about-bar {
42 | float: right;
43 | padding: 15px;
44 | /* position: relative;
45 | left: 40px;
46 | top: 15px; */
47 | }
48 | .about-bar:hover {
49 | text-decoration: underline;
50 | }
51 | .load-wrap {
52 | position: absolute;
53 | top: 50%;
54 | left: 24%;
55 | margin-right: -50%;
56 | transform: translate(-50%, -50%);
57 | }
58 |
--------------------------------------------------------------------------------
/src/style/SearchBox.css:
--------------------------------------------------------------------------------
1 | .home {
2 | position: absolute;
3 | top: 50%;
4 | left: 50%;
5 | margin-right: -50%;
6 | transform: translate(-50%, -50%);
7 | line-height: normal;
8 | }
9 | .home-logo {
10 | font-weight: 1000;
11 | display: inline;
12 | text-align: center;
13 | background-image: linear-gradient(to right, #ff5c1c 0%, #ffc04b 100%);
14 | -webkit-background-clip: text;
15 | -webkit-text-fill-color: transparent;
16 | }
17 | .home-logo-2 {
18 | display: inline;
19 | padding-left: 5.5px;
20 | text-align: center;
21 | color: #545454;
22 | margin-right: 50px;
23 | }
24 | input {
25 | width: 522px;
26 | height: 48px;
27 | margin: auto;
28 | font-size: inherit;
29 | padding: 0 58px 0 28px;
30 | border-radius: 30px;
31 | border: none;
32 | border: 1.3px solid #f2f2f2;
33 | outline: 0;
34 | box-shadow: 2px 4px 8px 0 rgba(0, 0, 0, 0.15);
35 | transition: all 300ms ease;
36 | }
37 | .glass {
38 | display: inline-block;
39 | position: relative;
40 | right: 50px;
41 | top: 8px;
42 | width: 30px;
43 | height: 30px;
44 | }
45 | .glass:hover {
46 | cursor: pointer;
47 | }
48 | @media only screen and (min-device-width: 320px) and (max-device-width: 480px) {
49 | * {
50 | max-width: 100%;
51 | }
52 | .home {
53 | text-align: center;
54 | }
55 | input {
56 | width: 61vw;
57 | margin-top: 2vh;
58 | -webkit-appearance: none;
59 | -moz-appearance: none;
60 | appearance: none;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const { expect } = require('chai');
3 |
4 | describe('Spresso Search', async function(){
5 | let browser;
6 | let page;
7 |
8 | const opts = {
9 | args: [ '--start-fullscreen' ]
10 | };
11 |
12 | this.timeout(20000);
13 | before(async function(){
14 | browser = await puppeteer.launch(opts);
15 | });
16 |
17 | beforeEach(async function(){
18 | page = await browser.newPage();
19 | await page.goto('http://spresso-search.herokuapp.com/', {
20 | waitUntil: 'load'
21 | });
22 | });
23 |
24 | afterEach(async function(){
25 | await page.close();
26 | });
27 |
28 | after(async function(){
29 | await browser.close();
30 | });
31 |
32 | it('Returns search results when a query is typed and enter is pressed ', async function(){
33 | await page.click('input');
34 | await page.keyboard.type('tesla');
35 | await page.keyboard.press('Enter');
36 | const cards = await page.waitForSelector('.card');
37 | expect(!!cards).to.be.true;
38 | });
39 |
40 | it('Returns search results when a query is typed and the magnifying glass icon is pressed', async function(){
41 | await page.click('input');
42 | await page.keyboard.type('tesla');
43 | await page.click('.glass');
44 | const cards = await page.waitForSelector('.card');
45 | expect(!!cards).to.be.true;
46 | });
47 |
48 | it('Renders more results on scroll down (Infinite Scroll)', async function(){
49 | await page.click('input');
50 | await page.keyboard.type('tesla');
51 | await page.keyboard.press('Enter');
52 | await page.waitForSelector('.card');
53 | const initialNumOfCards = await page.evaluate(() => {
54 | return document.getElementsByClassName('card').length;
55 | });
56 | await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
57 | await page.waitFor(7000);
58 | const numOfCardsAfterScroll = await page.evaluate(() => {
59 | return document.getElementsByClassName('card').length;
60 | });
61 | expect(numOfCardsAfterScroll).to.be.greaterThan(initialNumOfCards);
62 | });
63 |
64 | it('Text outline returns a title and text', async function(){
65 | await page.click('input');
66 | // Ensures that only text-rich results are returned
67 | await page.keyboard.type('tesla site:wikipedia.com');
68 | await page.keyboard.press('Enter');
69 | await page.waitForSelector('.card');
70 | await page.click('div.card:nth-child(1) > div:nth-child(2) > img:nth-child(3)');
71 | await page.waitForSelector('.outline > p:nth-child(3)');
72 | const outlineTitle = await page.evaluate(() => {
73 | return document.querySelector('.outline > h3:nth-child(2)').innerText;
74 | });
75 | const outlineText = await page.evaluate(() => {
76 | return document.querySelector('.outline > p:nth-child(3)').innerText;
77 | });
78 | expect(!!outlineTitle, !!outlineText).to.be.true;
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "build/dist",
5 | "module": "esnext",
6 | "target": "es5",
7 | "lib": ["esnext", "dom"],
8 | "sourceMap": true,
9 | "allowJs": true,
10 | "jsx": "react",
11 | "moduleResolution": "node",
12 | "rootDir": "src",
13 | "forceConsistentCasingInFileNames": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "importHelpers": true,
18 | "strictNullChecks": true,
19 | "suppressImplicitAnyIndexErrors": true,
20 | "noUnusedLocals": true
21 | },
22 | "exclude": [
23 | "node_modules",
24 | "build",
25 | "scripts",
26 | "acceptance-tests",
27 | "webpack",
28 | "jest",
29 | "src/setupTests.ts",
30 | "server",
31 | "test"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json"
3 | }
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
3 | "linterOptions": {
4 | "exclude": [
5 | "config/**/*.js",
6 | "node_modules/**/*.ts",
7 | "coverage/lcov-report/*.js",
8 | "server"
9 | ]
10 | },
11 | "rules": {
12 | "prefer-for-of": false,
13 | "grouped-imports": false,
14 | "object-literal-sort-keys": false,
15 | "ordered-imports": false,
16 | "jsx-no-lambda": false,
17 | "no-console": false,
18 | "only-arrow-functions": false
19 | }
20 | }
21 |
--------------------------------------------------------------------------------